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 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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ truss_sdk