truss-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.
- truss_sdk-0.1.0/PKG-INFO +10 -0
- truss_sdk-0.1.0/README.md +135 -0
- truss_sdk-0.1.0/pyproject.toml +22 -0
- truss_sdk-0.1.0/setup.cfg +4 -0
- truss_sdk-0.1.0/tests/test_client.py +118 -0
- truss_sdk-0.1.0/tests/test_crypto.py +48 -0
- truss_sdk-0.1.0/truss_sdk/__init__.py +30 -0
- truss_sdk-0.1.0/truss_sdk/client.py +178 -0
- truss_sdk-0.1.0/truss_sdk/crypto.py +47 -0
- truss_sdk-0.1.0/truss_sdk/models.py +85 -0
- truss_sdk-0.1.0/truss_sdk.egg-info/PKG-INFO +10 -0
- truss_sdk-0.1.0/truss_sdk.egg-info/SOURCES.txt +13 -0
- truss_sdk-0.1.0/truss_sdk.egg-info/dependency_links.txt +1 -0
- truss_sdk-0.1.0/truss_sdk.egg-info/requires.txt +5 -0
- truss_sdk-0.1.0/truss_sdk.egg-info/top_level.txt +1 -0
truss_sdk-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: truss-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the Truss trust infrastructure API
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Requires-Dist: pynacl>=1.6
|
|
8
|
+
Provides-Extra: dev
|
|
9
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
10
|
+
Requires-Dist: pytest-mock>=3; extra == "dev"
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
# Truss SDK — Python
|
|
4
|
+
|
|
5
|
+
**Python SDK for the Truss trust infrastructure API — create mandates, record actions, manage agents, and verify evidence.**
|
|
6
|
+
|
|
7
|
+
[](https://pypi.org/project/truss-sdk/)
|
|
8
|
+
[](https://pypi.org/project/truss-sdk/)
|
|
9
|
+
[](LICENSE)
|
|
10
|
+
[](https://github.com/tensflare/truss-sdk-python/actions)
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## What is Truss?
|
|
15
|
+
|
|
16
|
+
Truss is an **accountability layer for AI agents** — it records every agent action as a cryptographically signed, tamper-evident audit trail. [Learn more →](https://truss.tensflare.com/docs)
|
|
17
|
+
|
|
18
|
+
## Features
|
|
19
|
+
|
|
20
|
+
- **Ed25519 cryptography** — Key generation, payload signing, and signature verification via `PyNaCl`
|
|
21
|
+
- **`TrussClient`** — Full-featured HTTP client for the Truss API
|
|
22
|
+
- **`ActionContext`** — Builder pattern for recording actions with chain-of-custody linkage
|
|
23
|
+
- **Dataclass models** — Mirroring Truss TAP schemas with full type annotations
|
|
24
|
+
- **Python ≥3.9**
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install truss-sdk
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Quick start
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from truss_sdk import TrussClient, generate_keypair, sign_payload, verify_signature
|
|
36
|
+
|
|
37
|
+
# 1. Generate an Ed25519 keypair
|
|
38
|
+
kp = generate_keypair()
|
|
39
|
+
print(f"Public key: {kp.public_key}")
|
|
40
|
+
print(f"Private key: {kp.private_key}") # keep secret!
|
|
41
|
+
|
|
42
|
+
# 2. Sign and verify
|
|
43
|
+
sig = sign_payload({"action": "read", "value": 42}, kp.private_key)
|
|
44
|
+
assert verify_signature({"action": "read", "value": 42}, sig, kp.public_key)
|
|
45
|
+
print("Signature valid: True")
|
|
46
|
+
|
|
47
|
+
# 3. Create an API client
|
|
48
|
+
client = TrussClient(api_key="tr_your_api_key")
|
|
49
|
+
|
|
50
|
+
# 4. Register an agent
|
|
51
|
+
agent = client.create_agent(
|
|
52
|
+
name="My Agent",
|
|
53
|
+
public_key=kp.public_key,
|
|
54
|
+
description="My autonomous agent",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# 5. Create a mandate
|
|
58
|
+
mandate = client.create_mandate(
|
|
59
|
+
mandate_id="mnd_001",
|
|
60
|
+
agent_id=agent.id,
|
|
61
|
+
agent_name=agent.name,
|
|
62
|
+
issuing_principal={
|
|
63
|
+
"entity": "org_1",
|
|
64
|
+
"human_id": "usr_1",
|
|
65
|
+
"role": "Admin",
|
|
66
|
+
},
|
|
67
|
+
scope={"permitted_actions": ["read", "write"]},
|
|
68
|
+
jurisdiction_context={
|
|
69
|
+
"deploying_org_jurisdiction": "US",
|
|
70
|
+
"operating_jurisdictions": ["US"],
|
|
71
|
+
},
|
|
72
|
+
validity={
|
|
73
|
+
"issued_at": "2026-06-06T00:00:00Z",
|
|
74
|
+
"expires_at": "2026-12-31T23:59:59Z",
|
|
75
|
+
},
|
|
76
|
+
private_key=kp.private_key,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# 6. Record an action using the builder pattern
|
|
80
|
+
ctx = client.action("read", mandate.id, kp.private_key)
|
|
81
|
+
ctx.record_input({"file": "/data/report.pdf"})
|
|
82
|
+
ctx.record_output({"summary": "Report contains 42 records"})
|
|
83
|
+
result = ctx.commit(agent_id=agent.id, chain_position=1, prev_record_hash=None)
|
|
84
|
+
print(f"Action recorded: {result.id}")
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## API reference
|
|
88
|
+
|
|
89
|
+
### `generate_keypair()`
|
|
90
|
+
|
|
91
|
+
Returns a `Keypair` namedtuple with `.public_key` and `.private_key` fields.
|
|
92
|
+
|
|
93
|
+
### `sign_payload(payload, private_key)`
|
|
94
|
+
|
|
95
|
+
Signs any JSON-serialisable `dict` and returns a hex-encoded Ed25519 signature string.
|
|
96
|
+
|
|
97
|
+
### `verify_signature(payload, signature, public_key)`
|
|
98
|
+
|
|
99
|
+
Verifies an Ed25519 signature. Returns `bool`.
|
|
100
|
+
|
|
101
|
+
### `TrussClient(api_key, api_url=None)`
|
|
102
|
+
|
|
103
|
+
| Parameter | Type | Description |
|
|
104
|
+
|---|---|---|
|
|
105
|
+
| `api_key` | `str` | Truss API key (`tr_` prefix) |
|
|
106
|
+
| `api_url` | `str` | API base URL (default `http://localhost:4000`) |
|
|
107
|
+
|
|
108
|
+
**Client methods:** `create_agent`, `get_agent`, `list_agents`, `create_mandate`, `get_mandate`, `list_mandates`, `record_action`, `get_action`, `list_actions`, `create_delegation`, `generate_evidence`, `verify_evidence`, and more.
|
|
109
|
+
|
|
110
|
+
### `client.action(action_type, mandate_id, private_key)`
|
|
111
|
+
|
|
112
|
+
Returns an `ActionContext` builder with `.record_input()`, `.record_output()`, `.commit()`.
|
|
113
|
+
|
|
114
|
+
## Related packages
|
|
115
|
+
|
|
116
|
+
| Package | Description |
|
|
117
|
+
|---|---|
|
|
118
|
+
| [@tensflare/tap](https://github.com/tensflare/truss-tap) | Core Zod schemas for mandates, actions, and delegations |
|
|
119
|
+
| [@tensflare/truss-sdk](https://github.com/tensflare/truss-sdk-js) | TypeScript SDK equivalent |
|
|
120
|
+
| [@tensflare/cli](https://github.com/tensflare/truss-cli) | Command-line interface |
|
|
121
|
+
|
|
122
|
+
## Development
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
pip install -e ".[dev]"
|
|
126
|
+
pytest
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Contributing
|
|
130
|
+
|
|
131
|
+
Pull requests are welcome. Please see the [contribution guidelines](https://truss.tensflare.com/docs/contributing).
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
Apache 2.0 — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "truss-sdk"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python SDK for the Truss trust infrastructure API"
|
|
9
|
+
requires-python = ">=3.9"
|
|
10
|
+
license = { text = "Apache-2.0" }
|
|
11
|
+
dependencies = [
|
|
12
|
+
"pynacl>=1.6",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.optional-dependencies]
|
|
16
|
+
dev = [
|
|
17
|
+
"pytest>=7",
|
|
18
|
+
"pytest-mock>=3",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[tool.setuptools.packages.find]
|
|
22
|
+
include = ["truss_sdk*"]
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from unittest.mock import patch, MagicMock
|
|
3
|
+
from truss_sdk.client import TrussClient, TrussClientError, ActionContext
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TestTrussClient:
|
|
7
|
+
def test_generate_keypair_returns_keypair(self):
|
|
8
|
+
kp = TrussClient.generate_keypair()
|
|
9
|
+
assert len(kp.public_key) == 64
|
|
10
|
+
assert len(kp.private_key) == 128
|
|
11
|
+
|
|
12
|
+
@patch("truss_sdk.client.urlopen")
|
|
13
|
+
def test_create_mandate(self, mock_urlopen):
|
|
14
|
+
mock_resp = MagicMock()
|
|
15
|
+
mock_resp.read.return_value = b'{"mandate_id": "mnd_1", "status": "active"}'
|
|
16
|
+
mock_urlopen.return_value.__enter__.return_value = mock_resp
|
|
17
|
+
|
|
18
|
+
client = TrussClient(api_key="tr_test_key")
|
|
19
|
+
result = client.create_mandate(
|
|
20
|
+
mandate_id="mnd_1",
|
|
21
|
+
agent_id="agt_1",
|
|
22
|
+
agent_name="Test Agent",
|
|
23
|
+
issuing_principal={"entity": "org_1", "human_id": "usr_1", "role": "Admin"},
|
|
24
|
+
scope={"permitted_actions": ["read"]},
|
|
25
|
+
jurisdiction_context={"deploying_org_jurisdiction": "US", "operating_jurisdictions": ["US"]},
|
|
26
|
+
validity={"issued_at": "2026-01-01T00:00:00Z", "expires_at": "2026-12-31T00:00:00Z"},
|
|
27
|
+
private_key="ab" * 64,
|
|
28
|
+
)
|
|
29
|
+
assert result["mandate_id"] == "mnd_1"
|
|
30
|
+
assert result["status"] == "active"
|
|
31
|
+
|
|
32
|
+
request = mock_urlopen.call_args[0][0]
|
|
33
|
+
assert request.method == "POST"
|
|
34
|
+
assert request.full_url.endswith("/mandates")
|
|
35
|
+
assert "Bearer tr_test_key" in request.headers.get("Authorization", "")
|
|
36
|
+
|
|
37
|
+
@patch("truss_sdk.client.urlopen")
|
|
38
|
+
def test_get_mandate(self, mock_urlopen):
|
|
39
|
+
mock_resp = MagicMock()
|
|
40
|
+
mock_resp.read.return_value = b'{"mandate_id": "mnd_1", "status": "active"}'
|
|
41
|
+
mock_urlopen.return_value.__enter__.return_value = mock_resp
|
|
42
|
+
|
|
43
|
+
client = TrussClient(api_key="tr_test_key")
|
|
44
|
+
result = client.get_mandate("mnd_1")
|
|
45
|
+
assert result["mandate_id"] == "mnd_1"
|
|
46
|
+
|
|
47
|
+
request = mock_urlopen.call_args[0][0]
|
|
48
|
+
assert request.full_url.endswith("/mandates/mnd_1")
|
|
49
|
+
|
|
50
|
+
@patch("truss_sdk.client.urlopen")
|
|
51
|
+
def test_record_action(self, mock_urlopen):
|
|
52
|
+
mock_resp = MagicMock()
|
|
53
|
+
mock_resp.read.return_value = b'{"record_id": "act_1", "chain_position": 1}'
|
|
54
|
+
mock_urlopen.return_value.__enter__.return_value = mock_resp
|
|
55
|
+
|
|
56
|
+
client = TrussClient(api_key="tr_test_key")
|
|
57
|
+
result = client.record_action(
|
|
58
|
+
record_id="act_1",
|
|
59
|
+
mandate_id="mnd_1",
|
|
60
|
+
action_type="read",
|
|
61
|
+
timestamp="2026-01-01T00:00:00Z",
|
|
62
|
+
agent_id="agt_1",
|
|
63
|
+
input_hash="sha256:abc",
|
|
64
|
+
output_hash="sha256:def",
|
|
65
|
+
chain_position=1,
|
|
66
|
+
prev_record_hash=None,
|
|
67
|
+
private_key="ab" * 64,
|
|
68
|
+
)
|
|
69
|
+
assert result["record_id"] == "act_1"
|
|
70
|
+
|
|
71
|
+
@patch("truss_sdk.client.urlopen")
|
|
72
|
+
def test_http_error_raises_truss_error(self, mock_urlopen):
|
|
73
|
+
import urllib.error
|
|
74
|
+
|
|
75
|
+
error_resp = MagicMock()
|
|
76
|
+
error_resp.read.return_value = b'{"error": "Invalid API key"}'
|
|
77
|
+
error_resp.code = 401
|
|
78
|
+
mock_urlopen.side_effect = urllib.error.HTTPError(
|
|
79
|
+
"http://localhost:4000/me", 401, "Unauthorized", {}, error_resp
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
client = TrussClient(api_key="bad_key")
|
|
83
|
+
with pytest.raises(TrussClientError, match="401"):
|
|
84
|
+
client.get_mandate("mnd_1")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class TestActionContext:
|
|
88
|
+
@patch("truss_sdk.client.urlopen")
|
|
89
|
+
def test_commit_requires_input_and_output(self, mock_urlopen):
|
|
90
|
+
client = TrussClient(api_key="tr_test_key")
|
|
91
|
+
ctx = client.action("read", "mnd_1", "ab" * 64)
|
|
92
|
+
|
|
93
|
+
with pytest.raises(TrussClientError, match="input_hash"):
|
|
94
|
+
ctx.commit("agt_1", 1, None)
|
|
95
|
+
|
|
96
|
+
ctx.record_input("sha256:abc")
|
|
97
|
+
with pytest.raises(TrussClientError, match="output_hash"):
|
|
98
|
+
ctx.commit("agt_1", 1, None)
|
|
99
|
+
|
|
100
|
+
@patch("truss_sdk.client.urlopen")
|
|
101
|
+
def test_commit_sends_signed_action(self, mock_urlopen):
|
|
102
|
+
mock_resp = MagicMock()
|
|
103
|
+
mock_resp.read.return_value = b'{"record_id": "act_xyz", "chain_position": 1}'
|
|
104
|
+
mock_urlopen.return_value.__enter__.return_value = mock_resp
|
|
105
|
+
|
|
106
|
+
client = TrussClient(api_key="tr_test_key")
|
|
107
|
+
ctx = client.action("write", "mnd_1", "ab" * 64)
|
|
108
|
+
ctx.record_input("sha256:in")
|
|
109
|
+
ctx.record_output("sha256:out")
|
|
110
|
+
|
|
111
|
+
result = ctx.commit(agent_id="agt_1", chain_position=1, prev_record_hash=None)
|
|
112
|
+
assert result["record_id"] == "act_xyz"
|
|
113
|
+
|
|
114
|
+
request = mock_urlopen.call_args[0][0]
|
|
115
|
+
body = request.data
|
|
116
|
+
assert b"sha256:in" in body
|
|
117
|
+
assert b"sha256:out" in body
|
|
118
|
+
assert b"signature" in body
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from truss_sdk.crypto import generate_keypair, sign_payload, verify_signature
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class TestCrypto:
|
|
6
|
+
def test_generate_keypair_returns_hex_keys(self):
|
|
7
|
+
kp = generate_keypair()
|
|
8
|
+
assert len(kp.public_key) == 64
|
|
9
|
+
assert len(kp.private_key) == 128
|
|
10
|
+
int(kp.public_key, 16)
|
|
11
|
+
int(kp.private_key, 16)
|
|
12
|
+
|
|
13
|
+
def test_sign_payload_and_verify_round_trip(self):
|
|
14
|
+
kp = generate_keypair()
|
|
15
|
+
payload = {"action": "test", "value": 42}
|
|
16
|
+
|
|
17
|
+
sig = sign_payload(payload, kp.private_key)
|
|
18
|
+
assert len(sig) == 128
|
|
19
|
+
int(sig, 16)
|
|
20
|
+
|
|
21
|
+
valid = verify_signature(payload, sig, kp.public_key)
|
|
22
|
+
assert valid is True
|
|
23
|
+
|
|
24
|
+
def test_verify_signature_rejects_wrong_key(self):
|
|
25
|
+
kp1 = generate_keypair()
|
|
26
|
+
kp2 = generate_keypair()
|
|
27
|
+
payload = {"action": "test"}
|
|
28
|
+
|
|
29
|
+
sig = sign_payload(payload, kp1.private_key)
|
|
30
|
+
valid = verify_signature(payload, sig, kp2.public_key)
|
|
31
|
+
assert valid is False
|
|
32
|
+
|
|
33
|
+
def test_verify_signature_rejects_tampered_payload(self):
|
|
34
|
+
kp = generate_keypair()
|
|
35
|
+
payload = {"action": "test"}
|
|
36
|
+
|
|
37
|
+
sig = sign_payload(payload, kp.private_key)
|
|
38
|
+
tampered = {"action": "tampered"}
|
|
39
|
+
valid = verify_signature(tampered, sig, kp.public_key)
|
|
40
|
+
assert valid is False
|
|
41
|
+
|
|
42
|
+
def test_signature_is_deterministic(self):
|
|
43
|
+
kp = generate_keypair()
|
|
44
|
+
payload = {"msg": "hello", "num": 1}
|
|
45
|
+
|
|
46
|
+
sig1 = sign_payload(payload, kp.private_key)
|
|
47
|
+
sig2 = sign_payload(payload, kp.private_key)
|
|
48
|
+
assert sig1 == sig2
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from .crypto import generate_keypair, sign_payload, verify_signature
|
|
2
|
+
from .client import TrussClient, ActionContext
|
|
3
|
+
from .models import (
|
|
4
|
+
Keypair,
|
|
5
|
+
Mandate,
|
|
6
|
+
ActionRecord,
|
|
7
|
+
IssuingPrincipal,
|
|
8
|
+
Scope,
|
|
9
|
+
JurisdictionContext,
|
|
10
|
+
Validity,
|
|
11
|
+
JurisdictionEvaluation,
|
|
12
|
+
JurisdictionFlag,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"generate_keypair",
|
|
17
|
+
"sign_payload",
|
|
18
|
+
"verify_signature",
|
|
19
|
+
"TrussClient",
|
|
20
|
+
"ActionContext",
|
|
21
|
+
"Keypair",
|
|
22
|
+
"Mandate",
|
|
23
|
+
"ActionRecord",
|
|
24
|
+
"IssuingPrincipal",
|
|
25
|
+
"Scope",
|
|
26
|
+
"JurisdictionContext",
|
|
27
|
+
"Validity",
|
|
28
|
+
"JurisdictionEvaluation",
|
|
29
|
+
"JurisdictionFlag",
|
|
30
|
+
]
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import uuid
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from urllib.request import Request, urlopen
|
|
5
|
+
from urllib.error import HTTPError
|
|
6
|
+
from typing import Optional
|
|
7
|
+
from .crypto import sign_payload, generate_keypair
|
|
8
|
+
from .models import Keypair
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TrussClientError(Exception):
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TrussClient:
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
api_key: str,
|
|
19
|
+
base_url: str = "http://localhost:4000",
|
|
20
|
+
agent_id: Optional[str] = None,
|
|
21
|
+
):
|
|
22
|
+
self.api_key = api_key
|
|
23
|
+
self.base_url = base_url.rstrip("/")
|
|
24
|
+
self.agent_id = agent_id
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
def generate_keypair() -> Keypair:
|
|
28
|
+
return generate_keypair()
|
|
29
|
+
|
|
30
|
+
def _request(
|
|
31
|
+
self,
|
|
32
|
+
method: str,
|
|
33
|
+
path: str,
|
|
34
|
+
body: Optional[dict] = None,
|
|
35
|
+
) -> dict:
|
|
36
|
+
url = f"{self.base_url}{path}"
|
|
37
|
+
headers = {
|
|
38
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
39
|
+
"Content-Type": "application/json",
|
|
40
|
+
}
|
|
41
|
+
data = json.dumps(body, separators=(",", ":"), sort_keys=True).encode("utf-8") if body else None
|
|
42
|
+
req = Request(url, data=data, headers=headers, method=method)
|
|
43
|
+
try:
|
|
44
|
+
with urlopen(req) as resp:
|
|
45
|
+
return json.loads(resp.read().decode("utf-8"))
|
|
46
|
+
except HTTPError as e:
|
|
47
|
+
error_body = e.read().decode("utf-8")
|
|
48
|
+
raise TrussClientError(
|
|
49
|
+
f"HTTP {e.code}: {error_body}"
|
|
50
|
+
) from e
|
|
51
|
+
|
|
52
|
+
def create_mandate(
|
|
53
|
+
self,
|
|
54
|
+
mandate_id: str,
|
|
55
|
+
agent_id: str,
|
|
56
|
+
agent_name: str,
|
|
57
|
+
issuing_principal: dict,
|
|
58
|
+
scope: dict,
|
|
59
|
+
jurisdiction_context: dict,
|
|
60
|
+
validity: dict,
|
|
61
|
+
private_key: str,
|
|
62
|
+
) -> dict:
|
|
63
|
+
body = {
|
|
64
|
+
"mandate_id": mandate_id,
|
|
65
|
+
"agent_id": agent_id,
|
|
66
|
+
"agent_name": agent_name,
|
|
67
|
+
"issuing_principal": issuing_principal,
|
|
68
|
+
"scope": scope,
|
|
69
|
+
"jurisdiction_context": jurisdiction_context,
|
|
70
|
+
"validity": validity,
|
|
71
|
+
"version": "1.0",
|
|
72
|
+
"issuer_public_key": "",
|
|
73
|
+
"signature": "",
|
|
74
|
+
}
|
|
75
|
+
sig_payload = {k: v for k, v in body.items() if k != "signature"}
|
|
76
|
+
body["signature"] = sign_payload(sig_payload, private_key)
|
|
77
|
+
return self._request("POST", "/mandates", body)
|
|
78
|
+
|
|
79
|
+
def record_action(
|
|
80
|
+
self,
|
|
81
|
+
record_id: str,
|
|
82
|
+
mandate_id: str,
|
|
83
|
+
action_type: str,
|
|
84
|
+
timestamp: str,
|
|
85
|
+
agent_id: str,
|
|
86
|
+
input_hash: str,
|
|
87
|
+
output_hash: str,
|
|
88
|
+
chain_position: int,
|
|
89
|
+
prev_record_hash: Optional[str],
|
|
90
|
+
private_key: str,
|
|
91
|
+
) -> dict:
|
|
92
|
+
body = {
|
|
93
|
+
"record_id": record_id,
|
|
94
|
+
"mandate_id": mandate_id,
|
|
95
|
+
"action_type": action_type,
|
|
96
|
+
"timestamp": timestamp,
|
|
97
|
+
"agent_id": agent_id,
|
|
98
|
+
"input_hash": input_hash,
|
|
99
|
+
"output_hash": output_hash,
|
|
100
|
+
"chain_position": chain_position,
|
|
101
|
+
"prev_record_hash": prev_record_hash,
|
|
102
|
+
"within_mandate": True,
|
|
103
|
+
"signature": "",
|
|
104
|
+
}
|
|
105
|
+
sig_payload = {k: v for k, v in body.items() if k != "signature"}
|
|
106
|
+
body["signature"] = sign_payload(sig_payload, private_key)
|
|
107
|
+
return self._request("POST", "/actions", body)
|
|
108
|
+
|
|
109
|
+
def get_mandate(self, mandate_id: str) -> dict:
|
|
110
|
+
return self._request("GET", f"/mandates/{mandate_id}")
|
|
111
|
+
|
|
112
|
+
def get_action(self, record_id: str) -> dict:
|
|
113
|
+
return self._request("GET", f"/actions/{record_id}")
|
|
114
|
+
|
|
115
|
+
def get_mandate_chain(self, mandate_id: str) -> dict:
|
|
116
|
+
return self._request("GET", f"/mandates/{mandate_id}/chain")
|
|
117
|
+
|
|
118
|
+
def action(
|
|
119
|
+
self,
|
|
120
|
+
action_type: str,
|
|
121
|
+
mandate_id: str,
|
|
122
|
+
private_key: str,
|
|
123
|
+
) -> "ActionContext":
|
|
124
|
+
return ActionContext(
|
|
125
|
+
client=self,
|
|
126
|
+
action_type=action_type,
|
|
127
|
+
mandate_id=mandate_id,
|
|
128
|
+
private_key=private_key,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class ActionContext:
|
|
133
|
+
def __init__(
|
|
134
|
+
self,
|
|
135
|
+
client: TrussClient,
|
|
136
|
+
action_type: str,
|
|
137
|
+
mandate_id: str,
|
|
138
|
+
private_key: str,
|
|
139
|
+
):
|
|
140
|
+
self._client = client
|
|
141
|
+
self._action_type = action_type
|
|
142
|
+
self._mandate_id = mandate_id
|
|
143
|
+
self._private_key = private_key
|
|
144
|
+
self._input_hash: Optional[str] = None
|
|
145
|
+
self._output_hash: Optional[str] = None
|
|
146
|
+
|
|
147
|
+
def record_input(self, input_hash: str) -> None:
|
|
148
|
+
self._input_hash = input_hash
|
|
149
|
+
|
|
150
|
+
def record_output(self, output_hash: str) -> None:
|
|
151
|
+
self._output_hash = output_hash
|
|
152
|
+
|
|
153
|
+
def commit(
|
|
154
|
+
self,
|
|
155
|
+
agent_id: str,
|
|
156
|
+
chain_position: int,
|
|
157
|
+
prev_record_hash: Optional[str],
|
|
158
|
+
) -> dict:
|
|
159
|
+
if self._input_hash is None:
|
|
160
|
+
raise TrussClientError("input_hash not set. Call record_input() before commit.")
|
|
161
|
+
if self._output_hash is None:
|
|
162
|
+
raise TrussClientError("output_hash not set. Call record_output() before commit.")
|
|
163
|
+
|
|
164
|
+
record_id = f"act_{uuid.uuid4().hex[:24]}"
|
|
165
|
+
timestamp = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
166
|
+
|
|
167
|
+
return self._client.record_action(
|
|
168
|
+
record_id=record_id,
|
|
169
|
+
mandate_id=self._mandate_id,
|
|
170
|
+
action_type=self._action_type,
|
|
171
|
+
timestamp=timestamp,
|
|
172
|
+
agent_id=agent_id,
|
|
173
|
+
input_hash=self._input_hash,
|
|
174
|
+
output_hash=self._output_hash,
|
|
175
|
+
chain_position=chain_position,
|
|
176
|
+
prev_record_hash=prev_record_hash,
|
|
177
|
+
private_key=self._private_key,
|
|
178
|
+
)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from nacl.signing import SigningKey, VerifyKey
|
|
3
|
+
from nacl.encoding import HexEncoder
|
|
4
|
+
from nacl.exceptions import BadSignatureError
|
|
5
|
+
from .models import Keypair
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def generate_keypair() -> Keypair:
|
|
9
|
+
signing_key = SigningKey.generate()
|
|
10
|
+
public_key = signing_key.verify_key.encode(encoder=HexEncoder).decode("ascii")
|
|
11
|
+
seed = signing_key.encode(encoder=HexEncoder).decode("ascii")
|
|
12
|
+
private_key = seed + public_key
|
|
13
|
+
return Keypair(public_key=public_key, private_key=private_key)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _canonical_json(payload: dict) -> bytes:
|
|
17
|
+
return json.dumps(payload, separators=(",", ":"), sort_keys=True).encode("utf-8")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _hex_to_seed(key_hex: str) -> bytes:
|
|
21
|
+
raw = bytes.fromhex(key_hex)
|
|
22
|
+
if len(raw) == 32:
|
|
23
|
+
return raw
|
|
24
|
+
if len(raw) == 64:
|
|
25
|
+
return raw[:32]
|
|
26
|
+
raise ValueError(
|
|
27
|
+
f"Invalid key length: expected 32 or 64 bytes, got {len(raw)}"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def sign_payload(payload: dict, private_key_hex: str) -> str:
|
|
32
|
+
canonical = _canonical_json(payload)
|
|
33
|
+
seed = _hex_to_seed(private_key_hex)
|
|
34
|
+
signing_key = SigningKey(seed)
|
|
35
|
+
signed = signing_key.sign(canonical)
|
|
36
|
+
return signed.signature.hex()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def verify_signature(payload: dict, signature_hex: str, public_key_hex: str) -> bool:
|
|
40
|
+
canonical = _canonical_json(payload)
|
|
41
|
+
signature = bytes.fromhex(signature_hex)
|
|
42
|
+
verify_key = VerifyKey(public_key_hex, encoder=HexEncoder)
|
|
43
|
+
try:
|
|
44
|
+
verify_key.verify(canonical, signature)
|
|
45
|
+
return True
|
|
46
|
+
except BadSignatureError:
|
|
47
|
+
return False
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class Keypair:
|
|
7
|
+
public_key: str
|
|
8
|
+
private_key: str
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class IssuingPrincipal:
|
|
13
|
+
entity: str
|
|
14
|
+
human_id: str
|
|
15
|
+
role: str
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class Scope:
|
|
20
|
+
permitted_actions: list[str]
|
|
21
|
+
forbidden_actions: list[str] = field(default_factory=list)
|
|
22
|
+
permitted_data_classes: list[str] = field(default_factory=list)
|
|
23
|
+
max_delegation_depth: int = 0
|
|
24
|
+
resource_bounds: list[str] = field(default_factory=list)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class JurisdictionContext:
|
|
29
|
+
deploying_org_jurisdiction: str
|
|
30
|
+
operating_jurisdictions: list[str]
|
|
31
|
+
regulatory_frameworks: list[str] = field(default_factory=list)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class Validity:
|
|
36
|
+
issued_at: str
|
|
37
|
+
expires_at: str
|
|
38
|
+
single_use: bool = False
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class Mandate:
|
|
43
|
+
mandate_id: str
|
|
44
|
+
version: str
|
|
45
|
+
agent_id: str
|
|
46
|
+
agent_name: str
|
|
47
|
+
issuing_principal: IssuingPrincipal
|
|
48
|
+
scope: Scope
|
|
49
|
+
jurisdiction_context: JurisdictionContext
|
|
50
|
+
validity: Validity
|
|
51
|
+
signature: str
|
|
52
|
+
issuer_public_key: str
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class JurisdictionFlag:
|
|
57
|
+
framework: str
|
|
58
|
+
obligation: str
|
|
59
|
+
severity: str
|
|
60
|
+
article: Optional[str] = None
|
|
61
|
+
guidance: Optional[str] = None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class JurisdictionEvaluation:
|
|
66
|
+
evaluated_at: str
|
|
67
|
+
frameworks_applied: list[str] = field(default_factory=list)
|
|
68
|
+
status: str = "unknown"
|
|
69
|
+
flags: list[JurisdictionFlag] = field(default_factory=list)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class ActionRecord:
|
|
74
|
+
record_id: str
|
|
75
|
+
mandate_id: str
|
|
76
|
+
action_type: str
|
|
77
|
+
timestamp: str
|
|
78
|
+
agent_id: str
|
|
79
|
+
input_hash: str
|
|
80
|
+
output_hash: str
|
|
81
|
+
within_mandate: bool = True
|
|
82
|
+
jurisdiction_evaluation: Optional[JurisdictionEvaluation] = None
|
|
83
|
+
chain_position: int = 0
|
|
84
|
+
prev_record_hash: Optional[str] = None
|
|
85
|
+
signature: str = ""
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: truss-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the Truss trust infrastructure API
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Requires-Dist: pynacl>=1.6
|
|
8
|
+
Provides-Extra: dev
|
|
9
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
10
|
+
Requires-Dist: pytest-mock>=3; extra == "dev"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
tests/test_client.py
|
|
4
|
+
tests/test_crypto.py
|
|
5
|
+
truss_sdk/__init__.py
|
|
6
|
+
truss_sdk/client.py
|
|
7
|
+
truss_sdk/crypto.py
|
|
8
|
+
truss_sdk/models.py
|
|
9
|
+
truss_sdk.egg-info/PKG-INFO
|
|
10
|
+
truss_sdk.egg-info/SOURCES.txt
|
|
11
|
+
truss_sdk.egg-info/dependency_links.txt
|
|
12
|
+
truss_sdk.egg-info/requires.txt
|
|
13
|
+
truss_sdk.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
truss_sdk
|