constellation-node-sdk 1.0.1__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.
Files changed (45) hide show
  1. constellation_node_sdk/__init__.py +123 -0
  2. constellation_node_sdk/gate/__init__.py +47 -0
  3. constellation_node_sdk/gate/client.py +108 -0
  4. constellation_node_sdk/gate/config.py +153 -0
  5. constellation_node_sdk/gate/errors.py +17 -0
  6. constellation_node_sdk/gate/policy.py +70 -0
  7. constellation_node_sdk/gate/registration.py +144 -0
  8. constellation_node_sdk/orchestrator/__init__.py +3 -0
  9. constellation_node_sdk/orchestrator/base.py +57 -0
  10. constellation_node_sdk/orchestrator/merge.py +47 -0
  11. constellation_node_sdk/orchestrator/packet_builder.py +49 -0
  12. constellation_node_sdk/orchestrator/retry.py +61 -0
  13. constellation_node_sdk/orchestrator/state.py +78 -0
  14. constellation_node_sdk/orchestrator/step_executor.py +87 -0
  15. constellation_node_sdk/py.typed +0 -0
  16. constellation_node_sdk/runtime/__init__.py +52 -0
  17. constellation_node_sdk/runtime/app.py +274 -0
  18. constellation_node_sdk/runtime/config.py +307 -0
  19. constellation_node_sdk/runtime/errors.py +112 -0
  20. constellation_node_sdk/runtime/execution.py +249 -0
  21. constellation_node_sdk/runtime/handlers.py +45 -0
  22. constellation_node_sdk/runtime/inbound_policy.py +86 -0
  23. constellation_node_sdk/runtime/lifecycle.py +29 -0
  24. constellation_node_sdk/runtime/observability.py +72 -0
  25. constellation_node_sdk/runtime/preflight.py +122 -0
  26. constellation_node_sdk/security/__init__.py +36 -0
  27. constellation_node_sdk/security/delegation.py +57 -0
  28. constellation_node_sdk/security/errors.py +25 -0
  29. constellation_node_sdk/security/signing.py +121 -0
  30. constellation_node_sdk/security/validation.py +230 -0
  31. constellation_node_sdk/security/verification.py +107 -0
  32. constellation_node_sdk/transport/__init__.py +88 -0
  33. constellation_node_sdk/transport/codec.py +21 -0
  34. constellation_node_sdk/transport/errors.py +41 -0
  35. constellation_node_sdk/transport/hashing.py +73 -0
  36. constellation_node_sdk/transport/hop_trace.py +378 -0
  37. constellation_node_sdk/transport/lineage.py +28 -0
  38. constellation_node_sdk/transport/models.py +447 -0
  39. constellation_node_sdk/transport/packet.py +331 -0
  40. constellation_node_sdk/transport/provenance.py +53 -0
  41. constellation_node_sdk/transport/tenant.py +76 -0
  42. constellation_node_sdk-1.0.1.dist-info/METADATA +141 -0
  43. constellation_node_sdk-1.0.1.dist-info/RECORD +45 -0
  44. constellation_node_sdk-1.0.1.dist-info/WHEEL +5 -0
  45. constellation_node_sdk-1.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ from constellation_node_sdk.transport.errors import (
4
+ TransportAuthenticationError,
5
+ TransportAuthorizationError,
6
+ TransportExpiredError,
7
+ TransportIntegrityError,
8
+ TransportNotYetValidError,
9
+ TransportValidationError,
10
+ )
11
+
12
+
13
+ class SecurityError(Exception):
14
+ """Base exception for the SDK security layer."""
15
+
16
+
17
+ __all__ = [
18
+ "SecurityError",
19
+ "TransportAuthenticationError",
20
+ "TransportAuthorizationError",
21
+ "TransportExpiredError",
22
+ "TransportIntegrityError",
23
+ "TransportNotYetValidError",
24
+ "TransportValidationError",
25
+ ]
@@ -0,0 +1,121 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import hashlib
5
+ import hmac
6
+
7
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
8
+ from cryptography.hazmat.primitives.serialization import load_pem_private_key
9
+
10
+ from constellation_node_sdk.transport.hashing import compute_payload_hash, compute_transport_hash
11
+ from constellation_node_sdk.transport.models import TransportSecurity
12
+ from constellation_node_sdk.transport.packet import TransportPacket
13
+
14
+ from .errors import TransportAuthenticationError
15
+
16
+
17
+ def _coerce_bytes(value: str | bytes) -> bytes:
18
+ if isinstance(value, bytes):
19
+ return value
20
+ normalized = value.strip()
21
+ if normalized.startswith("-----BEGIN"):
22
+ return normalized.encode("utf-8")
23
+ try:
24
+ return base64.b64decode(normalized, validate=True)
25
+ except Exception:
26
+ return normalized.encode("utf-8")
27
+
28
+
29
+ def _load_ed25519_private_key(raw: bytes) -> Ed25519PrivateKey:
30
+ if raw.startswith(b"-----BEGIN"):
31
+ loaded = load_pem_private_key(raw, password=None)
32
+ if not isinstance(loaded, Ed25519PrivateKey):
33
+ raise TransportAuthenticationError("PEM private key is not Ed25519")
34
+ return loaded
35
+ if len(raw) == 32:
36
+ return Ed25519PrivateKey.from_private_bytes(raw)
37
+ raise TransportAuthenticationError("unsupported ed25519 private key format")
38
+
39
+
40
+ def recompute_transport_core(packet: TransportPacket) -> TransportPacket:
41
+ """
42
+ Recompute payload_hash and transport_hash while preserving hop_trace.
43
+
44
+ hop_trace is intentionally excluded from transport_hash.
45
+ Any existing packet signature is cleared and must be re-applied by the caller.
46
+ """
47
+ payload_hash = compute_payload_hash(packet.payload)
48
+
49
+ provisional = packet.model_copy(
50
+ update={
51
+ "security": packet.security.model_copy(
52
+ update={
53
+ "payload_hash": payload_hash,
54
+ "transport_hash": "0" * 64,
55
+ "signature": None,
56
+ "signature_algorithm": None,
57
+ "signing_key_id": None,
58
+ }
59
+ )
60
+ }
61
+ )
62
+
63
+ transport_hash = compute_transport_hash(provisional)
64
+ finalized = provisional.model_copy(
65
+ update={
66
+ "security": provisional.security.model_copy(
67
+ update={
68
+ "transport_hash": transport_hash,
69
+ }
70
+ )
71
+ }
72
+ )
73
+
74
+ return TransportPacket.model_validate(finalized)
75
+
76
+
77
+ def sign_transport_packet(
78
+ packet: TransportPacket,
79
+ *,
80
+ key: bytes | str,
81
+ key_id: str,
82
+ algorithm: str,
83
+ ) -> TransportPacket:
84
+ """
85
+ Sign a transport packet over its stable transport_hash.
86
+
87
+ The packet's hop_trace is preserved, but is not included in the signature domain.
88
+ """
89
+ normalized_algorithm = algorithm.strip().lower()
90
+ raw_key = _coerce_bytes(key)
91
+ normalized = recompute_transport_core(packet)
92
+
93
+ if normalized_algorithm == "hmac-sha256":
94
+ signature = hmac.new(
95
+ raw_key,
96
+ normalized.security.transport_hash.encode("utf-8"),
97
+ hashlib.sha256,
98
+ ).hexdigest()
99
+ elif normalized_algorithm == "ed25519":
100
+ private_key = _load_ed25519_private_key(raw_key)
101
+ signature = private_key.sign(normalized.security.transport_hash.encode("utf-8")).hex()
102
+ else:
103
+ raise TransportAuthenticationError(
104
+ f"unsupported signature algorithm: {normalized_algorithm}"
105
+ )
106
+
107
+ signed = normalized.model_copy(
108
+ update={
109
+ "security": TransportSecurity(
110
+ payload_hash=normalized.security.payload_hash,
111
+ transport_hash=normalized.security.transport_hash,
112
+ signature=signature,
113
+ signature_algorithm=normalized_algorithm,
114
+ signing_key_id=key_id.strip(),
115
+ classification=normalized.security.classification,
116
+ encryption_status=normalized.security.encryption_status,
117
+ pii_fields=normalized.security.pii_fields,
118
+ )
119
+ }
120
+ )
121
+ return TransportPacket.model_validate(signed)
@@ -0,0 +1,230 @@
1
+ from __future__ import annotations
2
+
3
+ import ipaddress
4
+ import json
5
+ from collections.abc import Callable, Mapping
6
+ from datetime import UTC, datetime, timedelta
7
+ from urllib.parse import urlparse
8
+
9
+ from constellation_node_sdk.security.verification import verify_transport_packet_signature
10
+ from constellation_node_sdk.transport.errors import (
11
+ PacketSizeError,
12
+ SchemaVersionError,
13
+ TenantMutationError,
14
+ TransportAuthenticationError,
15
+ TransportAuthorizationError,
16
+ TransportExpiredError,
17
+ TransportIntegrityError,
18
+ TransportNotYetValidError,
19
+ TransportValidationError,
20
+ )
21
+ from constellation_node_sdk.transport.hop_trace import validate_hop_trace
22
+ from constellation_node_sdk.transport.packet import TransportPacket
23
+ from constellation_node_sdk.transport.tenant import assert_tenant_immutable
24
+
25
+
26
+ def transport_packet_size_bytes(packet: TransportPacket) -> int:
27
+ return len(
28
+ json.dumps(
29
+ packet.model_dump_json_dict(),
30
+ sort_keys=True,
31
+ separators=(",", ":"),
32
+ ensure_ascii=False,
33
+ ).encode("utf-8")
34
+ )
35
+
36
+
37
+ def validate_derived_transport_packet(parent: TransportPacket, child: TransportPacket) -> None:
38
+ try:
39
+ assert_tenant_immutable(parent.tenant, child.tenant)
40
+ except TenantMutationError:
41
+ raise
42
+ except Exception as exc:
43
+ raise TenantMutationError(str(exc)) from exc
44
+
45
+ if child.lineage.parent_id != parent.header.packet_id:
46
+ raise TransportValidationError("derived packet parent_id mismatch")
47
+ if child.lineage.root_id != parent.lineage.root_id:
48
+ raise TransportValidationError("derived packet root_id mismatch")
49
+ if child.lineage.generation != parent.lineage.generation + 1:
50
+ raise TransportValidationError("derived packet generation must increment by 1")
51
+
52
+
53
+ def validate_transport_packet(
54
+ packet: TransportPacket,
55
+ *,
56
+ key_resolver: Callable[[str | None], str | bytes | None]
57
+ | Mapping[str, str | bytes]
58
+ | str
59
+ | bytes
60
+ | None = None,
61
+ require_signature: bool = False,
62
+ max_packet_bytes: int = 262_144,
63
+ max_hop_depth: int = 64,
64
+ max_delegation_depth: int = 8,
65
+ max_attachments: int = 32,
66
+ max_attachment_size_bytes: int = 10_485_760,
67
+ allowed_attachment_schemes: tuple[str, ...] = (),
68
+ allow_private_attachment_hosts: bool = False,
69
+ allowed_clock_skew_seconds: int = 30,
70
+ local_node: str | None = None,
71
+ allowed_actions: tuple[str, ...] | None = None,
72
+ allowed_packet_types: tuple[str, ...] | None = None,
73
+ required_idempotency_actions: tuple[str, ...] | None = None,
74
+ replay_enabled: bool = True,
75
+ now: datetime | None = None,
76
+ dev_mode: bool = False,
77
+ verify_hop_signatures: bool = False,
78
+ hop_key_resolver: Callable[[str | None], str | bytes | None]
79
+ | Mapping[str, str | bytes]
80
+ | str
81
+ | bytes
82
+ | None = None,
83
+ require_monotonic_hop_timestamps: bool = True,
84
+ ) -> None:
85
+ current_time = (now or datetime.now(UTC)).astimezone(UTC)
86
+
87
+ if packet.header.schema_version != "1.0":
88
+ raise SchemaVersionError(f"unsupported schema_version: {packet.header.schema_version}")
89
+
90
+ if allowed_packet_types is not None and packet.header.packet_type not in set(
91
+ allowed_packet_types
92
+ ):
93
+ raise TransportAuthorizationError(f"packet_type not allowed: {packet.header.packet_type}")
94
+
95
+ if allowed_actions is not None and packet.header.action not in set(allowed_actions):
96
+ raise TransportAuthorizationError(f"action not allowed: {packet.header.action}")
97
+
98
+ if local_node is not None and packet.address.destination_node != local_node.strip().lower():
99
+ raise TransportAuthorizationError("packet destination does not match this node")
100
+
101
+ if packet.header.created_at > current_time + timedelta(seconds=allowed_clock_skew_seconds):
102
+ raise TransportValidationError("packet created_at exceeds allowed clock skew")
103
+
104
+ if transport_packet_size_bytes(packet) > max_packet_bytes:
105
+ raise PacketSizeError("packet exceeds maximum allowed size")
106
+
107
+ if len(packet.hop_trace) > max_hop_depth:
108
+ raise TransportValidationError("hop trace exceeds maximum depth")
109
+
110
+ if len(packet.delegation_chain) > max_delegation_depth:
111
+ raise TransportValidationError("delegation chain exceeds maximum depth")
112
+
113
+ if len(packet.attachments) > max_attachments:
114
+ raise TransportValidationError("attachment count exceeds maximum")
115
+
116
+ for attachment in packet.attachments:
117
+ if attachment.size_bytes > max_attachment_size_bytes:
118
+ raise TransportValidationError("attachment exceeds maximum allowed size")
119
+ _validate_attachment_uri(
120
+ attachment.uri,
121
+ allowed_schemes=allowed_attachment_schemes,
122
+ allow_private_hosts=allow_private_attachment_hosts,
123
+ )
124
+
125
+ if not packet.security.payload_hash or not packet.security.transport_hash:
126
+ raise TransportIntegrityError("packet hashes are missing")
127
+
128
+ if packet.header.not_before is not None and current_time < packet.header.not_before:
129
+ raise TransportNotYetValidError("packet not valid yet")
130
+
131
+ if packet.header.expires_at is not None and current_time > packet.header.expires_at:
132
+ raise TransportExpiredError("packet TTL exceeded")
133
+
134
+ if packet.header.packet_type == "replay_request":
135
+ if not packet.header.replay_mode:
136
+ raise TransportValidationError("replay_request packets must set replay_mode=true")
137
+ if not replay_enabled:
138
+ raise TransportAuthorizationError("replay is disabled on this service")
139
+
140
+ if packet.header.replay_mode and packet.header.packet_type != "replay_request":
141
+ raise TransportValidationError("replay_mode is only permitted for replay_request packets")
142
+
143
+ if required_idempotency_actions and packet.header.action in set(required_idempotency_actions):
144
+ if not packet.header.idempotency_key:
145
+ raise TransportValidationError("idempotency_key required for this action")
146
+
147
+ if "GDPR" in packet.governance.compliance_tags and not packet.governance.data_subject_id:
148
+ raise TransportValidationError("GDPR packets require data_subject_id")
149
+
150
+ if (
151
+ packet.security.classification == "restricted"
152
+ and packet.governance.audit_required is not True
153
+ ):
154
+ raise TransportValidationError("restricted packets must set audit_required=true")
155
+
156
+ _validate_delegation_chain(packet, now=current_time)
157
+
158
+ if packet.security.signature is None:
159
+ if (require_signature or packet.security.classification == "restricted") and not dev_mode:
160
+ raise TransportAuthenticationError("signature required but not present")
161
+ else:
162
+ if not (dev_mode and key_resolver is None):
163
+ if not verify_transport_packet_signature(packet, key_resolver=key_resolver):
164
+ raise TransportAuthenticationError("invalid transport signature")
165
+
166
+ validate_hop_trace(
167
+ packet,
168
+ verify_hop_signatures=verify_hop_signatures,
169
+ hop_key_resolver=hop_key_resolver,
170
+ require_monotonic_timestamps=require_monotonic_hop_timestamps,
171
+ )
172
+
173
+
174
+ def _validate_delegation_chain(packet: TransportPacket, *, now: datetime) -> None:
175
+ previous_scope: set[str] | None = None
176
+
177
+ for index, link in enumerate(packet.delegation_chain):
178
+ current_scope = set(link.scope)
179
+
180
+ if previous_scope is not None and not current_scope.issubset(previous_scope):
181
+ raise TransportAuthorizationError("delegation scope escalation detected")
182
+
183
+ if link.expires_at is not None and now > link.expires_at.astimezone(UTC):
184
+ raise TransportAuthorizationError("delegation link expired")
185
+
186
+ if index == len(packet.delegation_chain) - 1 and packet.header.packet_type == "delegation":
187
+ if packet.header.action not in current_scope:
188
+ raise TransportAuthorizationError(
189
+ "delegated packet action not permitted by last delegation scope"
190
+ )
191
+ if packet.address.destination_node != link.delegatee:
192
+ raise TransportAuthorizationError(
193
+ "delegated packet destination does not match last delegation target"
194
+ )
195
+
196
+ previous_scope = current_scope
197
+
198
+
199
+ def _validate_attachment_uri(
200
+ uri: str, *, allowed_schemes: tuple[str, ...], allow_private_hosts: bool
201
+ ) -> None:
202
+ parsed = urlparse(uri)
203
+ scheme = parsed.scheme.lower()
204
+
205
+ if not scheme:
206
+ raise TransportValidationError("attachment uri must include a scheme")
207
+ if allowed_schemes and scheme not in set(allowed_schemes):
208
+ raise TransportValidationError(f"attachment scheme not allowed: {scheme}")
209
+ if scheme in {"file", "ftp"}:
210
+ raise TransportValidationError(f"attachment scheme not allowed: {scheme}")
211
+
212
+ hostname = parsed.hostname
213
+ if hostname is None:
214
+ if scheme in {"https", "http"}:
215
+ raise TransportValidationError("http(s) attachment uri must include a host")
216
+ return
217
+
218
+ normalized_host = hostname.lower()
219
+ if normalized_host in {"localhost", "metadata.google.internal"}:
220
+ raise TransportValidationError("attachment host not allowed")
221
+
222
+ try:
223
+ host_ip = ipaddress.ip_address(normalized_host)
224
+ except ValueError:
225
+ return
226
+
227
+ if not allow_private_hosts and (
228
+ host_ip.is_private or host_ip.is_loopback or host_ip.is_link_local
229
+ ):
230
+ raise TransportValidationError("private attachment hosts are not allowed")
@@ -0,0 +1,107 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import hashlib
5
+ import hmac
6
+ from collections.abc import Callable, Mapping
7
+
8
+ from cryptography.exceptions import InvalidSignature
9
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
10
+ from cryptography.hazmat.primitives.serialization import load_pem_public_key
11
+
12
+ from constellation_node_sdk.transport.errors import TransportAuthenticationError
13
+ from constellation_node_sdk.transport.packet import TransportPacket
14
+
15
+
16
+ def _coerce_bytes(value: str | bytes) -> bytes:
17
+ if isinstance(value, bytes):
18
+ return value
19
+ normalized = value.strip()
20
+ if normalized.startswith("-----BEGIN"):
21
+ return normalized.encode("utf-8")
22
+ try:
23
+ return base64.b64decode(normalized, validate=True)
24
+ except Exception:
25
+ return normalized.encode("utf-8")
26
+
27
+
28
+ def _resolve_key(
29
+ key_resolver: Callable[[str | None], str | bytes | None]
30
+ | Mapping[str, str | bytes]
31
+ | str
32
+ | bytes
33
+ | None,
34
+ key_id: str | None,
35
+ ) -> bytes | None:
36
+ if key_resolver is None:
37
+ return None
38
+ if isinstance(key_resolver, (bytes, str)):
39
+ return _coerce_bytes(key_resolver)
40
+ if callable(key_resolver):
41
+ value = key_resolver(key_id)
42
+ else:
43
+ if key_id is None:
44
+ return None
45
+ value = key_resolver.get(key_id)
46
+ if value is None:
47
+ return None
48
+ return _coerce_bytes(value)
49
+
50
+
51
+ def _load_ed25519_public_key(raw: bytes) -> Ed25519PublicKey:
52
+ if raw.startswith(b"-----BEGIN"):
53
+ loaded = load_pem_public_key(raw)
54
+ if not isinstance(loaded, Ed25519PublicKey):
55
+ raise TransportAuthenticationError("PEM public key is not Ed25519")
56
+ return loaded
57
+ if len(raw) == 32:
58
+ return Ed25519PublicKey.from_public_bytes(raw)
59
+ raise TransportAuthenticationError("unsupported ed25519 public key format")
60
+
61
+
62
+ def verify_transport_packet_signature(
63
+ packet: TransportPacket,
64
+ *,
65
+ key_resolver: Callable[[str | None], str | bytes | None]
66
+ | Mapping[str, str | bytes]
67
+ | str
68
+ | bytes
69
+ | None,
70
+ ) -> bool:
71
+ """
72
+ Verify the packet signature against packet.security.transport_hash.
73
+
74
+ Returns False for unsigned packets.
75
+ Raises TransportAuthenticationError for key resolution or verification failures.
76
+ """
77
+ if packet.security.signature is None or packet.security.signature_algorithm is None:
78
+ return False
79
+
80
+ key = _resolve_key(key_resolver, packet.security.signing_key_id)
81
+ if key is None:
82
+ raise TransportAuthenticationError(
83
+ "no verifying key available for transport signature verification"
84
+ )
85
+
86
+ if packet.security.signature_algorithm == "hmac-sha256":
87
+ expected = hmac.new(
88
+ key,
89
+ packet.security.transport_hash.encode("utf-8"),
90
+ hashlib.sha256,
91
+ ).hexdigest()
92
+ return hmac.compare_digest(packet.security.signature, expected)
93
+
94
+ if packet.security.signature_algorithm == "ed25519":
95
+ public_key = _load_ed25519_public_key(key)
96
+ try:
97
+ public_key.verify(
98
+ bytes.fromhex(packet.security.signature),
99
+ packet.security.transport_hash.encode("utf-8"),
100
+ )
101
+ return True
102
+ except (ValueError, InvalidSignature) as exc:
103
+ raise TransportAuthenticationError("invalid ed25519 transport signature") from exc
104
+
105
+ raise TransportAuthenticationError(
106
+ f"unsupported signature algorithm: {packet.security.signature_algorithm}"
107
+ )
@@ -0,0 +1,88 @@
1
+ from __future__ import annotations
2
+
3
+ from constellation_node_sdk.transport.errors import (
4
+ PacketSizeError,
5
+ SchemaVersionError,
6
+ TenantMutationError,
7
+ TransportAuthenticationError,
8
+ TransportAuthorizationError,
9
+ TransportError,
10
+ TransportExpiredError,
11
+ TransportIntegrityError,
12
+ TransportNotYetValidError,
13
+ TransportValidationError,
14
+ )
15
+ from constellation_node_sdk.transport.hashing import (
16
+ canonical_json,
17
+ compute_payload_hash,
18
+ compute_transport_hash,
19
+ )
20
+ from constellation_node_sdk.transport.hop_trace import (
21
+ compute_hop_hash,
22
+ last_hop_hash,
23
+ make_dispatch_hop,
24
+ make_execution_hop,
25
+ make_ingress_hop,
26
+ make_response_hop,
27
+ sign_hop,
28
+ validate_hop_trace,
29
+ verify_hop_signature,
30
+ )
31
+ from constellation_node_sdk.transport.models import (
32
+ DelegationLink,
33
+ TransportAddress,
34
+ TransportAttachment,
35
+ TransportGovernance,
36
+ TransportHeader,
37
+ TransportHop,
38
+ TransportLineage,
39
+ TransportSecurity,
40
+ utc_now,
41
+ )
42
+ from constellation_node_sdk.transport.packet import TransportPacket, create_transport_packet
43
+ from constellation_node_sdk.transport.provenance import RoutingProvenance
44
+ from constellation_node_sdk.transport.tenant import (
45
+ TenantContext,
46
+ assert_tenant_immutable,
47
+ ensure_tenant_context,
48
+ )
49
+
50
+ __all__ = [
51
+ "DelegationLink",
52
+ "PacketSizeError",
53
+ "RoutingProvenance",
54
+ "SchemaVersionError",
55
+ "TenantContext",
56
+ "TenantMutationError",
57
+ "TransportAddress",
58
+ "TransportAttachment",
59
+ "TransportAuthenticationError",
60
+ "TransportAuthorizationError",
61
+ "TransportError",
62
+ "TransportExpiredError",
63
+ "TransportGovernance",
64
+ "TransportHeader",
65
+ "TransportHop",
66
+ "TransportIntegrityError",
67
+ "TransportLineage",
68
+ "TransportNotYetValidError",
69
+ "TransportPacket",
70
+ "TransportSecurity",
71
+ "TransportValidationError",
72
+ "assert_tenant_immutable",
73
+ "canonical_json",
74
+ "compute_hop_hash",
75
+ "compute_payload_hash",
76
+ "compute_transport_hash",
77
+ "create_transport_packet",
78
+ "ensure_tenant_context",
79
+ "last_hop_hash",
80
+ "make_dispatch_hop",
81
+ "make_execution_hop",
82
+ "make_ingress_hop",
83
+ "make_response_hop",
84
+ "sign_hop",
85
+ "utc_now",
86
+ "validate_hop_trace",
87
+ "verify_hop_signature",
88
+ ]
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from constellation_node_sdk.transport.packet import TransportPacket
6
+
7
+
8
+ def encode_transport_packet(packet: TransportPacket) -> dict[str, Any]:
9
+ """
10
+ Encode a TransportPacket into a JSON-safe dict.
11
+ """
12
+ return packet.model_dump_json_dict()
13
+
14
+
15
+ def decode_transport_packet(payload: dict[str, Any]) -> TransportPacket:
16
+ """
17
+ Decode a JSON object into a canonical TransportPacket.
18
+ """
19
+ if not isinstance(payload, dict):
20
+ raise ValueError("transport payload must be a JSON object")
21
+ return TransportPacket.model_validate(payload)
@@ -0,0 +1,41 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ class TransportError(Exception):
5
+ """Base exception for transport contract failures."""
6
+
7
+
8
+ class TransportValidationError(TransportError):
9
+ """Raised when a transport packet violates structural or semantic rules."""
10
+
11
+
12
+ class TransportIntegrityError(TransportValidationError):
13
+ """Raised when payload or transport hashes do not match."""
14
+
15
+
16
+ class TransportAuthenticationError(TransportValidationError):
17
+ """Raised when transport authentication or signature state is invalid."""
18
+
19
+
20
+ class TransportAuthorizationError(TransportValidationError):
21
+ """Raised when transport routing or delegation violates policy."""
22
+
23
+
24
+ class TransportExpiredError(TransportValidationError):
25
+ """Raised when a packet has exceeded its expiry window."""
26
+
27
+
28
+ class TransportNotYetValidError(TransportValidationError):
29
+ """Raised when a packet is not valid yet due to not_before constraints."""
30
+
31
+
32
+ class TenantMutationError(TransportValidationError):
33
+ """Raised when a derived packet mutates immutable tenant context."""
34
+
35
+
36
+ class SchemaVersionError(TransportValidationError):
37
+ """Raised when an unsupported schema version is encountered."""
38
+
39
+
40
+ class PacketSizeError(TransportValidationError):
41
+ """Raised when a packet exceeds maximum allowed size."""