truss-sdk 0.1.0__py3-none-any.whl
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/__init__.py +30 -0
- truss_sdk/client.py +178 -0
- truss_sdk/crypto.py +47 -0
- truss_sdk/models.py +85 -0
- truss_sdk-0.1.0.dist-info/METADATA +10 -0
- truss_sdk-0.1.0.dist-info/RECORD +8 -0
- truss_sdk-0.1.0.dist-info/WHEEL +5 -0
- truss_sdk-0.1.0.dist-info/top_level.txt +1 -0
truss_sdk/__init__.py
ADDED
|
@@ -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
|
+
]
|
truss_sdk/client.py
ADDED
|
@@ -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
|
+
)
|
truss_sdk/crypto.py
ADDED
|
@@ -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
|
truss_sdk/models.py
ADDED
|
@@ -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,8 @@
|
|
|
1
|
+
truss_sdk/__init__.py,sha256=OzGHyEwhqfoDcQdop3mx_aZuqRu3KYDvEps09HxjF0I,613
|
|
2
|
+
truss_sdk/client.py,sha256=S1YLjs5KLlDny2cuauORCvxmxipINiN428aiVJQ2C4I,5528
|
|
3
|
+
truss_sdk/crypto.py,sha256=dFdEyMCGbofAUwEsGTOiRMPZpt2BPuJM3BTZ25cIOQo,1517
|
|
4
|
+
truss_sdk/models.py,sha256=aaBu_DtJKcWI6t0S_geL9ZvVhYNsVu4vJo5nErV2b28,1813
|
|
5
|
+
truss_sdk-0.1.0.dist-info/METADATA,sha256=dJy2viOivfOVHWYyGyE12tg7HadHMWmQylJ-9vL7uaQ,289
|
|
6
|
+
truss_sdk-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
7
|
+
truss_sdk-0.1.0.dist-info/top_level.txt,sha256=aM4ULz8m7W-gNF8EaYRGi9hojX-hd3tLubapTjs1yVA,10
|
|
8
|
+
truss_sdk-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
truss_sdk
|