truthlocks-maip 1.0.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.
- truthlocks_maip-1.0.0/.gitignore +11 -0
- truthlocks_maip-1.0.0/PKG-INFO +132 -0
- truthlocks_maip-1.0.0/README.md +108 -0
- truthlocks_maip-1.0.0/pyproject.toml +38 -0
- truthlocks_maip-1.0.0/src/truthlocks_maip/__init__.py +75 -0
- truthlocks_maip-1.0.0/src/truthlocks_maip/client.py +269 -0
- truthlocks_maip-1.0.0/src/truthlocks_maip/errors.py +64 -0
- truthlocks_maip-1.0.0/src/truthlocks_maip/types.py +230 -0
- truthlocks_maip-1.0.0/src/truthlocks_maip/verify.py +126 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: truthlocks-maip
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python SDK for the MAIP (Machine Agent Identity Protocol)
|
|
5
|
+
Project-URL: Homepage, https://github.com/truthlocks/maip
|
|
6
|
+
Project-URL: Repository, https://github.com/truthlocks/maip
|
|
7
|
+
Project-URL: Documentation, https://github.com/truthlocks/maip/tree/main/sdk/python
|
|
8
|
+
Author-email: "Truthlocks Inc." <engineering@truthlocks.com>
|
|
9
|
+
License-Expression: Apache-2.0
|
|
10
|
+
Keywords: agent-identity,ai-agents,delegation,maip,trust
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: Apache Software 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: httpx>=0.25.0
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# truthlocks-maip
|
|
26
|
+
|
|
27
|
+
Python SDK for the **MAIP (Machine Agent Identity Protocol)**.
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install truthlocks-maip
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from truthlocks_maip import (
|
|
39
|
+
MaipClient,
|
|
40
|
+
CreateAgentRequest,
|
|
41
|
+
CreateSessionRequest,
|
|
42
|
+
OfferDelegationRequest,
|
|
43
|
+
CheckGuardrailsRequest,
|
|
44
|
+
Scope,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
client = MaipClient(api_key="your-api-key")
|
|
48
|
+
# For self-hosted: MaipClient(api_key="...", base_url="https://maip.your-company.com")
|
|
49
|
+
|
|
50
|
+
# Register an agent
|
|
51
|
+
agent = client.register_agent(CreateAgentRequest(
|
|
52
|
+
name="my-agent",
|
|
53
|
+
scope=Scope(actions=["read", "write"], resources=["documents/*"]),
|
|
54
|
+
public_key="base64-encoded-public-key",
|
|
55
|
+
))
|
|
56
|
+
|
|
57
|
+
# Create a session
|
|
58
|
+
session = client.create_session(CreateSessionRequest(
|
|
59
|
+
agent_id=agent["id"],
|
|
60
|
+
ttl_seconds=3600,
|
|
61
|
+
))
|
|
62
|
+
|
|
63
|
+
# Get trust score
|
|
64
|
+
trust = client.get_trust_score(agent["id"])
|
|
65
|
+
print(f"Trust level: {trust['level']}, score: {trust['score']}")
|
|
66
|
+
|
|
67
|
+
# Delegate trust
|
|
68
|
+
delegation = client.offer_delegation(OfferDelegationRequest(
|
|
69
|
+
from_agent_id=agent["id"],
|
|
70
|
+
to_agent_id="other-agent-id",
|
|
71
|
+
scope=Scope(actions=["read"], resources=["documents/*"]),
|
|
72
|
+
ttl_seconds=1800,
|
|
73
|
+
))
|
|
74
|
+
|
|
75
|
+
# Check guardrails
|
|
76
|
+
result = client.check_guardrails(CheckGuardrailsRequest(
|
|
77
|
+
agent_id=agent["id"],
|
|
78
|
+
action="write",
|
|
79
|
+
resource_id="documents/report.pdf",
|
|
80
|
+
))
|
|
81
|
+
|
|
82
|
+
if result["allowed"]:
|
|
83
|
+
pass # Proceed with the action
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Offline Bundle Verification
|
|
87
|
+
|
|
88
|
+
Verify receipt bundles without network access:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from truthlocks_maip import verify_bundle
|
|
92
|
+
|
|
93
|
+
result = verify_bundle(bundle)
|
|
94
|
+
if result.valid:
|
|
95
|
+
print(f"Verified {result.receipt_count} receipts")
|
|
96
|
+
else:
|
|
97
|
+
print("Bundle verification failed")
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## API Reference
|
|
101
|
+
|
|
102
|
+
### `MaipClient`
|
|
103
|
+
|
|
104
|
+
| Method | Description |
|
|
105
|
+
|---|---|
|
|
106
|
+
| `register_agent(request)` | Register a new agent identity |
|
|
107
|
+
| `list_agents(status?, offset?, limit?)` | List agents with optional filters |
|
|
108
|
+
| `get_agent(agent_id)` | Get an agent by ID |
|
|
109
|
+
| `suspend_agent(agent_id)` | Suspend an active agent |
|
|
110
|
+
| `revoke_agent(agent_id)` | Permanently revoke an agent |
|
|
111
|
+
| `create_session(request)` | Create an authenticated session |
|
|
112
|
+
| `terminate_session(session_id)` | Terminate a session |
|
|
113
|
+
| `get_trust_score(agent_id)` | Get the current trust score |
|
|
114
|
+
| `compute_trust_score(request)` | Compute a fresh trust score |
|
|
115
|
+
| `offer_delegation(request)` | Offer trust delegation |
|
|
116
|
+
| `accept_delegation(delegation_id)` | Accept a delegation |
|
|
117
|
+
| `execute_orchestration(request)` | Execute multi-agent orchestration |
|
|
118
|
+
| `check_guardrails(request)` | Check guardrails before an action |
|
|
119
|
+
|
|
120
|
+
### Error Types
|
|
121
|
+
|
|
122
|
+
| Error | HTTP Status | Description |
|
|
123
|
+
|---|---|---|
|
|
124
|
+
| `MaipError` | any | Base error class |
|
|
125
|
+
| `UnauthorizedError` | 401 | Invalid or missing API key |
|
|
126
|
+
| `NotFoundError` | 404 | Resource not found |
|
|
127
|
+
| `LimitExceededError` | 429 | Rate limit or quota exceeded |
|
|
128
|
+
| `VerificationError` | n/a | Bundle/receipt verification failed |
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
Apache-2.0
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# truthlocks-maip
|
|
2
|
+
|
|
3
|
+
Python SDK for the **MAIP (Machine Agent Identity Protocol)**.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install truthlocks-maip
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from truthlocks_maip import (
|
|
15
|
+
MaipClient,
|
|
16
|
+
CreateAgentRequest,
|
|
17
|
+
CreateSessionRequest,
|
|
18
|
+
OfferDelegationRequest,
|
|
19
|
+
CheckGuardrailsRequest,
|
|
20
|
+
Scope,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
client = MaipClient(api_key="your-api-key")
|
|
24
|
+
# For self-hosted: MaipClient(api_key="...", base_url="https://maip.your-company.com")
|
|
25
|
+
|
|
26
|
+
# Register an agent
|
|
27
|
+
agent = client.register_agent(CreateAgentRequest(
|
|
28
|
+
name="my-agent",
|
|
29
|
+
scope=Scope(actions=["read", "write"], resources=["documents/*"]),
|
|
30
|
+
public_key="base64-encoded-public-key",
|
|
31
|
+
))
|
|
32
|
+
|
|
33
|
+
# Create a session
|
|
34
|
+
session = client.create_session(CreateSessionRequest(
|
|
35
|
+
agent_id=agent["id"],
|
|
36
|
+
ttl_seconds=3600,
|
|
37
|
+
))
|
|
38
|
+
|
|
39
|
+
# Get trust score
|
|
40
|
+
trust = client.get_trust_score(agent["id"])
|
|
41
|
+
print(f"Trust level: {trust['level']}, score: {trust['score']}")
|
|
42
|
+
|
|
43
|
+
# Delegate trust
|
|
44
|
+
delegation = client.offer_delegation(OfferDelegationRequest(
|
|
45
|
+
from_agent_id=agent["id"],
|
|
46
|
+
to_agent_id="other-agent-id",
|
|
47
|
+
scope=Scope(actions=["read"], resources=["documents/*"]),
|
|
48
|
+
ttl_seconds=1800,
|
|
49
|
+
))
|
|
50
|
+
|
|
51
|
+
# Check guardrails
|
|
52
|
+
result = client.check_guardrails(CheckGuardrailsRequest(
|
|
53
|
+
agent_id=agent["id"],
|
|
54
|
+
action="write",
|
|
55
|
+
resource_id="documents/report.pdf",
|
|
56
|
+
))
|
|
57
|
+
|
|
58
|
+
if result["allowed"]:
|
|
59
|
+
pass # Proceed with the action
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Offline Bundle Verification
|
|
63
|
+
|
|
64
|
+
Verify receipt bundles without network access:
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from truthlocks_maip import verify_bundle
|
|
68
|
+
|
|
69
|
+
result = verify_bundle(bundle)
|
|
70
|
+
if result.valid:
|
|
71
|
+
print(f"Verified {result.receipt_count} receipts")
|
|
72
|
+
else:
|
|
73
|
+
print("Bundle verification failed")
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## API Reference
|
|
77
|
+
|
|
78
|
+
### `MaipClient`
|
|
79
|
+
|
|
80
|
+
| Method | Description |
|
|
81
|
+
|---|---|
|
|
82
|
+
| `register_agent(request)` | Register a new agent identity |
|
|
83
|
+
| `list_agents(status?, offset?, limit?)` | List agents with optional filters |
|
|
84
|
+
| `get_agent(agent_id)` | Get an agent by ID |
|
|
85
|
+
| `suspend_agent(agent_id)` | Suspend an active agent |
|
|
86
|
+
| `revoke_agent(agent_id)` | Permanently revoke an agent |
|
|
87
|
+
| `create_session(request)` | Create an authenticated session |
|
|
88
|
+
| `terminate_session(session_id)` | Terminate a session |
|
|
89
|
+
| `get_trust_score(agent_id)` | Get the current trust score |
|
|
90
|
+
| `compute_trust_score(request)` | Compute a fresh trust score |
|
|
91
|
+
| `offer_delegation(request)` | Offer trust delegation |
|
|
92
|
+
| `accept_delegation(delegation_id)` | Accept a delegation |
|
|
93
|
+
| `execute_orchestration(request)` | Execute multi-agent orchestration |
|
|
94
|
+
| `check_guardrails(request)` | Check guardrails before an action |
|
|
95
|
+
|
|
96
|
+
### Error Types
|
|
97
|
+
|
|
98
|
+
| Error | HTTP Status | Description |
|
|
99
|
+
|---|---|---|
|
|
100
|
+
| `MaipError` | any | Base error class |
|
|
101
|
+
| `UnauthorizedError` | 401 | Invalid or missing API key |
|
|
102
|
+
| `NotFoundError` | 404 | Resource not found |
|
|
103
|
+
| `LimitExceededError` | 429 | Rate limit or quota exceeded |
|
|
104
|
+
| `VerificationError` | n/a | Bundle/receipt verification failed |
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
Apache-2.0
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "truthlocks-maip"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Python SDK for the MAIP (Machine Agent Identity Protocol)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "Apache-2.0"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Truthlocks Inc.", email = "engineering@truthlocks.com" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["maip", "agent-identity", "trust", "delegation", "ai-agents"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 5 - Production/Stable",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: Apache Software License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.9",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Typing :: Typed",
|
|
27
|
+
]
|
|
28
|
+
dependencies = [
|
|
29
|
+
"httpx>=0.25.0",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/truthlocks/maip"
|
|
34
|
+
Repository = "https://github.com/truthlocks/maip"
|
|
35
|
+
Documentation = "https://github.com/truthlocks/maip/tree/main/sdk/python"
|
|
36
|
+
|
|
37
|
+
[tool.hatch.build.targets.wheel]
|
|
38
|
+
packages = ["src/truthlocks_maip"]
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Copyright 2026 Truthlocks Inc.
|
|
2
|
+
# Licensed under the Apache License, Version 2.0
|
|
3
|
+
|
|
4
|
+
"""Python SDK for the MAIP (Machine Agent Identity Protocol)."""
|
|
5
|
+
|
|
6
|
+
from truthlocks_maip.client import MaipClient
|
|
7
|
+
from truthlocks_maip.errors import (
|
|
8
|
+
LimitExceededError,
|
|
9
|
+
MaipError,
|
|
10
|
+
NotFoundError,
|
|
11
|
+
UnauthorizedError,
|
|
12
|
+
VerificationError,
|
|
13
|
+
)
|
|
14
|
+
from truthlocks_maip.types import (
|
|
15
|
+
Agent,
|
|
16
|
+
AgentStatus,
|
|
17
|
+
Bundle,
|
|
18
|
+
CheckGuardrailsRequest,
|
|
19
|
+
ComputeTrustScoreRequest,
|
|
20
|
+
CreateAgentRequest,
|
|
21
|
+
CreateSessionRequest,
|
|
22
|
+
Delegation,
|
|
23
|
+
DelegationStatus,
|
|
24
|
+
ExecuteOrchestrationRequest,
|
|
25
|
+
GuardrailResult,
|
|
26
|
+
GuardrailViolation,
|
|
27
|
+
ListResponse,
|
|
28
|
+
OfferDelegationRequest,
|
|
29
|
+
Orchestration,
|
|
30
|
+
OrchestrationStatus,
|
|
31
|
+
Receipt,
|
|
32
|
+
Scope,
|
|
33
|
+
Session,
|
|
34
|
+
SessionStatus,
|
|
35
|
+
TrustFactor,
|
|
36
|
+
TrustLevel,
|
|
37
|
+
TrustScore,
|
|
38
|
+
)
|
|
39
|
+
from truthlocks_maip.verify import verify_bundle, verify_receipt_hash
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
"MaipClient",
|
|
43
|
+
"MaipError",
|
|
44
|
+
"LimitExceededError",
|
|
45
|
+
"UnauthorizedError",
|
|
46
|
+
"NotFoundError",
|
|
47
|
+
"VerificationError",
|
|
48
|
+
"Agent",
|
|
49
|
+
"AgentStatus",
|
|
50
|
+
"Bundle",
|
|
51
|
+
"CheckGuardrailsRequest",
|
|
52
|
+
"ComputeTrustScoreRequest",
|
|
53
|
+
"CreateAgentRequest",
|
|
54
|
+
"CreateSessionRequest",
|
|
55
|
+
"Delegation",
|
|
56
|
+
"DelegationStatus",
|
|
57
|
+
"ExecuteOrchestrationRequest",
|
|
58
|
+
"GuardrailResult",
|
|
59
|
+
"GuardrailViolation",
|
|
60
|
+
"ListResponse",
|
|
61
|
+
"OfferDelegationRequest",
|
|
62
|
+
"Orchestration",
|
|
63
|
+
"OrchestrationStatus",
|
|
64
|
+
"Receipt",
|
|
65
|
+
"Scope",
|
|
66
|
+
"Session",
|
|
67
|
+
"SessionStatus",
|
|
68
|
+
"TrustFactor",
|
|
69
|
+
"TrustLevel",
|
|
70
|
+
"TrustScore",
|
|
71
|
+
"verify_bundle",
|
|
72
|
+
"verify_receipt_hash",
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
# Copyright 2026 Truthlocks Inc.
|
|
2
|
+
# Licensed under the Apache License, Version 2.0
|
|
3
|
+
|
|
4
|
+
"""MAIP SDK client for interacting with the Machine Agent Identity Protocol API."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from dataclasses import asdict
|
|
10
|
+
from typing import Any, Optional
|
|
11
|
+
from urllib.parse import quote
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
|
|
15
|
+
from truthlocks_maip.errors import (
|
|
16
|
+
LimitExceededError,
|
|
17
|
+
MaipError,
|
|
18
|
+
NotFoundError,
|
|
19
|
+
UnauthorizedError,
|
|
20
|
+
)
|
|
21
|
+
from truthlocks_maip.types import (
|
|
22
|
+
Agent,
|
|
23
|
+
AgentStatus,
|
|
24
|
+
CheckGuardrailsRequest,
|
|
25
|
+
ComputeTrustScoreRequest,
|
|
26
|
+
CreateAgentRequest,
|
|
27
|
+
CreateSessionRequest,
|
|
28
|
+
Delegation,
|
|
29
|
+
ExecuteOrchestrationRequest,
|
|
30
|
+
GuardrailResult,
|
|
31
|
+
ListResponse,
|
|
32
|
+
OfferDelegationRequest,
|
|
33
|
+
Orchestration,
|
|
34
|
+
Session,
|
|
35
|
+
TrustScore,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
DEFAULT_BASE_URL = "https://api.truthlocks.com"
|
|
39
|
+
DEFAULT_TIMEOUT = 30.0
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class MaipClient:
|
|
43
|
+
"""MAIP SDK client.
|
|
44
|
+
|
|
45
|
+
Supports both the hosted Truthlocks API and self-hosted deployments
|
|
46
|
+
by configuring the ``base_url`` parameter.
|
|
47
|
+
|
|
48
|
+
Example::
|
|
49
|
+
|
|
50
|
+
from truthlocks_maip import MaipClient
|
|
51
|
+
|
|
52
|
+
client = MaipClient(api_key="your-api-key")
|
|
53
|
+
|
|
54
|
+
agent = client.register_agent(CreateAgentRequest(
|
|
55
|
+
name="my-agent",
|
|
56
|
+
scope=Scope(actions=["read"], resources=["*"]),
|
|
57
|
+
public_key="base64-encoded-public-key",
|
|
58
|
+
))
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def __init__(
|
|
62
|
+
self,
|
|
63
|
+
api_key: str,
|
|
64
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
65
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
66
|
+
) -> None:
|
|
67
|
+
if not api_key:
|
|
68
|
+
raise MaipError("api_key is required")
|
|
69
|
+
|
|
70
|
+
self._base_url = base_url.rstrip("/")
|
|
71
|
+
self._client = httpx.Client(
|
|
72
|
+
base_url=self._base_url,
|
|
73
|
+
headers={
|
|
74
|
+
"Authorization": f"Bearer {api_key}",
|
|
75
|
+
"Content-Type": "application/json",
|
|
76
|
+
"Accept": "application/json",
|
|
77
|
+
"User-Agent": "truthlocks-maip-python/0.1.0",
|
|
78
|
+
},
|
|
79
|
+
timeout=timeout,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def close(self) -> None:
|
|
83
|
+
"""Close the underlying HTTP client."""
|
|
84
|
+
self._client.close()
|
|
85
|
+
|
|
86
|
+
def __enter__(self) -> MaipClient:
|
|
87
|
+
return self
|
|
88
|
+
|
|
89
|
+
def __exit__(self, *args: Any) -> None:
|
|
90
|
+
self.close()
|
|
91
|
+
|
|
92
|
+
# -------------------------------------------------------------------------
|
|
93
|
+
# Agent Management
|
|
94
|
+
# -------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
def register_agent(self, request: CreateAgentRequest) -> dict[str, Any]:
|
|
97
|
+
"""Register a new agent identity."""
|
|
98
|
+
return self._post("/v1/agents", self._to_api_dict(request))
|
|
99
|
+
|
|
100
|
+
def list_agents(
|
|
101
|
+
self,
|
|
102
|
+
status: Optional[AgentStatus] = None,
|
|
103
|
+
offset: Optional[int] = None,
|
|
104
|
+
limit: Optional[int] = None,
|
|
105
|
+
) -> dict[str, Any]:
|
|
106
|
+
"""List agents with optional filters."""
|
|
107
|
+
params: dict[str, Any] = {}
|
|
108
|
+
if status is not None:
|
|
109
|
+
params["status"] = status.value
|
|
110
|
+
if offset is not None:
|
|
111
|
+
params["offset"] = offset
|
|
112
|
+
if limit is not None:
|
|
113
|
+
params["limit"] = limit
|
|
114
|
+
return self._get("/v1/agents", params=params)
|
|
115
|
+
|
|
116
|
+
def get_agent(self, agent_id: str) -> dict[str, Any]:
|
|
117
|
+
"""Retrieve a single agent by ID."""
|
|
118
|
+
return self._get(f"/v1/agents/{quote(agent_id, safe='')}")
|
|
119
|
+
|
|
120
|
+
def suspend_agent(self, agent_id: str) -> dict[str, Any]:
|
|
121
|
+
"""Suspend an active agent."""
|
|
122
|
+
return self._post(
|
|
123
|
+
f"/v1/agents/{quote(agent_id, safe='')}/suspend", {}
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
def revoke_agent(self, agent_id: str) -> dict[str, Any]:
|
|
127
|
+
"""Permanently revoke an agent identity."""
|
|
128
|
+
return self._post(
|
|
129
|
+
f"/v1/agents/{quote(agent_id, safe='')}/revoke", {}
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# -------------------------------------------------------------------------
|
|
133
|
+
# Session Management
|
|
134
|
+
# -------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
def create_session(self, request: CreateSessionRequest) -> dict[str, Any]:
|
|
137
|
+
"""Create a new authenticated session for an agent."""
|
|
138
|
+
return self._post("/v1/sessions", self._to_api_dict(request))
|
|
139
|
+
|
|
140
|
+
def terminate_session(self, session_id: str) -> dict[str, Any]:
|
|
141
|
+
"""Terminate an active session."""
|
|
142
|
+
return self._post(
|
|
143
|
+
f"/v1/sessions/{quote(session_id, safe='')}/terminate", {}
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# -------------------------------------------------------------------------
|
|
147
|
+
# Trust
|
|
148
|
+
# -------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
def get_trust_score(self, agent_id: str) -> dict[str, Any]:
|
|
151
|
+
"""Get the current trust score for an agent."""
|
|
152
|
+
return self._get(
|
|
153
|
+
f"/v1/agents/{quote(agent_id, safe='')}/trust-score"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def compute_trust_score(
|
|
157
|
+
self, request: ComputeTrustScoreRequest
|
|
158
|
+
) -> dict[str, Any]:
|
|
159
|
+
"""Compute a fresh trust score for an agent."""
|
|
160
|
+
agent_id = request.agent_id
|
|
161
|
+
return self._post(
|
|
162
|
+
f"/v1/agents/{quote(agent_id, safe='')}/trust-score/compute",
|
|
163
|
+
self._to_api_dict(request),
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# -------------------------------------------------------------------------
|
|
167
|
+
# Delegation
|
|
168
|
+
# -------------------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
def offer_delegation(
|
|
171
|
+
self, request: OfferDelegationRequest
|
|
172
|
+
) -> dict[str, Any]:
|
|
173
|
+
"""Offer a trust delegation from one agent to another."""
|
|
174
|
+
return self._post("/v1/delegations", self._to_api_dict(request))
|
|
175
|
+
|
|
176
|
+
def accept_delegation(self, delegation_id: str) -> dict[str, Any]:
|
|
177
|
+
"""Accept an offered delegation."""
|
|
178
|
+
return self._post(
|
|
179
|
+
f"/v1/delegations/{quote(delegation_id, safe='')}/accept", {}
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# -------------------------------------------------------------------------
|
|
183
|
+
# Orchestration
|
|
184
|
+
# -------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
def execute_orchestration(
|
|
187
|
+
self, request: ExecuteOrchestrationRequest
|
|
188
|
+
) -> dict[str, Any]:
|
|
189
|
+
"""Execute a multi-agent orchestration."""
|
|
190
|
+
return self._post("/v1/orchestrations", self._to_api_dict(request))
|
|
191
|
+
|
|
192
|
+
# -------------------------------------------------------------------------
|
|
193
|
+
# Guardrails
|
|
194
|
+
# -------------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
def check_guardrails(
|
|
197
|
+
self, request: CheckGuardrailsRequest
|
|
198
|
+
) -> dict[str, Any]:
|
|
199
|
+
"""Check guardrails before performing an action."""
|
|
200
|
+
return self._post("/v1/guardrails/check", self._to_api_dict(request))
|
|
201
|
+
|
|
202
|
+
# -------------------------------------------------------------------------
|
|
203
|
+
# Internal helpers
|
|
204
|
+
# -------------------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
@staticmethod
|
|
207
|
+
def _to_api_dict(obj: Any) -> dict[str, Any]:
|
|
208
|
+
"""Convert a dataclass to a dict with camelCase keys for the API."""
|
|
209
|
+
raw = asdict(obj)
|
|
210
|
+
return MaipClient._snake_to_camel_dict(raw)
|
|
211
|
+
|
|
212
|
+
@staticmethod
|
|
213
|
+
def _snake_to_camel(name: str) -> str:
|
|
214
|
+
parts = name.split("_")
|
|
215
|
+
return parts[0] + "".join(p.capitalize() for p in parts[1:])
|
|
216
|
+
|
|
217
|
+
@staticmethod
|
|
218
|
+
def _snake_to_camel_dict(d: dict[str, Any]) -> dict[str, Any]:
|
|
219
|
+
result: dict[str, Any] = {}
|
|
220
|
+
for key, value in d.items():
|
|
221
|
+
camel_key = MaipClient._snake_to_camel(key)
|
|
222
|
+
if isinstance(value, dict):
|
|
223
|
+
result[camel_key] = MaipClient._snake_to_camel_dict(value)
|
|
224
|
+
elif value is not None:
|
|
225
|
+
result[camel_key] = value
|
|
226
|
+
return result
|
|
227
|
+
|
|
228
|
+
def _get(
|
|
229
|
+
self, path: str, params: Optional[dict[str, Any]] = None
|
|
230
|
+
) -> dict[str, Any]:
|
|
231
|
+
try:
|
|
232
|
+
response = self._client.get(path, params=params)
|
|
233
|
+
self._raise_for_status(response)
|
|
234
|
+
return response.json() # type: ignore[no-any-return]
|
|
235
|
+
except httpx.HTTPError as exc:
|
|
236
|
+
raise MaipError(f"Request failed: {exc}") from exc
|
|
237
|
+
|
|
238
|
+
def _post(self, path: str, body: dict[str, Any]) -> dict[str, Any]:
|
|
239
|
+
try:
|
|
240
|
+
response = self._client.post(path, json=body)
|
|
241
|
+
self._raise_for_status(response)
|
|
242
|
+
return response.json() # type: ignore[no-any-return]
|
|
243
|
+
except httpx.HTTPError as exc:
|
|
244
|
+
raise MaipError(f"Request failed: {exc}") from exc
|
|
245
|
+
|
|
246
|
+
@staticmethod
|
|
247
|
+
def _raise_for_status(response: httpx.Response) -> None:
|
|
248
|
+
if response.is_success:
|
|
249
|
+
return
|
|
250
|
+
|
|
251
|
+
try:
|
|
252
|
+
error_body = response.json()
|
|
253
|
+
message = error_body.get("message", f"HTTP {response.status_code}")
|
|
254
|
+
except Exception:
|
|
255
|
+
message = f"HTTP {response.status_code}"
|
|
256
|
+
|
|
257
|
+
status = response.status_code
|
|
258
|
+
if status == 401:
|
|
259
|
+
raise UnauthorizedError(message)
|
|
260
|
+
elif status == 404:
|
|
261
|
+
raise NotFoundError("Resource", message)
|
|
262
|
+
elif status == 429:
|
|
263
|
+
retry_after = response.headers.get("Retry-After")
|
|
264
|
+
raise LimitExceededError(
|
|
265
|
+
message,
|
|
266
|
+
retry_after_seconds=int(retry_after) if retry_after else None,
|
|
267
|
+
)
|
|
268
|
+
else:
|
|
269
|
+
raise MaipError(message, status_code=status)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Copyright 2026 Truthlocks Inc.
|
|
2
|
+
# Licensed under the Apache License, Version 2.0
|
|
3
|
+
|
|
4
|
+
"""Error types for the MAIP SDK."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MaipError(Exception):
|
|
12
|
+
"""Base error class for all MAIP SDK errors."""
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
message: str,
|
|
17
|
+
status_code: Optional[int] = None,
|
|
18
|
+
code: Optional[str] = None,
|
|
19
|
+
) -> None:
|
|
20
|
+
super().__init__(message)
|
|
21
|
+
self.message = message
|
|
22
|
+
self.status_code = status_code
|
|
23
|
+
self.code = code
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class LimitExceededError(MaipError):
|
|
27
|
+
"""Raised when the caller exceeds a rate limit or quota."""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
message: str = "Rate limit exceeded",
|
|
32
|
+
retry_after_seconds: Optional[int] = None,
|
|
33
|
+
) -> None:
|
|
34
|
+
super().__init__(message, status_code=429, code="LIMIT_EXCEEDED")
|
|
35
|
+
self.retry_after_seconds = retry_after_seconds
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class UnauthorizedError(MaipError):
|
|
39
|
+
"""Raised when the API key is missing, invalid, or lacks permission."""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self, message: str = "Unauthorized: invalid or missing API key"
|
|
43
|
+
) -> None:
|
|
44
|
+
super().__init__(message, status_code=401, code="UNAUTHORIZED")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class NotFoundError(MaipError):
|
|
48
|
+
"""Raised when a requested resource is not found."""
|
|
49
|
+
|
|
50
|
+
def __init__(self, resource: str, identifier: str) -> None:
|
|
51
|
+
super().__init__(
|
|
52
|
+
f"{resource} not found: {identifier}",
|
|
53
|
+
status_code=404,
|
|
54
|
+
code="NOT_FOUND",
|
|
55
|
+
)
|
|
56
|
+
self.resource = resource
|
|
57
|
+
self.identifier = identifier
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class VerificationError(MaipError):
|
|
61
|
+
"""Raised when bundle or receipt verification fails."""
|
|
62
|
+
|
|
63
|
+
def __init__(self, message: str) -> None:
|
|
64
|
+
super().__init__(message, code="VERIFICATION_FAILED")
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# Copyright 2026 Truthlocks Inc.
|
|
2
|
+
# Licensed under the Apache License, Version 2.0
|
|
3
|
+
|
|
4
|
+
"""Data types for the MAIP SDK."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import Any, Generic, Optional, TypeVar
|
|
11
|
+
|
|
12
|
+
T = TypeVar("T")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AgentStatus(str, Enum):
|
|
16
|
+
ACTIVE = "active"
|
|
17
|
+
SUSPENDED = "suspended"
|
|
18
|
+
REVOKED = "revoked"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SessionStatus(str, Enum):
|
|
22
|
+
ACTIVE = "active"
|
|
23
|
+
TERMINATED = "terminated"
|
|
24
|
+
EXPIRED = "expired"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TrustLevel(str, Enum):
|
|
28
|
+
CRITICAL = "critical"
|
|
29
|
+
LOW = "low"
|
|
30
|
+
MEDIUM = "medium"
|
|
31
|
+
HIGH = "high"
|
|
32
|
+
VERIFIED = "verified"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class DelegationStatus(str, Enum):
|
|
36
|
+
OFFERED = "offered"
|
|
37
|
+
ACCEPTED = "accepted"
|
|
38
|
+
REJECTED = "rejected"
|
|
39
|
+
REVOKED = "revoked"
|
|
40
|
+
EXPIRED = "expired"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class OrchestrationStatus(str, Enum):
|
|
44
|
+
PENDING = "pending"
|
|
45
|
+
RUNNING = "running"
|
|
46
|
+
COMPLETED = "completed"
|
|
47
|
+
FAILED = "failed"
|
|
48
|
+
CANCELLED = "cancelled"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ViolationSeverity(str, Enum):
|
|
52
|
+
INFO = "info"
|
|
53
|
+
WARNING = "warning"
|
|
54
|
+
ERROR = "error"
|
|
55
|
+
CRITICAL = "critical"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class Scope:
|
|
60
|
+
"""Permissions and boundaries for an agent."""
|
|
61
|
+
actions: list[str]
|
|
62
|
+
resources: list[str]
|
|
63
|
+
constraints: dict[str, str] = field(default_factory=dict)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True)
|
|
67
|
+
class Agent:
|
|
68
|
+
"""A registered machine agent identity."""
|
|
69
|
+
id: str
|
|
70
|
+
name: str
|
|
71
|
+
status: AgentStatus
|
|
72
|
+
scope: Scope
|
|
73
|
+
public_key: str
|
|
74
|
+
metadata: dict[str, str]
|
|
75
|
+
created_at: str
|
|
76
|
+
updated_at: str
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True)
|
|
80
|
+
class Session:
|
|
81
|
+
"""An authenticated agent session."""
|
|
82
|
+
id: str
|
|
83
|
+
agent_id: str
|
|
84
|
+
status: SessionStatus
|
|
85
|
+
scope: Scope
|
|
86
|
+
expires_at: str
|
|
87
|
+
created_at: str
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass(frozen=True)
|
|
91
|
+
class TrustFactor:
|
|
92
|
+
"""A single factor contributing to a trust score."""
|
|
93
|
+
name: str
|
|
94
|
+
weight: float
|
|
95
|
+
value: float
|
|
96
|
+
description: str
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass(frozen=True)
|
|
100
|
+
class TrustScore:
|
|
101
|
+
"""Computed trust level for an agent."""
|
|
102
|
+
agent_id: str
|
|
103
|
+
score: float
|
|
104
|
+
level: TrustLevel
|
|
105
|
+
factors: list[TrustFactor]
|
|
106
|
+
computed_at: str
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass(frozen=True)
|
|
110
|
+
class Delegation:
|
|
111
|
+
"""Trust delegation from one agent to another."""
|
|
112
|
+
id: str
|
|
113
|
+
from_agent_id: str
|
|
114
|
+
to_agent_id: str
|
|
115
|
+
scope: Scope
|
|
116
|
+
status: DelegationStatus
|
|
117
|
+
expires_at: str
|
|
118
|
+
created_at: str
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@dataclass(frozen=True)
|
|
122
|
+
class Orchestration:
|
|
123
|
+
"""A multi-agent orchestration session."""
|
|
124
|
+
id: str
|
|
125
|
+
coordinator_agent_id: str
|
|
126
|
+
participant_agent_ids: list[str]
|
|
127
|
+
status: OrchestrationStatus
|
|
128
|
+
scope: Scope
|
|
129
|
+
created_at: str
|
|
130
|
+
result: Optional[dict[str, Any]] = None
|
|
131
|
+
completed_at: Optional[str] = None
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@dataclass(frozen=True)
|
|
135
|
+
class Receipt:
|
|
136
|
+
"""Cryptographic proof of an action taken by an agent."""
|
|
137
|
+
id: str
|
|
138
|
+
agent_id: str
|
|
139
|
+
session_id: str
|
|
140
|
+
action: str
|
|
141
|
+
resource_id: str
|
|
142
|
+
timestamp: str
|
|
143
|
+
signature: str
|
|
144
|
+
metadata: dict[str, str]
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@dataclass(frozen=True)
|
|
148
|
+
class Bundle:
|
|
149
|
+
"""Collection of receipts that can be verified offline."""
|
|
150
|
+
id: str
|
|
151
|
+
receipts: list[Receipt]
|
|
152
|
+
root_hash: str
|
|
153
|
+
signature: str
|
|
154
|
+
created_at: str
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@dataclass(frozen=True)
|
|
158
|
+
class GuardrailViolation:
|
|
159
|
+
"""A guardrail rule violation."""
|
|
160
|
+
rule: str
|
|
161
|
+
severity: ViolationSeverity
|
|
162
|
+
message: str
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@dataclass(frozen=True)
|
|
166
|
+
class GuardrailResult:
|
|
167
|
+
"""Outcome of a guardrails check."""
|
|
168
|
+
allowed: bool
|
|
169
|
+
violations: list[GuardrailViolation]
|
|
170
|
+
checked_at: str
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@dataclass(frozen=True)
|
|
174
|
+
class ListResponse(Generic[T]):
|
|
175
|
+
"""Paginated list response."""
|
|
176
|
+
items: list[T]
|
|
177
|
+
total: int
|
|
178
|
+
offset: int
|
|
179
|
+
limit: int
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@dataclass(frozen=True)
|
|
183
|
+
class CreateAgentRequest:
|
|
184
|
+
"""Options for creating an agent."""
|
|
185
|
+
name: str
|
|
186
|
+
scope: Scope
|
|
187
|
+
public_key: str
|
|
188
|
+
metadata: dict[str, str] = field(default_factory=dict)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@dataclass(frozen=True)
|
|
192
|
+
class CreateSessionRequest:
|
|
193
|
+
"""Options for creating a session."""
|
|
194
|
+
agent_id: str
|
|
195
|
+
scope: Optional[Scope] = None
|
|
196
|
+
ttl_seconds: Optional[int] = None
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@dataclass(frozen=True)
|
|
200
|
+
class ComputeTrustScoreRequest:
|
|
201
|
+
"""Options for computing a trust score."""
|
|
202
|
+
agent_id: str
|
|
203
|
+
factors: Optional[list[dict[str, Any]]] = None
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@dataclass(frozen=True)
|
|
207
|
+
class OfferDelegationRequest:
|
|
208
|
+
"""Options for offering a delegation."""
|
|
209
|
+
from_agent_id: str
|
|
210
|
+
to_agent_id: str
|
|
211
|
+
scope: Scope
|
|
212
|
+
ttl_seconds: Optional[int] = None
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@dataclass(frozen=True)
|
|
216
|
+
class ExecuteOrchestrationRequest:
|
|
217
|
+
"""Options for executing an orchestration."""
|
|
218
|
+
coordinator_agent_id: str
|
|
219
|
+
participant_agent_ids: list[str]
|
|
220
|
+
scope: Scope
|
|
221
|
+
parameters: Optional[dict[str, Any]] = None
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@dataclass(frozen=True)
|
|
225
|
+
class CheckGuardrailsRequest:
|
|
226
|
+
"""Options for checking guardrails."""
|
|
227
|
+
agent_id: str
|
|
228
|
+
action: str
|
|
229
|
+
resource_id: str
|
|
230
|
+
context: Optional[dict[str, Any]] = None
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Copyright 2026 Truthlocks Inc.
|
|
2
|
+
# Licensed under the Apache License, Version 2.0
|
|
3
|
+
|
|
4
|
+
"""Offline bundle and receipt verification.
|
|
5
|
+
|
|
6
|
+
All functions in this module are pure and make no network calls.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
import json
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
|
|
15
|
+
from truthlocks_maip.errors import VerificationError
|
|
16
|
+
from truthlocks_maip.types import Bundle, Receipt
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _sha256_hex(data: str) -> str:
|
|
20
|
+
"""Compute SHA-256 hex digest of a string."""
|
|
21
|
+
return hashlib.sha256(data.encode("utf-8")).hexdigest()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _hash_receipt(receipt: Receipt) -> str:
|
|
25
|
+
"""Compute the canonical hash of a receipt for Merkle tree inclusion."""
|
|
26
|
+
canonical = json.dumps(
|
|
27
|
+
{
|
|
28
|
+
"action": receipt.action,
|
|
29
|
+
"agentId": receipt.agent_id,
|
|
30
|
+
"id": receipt.id,
|
|
31
|
+
"metadata": receipt.metadata,
|
|
32
|
+
"resourceId": receipt.resource_id,
|
|
33
|
+
"sessionId": receipt.session_id,
|
|
34
|
+
"signature": receipt.signature,
|
|
35
|
+
"timestamp": receipt.timestamp,
|
|
36
|
+
},
|
|
37
|
+
sort_keys=False,
|
|
38
|
+
separators=(",", ":"),
|
|
39
|
+
)
|
|
40
|
+
return _sha256_hex(canonical)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _compute_merkle_root(leaf_hashes: list[str]) -> str:
|
|
44
|
+
"""Compute the Merkle root from an ordered list of leaf hashes."""
|
|
45
|
+
if not leaf_hashes:
|
|
46
|
+
return _sha256_hex("")
|
|
47
|
+
if len(leaf_hashes) == 1:
|
|
48
|
+
return leaf_hashes[0]
|
|
49
|
+
|
|
50
|
+
level = list(leaf_hashes)
|
|
51
|
+
while len(level) > 1:
|
|
52
|
+
next_level: list[str] = []
|
|
53
|
+
for i in range(0, len(level), 2):
|
|
54
|
+
left = level[i]
|
|
55
|
+
right = level[i + 1] if i + 1 < len(level) else left
|
|
56
|
+
next_level.append(_sha256_hex(left + right))
|
|
57
|
+
level = next_level
|
|
58
|
+
return level[0]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class VerifyBundleResult:
|
|
63
|
+
"""Result of a bundle verification."""
|
|
64
|
+
valid: bool
|
|
65
|
+
computed_root_hash: str
|
|
66
|
+
declared_root_hash: str
|
|
67
|
+
receipt_count: int
|
|
68
|
+
receipt_hashes: list[str]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def verify_bundle(bundle: Bundle) -> VerifyBundleResult:
|
|
72
|
+
"""Verify a receipt bundle offline.
|
|
73
|
+
|
|
74
|
+
Checks that the Merkle root hash matches the receipts in the bundle.
|
|
75
|
+
Does NOT verify cryptographic signatures against agent public keys
|
|
76
|
+
(that requires network access to retrieve keys).
|
|
77
|
+
|
|
78
|
+
Pure function: no network calls are made.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
bundle: The bundle to verify.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
The verification result.
|
|
85
|
+
|
|
86
|
+
Raises:
|
|
87
|
+
VerificationError: If the bundle structure is invalid.
|
|
88
|
+
"""
|
|
89
|
+
if not bundle.receipts or not isinstance(bundle.receipts, list):
|
|
90
|
+
raise VerificationError("Bundle has no receipts list")
|
|
91
|
+
if not bundle.root_hash:
|
|
92
|
+
raise VerificationError("Bundle has no root_hash")
|
|
93
|
+
|
|
94
|
+
receipt_hashes: list[str] = []
|
|
95
|
+
for receipt in bundle.receipts:
|
|
96
|
+
if not receipt.id or not receipt.agent_id or not receipt.timestamp:
|
|
97
|
+
raise VerificationError(
|
|
98
|
+
f"Receipt is missing required fields: {receipt}"
|
|
99
|
+
)
|
|
100
|
+
receipt_hashes.append(_hash_receipt(receipt))
|
|
101
|
+
|
|
102
|
+
computed_root_hash = _compute_merkle_root(receipt_hashes)
|
|
103
|
+
|
|
104
|
+
return VerifyBundleResult(
|
|
105
|
+
valid=computed_root_hash == bundle.root_hash,
|
|
106
|
+
computed_root_hash=computed_root_hash,
|
|
107
|
+
declared_root_hash=bundle.root_hash,
|
|
108
|
+
receipt_count=len(bundle.receipts),
|
|
109
|
+
receipt_hashes=receipt_hashes,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def verify_receipt_hash(receipt: Receipt, expected_hash: str) -> bool:
|
|
114
|
+
"""Verify a single receipt hash against an expected value.
|
|
115
|
+
|
|
116
|
+
Useful for spot-checking individual receipts from a bundle.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
receipt: The receipt to hash.
|
|
120
|
+
expected_hash: The expected SHA-256 hex hash.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
True if the computed hash matches the expected hash.
|
|
124
|
+
"""
|
|
125
|
+
computed = _hash_receipt(receipt)
|
|
126
|
+
return computed == expected_hash
|