constellation-node-sdk 1.2.0__tar.gz → 1.3.2__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.
Files changed (62) hide show
  1. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/PKG-INFO +1 -1
  2. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/pyproject.toml +1 -1
  3. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk/__init__.py +12 -0
  4. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk/gate_authority/dispatch.py +14 -0
  5. constellation_node_sdk-1.3.2/src/constellation_node_sdk/trace_context.py +278 -0
  6. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk.egg-info/PKG-INFO +1 -1
  7. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk.egg-info/SOURCES.txt +1 -0
  8. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/LICENSE +0 -0
  9. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/README.md +0 -0
  10. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/setup.cfg +0 -0
  11. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk/_packet_http.py +0 -0
  12. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk/gate/__init__.py +0 -0
  13. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk/gate/admission.py +0 -0
  14. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk/gate/capabilities.py +0 -0
  15. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk/gate/capability_client.py +0 -0
  16. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk/gate/client.py +0 -0
  17. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk/gate/config.py +0 -0
  18. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk/gate/errors.py +0 -0
  19. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk/gate/policy.py +0 -0
  20. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk/gate/registration.py +0 -0
  21. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk/gate_authority/__init__.py +0 -0
  22. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk/gate_authority/config.py +0 -0
  23. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk/gate_authority/errors.py +0 -0
  24. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk/orchestrator/__init__.py +0 -0
  25. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk/orchestrator/base.py +0 -0
  26. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk/orchestrator/merge.py +0 -0
  27. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk/orchestrator/packet_builder.py +0 -0
  28. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk/orchestrator/retry.py +0 -0
  29. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk/orchestrator/state.py +0 -0
  30. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk/orchestrator/step_executor.py +0 -0
  31. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk/py.typed +0 -0
  32. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk/runtime/__init__.py +0 -0
  33. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk/runtime/app.py +0 -0
  34. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk/runtime/config.py +0 -0
  35. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk/runtime/errors.py +0 -0
  36. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk/runtime/execution.py +0 -0
  37. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk/runtime/handlers.py +0 -0
  38. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk/runtime/inbound_policy.py +0 -0
  39. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk/runtime/lifecycle.py +0 -0
  40. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk/runtime/observability.py +0 -0
  41. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk/runtime/participation.py +0 -0
  42. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk/runtime/preflight.py +0 -0
  43. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk/security/__init__.py +0 -0
  44. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk/security/delegation.py +0 -0
  45. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk/security/errors.py +0 -0
  46. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk/security/signing.py +0 -0
  47. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk/security/validation.py +0 -0
  48. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk/security/verification.py +0 -0
  49. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk/transport/__init__.py +0 -0
  50. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk/transport/codec.py +0 -0
  51. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk/transport/errors.py +0 -0
  52. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk/transport/hashing.py +0 -0
  53. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk/transport/hop_trace.py +0 -0
  54. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk/transport/lineage.py +0 -0
  55. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk/transport/models.py +0 -0
  56. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk/transport/packet.py +0 -0
  57. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk/transport/provenance.py +0 -0
  58. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk/transport/tenant.py +0 -0
  59. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk.egg-info/dependency_links.txt +0 -0
  60. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk.egg-info/requires.txt +0 -0
  61. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/src/constellation_node_sdk.egg-info/top_level.txt +0 -0
  62. {constellation_node_sdk-1.2.0 → constellation_node_sdk-1.3.2}/tests/test_release_identity.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: constellation-node-sdk
3
- Version: 1.2.0
3
+ Version: 1.3.2
4
4
  Summary: L9 Constellation Node SDK — TransportPacket runtime, Gate client, and node factory
5
5
  License: Proprietary
6
6
  Requires-Python: >=3.12
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "constellation-node-sdk"
7
- version = "1.2.0"
7
+ version = "1.3.2"
8
8
  description = "L9 Constellation Node SDK — TransportPacket runtime, Gate client, and node factory"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -56,6 +56,13 @@ from constellation_node_sdk.security import (
56
56
  validate_transport_packet,
57
57
  verify_transport_packet_signature,
58
58
  )
