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.
- constellation_node_sdk/__init__.py +123 -0
- constellation_node_sdk/gate/__init__.py +47 -0
- constellation_node_sdk/gate/client.py +108 -0
- constellation_node_sdk/gate/config.py +153 -0
- constellation_node_sdk/gate/errors.py +17 -0
- constellation_node_sdk/gate/policy.py +70 -0
- constellation_node_sdk/gate/registration.py +144 -0
- constellation_node_sdk/orchestrator/__init__.py +3 -0
- constellation_node_sdk/orchestrator/base.py +57 -0
- constellation_node_sdk/orchestrator/merge.py +47 -0
- constellation_node_sdk/orchestrator/packet_builder.py +49 -0
- constellation_node_sdk/orchestrator/retry.py +61 -0
- constellation_node_sdk/orchestrator/state.py +78 -0
- constellation_node_sdk/orchestrator/step_executor.py +87 -0
- constellation_node_sdk/py.typed +0 -0
- constellation_node_sdk/runtime/__init__.py +52 -0
- constellation_node_sdk/runtime/app.py +274 -0
- constellation_node_sdk/runtime/config.py +307 -0
- constellation_node_sdk/runtime/errors.py +112 -0
- constellation_node_sdk/runtime/execution.py +249 -0
- constellation_node_sdk/runtime/handlers.py +45 -0
- constellation_node_sdk/runtime/inbound_policy.py +86 -0
- constellation_node_sdk/runtime/lifecycle.py +29 -0
- constellation_node_sdk/runtime/observability.py +72 -0
- constellation_node_sdk/runtime/preflight.py +122 -0
- constellation_node_sdk/security/__init__.py +36 -0
- constellation_node_sdk/security/delegation.py +57 -0
- constellation_node_sdk/security/errors.py +25 -0
- constellation_node_sdk/security/signing.py +121 -0
- constellation_node_sdk/security/validation.py +230 -0
- constellation_node_sdk/security/verification.py +107 -0
- constellation_node_sdk/transport/__init__.py +88 -0
- constellation_node_sdk/transport/codec.py +21 -0
- constellation_node_sdk/transport/errors.py +41 -0
- constellation_node_sdk/transport/hashing.py +73 -0
- constellation_node_sdk/transport/hop_trace.py +378 -0
- constellation_node_sdk/transport/lineage.py +28 -0
- constellation_node_sdk/transport/models.py +447 -0
- constellation_node_sdk/transport/packet.py +331 -0
- constellation_node_sdk/transport/provenance.py +53 -0
- constellation_node_sdk/transport/tenant.py +76 -0
- constellation_node_sdk-1.0.1.dist-info/METADATA +141 -0
- constellation_node_sdk-1.0.1.dist-info/RECORD +45 -0
- constellation_node_sdk-1.0.1.dist-info/WHEEL +5 -0
- constellation_node_sdk-1.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Any
|
|
5
|
+
from uuid import uuid4
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, field_validator, model_validator
|
|
8
|
+
|
|
9
|
+
from .errors import TransportIntegrityError
|
|
10
|
+
from .hashing import compute_payload_hash, compute_transport_hash
|
|
11
|
+
from .models import (
|
|
12
|
+
DelegationLink,
|
|
13
|
+
TransportAddress,
|
|
14
|
+
TransportAttachment,
|
|
15
|
+
TransportGovernance,
|
|
16
|
+
TransportHeader,
|
|
17
|
+
TransportHop,
|
|
18
|
+
TransportLineage,
|
|
19
|
+
TransportSecurity,
|
|
20
|
+
utc_now,
|
|
21
|
+
)
|
|
22
|
+
from .provenance import RoutingProvenance
|
|
23
|
+
from .tenant import TenantContext, assert_tenant_immutable, ensure_tenant_context
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TransportPacket(BaseModel):
|
|
27
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
28
|
+
|
|
29
|
+
header: TransportHeader
|
|
30
|
+
address: TransportAddress
|
|
31
|
+
tenant: TenantContext
|
|
32
|
+
payload: dict[str, Any]
|
|
33
|
+
security: TransportSecurity
|
|
34
|
+
governance: TransportGovernance
|
|
35
|
+
provenance: RoutingProvenance
|
|
36
|
+
delegation_chain: tuple[DelegationLink, ...] = ()
|
|
37
|
+
hop_trace: tuple[TransportHop, ...] = ()
|
|
38
|
+
lineage: TransportLineage
|
|
39
|
+
attachments: tuple[TransportAttachment, ...] = ()
|
|
40
|
+
|
|
41
|
+
@field_validator("payload")
|
|
42
|
+
@classmethod
|
|
43
|
+
def validate_payload(cls, value: dict[str, Any]) -> dict[str, Any]:
|
|
44
|
+
if not isinstance(value, dict):
|
|
45
|
+
raise TypeError("payload must be a dict")
|
|
46
|
+
return value
|
|
47
|
+
|
|
48
|
+
@model_validator(mode="after")
|
|
49
|
+
def validate_integrity(self) -> TransportPacket:
|
|
50
|
+
expected_payload_hash = compute_payload_hash(self.payload)
|
|
51
|
+
if self.security.payload_hash != expected_payload_hash:
|
|
52
|
+
raise TransportIntegrityError("payload_hash does not match payload")
|
|
53
|
+
|
|
54
|
+
expected_transport_hash = compute_transport_hash(self)
|
|
55
|
+
if self.security.transport_hash != expected_transport_hash:
|
|
56
|
+
raise TransportIntegrityError("transport_hash does not match packet")
|
|
57
|
+
|
|
58
|
+
return self
|
|
59
|
+
|
|
60
|
+
def model_dump_json_dict(self) -> dict[str, Any]:
|
|
61
|
+
"""Return a JSON-safe dict representation."""
|
|
62
|
+
return self.model_dump(mode="json", exclude_none=True)
|
|
63
|
+
|
|
64
|
+
def with_hop(self, hop: TransportHop) -> TransportPacket:
|
|
65
|
+
"""
|
|
66
|
+
Append a hop without mutating the signed transport core.
|
|
67
|
+
|
|
68
|
+
hop_trace is intentionally excluded from transport_hash, so a hop append
|
|
69
|
+
must preserve payload_hash, transport_hash, and any existing transport
|
|
70
|
+
signature.
|
|
71
|
+
"""
|
|
72
|
+
if hop.packet_id != self.header.packet_id:
|
|
73
|
+
raise ValueError("hop.packet_id must match packet.header.packet_id")
|
|
74
|
+
|
|
75
|
+
appended = self.model_copy(update={"hop_trace": self.hop_trace + (hop,)})
|
|
76
|
+
return TransportPacket(
|
|
77
|
+
header=appended.header,
|
|
78
|
+
address=appended.address,
|
|
79
|
+
tenant=appended.tenant,
|
|
80
|
+
payload=appended.payload,
|
|
81
|
+
security=appended.security,
|
|
82
|
+
governance=appended.governance,
|
|
83
|
+
provenance=appended.provenance,
|
|
84
|
+
delegation_chain=tuple(appended.delegation_chain),
|
|
85
|
+
hop_trace=tuple(appended.hop_trace),
|
|
86
|
+
lineage=appended.lineage,
|
|
87
|
+
attachments=tuple(appended.attachments),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def derive(
|
|
91
|
+
self,
|
|
92
|
+
*,
|
|
93
|
+
packet_type: str | None = None,
|
|
94
|
+
action: str | None = None,
|
|
95
|
+
source_node: str | None = None,
|
|
96
|
+
destination_node: str | None = None,
|
|
97
|
+
reply_to: str | None = None,
|
|
98
|
+
payload: dict[str, Any] | None = None,
|
|
99
|
+
provenance: RoutingProvenance | None = None,
|
|
100
|
+
hop: TransportHop | None = None,
|
|
101
|
+
delegation_link: DelegationLink | None = None,
|
|
102
|
+
governance: TransportGovernance | None = None,
|
|
103
|
+
priority: int | None = None,
|
|
104
|
+
expires_at: datetime | None = None,
|
|
105
|
+
timeout_ms: int | None = None,
|
|
106
|
+
not_before: datetime | None = None,
|
|
107
|
+
replay_mode: bool | None = None,
|
|
108
|
+
) -> TransportPacket:
|
|
109
|
+
"""Create an immutable child packet with correct lineage and refreshed hashes."""
|
|
110
|
+
new_header = TransportHeader(
|
|
111
|
+
packet_id=uuid4(),
|
|
112
|
+
packet_type=packet_type or self.header.packet_type,
|
|
113
|
+
action=action or self.header.action,
|
|
114
|
+
priority=self.header.priority if priority is None else priority,
|
|
115
|
+
created_at=utc_now(),
|
|
116
|
+
expires_at=self.header.expires_at if expires_at is None else expires_at,
|
|
117
|
+
timeout_ms=self.header.timeout_ms if timeout_ms is None else timeout_ms,
|
|
118
|
+
schema_version=self.header.schema_version,
|
|
119
|
+
idempotency_key=self.header.idempotency_key,
|
|
120
|
+
trace_id=self.header.trace_id,
|
|
121
|
+
correlation_id=self.header.correlation_id,
|
|
122
|
+
causation_id=self.header.packet_id,
|
|
123
|
+
retry_count=0,
|
|
124
|
+
replay_mode=self.header.replay_mode if replay_mode is None else replay_mode,
|
|
125
|
+
not_before=self.header.not_before if not_before is None else not_before,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
new_address = TransportAddress(
|
|
129
|
+
source_node=(source_node or self.address.source_node).strip().lower(),
|
|
130
|
+
destination_node=(destination_node or self.address.destination_node).strip().lower(),
|
|
131
|
+
reply_to=(reply_to or self.address.reply_to).strip().lower(),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
new_payload = dict(self.payload if payload is None else payload)
|
|
135
|
+
new_governance = governance or self.governance
|
|
136
|
+
new_provenance = provenance or self.provenance
|
|
137
|
+
new_delegation_chain = self.delegation_chain + (
|
|
138
|
+
(delegation_link,) if delegation_link is not None else ()
|
|
139
|
+
)
|
|
140
|
+
new_hop_trace = self.hop_trace + ((hop,) if hop is not None else ())
|
|
141
|
+
new_lineage = TransportLineage(
|
|
142
|
+
parent_id=self.header.packet_id,
|
|
143
|
+
root_id=self.lineage.root_id,
|
|
144
|
+
generation=self.lineage.generation + 1,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
provisional = TransportPacket.model_construct(
|
|
148
|
+
header=new_header,
|
|
149
|
+
address=new_address,
|
|
150
|
+
tenant=self.tenant,
|
|
151
|
+
payload=new_payload,
|
|
152
|
+
security=TransportSecurity(
|
|
153
|
+
payload_hash="0" * 64,
|
|
154
|
+
transport_hash="0" * 64,
|
|
155
|
+
signature=None,
|
|
156
|
+
signature_algorithm=None,
|
|
157
|
+
signing_key_id=None,
|
|
158
|
+
classification=self.security.classification,
|
|
159
|
+
encryption_status=self.security.encryption_status,
|
|
160
|
+
pii_fields=self.security.pii_fields,
|
|
161
|
+
),
|
|
162
|
+
governance=new_governance,
|
|
163
|
+
provenance=new_provenance,
|
|
164
|
+
delegation_chain=new_delegation_chain,
|
|
165
|
+
hop_trace=new_hop_trace,
|
|
166
|
+
lineage=new_lineage,
|
|
167
|
+
attachments=self.attachments,
|
|
168
|
+
)
|
|
169
|
+
child = _finalize_transport_packet(provisional, preserve_signature=False)
|
|
170
|
+
assert_tenant_immutable(self.tenant, child.tenant)
|
|
171
|
+
return child
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _default_provenance(*, source_node: str, action: str) -> RoutingProvenance:
|
|
175
|
+
source = source_node.strip().lower()
|
|
176
|
+
origin_kind = "client" if source == "client" else "gate" if source == "gate" else "node"
|
|
177
|
+
return RoutingProvenance(
|
|
178
|
+
origin_kind=origin_kind,
|
|
179
|
+
requested_action=action.strip().lower(),
|
|
180
|
+
resolved_by_gate=False,
|
|
181
|
+
original_source_node=None if origin_kind == "client" else source,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _finalize_transport_packet(
|
|
186
|
+
packet: TransportPacket, *, preserve_signature: bool
|
|
187
|
+
) -> TransportPacket:
|
|
188
|
+
payload_hash = compute_payload_hash(packet.payload)
|
|
189
|
+
provisional = packet.model_copy(
|
|
190
|
+
update={
|
|
191
|
+
"security": packet.security.model_copy(
|
|
192
|
+
update={
|
|
193
|
+
"payload_hash": payload_hash,
|
|
194
|
+
"transport_hash": "0" * 64,
|
|
195
|
+
"signature": packet.security.signature if preserve_signature else None,
|
|
196
|
+
"signature_algorithm": packet.security.signature_algorithm
|
|
197
|
+
if preserve_signature
|
|
198
|
+
else None,
|
|
199
|
+
"signing_key_id": packet.security.signing_key_id
|
|
200
|
+
if preserve_signature
|
|
201
|
+
else None,
|
|
202
|
+
}
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
)
|
|
206
|
+
transport_hash = compute_transport_hash(provisional)
|
|
207
|
+
signature_still_valid = (
|
|
208
|
+
preserve_signature
|
|
209
|
+
and packet.security.payload_hash == payload_hash
|
|
210
|
+
and packet.security.transport_hash == transport_hash
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
finalized = provisional.model_copy(
|
|
214
|
+
update={
|
|
215
|
+
"security": provisional.security.model_copy(
|
|
216
|
+
update={
|
|
217
|
+
"transport_hash": transport_hash,
|
|
218
|
+
"signature": packet.security.signature if signature_still_valid else None,
|
|
219
|
+
"signature_algorithm": packet.security.signature_algorithm
|
|
220
|
+
if signature_still_valid
|
|
221
|
+
else None,
|
|
222
|
+
"signing_key_id": packet.security.signing_key_id
|
|
223
|
+
if signature_still_valid
|
|
224
|
+
else None,
|
|
225
|
+
}
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
)
|
|
229
|
+
return TransportPacket(
|
|
230
|
+
header=finalized.header,
|
|
231
|
+
address=finalized.address,
|
|
232
|
+
tenant=finalized.tenant,
|
|
233
|
+
payload=finalized.payload,
|
|
234
|
+
security=finalized.security,
|
|
235
|
+
governance=finalized.governance,
|
|
236
|
+
provenance=finalized.provenance,
|
|
237
|
+
delegation_chain=tuple(finalized.delegation_chain),
|
|
238
|
+
hop_trace=tuple(finalized.hop_trace),
|
|
239
|
+
lineage=finalized.lineage,
|
|
240
|
+
attachments=tuple(finalized.attachments),
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def create_transport_packet(
|
|
245
|
+
*,
|
|
246
|
+
action: str,
|
|
247
|
+
payload: dict[str, Any],
|
|
248
|
+
tenant: str | dict[str, Any] | TenantContext,
|
|
249
|
+
destination_node: str = "gate",
|
|
250
|
+
source_node: str = "client",
|
|
251
|
+
reply_to: str = "client",
|
|
252
|
+
priority: int = 2,
|
|
253
|
+
timeout_ms: int = 30_000,
|
|
254
|
+
classification: str = "internal",
|
|
255
|
+
compliance_tags: tuple[str, ...] = (),
|
|
256
|
+
retention_days: int = 90,
|
|
257
|
+
expires_at: datetime | None = None,
|
|
258
|
+
idempotency_key: str | None = None,
|
|
259
|
+
trace_id: str | None = None,
|
|
260
|
+
correlation_id: str | None = None,
|
|
261
|
+
provenance: RoutingProvenance | None = None,
|
|
262
|
+
) -> TransportPacket:
|
|
263
|
+
"""Build a canonical root transport packet with initialized lineage and hashes."""
|
|
264
|
+
packet_id = uuid4()
|
|
265
|
+
normalized_tenant = ensure_tenant_context(tenant)
|
|
266
|
+
normalized_action = action.strip().lower()
|
|
267
|
+
normalized_source = source_node.strip().lower()
|
|
268
|
+
normalized_destination = destination_node.strip().lower()
|
|
269
|
+
normalized_reply_to = reply_to.strip().lower()
|
|
270
|
+
|
|
271
|
+
header = TransportHeader(
|
|
272
|
+
packet_id=packet_id,
|
|
273
|
+
packet_type="request",
|
|
274
|
+
action=normalized_action,
|
|
275
|
+
priority=priority,
|
|
276
|
+
created_at=utc_now(),
|
|
277
|
+
expires_at=expires_at,
|
|
278
|
+
timeout_ms=timeout_ms,
|
|
279
|
+
schema_version="1.0",
|
|
280
|
+
idempotency_key=idempotency_key,
|
|
281
|
+
trace_id=trace_id or str(packet_id),
|
|
282
|
+
correlation_id=correlation_id or str(packet_id),
|
|
283
|
+
causation_id=None,
|
|
284
|
+
retry_count=0,
|
|
285
|
+
replay_mode=False,
|
|
286
|
+
not_before=None,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
address = TransportAddress(
|
|
290
|
+
source_node=normalized_source,
|
|
291
|
+
destination_node=normalized_destination,
|
|
292
|
+
reply_to=normalized_reply_to,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
governance = TransportGovernance(
|
|
296
|
+
intent=normalized_action,
|
|
297
|
+
compliance_tags=compliance_tags,
|
|
298
|
+
retention_days=retention_days,
|
|
299
|
+
redaction_applied=False,
|
|
300
|
+
audit_required=False,
|
|
301
|
+
data_subject_id=None,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
packet = TransportPacket.model_construct(
|
|
305
|
+
header=header,
|
|
306
|
+
address=address,
|
|
307
|
+
tenant=normalized_tenant,
|
|
308
|
+
payload=dict(payload),
|
|
309
|
+
security=TransportSecurity(
|
|
310
|
+
payload_hash="0" * 64,
|
|
311
|
+
transport_hash="0" * 64,
|
|
312
|
+
signature=None,
|
|
313
|
+
signature_algorithm=None,
|
|
314
|
+
signing_key_id=None,
|
|
315
|
+
classification=classification,
|
|
316
|
+
encryption_status="plaintext",
|
|
317
|
+
pii_fields=(),
|
|
318
|
+
),
|
|
319
|
+
governance=governance,
|
|
320
|
+
provenance=provenance
|
|
321
|
+
or _default_provenance(source_node=normalized_source, action=normalized_action),
|
|
322
|
+
delegation_chain=(),
|
|
323
|
+
hop_trace=(),
|
|
324
|
+
lineage=TransportLineage(
|
|
325
|
+
parent_id=None,
|
|
326
|
+
root_id=packet_id,
|
|
327
|
+
generation=0,
|
|
328
|
+
),
|
|
329
|
+
attachments=(),
|
|
330
|
+
)
|
|
331
|
+
return _finalize_transport_packet(packet, preserve_signature=False)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict, field_validator
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class RoutingProvenance(BaseModel):
|
|
7
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
8
|
+
|
|
9
|
+
origin_kind: str
|
|
10
|
+
requested_action: str
|
|
11
|
+
resolved_by_gate: bool = False
|
|
12
|
+
route_kind: str | None = None
|
|
13
|
+
original_source_node: str | None = None
|
|
14
|
+
|
|
15
|
+
@field_validator("origin_kind")
|
|
16
|
+
@classmethod
|
|
17
|
+
def validate_origin_kind(cls, value: str) -> str:
|
|
18
|
+
normalized = value.strip().lower()
|
|
19
|
+
allowed = {"client", "node", "gate"}
|
|
20
|
+
if normalized not in allowed:
|
|
21
|
+
msg = f"origin_kind must be one of {sorted(allowed)}"
|
|
22
|
+
raise ValueError(msg)
|
|
23
|
+
return normalized
|
|
24
|
+
|
|
25
|
+
@field_validator("requested_action")
|
|
26
|
+
@classmethod
|
|
27
|
+
def validate_requested_action(cls, value: str) -> str:
|
|
28
|
+
normalized = value.strip().lower()
|
|
29
|
+
if not normalized:
|
|
30
|
+
raise ValueError("requested_action must not be empty")
|
|
31
|
+
return normalized
|
|
32
|
+
|
|
33
|
+
@field_validator("route_kind")
|
|
34
|
+
@classmethod
|
|
35
|
+
def validate_route_kind(cls, value: str | None) -> str | None:
|
|
36
|
+
if value is None:
|
|
37
|
+
return None
|
|
38
|
+
normalized = value.strip().lower()
|
|
39
|
+
allowed = {"external_ingress", "gate_relay"}
|
|
40
|
+
if normalized not in allowed:
|
|
41
|
+
msg = f"route_kind must be one of {sorted(allowed)}"
|
|
42
|
+
raise ValueError(msg)
|
|
43
|
+
return normalized
|
|
44
|
+
|
|
45
|
+
@field_validator("original_source_node")
|
|
46
|
+
@classmethod
|
|
47
|
+
def validate_original_source_node(cls, value: str | None) -> str | None:
|
|
48
|
+
if value is None:
|
|
49
|
+
return None
|
|
50
|
+
normalized = value.strip().lower()
|
|
51
|
+
if not normalized:
|
|
52
|
+
raise ValueError("original_source_node must not be blank")
|
|
53
|
+
return normalized
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict, field_validator
|
|
6
|
+
|
|
7
|
+
from .errors import TenantMutationError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TenantContext(BaseModel):
|
|
11
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
12
|
+
|
|
13
|
+
actor: str
|
|
14
|
+
on_behalf_of: str
|
|
15
|
+
originator: str
|
|
16
|
+
org_id: str
|
|
17
|
+
user_id: str | None = None
|
|
18
|
+
|
|
19
|
+
@field_validator("actor", "on_behalf_of", "originator", "org_id")
|
|
20
|
+
@classmethod
|
|
21
|
+
def validate_required_strings(cls, value: str) -> str:
|
|
22
|
+
normalized = value.strip()
|
|
23
|
+
if not normalized:
|
|
24
|
+
raise ValueError("tenant fields must not be empty")
|
|
25
|
+
return normalized
|
|
26
|
+
|
|
27
|
+
@field_validator("user_id")
|
|
28
|
+
@classmethod
|
|
29
|
+
def validate_optional_string(cls, value: str | None) -> str | None:
|
|
30
|
+
if value is None:
|
|
31
|
+
return None
|
|
32
|
+
normalized = value.strip()
|
|
33
|
+
if not normalized:
|
|
34
|
+
raise ValueError("user_id must not be blank")
|
|
35
|
+
return normalized
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def ensure_tenant_context(value: str | dict[str, Any] | TenantContext) -> TenantContext:
|
|
39
|
+
"""Normalize a string, dict, or TenantContext into a canonical TenantContext."""
|
|
40
|
+
if isinstance(value, TenantContext):
|
|
41
|
+
return value
|
|
42
|
+
|
|
43
|
+
if isinstance(value, str):
|
|
44
|
+
normalized = value.strip()
|
|
45
|
+
if not normalized:
|
|
46
|
+
raise ValueError("tenant string must not be empty")
|
|
47
|
+
return TenantContext(
|
|
48
|
+
actor=normalized,
|
|
49
|
+
on_behalf_of=normalized,
|
|
50
|
+
originator=normalized,
|
|
51
|
+
org_id=normalized,
|
|
52
|
+
user_id=None,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
payload = dict(value or {})
|
|
56
|
+
if "tenant_id" in payload and "org_id" not in payload:
|
|
57
|
+
payload["org_id"] = payload["tenant_id"]
|
|
58
|
+
|
|
59
|
+
org_id = str(payload.get("org_id") or payload.get("actor") or "default").strip()
|
|
60
|
+
actor = str(payload.get("actor") or org_id).strip()
|
|
61
|
+
on_behalf_of = str(payload.get("on_behalf_of") or actor).strip()
|
|
62
|
+
originator = str(payload.get("originator") or actor).strip()
|
|
63
|
+
|
|
64
|
+
return TenantContext(
|
|
65
|
+
actor=actor,
|
|
66
|
+
on_behalf_of=on_behalf_of,
|
|
67
|
+
originator=originator,
|
|
68
|
+
org_id=org_id,
|
|
69
|
+
user_id=payload.get("user_id"),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def assert_tenant_immutable(parent: TenantContext, child: TenantContext) -> None:
|
|
74
|
+
"""Raise if a derived packet mutates tenant context."""
|
|
75
|
+
if parent != child:
|
|
76
|
+
raise TenantMutationError("tenant context is immutable across derived packets")
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: constellation-node-sdk
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: L9 Constellation Node SDK — TransportPacket runtime, Gate client, and node factory
|
|
5
|
+
License: Proprietary
|
|
6
|
+
Requires-Python: >=3.12
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: fastapi>=0.115.0
|
|
9
|
+
Requires-Dist: uvicorn[standard]>=0.32.0
|
|
10
|
+
Requires-Dist: pydantic>=2.9.0
|
|
11
|
+
Requires-Dist: httpx>=0.27.0
|
|
12
|
+
Requires-Dist: cryptography>=43.0.0
|
|
13
|
+
Requires-Dist: prometheus-client>=0.21.0
|
|
14
|
+
Requires-Dist: python-json-logger>=3.2.0
|
|
15
|
+
Requires-Dist: pyyaml>=6.0.2
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: mypy>=1.11.0; extra == "dev"
|
|
18
|
+
Requires-Dist: pytest>=8.3.0; extra == "dev"
|
|
19
|
+
Requires-Dist: pytest-asyncio>=0.23.8; extra == "dev"
|
|
20
|
+
Requires-Dist: ruff>=0.6.0; extra == "dev"
|
|
21
|
+
Requires-Dist: types-PyYAML>=6.0.12; extra == "dev"
|
|
22
|
+
Requires-Dist: types-requests; extra == "dev"
|
|
23
|
+
|
|
24
|
+
# Constellation Node SDK
|
|
25
|
+
|
|
26
|
+
`constellation-node-sdk` is the canonical Python SDK for Constellation worker and orchestrator nodes.
|
|
27
|
+
|
|
28
|
+
It defines the packet-native transport contract used by the Constellation system:
|
|
29
|
+
|
|
30
|
+
- `TransportPacket` is the single canonical wire format
|
|
31
|
+
- all node-originated follow-up work goes back through **Gate**
|
|
32
|
+
- hop trace is append-only and tamper-evident
|
|
33
|
+
- transport integrity, lineage, provenance, and delegation are first-class protocol concepts
|
|
34
|
+
|
|
35
|
+
## What this SDK provides
|
|
36
|
+
|
|
37
|
+
- canonical `TransportPacket` models and helpers
|
|
38
|
+
- transport hashing, signing, verification, and validation
|
|
39
|
+
- Gate-only outbound client
|
|
40
|
+
- reusable node runtime with `/v1/execute` and `/v1/health`
|
|
41
|
+
- orchestrator helpers for workflow composition through Gate
|
|
42
|
+
- tests and examples for worker and orchestrator nodes
|
|
43
|
+
|
|
44
|
+
## Architectural rules
|
|
45
|
+
|
|
46
|
+
1. Nodes **must not** know peer node URLs.
|
|
47
|
+
2. Nodes **must only** send follow-up work to `GATE_URL`.
|
|
48
|
+
3. Gate is the sole routing authority.
|
|
49
|
+
4. `TransportPacket` is the only supported transport format.
|
|
50
|
+
5. Semantic packet changes create child packets via `derive()`.
|
|
51
|
+
6. Observational movement appends hop trace entries without changing `transport_hash`.
|
|
52
|
+
|
|
53
|
+
## Install
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install -e .
|
|
57
|
+
For development:
|
|
58
|
+
|
|
59
|
+
pip install -e ".[dev]"
|
|
60
|
+
Quick start
|
|
61
|
+
Worker node
|
|
62
|
+
from constellation_node_sdk import create_node_app, register_handler
|
|
63
|
+
|
|
64
|
+
@register_handler("score")
|
|
65
|
+
async def handle_score(_tenant: str, payload: dict) -> dict:
|
|
66
|
+
return {
|
|
67
|
+
"status": "completed",
|
|
68
|
+
"score": 91,
|
|
69
|
+
"entity_id": payload["entity_id"],
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
app = create_node_app(
|
|
73
|
+
service_name="score-node",
|
|
74
|
+
version="1.0.0",
|
|
75
|
+
)
|
|
76
|
+
Orchestrator node
|
|
77
|
+
from constellation_node_sdk import create_node_app, register_handler
|
|
78
|
+
from constellation_node_sdk.gate import GateClient, get_gate_client_config_from_env
|
|
79
|
+
from constellation_node_sdk.orchestrator.step_executor import StepExecutor
|
|
80
|
+
|
|
81
|
+
gate_client = GateClient(get_gate_client_config_from_env())
|
|
82
|
+
step_executor = StepExecutor(gate_client=gate_client, source_node="orchestrator")
|
|
83
|
+
|
|
84
|
+
@register_handler("full_pipeline")
|
|
85
|
+
async def handle_full_pipeline(_tenant: str, payload: dict, packet):
|
|
86
|
+
enrich = await step_executor.execute_step(
|
|
87
|
+
parent=packet,
|
|
88
|
+
action="enrich",
|
|
89
|
+
payload={"entity_id": payload["entity_id"]},
|
|
90
|
+
)
|
|
91
|
+
score = await step_executor.execute_step(
|
|
92
|
+
parent=packet,
|
|
93
|
+
action="score",
|
|
94
|
+
payload={**payload, **enrich.payload},
|
|
95
|
+
)
|
|
96
|
+
return {
|
|
97
|
+
"status": "completed",
|
|
98
|
+
"entity_id": payload["entity_id"],
|
|
99
|
+
"enrich": enrich.payload,
|
|
100
|
+
"score": score.payload,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
app = create_node_app(
|
|
104
|
+
service_name="orchestrator",
|
|
105
|
+
version="1.0.0",
|
|
106
|
+
)
|
|
107
|
+
Environment
|
|
108
|
+
See .env.example for runtime configuration.
|
|
109
|
+
|
|
110
|
+
Repo structure
|
|
111
|
+
src/constellation_node_sdk/transport/ — transport contract and hashing
|
|
112
|
+
|
|
113
|
+
src/constellation_node_sdk/security/ — signing, verification, validation
|
|
114
|
+
|
|
115
|
+
src/constellation_node_sdk/gate/ — Gate-only client and registration
|
|
116
|
+
|
|
117
|
+
src/constellation_node_sdk/runtime/ — reusable node runtime
|
|
118
|
+
|
|
119
|
+
src/constellation_node_sdk/orchestrator/ — workflow composition helpers
|
|
120
|
+
|
|
121
|
+
contracts/ — formal contract specs
|
|
122
|
+
|
|
123
|
+
examples/ — runnable worker and orchestrator examples
|
|
124
|
+
|
|
125
|
+
tests/ — unit and integration coverage
|
|
126
|
+
|
|
127
|
+
Status
|
|
128
|
+
This repo is intended to be:
|
|
129
|
+
|
|
130
|
+
packet-native
|
|
131
|
+
|
|
132
|
+
Gate-routed
|
|
133
|
+
|
|
134
|
+
composable
|
|
135
|
+
|
|
136
|
+
production-oriented
|
|
137
|
+
|
|
138
|
+
strict about protocol correctness
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
```markdown
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
constellation_node_sdk/__init__.py,sha256=7DIaR8opok0gPv4VUvorlrVEoEkJQKnPTX4tBR8VQHQ,3197
|
|
2
|
+
constellation_node_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
constellation_node_sdk/gate/__init__.py,sha256=0BmOmw0BGZnNIUJBaZmoaoy8tSaSZiE-p0bEapDbx5M,1275
|
|
4
|
+
constellation_node_sdk/gate/client.py,sha256=MzAreAblo2uQ2NPYbDIHh8fPtd6ZU-eadsBke6O_OFk,4070
|
|
5
|
+
constellation_node_sdk/gate/config.py,sha256=HPn3mIm6zZ_rNd_RpfJy7Xuav1eftxlBEA44CjjETvw,5726
|
|
6
|
+
constellation_node_sdk/gate/errors.py,sha256=-gQmOrRuFU3KoOBuuUPWAUHbU9dazk527kFctUMgydw,473
|
|
7
|
+
constellation_node_sdk/gate/policy.py,sha256=Qtwt8jR6nNnWfd0DajbRvIBAIQiyEWyXqu-E7bx7DTc,2592
|
|
8
|
+
constellation_node_sdk/gate/registration.py,sha256=9uymIBmBA6yfKAKYzDSoX79PDXyBAWpogYE1Uzqs1ng,4609
|
|
9
|
+
constellation_node_sdk/orchestrator/__init__.py,sha256=RmS0Ok-1-CwWOZL_socLWksEk9MfsshL9eQx879I4mU,60
|
|
10
|
+
constellation_node_sdk/orchestrator/base.py,sha256=2YjLWfoplVCu1EAECrKiFkPQluJCYHS9Y6hBbCjfkTo,1739
|
|
11
|
+
constellation_node_sdk/orchestrator/merge.py,sha256=k6PWq7eTacz_lmAdnmaSXbGhr-FOb3RUQ5ZoB0xMM_U,1344
|
|
12
|
+
constellation_node_sdk/orchestrator/packet_builder.py,sha256=e6gyeGwgRcLIFF1Q4Fjl7RPWORvFMevXOBvH8bpujSs,1472
|
|
13
|
+
constellation_node_sdk/orchestrator/retry.py,sha256=GMpWjnYP0QowL4ik2tjAjzkyQmqNnJcGZYkQkKxSjBA,2108
|
|
14
|
+
constellation_node_sdk/orchestrator/state.py,sha256=bh82_Jpq3UnegSrXZuogNVf4YtESVAfD8FNVXlbjjxk,2618
|
|
15
|
+
constellation_node_sdk/orchestrator/step_executor.py,sha256=q_TMyCichml0GpQWLEvpSEipYb2YjQFoMCkon78T5no,2902
|
|
16
|
+
constellation_node_sdk/runtime/__init__.py,sha256=EQ6fwCQRamm78Kq8UMVL2q5lDUIL9OfZKE-r4JDfpJg,1547
|
|
17
|
+
constellation_node_sdk/runtime/app.py,sha256=bcIvglSfRwGlQAQ_dgk6LmYbb-DgRbkMAM2OyNNYgxc,11127
|
|
18
|
+
constellation_node_sdk/runtime/config.py,sha256=pEhjf26Uy7tr1LjKpdKVmPos2EoouS1NSofcCaR68lk,12916
|
|
19
|
+
constellation_node_sdk/runtime/errors.py,sha256=TbQMtDi4yZL1WAqGcTMlpsHsBD1PfgwYkKU2FByB4i4,3014
|
|
20
|
+
constellation_node_sdk/runtime/execution.py,sha256=DFHxEPHgeOKUABkBZv9NXmjfS7SNMpZoQ4O8GwjHvhg,8436
|
|
21
|
+
constellation_node_sdk/runtime/handlers.py,sha256=VBU3kdon0gR3xsPDEh7V5IoP4i7wxfrEe2S_QWwmbcM,1223
|
|
22
|
+
constellation_node_sdk/runtime/inbound_policy.py,sha256=sqTJr6ur0n-_qmFSaTf7cLBDyfU-2K9Do4jNQkmy8xs,3140
|
|
23
|
+
constellation_node_sdk/runtime/lifecycle.py,sha256=17ZNWTRdg4rA8Bi7-Gh0CIunYdXlYgWCn1Gri33UvoQ,651
|
|
24
|
+
constellation_node_sdk/runtime/observability.py,sha256=JRLwQ_97_GnYTQSoqsGMRaacHMdnSgWaoJRlGtsPuK8,1799
|
|
25
|
+
constellation_node_sdk/runtime/preflight.py,sha256=5SHhoPJWomjJgUr6Ve9l4ZRF7h0CBoPMomxjkYjXeXc,4735
|
|
26
|
+
constellation_node_sdk/security/__init__.py,sha256=4JTsEHsO5d6tHo_H4yeqTo0rRTtlFlVkd3b7fVBJxn4,1202
|
|
27
|
+
constellation_node_sdk/security/delegation.py,sha256=MlT81UFxRhiNsi25r8GxKNlUXs0KuH2q2gWFR_iK-JM,1707
|
|
28
|
+
constellation_node_sdk/security/errors.py,sha256=l6Eel5pbr3meFib0qvDPdGXkMueVoqvLIjJhn1W6SOY,596
|
|
29
|
+
constellation_node_sdk/security/signing.py,sha256=ahniFXeXdV7EDhg1LxpPXVPpIX2_l5kHWgecTakj9QU,4122
|
|
30
|
+
constellation_node_sdk/security/validation.py,sha256=nktaWgSdleZvm5lxsKfrT6g8iDaUWj2039-Nwug3ACU,9453
|
|
31
|
+
constellation_node_sdk/security/verification.py,sha256=-JOavVW7CZHrq3fMolkDnv_g2UX08Whmv3RD6BWMpn0,3508
|
|
32
|
+
constellation_node_sdk/transport/__init__.py,sha256=AvwIqfzguRjCCsz7R3ASFKJF_uzZ5dPrVIHzcCh3E-0,2267
|
|
33
|
+
constellation_node_sdk/transport/codec.py,sha256=SFalmKhwxcv7V-USPxnmK1lKDHh3q-n-kgFtah6vZ98,618
|
|
34
|
+
constellation_node_sdk/transport/errors.py,sha256=ohgC0NoRYepEKeh3EkgZZCMZIJ5D8cQjJfxp-0ndg5k,1279
|
|
35
|
+
constellation_node_sdk/transport/hashing.py,sha256=Ds6Z109gCDUVLbLQO7-3fBZdFYqSUf5n--kjvb0H_KM,2477
|
|
36
|
+
constellation_node_sdk/transport/hop_trace.py,sha256=bgydTputZNimNJAVON89tLsvFfjJT6DGTYWEFdbixtU,11761
|
|
37
|
+
constellation_node_sdk/transport/lineage.py,sha256=xswFwlfELGZcgd-alncuMR6lOxFP-i4uv5awQ_TsPmE,1172
|
|
38
|
+
constellation_node_sdk/transport/models.py,sha256=-xc0KYLMrXSK_iLIKmMxx-vWI0MHbOq4TCKDLduQaLo,16174
|
|
39
|
+
constellation_node_sdk/transport/packet.py,sha256=aaqY_BZVWDT4zSpKPkqUeHu6bkbb0ZTVQFcjEUg-0f8,12295
|
|
40
|
+
constellation_node_sdk/transport/provenance.py,sha256=QwmAJvnxB7254nNkySzmkv93OHM_z-dsySyWFpmMJOw,1781
|
|
41
|
+
constellation_node_sdk/transport/tenant.py,sha256=YJxAZ6aLoXt7WHy4rV01VEaaon90FPEVSI9B6s7Vcpo,2427
|
|
42
|
+
constellation_node_sdk-1.0.1.dist-info/METADATA,sha256=obHcV6S2efypObELHH0nlj0jK2GzYcKMmlHjD2bo4yA,4220
|
|
43
|
+
constellation_node_sdk-1.0.1.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
44
|
+
constellation_node_sdk-1.0.1.dist-info/top_level.txt,sha256=JBNbpbcgca5h--BJwxqRpIXRW4VCWqufmj_djAZI6Fk,23
|
|
45
|
+
constellation_node_sdk-1.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
constellation_node_sdk
|