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.
- constellation_node_sdk-1.2.0/LICENSE +104 -0
- {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/PKG-INFO +37 -1
- {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/README.md +32 -0
- {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/pyproject.toml +3 -1
- {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/__init__.py +38 -0
- constellation_node_sdk-1.2.0/src/constellation_node_sdk/_packet_http.py +150 -0
- {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/gate/__init__.py +31 -1
- constellation_node_sdk-1.2.0/src/constellation_node_sdk/gate/admission.py +81 -0
- constellation_node_sdk-1.2.0/src/constellation_node_sdk/gate/capabilities.py +88 -0
- constellation_node_sdk-1.2.0/src/constellation_node_sdk/gate/capability_client.py +148 -0
- constellation_node_sdk-1.2.0/src/constellation_node_sdk/gate/client.py +576 -0
- constellation_node_sdk-1.2.0/src/constellation_node_sdk/gate/config.py +244 -0
- constellation_node_sdk-1.2.0/src/constellation_node_sdk/gate/errors.py +214 -0
- {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/gate/policy.py +8 -6
- constellation_node_sdk-1.2.0/src/constellation_node_sdk/gate/registration.py +354 -0
- constellation_node_sdk-1.2.0/src/constellation_node_sdk/gate_authority/__init__.py +47 -0
- constellation_node_sdk-1.2.0/src/constellation_node_sdk/gate_authority/config.py +77 -0
- constellation_node_sdk-1.2.0/src/constellation_node_sdk/gate_authority/dispatch.py +460 -0
- constellation_node_sdk-1.2.0/src/constellation_node_sdk/gate_authority/errors.py +133 -0
- {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/runtime/__init__.py +8 -0
- {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/runtime/app.py +129 -27
- {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/runtime/config.py +10 -4
- {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/runtime/errors.py +25 -17
- constellation_node_sdk-1.2.0/src/constellation_node_sdk/runtime/observability.py +191 -0
- constellation_node_sdk-1.2.0/src/constellation_node_sdk/runtime/participation.py +306 -0
- {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/transport/hashing.py +5 -3
- {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/transport/packet.py +4 -1
- {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk.egg-info/PKG-INFO +37 -1
- {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk.egg-info/SOURCES.txt +12 -1
- {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk.egg-info/requires.txt +2 -0
- constellation_node_sdk-1.2.0/tests/test_release_identity.py +553 -0
- constellation_node_sdk-1.0.1/src/constellation_node_sdk/gate/client.py +0 -108
- constellation_node_sdk-1.0.1/src/constellation_node_sdk/gate/config.py +0 -153
- constellation_node_sdk-1.0.1/src/constellation_node_sdk/gate/errors.py +0 -17
- constellation_node_sdk-1.0.1/src/constellation_node_sdk/gate/registration.py +0 -144
- constellation_node_sdk-1.0.1/src/constellation_node_sdk/runtime/observability.py +0 -72
- {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/setup.cfg +0 -0
- {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/orchestrator/__init__.py +0 -0
- {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/orchestrator/base.py +0 -0
- {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/orchestrator/merge.py +0 -0
- {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/orchestrator/packet_builder.py +0 -0
- {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/orchestrator/retry.py +0 -0
- {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/orchestrator/state.py +0 -0
- {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/orchestrator/step_executor.py +0 -0
- {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/py.typed +0 -0
- {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/runtime/execution.py +0 -0
- {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/runtime/handlers.py +0 -0
- {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/runtime/inbound_policy.py +0 -0
- {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/runtime/lifecycle.py +0 -0
- {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/runtime/preflight.py +0 -0
- {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/security/__init__.py +0 -0
- {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/security/delegation.py +0 -0
- {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/security/errors.py +0 -0
- {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/security/signing.py +0 -0
- {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/security/validation.py +0 -0
- {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/security/verification.py +0 -0
- {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/transport/__init__.py +0 -0
- {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/transport/codec.py +0 -0
- {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/transport/errors.py +0 -0
- {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/transport/hop_trace.py +0 -0
- {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/transport/lineage.py +0 -0
- {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/transport/models.py +0 -0
- {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/transport/provenance.py +0 -0
- {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/transport/tenant.py +0 -0
- {constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk.egg-info/dependency_links.txt +0 -0
- {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
|
|
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
|
|
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",
|
{constellation_node_sdk-1.0.1 → constellation_node_sdk-1.2.0}/src/constellation_node_sdk/__init__.py
RENAMED
|
@@ -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.
|
|
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
|
+
]
|