59
+ from constellation_node_sdk.trace_context import (
60
+ TraceContext,
61
+ TraceContextError,
62
+ create_root_trace_context,
63
+ inject_trace_context,
64
+ parse_trace_context,
65
+ )
59
66
  from constellation_node_sdk.transport import (
60
67
  DelegationLink,
61
68
  RoutingProvenance,
@@ -126,6 +133,8 @@ __all__ = [
126
133
  "TransportPacket",
127
134
  "TransportSecurity",
128
135
  "TransportValidationError",
136
+ "TraceContext",
137
+ "TraceContextError",
129
138
  "assert_gate_only_destination",
130
139
  "assert_local_node_identity",
131
140
  "assert_node_origin_packet",
@@ -137,6 +146,7 @@ __all__ = [
137
146
  "compute_transport_hash",
138
147
  "create_error_transport_packet",
139
148
  "create_node_app",
149
+ "create_root_trace_context",
140
150
  "create_transport_packet",
141
151
  "ensure_tenant_context",
142
152
  "execute_transport_packet",
@@ -144,7 +154,9 @@ __all__ = [
144
154
  "get_gate_registration_config_from_env",
145
155
  "get_handler",
146
156
  "get_runtime_config",
157
+ "inject_trace_context",
147
158
  "load_node_spec",
159
+ "parse_trace_context",
148
160
  "recompute_transport_core",
149
161
  "register_from_env",
150
162
  "register_handler",
@@ -17,6 +17,7 @@ from constellation_node_sdk._packet_http import (
17
17
  from constellation_node_sdk.runtime.inbound_policy import validate_execute_ingress_packet
18
18
  from constellation_node_sdk.security.signing import sign_transport_packet
19
19
  from constellation_node_sdk.security.validation import validate_transport_packet
20
+ from constellation_node_sdk.trace_context import TraceContext, inject_trace_context
20
21
  from constellation_node_sdk.transport.errors import TransportError
21
22
  from constellation_node_sdk.transport.packet import TransportPacket
22
23
 
@@ -144,6 +145,7 @@ class GateDispatchTransport:
144
145
  packet: TransportPacket,
145
146
  target_node: str,
146
147
  worker_base_url: str,
148
+ trace_context: TraceContext | None = None,
147
149
  ) -> TransportPacket:
148
150
  """
149
151
  Send a Gate-authored dispatch packet to the worker Gate resolved.
@@ -168,6 +170,10 @@ class GateDispatchTransport:
168
170
  target_node: The node name Gate resolved.
169
171
  worker_base_url: That node's base URL from Gate's registry. The
170
172
  canonical ``/v1/execute`` endpoint is appended by the SDK.
173
+ trace_context: Optional validated W3C context authored by Gate.
174
+ It is transported only as strict ``traceparent`` and optional
175
+ ``tracestate`` headers; baggage and arbitrary headers are not
176
+ accepted on this authority boundary.
171
177
 
172
178
  Returns:
173
179
  The validated canonical ``TransportPacket`` the worker returned.
@@ -197,6 +203,13 @@ class GateDispatchTransport:
197
203
  signed_packet = self._maybe_sign(packet)
198
204
  self._validate_outbound_transport(signed_packet)
199
205
  timeout_seconds = self._network_timeout_seconds(signed_packet)
206
+ trace_headers: dict[str, str] | None = None
207
+ if trace_context is not None:
208
+ injected = inject_trace_context({}, trace_context)
209
+ trace_headers = {"traceparent": str(injected["traceparent"])}
210
+ tracestate = injected.get("tracestate")
211
+ if isinstance(tracestate, str):
212
+ trace_headers["tracestate"] = tracestate
200
213
 
201
214
  response = await post_packet_json(
202
215
  url=execute_url,
@@ -206,6 +219,7 @@ class GateDispatchTransport:
206
219
  what=f"worker {normalized_target!r}",
207
220
  client=self._client,
208
221
  transport=self._transport if self._client is None else None,
222
+ headers=trace_headers,
209
223
  )
210
224
  raise_for_status(
211
225
  response,
@@ -0,0 +1,278 @@
1
+ """Packet-neutral utilities for W3C Trace Context version 00 headers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ import secrets
7
+ from collections.abc import Iterator, Mapping, Sequence
8
+ from dataclasses import dataclass
9
+ from typing import NoReturn
10
+
11
+ __all__ = [
12
+ "TraceContext",
13
+ "TraceContextError",
14
+ "create_root_trace_context",
15
+ "inject_trace_context",
16
+ "parse_trace_context",
17
+ ]
18
+
19
+ _ERROR_MESSAGE = "invalid W3C trace context"
20
+ _TRACEPARENT_RE = re.compile(
21
+ r"00-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}\Z",
22
+ re.ASCII,
23
+ )
24
+ _TRACESTATE_KEY_RE = re.compile(
25
+ r"(?:[a-z][a-z0-9_*/-]{0,255}|[a-z0-9][a-z0-9_*/-]{0,240}@[a-z][a-z0-9_*/-]{0,13})\Z",
26
+ re.ASCII,
27
+ )
28
+ _CONTEXT_HEADER_NAMES = frozenset({"traceparent", "tracestate", "baggage"})
29
+ _MAX_SCANNED_HEADER_FIELDS = 64
30
+ _MAX_TRACEPARENT_VALUES = 1
31
+ _MAX_TRACESTATE_FIELD_VALUES = 32
32
+ _MAX_TRACESTATE_LENGTH = 512
33
+ _MAX_TRACESTATE_MEMBERS = 32
34
+ _MAX_TRACESTATE_VALUE_LENGTH = 256
35
+
36
+
37
+ class TraceContextError(ValueError):
38
+ """Raised when W3C trace-context input is malformed or unsafe."""
39
+
40
+
41
+ def _raise_invalid() -> NoReturn:
42
+ raise TraceContextError(_ERROR_MESSAGE) from None
43
+
44
+
45
+ def create_root_trace_context() -> TraceContext:
46
+ """Create a fresh valid, unsampled W3C v00 root context.
47
+
48
+ This establishes correlation only. It carries no caller identity, routing
49
+ authority, packet state, or sampling policy beyond the conservative
50
+ unsampled default. Deployment-specific sampling remains outside this
51
+ packet-neutral utility.
52
+ """
53
+ while True:
54
+ trace_id = secrets.token_hex(16)
55
+ parent_id = secrets.token_hex(8)
56
+ if trace_id != "0" * 32 and parent_id != "0" * 16:
57
+ return TraceContext(f"00-{trace_id}-{parent_id}-00")
58
+
59
+
60
+ @dataclass(frozen=True, slots=True, repr=False)
61
+ class TraceContext:
62
+ """Validated W3C version 00 trace headers without packet coupling."""
63
+
64
+ traceparent: str
65
+ tracestate: str | None = None
66
+
67
+ def __post_init__(self) -> None:
68
+ try:
69
+ traceparent = _validate_traceparent(self.traceparent)
70
+ tracestate = (
71
+ None if self.tracestate is None else _normalize_tracestate((self.tracestate,))
72
+ )
73
+ except Exception:
74
+ pass
75
+ else:
76
+ object.__setattr__(self, "traceparent", traceparent)
77
+ object.__setattr__(self, "tracestate", tracestate)
78
+ return
79
+ _raise_invalid()
80
+
81
+ def __repr__(self) -> str:
82
+ tracestate = "None" if self.tracestate is None else "<redacted>"
83
+ return f"TraceContext(traceparent=<redacted>, tracestate={tracestate})"
84
+
85
+
86
+ def parse_trace_context(
87
+ headers: Mapping[str, str | Sequence[str]],
88
+ ) -> TraceContext | None:
89
+ """Parse strict W3C v00 context from case-insensitive HTTP-style headers.
90
+
91
+ Baggage is intentionally ignored and never becomes part of ``TraceContext``.
92
+ A malformed ``tracestate`` is discarded when its ``traceparent`` is valid, as
93
+ required for receiver interoperability.
94
+ """
95
+ try:
96
+ if not isinstance(headers, Mapping):
97
+ _raise_invalid()
98
+
99
+ traceparent_values: tuple[str, ...] = ()
100
+ raw_tracestate_fields: list[object] = []
101
+ traceparent_fields = 0
102
+ tracestate_fields = 0
103
+
104
+ for header_name, header_value in _bounded_mapping_items(headers):
105
+ if not isinstance(header_name, str):
106
+ _raise_invalid()
107
+ normalized_name = header_name.lower()
108
+ if normalized_name == "traceparent":
109
+ traceparent_fields += 1
110
+ if traceparent_fields > 1:
111
+ _raise_invalid()
112
+ traceparent_values = _header_values(
113
+ header_value,
114
+ maximum=_MAX_TRACEPARENT_VALUES,
115
+ )
116
+ elif normalized_name == "tracestate":
117
+ tracestate_fields += 1
118
+ raw_tracestate_fields.append(header_value)
119
+
120
+ if traceparent_fields == 0:
121
+ if tracestate_fields:
122
+ _raise_invalid()
123
+ return None
124
+ if len(traceparent_values) != _MAX_TRACEPARENT_VALUES:
125
+ _raise_invalid()
126
+
127
+ traceparent = _validate_traceparent(traceparent_values[0])
128
+ tracestate: str | None = None
129
+ if tracestate_fields:
130
+ try:
131
+ tracestate_values = _tracestate_header_values(raw_tracestate_fields)
132
+ tracestate = _normalize_tracestate(
133
+ tracestate_values,
134
+ allow_empty_members=True,
135
+ )
136
+ except Exception:
137
+ tracestate = None
138
+
139
+ return TraceContext(traceparent=traceparent, tracestate=tracestate)
140
+ except Exception:
141
+ pass
142
+ _raise_invalid()
143
+
144
+
145
+ def inject_trace_context(headers: Mapping[str, object], context: TraceContext) -> dict[str, object]:
146
+ """Copy headers, replace trace context, and deliberately drop baggage."""
147
+ try:
148
+ if not isinstance(headers, Mapping) or not isinstance(context, TraceContext):
149
+ _raise_invalid()
150
+ validated_context = TraceContext(
151
+ traceparent=context.traceparent,
152
+ tracestate=context.tracestate,
153
+ )
154
+ injected: dict[str, object] = {}
155
+ for header_name, header_value in _bounded_mapping_items(headers):
156
+ if not isinstance(header_name, str):
157
+ _raise_invalid()
158
+ if header_name.lower() not in _CONTEXT_HEADER_NAMES:
159
+ injected[header_name] = header_value
160
+ injected["traceparent"] = validated_context.traceparent
161
+ if validated_context.tracestate is not None:
162
+ injected["tracestate"] = validated_context.tracestate
163
+ return injected
164
+ except Exception:
165
+ pass
166
+ _raise_invalid()
167
+
168
+
169
+ def _bounded_mapping_items[HeaderName, HeaderValue](
170
+ headers: Mapping[HeaderName, HeaderValue],
171
+ ) -> Iterator[tuple[HeaderName, HeaderValue]]:
172
+ """Yield at most a fixed number of mapping fields without using ``items()``."""
173
+ header_names = iter(headers)
174
+ for _ in range(_MAX_SCANNED_HEADER_FIELDS):
175
+ try:
176
+ header_name = next(header_names)
177
+ except StopIteration:
178
+ return
179
+ yield header_name, headers[header_name]
180
+ _raise_invalid()
181
+
182
+
183
+ def _header_values(value: object, *, maximum: int) -> tuple[str, ...]:
184
+ """Read a bounded HTTP header value sequence using indexed access only."""
185
+ if isinstance(value, str):
186
+ return (value,)
187
+ if isinstance(value, (bytes, bytearray)) or not isinstance(value, Sequence):
188
+ _raise_invalid()
189
+
190
+ value_count = len(value)
191
+ if value_count > maximum:
192
+ _raise_invalid()
193
+
194
+ values: list[str] = []
195
+ for index in range(value_count):
196
+ item = value[index]
197
+ if not isinstance(item, str):
198
+ _raise_invalid()
199
+ values.append(item)
200
+ return tuple(values)
201
+
202
+
203
+ def _tracestate_header_values(raw_values: Sequence[object]) -> tuple[str, ...]:
204
+ """Collect bounded tracestate fields without splitting an oversized value."""
205
+ values: list[str] = []
206
+ for raw_value in raw_values:
207
+ remaining = _MAX_TRACESTATE_FIELD_VALUES - len(values)
208
+ if remaining <= 0:
209
+ _raise_invalid()
210
+ for value in _header_values(raw_value, maximum=remaining):
211
+ if len(value) > _MAX_TRACESTATE_LENGTH:
212
+ _raise_invalid()
213
+ values.append(value)
214
+ return tuple(values)
215
+
216
+
217
+ def _validate_traceparent(value: object) -> str:
218
+ if not isinstance(value, str) or not _TRACEPARENT_RE.fullmatch(value):
219
+ _raise_invalid()
220
+ trace_id = value[3:35]
221
+ parent_id = value[36:52]
222
+ if trace_id == "0" * 32 or parent_id == "0" * 16:
223
+ _raise_invalid()
224
+ return value
225
+
226
+
227
+ def _normalize_tracestate(
228
+ values: Sequence[str],
229
+ *,
230
+ allow_empty_members: bool = False,
231
+ ) -> str | None:
232
+ if any(not isinstance(value, str) for value in values):
233
+ _raise_invalid()
234
+
235
+ raw_length = max(len(values) - 1, 0)
236
+ for value in values:
237
+ if len(value) > _MAX_TRACESTATE_LENGTH:
238
+ _raise_invalid()
239
+ raw_length += len(value)
240
+ if raw_length > _MAX_TRACESTATE_LENGTH:
241
+ _raise_invalid()
242
+
243
+ members: list[str] = []
244
+ seen_keys: set[str] = set()
245
+ for raw_value in values:
246
+ for raw_member in raw_value.split(","):
247
+ member = raw_member.strip(" \t")
248
+ if not member:
249
+ if allow_empty_members:
250
+ continue
251
+ _raise_invalid()
252
+ if member.count("=") != 1:
253
+ _raise_invalid()
254
+
255
+ key, value = member.split("=", 1)
256
+ if not key or not value or key[-1] in " \t" or value[0] in " \t":
257
+ _raise_invalid()
258
+ if not _TRACESTATE_KEY_RE.fullmatch(key) or key in seen_keys:
259
+ _raise_invalid()
260
+ if len(value) > _MAX_TRACESTATE_VALUE_LENGTH:
261
+ _raise_invalid()
262
+ if any(character in ",=" or not 0x20 <= ord(character) <= 0x7E for character in value):
263
+ _raise_invalid()
264
+
265
+ seen_keys.add(key)
266
+ members.append(f"{key}={value}")
267
+ if len(members) > _MAX_TRACESTATE_MEMBERS:
268
+ _raise_invalid()
269
+
270
+ if not members:
271
+ if allow_empty_members:
272
+ return None
273
+ _raise_invalid()
274
+
275
+ normalized = ",".join(members)
276
+ if len(normalized) > _MAX_TRACESTATE_LENGTH:
277
+ _raise_invalid()
278
+ return normalized
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: constellation-node-sdk
3
- Version: 1.2.0
3
+ Version: 1.3.2
4
4
  Summary: L9 Constellation Node SDK — TransportPacket runtime, Gate client, and node factory
5
5
  License: Proprietary
6
6
  Requires-Python: >=3.12
@@ -4,6 +4,7 @@ pyproject.toml
4
4
  src/constellation_node_sdk/__init__.py
5
5
  src/constellation_node_sdk/_packet_http.py
6
6
  src/constellation_node_sdk/py.typed
7
+ src/constellation_node_sdk/trace_context.py
7
8
  src/constellation_node_sdk.egg-info/PKG-INFO
8
9
  src/constellation_node_sdk.egg-info/SOURCES.txt
9
10
  src/constellation_node_sdk.egg-info/dependency_links.txt