constellation-node-sdk 1.0.1__tar.gz → 1.2.0__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 (66) hide show
  1. constellation_node_sdk-1.2.0/LICENSE +104 -0
  2. {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/PKG-INFO +37 -1
  3. {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/README.md +32 -0
  4. {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/pyproject.toml +3 -1
  5. {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/__init__.py +38 -0
  6. constellation_node_sdk-1.2.0/src/constellation_node_sdk/_packet_http.py +150 -0
  7. {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/gate/__init__.py +31 -1
  8. constellation_node_sdk-1.2.0/src/constellation_node_sdk/gate/admission.py +81 -0
  9. constellation_node_sdk-1.2.0/src/constellation_node_sdk/gate/capabilities.py +88 -0
  10. constellation_node_sdk-1.2.0/src/constellation_node_sdk/gate/capability_client.py +148 -0
  11. constellation_node_sdk-1.2.0/src/constellation_node_sdk/gate/client.py +576 -0
  12. constellation_node_sdk-1.2.0/src/constellation_node_sdk/gate/config.py +244 -0
  13. constellation_node_sdk-1.2.0/src/constellation_node_sdk/gate/errors.py +214 -0
  14. {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/gate/policy.py +8 -6
  15. constellation_node_sdk-1.2.0/src/constellation_node_sdk/gate/registration.py +354 -0
  16. constellation_node_sdk-1.2.0/src/constellation_node_sdk/gate_authority/__init__.py +47 -0
  17. constellation_node_sdk-1.2.0/src/constellation_node_sdk/gate_authority/config.py +77 -0
  18. constellation_node_sdk-1.2.0/src/constellation_node_sdk/gate_authority/dispatch.py +460 -0
  19. constellation_node_sdk-1.2.0/src/constellation_node_sdk/gate_authority/errors.py +133 -0
  20. {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/runtime/__init__.py +8 -0
  21. {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/runtime/app.py +129 -27
  22. {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/runtime/config.py +10 -4
  23. {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/runtime/errors.py +25 -17
  24. constellation_node_sdk-1.2.0/src/constellation_node_sdk/runtime/observability.py +191 -0
  25. constellation_node_sdk-1.2.0/src/constellation_node_sdk/runtime/participation.py +306 -0
  26. {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/transport/hashing.py +5 -3
  27. {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/transport/packet.py +4 -1
  28. {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk.egg-info/PKG-INFO +37 -1
  29. {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk.egg-info/SOURCES.txt +12 -1
  30. {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk.egg-info/requires.txt +2 -0
  31. constellation_node_sdk-1.2.0/tests/test_release_identity.py +553 -0
  32. constellation_node_sdk-1.0.1/src/constellation_node_sdk/gate/client.py +0 -108
  33. constellation_node_sdk-1.0.1/src/constellation_node_sdk/gate/config.py +0 -153
  34. constellation_node_sdk-1.0.1/src/constellation_node_sdk/gate/errors.py +0 -17
  35. constellation_node_sdk-1.0.1/src/constellation_node_sdk/gate/registration.py +0 -144
  36. constellation_node_sdk-1.0.1/src/constellation_node_sdk/runtime/observability.py +0 -72
  37. {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/setup.cfg +0 -0
  38. {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/orchestrator/__init__.py +0 -0
  39. {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/orchestrator/base.py +0 -0
  40. {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/orchestrator/merge.py +0 -0
  41. {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/orchestrator/packet_builder.py +0 -0
  42. {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/orchestrator/retry.py +0 -0
  43. {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/orchestrator/state.py +0 -0
  44. {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/orchestrator/step_executor.py +0 -0
  45. {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/py.typed +0 -0
  46. {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/runtime/execution.py +0 -0
  47. {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/runtime/handlers.py +0 -0
  48. {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/runtime/inbound_policy.py +0 -0
  49. {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/runtime/lifecycle.py +0 -0
  50. {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/runtime/preflight.py +0 -0
  51. {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/security/__init__.py +0 -0
  52. {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/security/delegation.py +0 -0
  53. {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/security/errors.py +0 -0
  54. {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/security/signing.py +0 -0
  55. {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/security/validation.py +0 -0
  56. {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/security/verification.py +0 -0
  57. {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/transport/__init__.py +0 -0
  58. {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/transport/codec.py +0 -0
  59. {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/transport/errors.py +0 -0
  60. {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/transport/hop_trace.py +0 -0
  61. {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/transport/lineage.py +0 -0
  62. {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/transport/models.py +0 -0
  63. {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/transport/provenance.py +0 -0
  64. {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/transport/tenant.py +0 -0
  65. {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk.egg-info/dependency_links.txt +0 -0
  66. {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk.egg-info/top_level.txt +0 -0
@@ -0,0 +1,104 @@
1
+ QUANTUM AI PARTNERS — L9 PROPRIETARY SOFTWARE LICENSE
2
+ Version 1.0 — 2026
3
+
4
+ Copyright (c) 2026 Quantum AI Partners ("Licensor"). All rights reserved.
5
+
6
+ This software and associated documentation files (the "Software") are the
7
+ proprietary and confidential property of Quantum AI Partners. The Software
8
+ is made source-available on this repository for transparency, audit, and
9
+ evaluation purposes only, under the terms below. NO OPEN-SOURCE LICENSE IS
10
+ GRANTED. No prior license grant by Licensor (if any) shall be construed as
11
+ a waiver of these terms.
12
+
13
+ 1. DEFINITIONS
14
+
15
+ "Software" means the source code, object code, documentation, domain
16
+ specs, configuration, and all other files contained in this repository,
17
+ and any modifications or derivative works thereof.
18
+
19
+ "Commercial Use" means any use of the Software, in whole or in part,
20
+ directly or indirectly, from which any person or entity derives revenue,
21
+ profit, cost savings, competitive advantage, or other commercial benefit.
22
+ This includes, without limitation: selling, sublicensing, or hosting the
23
+ Software or a derivative work; incorporating the Software into a product
24
+ or service offered to third parties (including as part of a SaaS,
25
+ managed service, or consulting engagement); and internal use by a
26
+ for-profit entity in its business operations beyond internal evaluation.
27
+
28
+ "You" / "Licensee" means any individual or entity that accesses, clones,
29
+ copies, or otherwise makes use of the Software.
30
+
31
+ 2. LIMITED GRANT
32
+
33
+ Subject to Your compliance with this License, Licensor grants You a
34
+ limited, non-exclusive, non-transferable, revocable license to view,
35
+ clone, and use the Software solely for personal, academic, or internal
36
+ evaluation purposes that do NOT constitute Commercial Use.
37
+
38
+ 3. COMMERCIAL USE REQUIRES A PAID LICENSE
39
+
40
+ Any Commercial Use of the Software requires a separate written commercial
41
+ license agreement with Quantum AI Partners, negotiated in advance, which
42
+ may include license fees, royalties, or a revenue/profit share. Engaging
43
+ in Commercial Use without such an agreement is a material breach of this
44
+ License and constitutes copyright infringement.
45
+
46
+ To request a commercial license, contact: eng@l9.dev
47
+
48
+ 4. RESTRICTIONS
49
+
50
+ Except as expressly permitted under Section 2, You may NOT, without prior
51
+ written consent from Licensor:
52
+
53
+ a. Copy, reproduce, or redistribute the Software, in source or object
54
+ form, to any third party;
55
+ b. Modify, create derivative works of, reverse-engineer, or decompile
56
+ the Software, except as necessary for permitted evaluation;
57
+ c. Sublicense, sell, rent, lease, or otherwise transfer any rights in
58
+ the Software;
59
+ d. Host, deploy, or offer the Software (or a derivative work) as a
60
+ hosted or managed service to any third party;
61
+ e. Remove, obscure, or alter any copyright, trademark, or proprietary
62
+ notice contained in the Software;
63
+ f. Use the Software to build, train, or benchmark a directly competing
64
+ product or service.
65
+
66
+ 5. OWNERSHIP
67
+
68
+ The Software is licensed, not sold. Licensor retains all right, title,
69
+ and interest in and to the Software, including all intellectual property
70
+ rights therein. No rights are granted to You other than as expressly set
71
+ forth in this License.
72
+
73
+ 6. TERMINATION
74
+
75
+ This License terminates automatically, without notice, if You breach any
76
+ term of this License. Upon termination, You must cease all use of the
77
+ Software and destroy all copies in Your possession or control. Sections
78
+ 3, 5, 7, and 8 survive termination.
79
+
80
+ 7. NO WARRANTY
81
+
82
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
83
+ OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
84
+ FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT.
85
+
86
+ 8. LIMITATION OF LIABILITY
87
+
88
+ IN NO EVENT SHALL LICENSOR BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
89
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING
90
+ FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
91
+ DEALINGS IN THE SOFTWARE.
92
+
93
+ 9. GOVERNING LAW
94
+
95
+ This License shall be governed by the laws of the jurisdiction in which
96
+ Quantum AI Partners is organized, without regard to conflict-of-law
97
+ principles. [Confirm jurisdiction/venue with counsel before relying on
98
+ this in a dispute.]
99
+
100
+ ---
101
+ NOTE: This template was drafted to match your stated intent (restrict
102
+ copying, require payment for commercial/profit-driven use) but has not been
103
+ reviewed by an attorney. Have counsel review before relying on it in an
104
+ actual dispute or before this repository accepts external contributions.
@@ -1,10 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: constellation-node-sdk
3
- Version: 1.0.1
3
+ Version: 1.2.0
4
4
  Summary: L9 Constellation Node SDK — TransportPacket runtime, Gate client, and node factory
5
5
  License: Proprietary
6
6
  Requires-Python: >=3.12
7
7
  Description-Content-Type: text/markdown
8
+ License-File: LICENSE
8
9
  Requires-Dist: fastapi>=0.115.0
9
10
  Requires-Dist: uvicorn[standard]>=0.32.0
10
11
  Requires-Dist: pydantic>=2.9.0
@@ -14,12 +15,15 @@ Requires-Dist: prometheus-client>=0.21.0
14
15
  Requires-Dist: python-json-logger>=3.2.0
15
16
  Requires-Dist: pyyaml>=6.0.2
16
17
  Provides-Extra: dev
18
+ Requires-Dist: build>=1.2.0; extra == "dev"
17
19
  Requires-Dist: mypy>=1.11.0; extra == "dev"
18
20
  Requires-Dist: pytest>=8.3.0; extra == "dev"
19
21
  Requires-Dist: pytest-asyncio>=0.23.8; extra == "dev"
22
+ Requires-Dist: pytest-cov>=5.0.0; extra == "dev"
20
23
  Requires-Dist: ruff>=0.6.0; extra == "dev"
21
24
  Requires-Dist: types-PyYAML>=6.0.12; extra == "dev"
22
25
  Requires-Dist: types-requests; extra == "dev"
26
+ Dynamic: license-file
23
27
 
24
28
  # Constellation Node SDK
25
29
 
@@ -58,6 +62,38 @@ For development:
58
62
 
59
63
  pip install -e ".[dev]"
60
64
  Quick start
65
+ Application — calling Gate
66
+ from constellation_node_sdk import GateClient, GateClientConfig
67
+
68
+ client = GateClient(
69
+ GateClientConfig(
70
+ gate_url="https://gate.internal:8000",
71
+ local_node="erp",
72
+ timeout_seconds=30.0,
73
+ )
74
+ )
75
+
76
+ response = await client.execute(
77
+ action="converge",
78
+ payload={"entity": {"id": "org-4711"}},
79
+ tenant="tenant-a",
80
+ idempotency_key="erp:enrichment:run-4711",
81
+ timeout_ms=25_000,
82
+ )
83
+ That is the whole outbound integration. The application supplies business
84
+ inputs; the SDK owns packet construction, the Gate destination, the deadline,
85
+ transport idempotency, signing, HTTP, response validation, and typed failures.
86
+ An application never builds a TransportPacket. See docs/gate-client.md and
87
+ examples/application_client/.
88
+
89
+ Gate → worker (Constellation.Gate only)
90
+ After Gate resolves a worker, the transport to it is SDK-owned as well, through
91
+ constellation_node_sdk.gate_authority. That surface is deliberately absent from
92
+ the package root and from the gate package: it is not part of an application
93
+ integration, and it accepts only packets carrying Gate's own routing authority,
94
+ so it cannot be used as a node-to-peer client. See
95
+ docs/gate-authority-transport.md.
96
+
61
97
  Worker node
62
98
  from constellation_node_sdk import create_node_app, register_handler
63
99
 
@@ -35,6 +35,38 @@ For development:
35
35
 
36
36
  pip install -e ".[dev]"
37
37
  Quick start
38
+ Application — calling Gate
39
+ from constellation_node_sdk import GateClient, GateClientConfig
40
+
41
+ client = GateClient(
42
+ GateClientConfig(
43
+ gate_url="https://gate.internal:8000",
44
+ local_node="erp",
45
+ timeout_seconds=30.0,
46
+ )
47
+ )
48
+
49
+ response = await client.execute(
50
+ action="converge",
51
+ payload={"entity": {"id": "org-4711"}},
52
+ tenant="tenant-a",
53
+ idempotency_key="erp:enrichment:run-4711",
54
+ timeout_ms=25_000,
55
+ )
56
+ That is the whole outbound integration. The application supplies business
57
+ inputs; the SDK owns packet construction, the Gate destination, the deadline,
58
+ transport idempotency, signing, HTTP, response validation, and typed failures.
59
+ An application never builds a TransportPacket. See docs/gate-client.md and
60
+ examples/application_client/.
61
+
62
+ Gate → worker (Constellation.Gate only)
63
+ After Gate resolves a worker, the transport to it is SDK-owned as well, through
64
+ constellation_node_sdk.gate_authority. That surface is deliberately absent from
65
+ the package root and from the gate package: it is not part of an application
66
+ integration, and it accepts only packets carrying Gate's own routing authority,
67
+ so it cannot be used as a node-to-peer client. See
68
+ docs/gate-authority-transport.md.
69
+
38
70
  Worker node
39
71
  from constellation_node_sdk import create_node_app, register_handler
40
72
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "constellation-node-sdk"
7
- version = "1.0.1"
7
+ version = "1.2.0"
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"
@@ -23,9 +23,11 @@ dependencies = [
23
23
 
24
24
  [project.optional-dependencies]
25
25
  dev = [
26
+ "build>=1.2.0",
26
27
  "mypy>=1.11.0",
27
28
  "pytest>=8.3.0",
28
29
  "pytest-asyncio>=0.23.8",
30
+ "pytest-cov>=5.0.0",
29
31
  "ruff>=0.6.0",
30
32
  "types-PyYAML>=6.0.12",
31
33
  "types-requests",
@@ -1,24 +1,43 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from constellation_node_sdk.gate import (
4
+ ADMISSION_ACTION,
5
+ ConsumerAccessReceipt,
6
+ GateAuthorizationError,
4
7
  GateClient,
5
8
  GateClientConfig,
9
+ GateClientError,
10
+ GateConfigurationError,
11
+ GateConnectionError,
12
+ GateHTTPError,
13
+ GatePolicyError,
6
14
  GateRegistrationConfig,
15
+ GateRegistrationError,
16
+ GateResponseError,
17
+ GateSecurityError,
18
+ GateTimeoutError,
19
+ NodeRegistration,
20
+ activate_consumer,
7
21
  assert_gate_only_destination,
8
22
  assert_local_node_identity,
9
23
  assert_node_origin_packet,
24
+ build_node_registration,
10
25
  build_registration_payload,
11
26
  get_gate_client_config_from_env,
12
27
  get_gate_registration_config_from_env,
13
28
  load_node_spec,
14
29
  register_from_env,
30
+ register_node,
15
31
  register_with_gate,
16
32
  validate_outbound_gate_packet,
17
33
  )
18
34
  from constellation_node_sdk.runtime import (
19
35
  LifecycleHook,
36
+ NodeParticipation,
20
37
  NodeRuntimeConfig,
21
38
  NoOpLifecycle,
39
+ ParticipationState,
40
+ ParticipationStatus,
22
41
  PreflightFailure,
23
42
  clear_handlers,
24
43
  create_error_transport_packet,
@@ -65,12 +84,29 @@ from constellation_node_sdk.transport import (
65
84
  )
66
85
 
67
86
  __all__ = [
87
+ "ADMISSION_ACTION",
88
+ "ConsumerAccessReceipt",
89
+ "GateAuthorizationError",
90
+ "NodeParticipation",
91
+ "ParticipationState",
92
+ "ParticipationStatus",
93
+ "activate_consumer",
68
94
  "DelegationLink",
69
95
  "GateClient",
70
96
  "GateClientConfig",
97
+ "GateClientError",
98
+ "GateConfigurationError",
99
+ "GateConnectionError",
100
+ "GateHTTPError",
101
+ "GatePolicyError",
71
102
  "GateRegistrationConfig",
103
+ "GateRegistrationError",
104
+ "GateResponseError",
105
+ "GateSecurityError",
106
+ "GateTimeoutError",
72
107
  "LifecycleHook",
73
108
  "NoOpLifecycle",
109
+ "NodeRegistration",
74
110
  "NodeRuntimeConfig",
75
111
  "PreflightFailure",
76
112
  "RoutingProvenance",
@@ -93,6 +129,7 @@ __all__ = [
93
129
  "assert_gate_only_destination",
94
130
  "assert_local_node_identity",
95
131
  "assert_node_origin_packet",
132
+ "build_node_registration",
96
133
  "build_registration_payload",
97
134
  "canonical_json",
98
135
  "clear_handlers",
@@ -111,6 +148,7 @@ __all__ = [
111
148
  "recompute_transport_core",
112
149
  "register_from_env",
113
150
  "register_handler",
151
+ "register_node",
114
152
  "register_with_gate",
115
153
  "registered_actions",
116
154
  "run_preflight",
@@ -0,0 +1,150 @@
1
+ """
2
+ Canonical-packet HTTP machinery, shared by both sides of Gate.
3
+
4
+ There is exactly one implementation of "POST a canonical packet and decode a
5
+ canonical packet back". ``GateClient`` uses it for application → Gate, and the
6
+ Gate-authority dispatch transport uses it for Gate → resolved worker.
7
+
8
+ A second copy would be the real risk here: two implementations drift, and the
9
+ one that drifts is whichever is exercised less. The two callers differ only in
10
+ which exception types they raise, so the error classes are injected rather than
11
+ the code being duplicated.
12
+
13
+ This module is private. Nothing here is part of the public SDK surface.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from collections.abc import Callable
19
+ from dataclasses import dataclass
20
+ from typing import Any
21
+
22
+ import httpx
23
+
24
+ _MAX_BODY_CHARS = 2048
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class PacketTransportErrors:
29
+ """
30
+ The exception types one caller raises for each transport failure category.
31
+
32
+ ``timeout`` and ``http`` are constructed with keyword arguments
33
+ (``timeout_seconds``, ``status_code`` / ``response_text``); ``connection``
34
+ and ``response`` take a message alone.
35
+ """
36
+
37
+ # Exception *factories*, not plain classes: the timeout and http types take
38
+ # keyword arguments, which `type[Exception]` cannot express.
39
+ timeout: Callable[..., Exception]
40
+ connection: Callable[..., Exception]
41
+ http: Callable[..., Exception]
42
+ response: Callable[..., Exception]
43
+
44
+
45
+ def describe_exception(exc: BaseException) -> str:
46
+ """
47
+ Render an exception with its type, never relying on ``str(exc)`` alone.
48
+
49
+ httpx timeout exceptions frequently stringify to ``""``. A consumer that
50
+ logged only the message recorded a blank failure reason in production, so
51
+ the type name is always carried.
52
+ """
53
+ detail = str(exc)
54
+ return f"{type(exc).__name__}: {detail}" if detail else type(exc).__name__
55
+
56
+
57
+ async def post_packet_json(
58
+ *,
59
+ url: str,
60
+ json_body: dict[str, Any],
61
+ timeout_seconds: float,
62
+ errors: PacketTransportErrors,
63
+ what: str,
64
+ client: httpx.AsyncClient | None = None,
65
+ transport: httpx.AsyncBaseTransport | None = None,
66
+ headers: dict[str, str] | None = None,
67
+ params: dict[str, str] | None = None,
68
+ ) -> httpx.Response:
69
+ """
70
+ Perform exactly one POST and translate every httpx failure into a typed error.
71
+
72
+ There is no retry here, on either side of Gate. A failed request is returned
73
+ to the caller as a typed failure: replaying it is a decision that needs
74
+ semantic authority and a stable idempotency key, which transport does not
75
+ have.
76
+
77
+ When ``client`` is supplied the request reuses that client's connection
78
+ pool, and ``timeout_seconds`` is applied per request so a pooled client's
79
+ own default can never widen this call's deadline.
80
+ """
81
+ request_headers = {"Content-Type": "application/json"}
82
+ if headers:
83
+ request_headers.update(headers)
84
+
85
+ try:
86
+ if client is not None:
87
+ return await client.post(
88
+ url,
89
+ json=json_body,
90
+ headers=request_headers,
91
+ params=params,
92
+ timeout=timeout_seconds,
93
+ )
94
+ async with httpx.AsyncClient(timeout=timeout_seconds, transport=transport) as owned:
95
+ return await owned.post(
96
+ url,
97
+ json=json_body,
98
+ headers=request_headers,
99
+ params=params,
100
+ )
101
+ except httpx.TimeoutException as exc:
102
+ # Ordered before TransportError: httpx.TimeoutException subclasses it,
103
+ # so reversing these collapses "deadline elapsed" into "unreachable".
104
+ raise errors.timeout(
105
+ f"{what} did not respond within {timeout_seconds}s ({describe_exception(exc)})",
106
+ timeout_seconds=timeout_seconds,
107
+ ) from exc
108
+ except httpx.HTTPError as exc:
109
+ raise errors.connection(
110
+ f"could not reach {what} at {url} ({describe_exception(exc)})"
111
+ ) from exc
112
+
113
+
114
+ def raise_for_status(
115
+ response: httpx.Response,
116
+ *,
117
+ context: str,
118
+ errors: PacketTransportErrors,
119
+ ) -> None:
120
+ """Translate a non-success status into a typed error carrying the status."""
121
+ if response.is_success:
122
+ return
123
+ raise errors.http(
124
+ f"{context} returned HTTP {response.status_code}",
125
+ status_code=response.status_code,
126
+ response_text=response.text,
127
+ )
128
+
129
+
130
+ def decode_packet_body(
131
+ response: httpx.Response,
132
+ *,
133
+ context: str,
134
+ errors: PacketTransportErrors,
135
+ ) -> dict[str, Any]:
136
+ """Decode a JSON object body, or raise the caller's response error type."""
137
+ try:
138
+ body = response.json()
139
+ except ValueError as exc:
140
+ raise errors.response(
141
+ f"{context} was not decodable JSON ({describe_exception(exc)})"
142
+ ) from exc
143
+ if not isinstance(body, dict):
144
+ raise errors.response(f"{context} must be a JSON object, got {type(body).__name__}")
145
+ return body
146
+
147
+
148
+ def truncate_body(text: str | None) -> str | None:
149
+ """Bound a response body carried on an exception."""
150
+ return text[:_MAX_BODY_CHARS] if text is not None else None
@@ -1,6 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
- from constellation_node_sdk.gate.client import GateClient
3
+ from constellation_node_sdk.gate.admission import ADMISSION_ACTION, ConsumerAccessReceipt
4
+ from constellation_node_sdk.gate.capabilities import (
5
+ CapabilityDescriptor,
6
+ CapabilityListResponse,
7
+ )
8
+ from constellation_node_sdk.gate.capability_client import GateCapabilityClient
9
+ from constellation_node_sdk.gate.client import GateClient, activate_consumer
4
10
  from constellation_node_sdk.gate.config import (
5
11
  GateClientConfig,
6
12
  GateRegistrationConfig,
@@ -8,10 +14,16 @@ from constellation_node_sdk.gate.config import (
8
14
  get_gate_registration_config_from_env,
9
15
  )
10
16
  from constellation_node_sdk.gate.errors import (
17
+ GateAuthorizationError,
11
18
  GateClientError,
19
+ GateConfigurationError,
20
+ GateConnectionError,
21
+ GateHTTPError,
12
22
  GatePolicyError,
13
23
  GateRegistrationError,
14
24
  GateResponseError,
25
+ GateSecurityError,
26
+ GateTimeoutError,
15
27
  )
16
28
  from constellation_node_sdk.gate.policy import (
17
29
  assert_gate_only_destination,
@@ -20,28 +32,46 @@ from constellation_node_sdk.gate.policy import (
20
32
  validate_outbound_gate_packet,
21
33
  )
22
34
  from constellation_node_sdk.gate.registration import (
35
+ NodeRegistration,
36
+ build_node_registration,
23
37
  build_registration_payload,
24
38
  load_node_spec,
25
39
  register_from_env,
40
+ register_node,
26
41
  register_with_gate,
27
42
  )
28
43
 
29
44
  __all__ = [
45
+ "ADMISSION_ACTION",
46
+ "CapabilityDescriptor",
47
+ "CapabilityListResponse",
48
+ "ConsumerAccessReceipt",
49
+ "GateAuthorizationError",
50
+ "GateCapabilityClient",
30
51
  "GateClient",
31
52
  "GateClientConfig",
32
53
  "GateClientError",
54
+ "GateConfigurationError",
55
+ "GateConnectionError",
56
+ "GateHTTPError",
33
57
  "GatePolicyError",
34
58
  "GateRegistrationConfig",
35
59
  "GateRegistrationError",
36
60
  "GateResponseError",
61
+ "GateSecurityError",
62
+ "GateTimeoutError",
63
+ "NodeRegistration",
64
+ "activate_consumer",
37
65
  "assert_gate_only_destination",
38
66
  "assert_local_node_identity",
39
67
  "assert_node_origin_packet",
68
+ "build_node_registration",
40
69
  "build_registration_payload",
41
70
  "get_gate_client_config_from_env",
42
71
  "get_gate_registration_config_from_env",
43
72
  "load_node_spec",
44
73
  "register_from_env",
74
+ "register_node",
45
75
  "register_with_gate",
46
76
  "validate_outbound_gate_packet",
47
77
  ]
@@ -0,0 +1,81 @@
1
+ """
2
+ Consumer admission: the client half of "configured -> admitted -> usable".
3
+
4
+ Gate alone decides whether a signing key is admitted and which actions it may
5
+ invoke (its verifying keyring and per-key action scopes). A consumer used to
6
+ learn that only from a 403 on a real call. :meth:`GateClient.activate` asks Gate
7
+ up front with a signed probe to ``POST /v1/admission`` and returns Gate's answer
8
+ as a typed :class:`ConsumerAccessReceipt`. The SDK never grants anything.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import Any, Literal
14
+
15
+ from pydantic import BaseModel, ConfigDict, Field, ValidationError
16
+
17
+ from .errors import GateResponseError
18
+
19
+ #: Reserved action of the admission probe. Gate refuses it on /v1/execute and
20
+ #: refuses any node that tries to register it.
21
+ ADMISSION_ACTION = "gate.admission"
22
+ ADMISSION_PATH = "/v1/admission"
23
+ ADMISSION_SCHEMA = "l9.gate.admission.v1"
24
+
25
+
26
+ class ConsumerAccessReceipt(BaseModel):
27
+ """Gate's answer to an admission probe, as seen by the consumer."""
28
+
29
+ model_config = ConfigDict(frozen=True, extra="forbid")
30
+
31
+ gate_node: str
32
+ key_id: str | None
33
+ source_node: str
34
+ scope: Literal["restricted", "unrestricted"]
35
+ scoped_actions: tuple[str, ...] | None = None
36
+ granted_actions: tuple[str, ...] = Field(default_factory=tuple)
37
+ routable_actions: tuple[str, ...] = Field(default_factory=tuple)
38
+ required_actions: tuple[str, ...] = Field(default_factory=tuple)
39
+
40
+ @property
41
+ def missing_actions(self) -> tuple[str, ...]:
42
+ """Required actions Gate did not grant (empty when fully admitted)."""
43
+ granted = set(self.granted_actions)
44
+ return tuple(a for a in self.required_actions if a not in granted)
45
+
46
+ @property
47
+ def admitted(self) -> bool:
48
+ return not self.missing_actions
49
+
50
+ def permits(self, action: str) -> bool:
51
+ return action.strip().lower() in self.granted_actions
52
+
53
+
54
+ def parse_admission_payload(
55
+ payload: dict[str, Any], *, required_actions: tuple[str, ...]
56
+ ) -> ConsumerAccessReceipt:
57
+ """Validate Gate's admission payload into a receipt, or raise GateResponseError."""
58
+ if payload.get("schema") != ADMISSION_SCHEMA:
59
+ raise GateResponseError(
60
+ f"Gate admission response has schema {payload.get('schema')!r}, "
61
+ f"expected {ADMISSION_SCHEMA!r}",
62
+ body=payload,
63
+ )
64
+ fields = {k: v for k, v in payload.items() if k != "schema"}
65
+ try:
66
+ return ConsumerAccessReceipt.model_validate(
67
+ {**fields, "required_actions": required_actions}
68
+ )
69
+ except ValidationError as exc:
70
+ raise GateResponseError(
71
+ f"Gate admission response is malformed: {exc}", body=payload
72
+ ) from exc
73
+
74
+
75
+ __all__ = [
76
+ "ADMISSION_ACTION",
77
+ "ADMISSION_PATH",
78
+ "ADMISSION_SCHEMA",
79
+ "ConsumerAccessReceipt",
80
+ "parse_admission_payload",
81
+ ]