zubbl-runtime 0.1.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.
- zubbl_runtime-0.1.0/PKG-INFO +28 -0
- zubbl_runtime-0.1.0/README.md +17 -0
- zubbl_runtime-0.1.0/pyproject.toml +22 -0
- zubbl_runtime-0.1.0/setup.cfg +4 -0
- zubbl_runtime-0.1.0/src/zubbl_runtime/__init__.py +138 -0
- zubbl_runtime-0.1.0/src/zubbl_runtime/bootstrap.py +166 -0
- zubbl_runtime-0.1.0/src/zubbl_runtime/client.py +493 -0
- zubbl_runtime-0.1.0/src/zubbl_runtime/diagnostics.py +43 -0
- zubbl_runtime-0.1.0/src/zubbl_runtime/endpoint_catalogue.py +156 -0
- zubbl_runtime-0.1.0/src/zubbl_runtime/enforcement_result.py +197 -0
- zubbl_runtime-0.1.0/src/zubbl_runtime/errors.py +28 -0
- zubbl_runtime-0.1.0/src/zubbl_runtime/models.py +15 -0
- zubbl_runtime-0.1.0/src/zubbl_runtime/policy_cache.py +126 -0
- zubbl_runtime-0.1.0/src/zubbl_runtime/policy_evaluator.py +379 -0
- zubbl_runtime-0.1.0/src/zubbl_runtime/policy_manager.py +396 -0
- zubbl_runtime-0.1.0/src/zubbl_runtime/policy_models.py +264 -0
- zubbl_runtime-0.1.0/src/zubbl_runtime/policy_normalizer.py +427 -0
- zubbl_runtime-0.1.0/src/zubbl_runtime/rate_limit_store.py +151 -0
- zubbl_runtime-0.1.0/src/zubbl_runtime/resilience.py +18 -0
- zubbl_runtime-0.1.0/src/zubbl_runtime/route_matching.py +55 -0
- zubbl_runtime-0.1.0/src/zubbl_runtime/runtime_context.py +92 -0
- zubbl_runtime-0.1.0/src/zubbl_runtime/runtime_enforcer.py +368 -0
- zubbl_runtime-0.1.0/src/zubbl_runtime/signing.py +104 -0
- zubbl_runtime-0.1.0/src/zubbl_runtime/transport.py +176 -0
- zubbl_runtime-0.1.0/src/zubbl_runtime.egg-info/PKG-INFO +28 -0
- zubbl_runtime-0.1.0/src/zubbl_runtime.egg-info/SOURCES.txt +44 -0
- zubbl_runtime-0.1.0/src/zubbl_runtime.egg-info/dependency_links.txt +1 -0
- zubbl_runtime-0.1.0/src/zubbl_runtime.egg-info/requires.txt +5 -0
- zubbl_runtime-0.1.0/src/zubbl_runtime.egg-info/top_level.txt +1 -0
- zubbl_runtime-0.1.0/tests/test_bootstrap.py +87 -0
- zubbl_runtime-0.1.0/tests/test_client.py +699 -0
- zubbl_runtime-0.1.0/tests/test_diagnostics.py +221 -0
- zubbl_runtime-0.1.0/tests/test_endpoint_catalogue.py +342 -0
- zubbl_runtime-0.1.0/tests/test_enforcement_result.py +159 -0
- zubbl_runtime-0.1.0/tests/test_policy_cache.py +140 -0
- zubbl_runtime-0.1.0/tests/test_policy_evaluator.py +455 -0
- zubbl_runtime-0.1.0/tests/test_policy_evaluator_registry.py +108 -0
- zubbl_runtime-0.1.0/tests/test_policy_manager.py +533 -0
- zubbl_runtime-0.1.0/tests/test_policy_models.py +165 -0
- zubbl_runtime-0.1.0/tests/test_policy_normalizer.py +284 -0
- zubbl_runtime-0.1.0/tests/test_rate_limit_store.py +293 -0
- zubbl_runtime-0.1.0/tests/test_route_matching.py +45 -0
- zubbl_runtime-0.1.0/tests/test_runtime_context.py +85 -0
- zubbl_runtime-0.1.0/tests/test_runtime_enforcer.py +434 -0
- zubbl_runtime-0.1.0/tests/test_signing.py +132 -0
- zubbl_runtime-0.1.0/tests/test_transport.py +158 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: zubbl-runtime
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Core runtime client for Zubbl Runtime Guard
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: httpx>=0.27.0
|
|
8
|
+
Provides-Extra: test
|
|
9
|
+
Requires-Dist: pytest>=8.0; extra == "test"
|
|
10
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "test"
|
|
11
|
+
|
|
12
|
+
# Zubbl Runtime
|
|
13
|
+
|
|
14
|
+
Core Python runtime client for Zubbl Runtime Guard.
|
|
15
|
+
|
|
16
|
+
`zubbl-runtime` provides the shared runtime, bootstrap, signing, policy enforcement, resilience and transport components used by the Zubbl framework adapters.
|
|
17
|
+
|
|
18
|
+
## Requirements
|
|
19
|
+
|
|
20
|
+
- Python 3.10+
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install zubbl-runtime
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Most developers should install a framework adapter such as `zubbl-fastapi`, which installs this package automatically.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Zubbl Runtime
|
|
2
|
+
|
|
3
|
+
Core Python runtime client for Zubbl Runtime Guard.
|
|
4
|
+
|
|
5
|
+
`zubbl-runtime` provides the shared runtime, bootstrap, signing, policy enforcement, resilience and transport components used by the Zubbl framework adapters.
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- Python 3.10+
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install zubbl-runtime
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Most developers should install a framework adapter such as `zubbl-fastapi`, which installs this package automatically.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "zubbl-runtime"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Core runtime client for Zubbl Runtime Guard"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"httpx>=0.27.0",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[tool.setuptools.packages.find]
|
|
16
|
+
where = ["src"]
|
|
17
|
+
|
|
18
|
+
[project.optional-dependencies]
|
|
19
|
+
test = [
|
|
20
|
+
"pytest>=8.0",
|
|
21
|
+
"pytest-asyncio>=0.23",
|
|
22
|
+
]
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
from .bootstrap import BootstrapClient, BootstrapError
|
|
2
|
+
from .client import RuntimeClient, RuntimeClientError
|
|
3
|
+
from .diagnostics import RuntimeDiagnostics
|
|
4
|
+
from .endpoint_catalogue import (
|
|
5
|
+
EndpointCatalogueClient,
|
|
6
|
+
EndpointCatalogueError,
|
|
7
|
+
EndpointCatalogueSyncResult,
|
|
8
|
+
)
|
|
9
|
+
from .enforcement_result import (
|
|
10
|
+
EnforcementResult,
|
|
11
|
+
EnforcementResultBuilder,
|
|
12
|
+
)
|
|
13
|
+
from .models import BootstrapConfig
|
|
14
|
+
from .transport import SignedTransport, TransportError
|
|
15
|
+
from .runtime_context import RuntimeRequestContext
|
|
16
|
+
from .runtime_enforcer import (
|
|
17
|
+
RuntimeAction,
|
|
18
|
+
RuntimeDecision,
|
|
19
|
+
RuntimeEnforcer,
|
|
20
|
+
)
|
|
21
|
+
from .rate_limit_store import (
|
|
22
|
+
InMemoryRateLimitStore,
|
|
23
|
+
RateLimitKey,
|
|
24
|
+
RateLimitResult,
|
|
25
|
+
)
|
|
26
|
+
from .route_matching import (
|
|
27
|
+
compile_route_pattern,
|
|
28
|
+
normalise_route_path,
|
|
29
|
+
route_matches,
|
|
30
|
+
)
|
|
31
|
+
from .policy_manager import PolicyManager
|
|
32
|
+
from .policy_evaluator import (
|
|
33
|
+
EvaluationAction,
|
|
34
|
+
GeoEvaluationHandler,
|
|
35
|
+
PolicyEvaluation,
|
|
36
|
+
PolicyEvaluationHandler,
|
|
37
|
+
PolicyEvaluator,
|
|
38
|
+
PolicyEvaluatorRegistry,
|
|
39
|
+
RateLimitEvaluationHandler,
|
|
40
|
+
VpnEvaluationHandler,
|
|
41
|
+
default_policy_evaluator_registry,
|
|
42
|
+
)
|
|
43
|
+
from .policy_normalizer import (
|
|
44
|
+
AdaptiveInstruction,
|
|
45
|
+
CanonicalPolicyInstruction,
|
|
46
|
+
EnforcementMode,
|
|
47
|
+
GeoInstruction,
|
|
48
|
+
PolicyInstruction,
|
|
49
|
+
PolicyKind,
|
|
50
|
+
RateLimitInstruction,
|
|
51
|
+
UnknownPolicyInstruction,
|
|
52
|
+
VpnInstruction,
|
|
53
|
+
normalise_policy_tile,
|
|
54
|
+
normalise_policy_tiles,
|
|
55
|
+
)
|
|
56
|
+
from .policy_cache import PolicyCache
|
|
57
|
+
from .policy_models import (
|
|
58
|
+
DecisionSource,
|
|
59
|
+
EffectivePolicy,
|
|
60
|
+
PolicyCacheKey,
|
|
61
|
+
PolicyDecision,
|
|
62
|
+
PolicyResolution,
|
|
63
|
+
PolicyScope,
|
|
64
|
+
PolicySnapshot,
|
|
65
|
+
PolicyTile,
|
|
66
|
+
PolicyValidationError,
|
|
67
|
+
)
|
|
68
|
+
from .signing import (
|
|
69
|
+
build_canonical_request,
|
|
70
|
+
generate_nonce,
|
|
71
|
+
sha256_hex,
|
|
72
|
+
sign_request,
|
|
73
|
+
verify_signature,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
__all__ = [
|
|
77
|
+
"BootstrapClient",
|
|
78
|
+
"BootstrapConfig",
|
|
79
|
+
"BootstrapError",
|
|
80
|
+
"PolicyValidationError",
|
|
81
|
+
"PolicyTile",
|
|
82
|
+
"PolicySnapshot",
|
|
83
|
+
"PolicyScope",
|
|
84
|
+
"PolicyResolution",
|
|
85
|
+
"normalise_policy_tiles",
|
|
86
|
+
"normalise_policy_tile",
|
|
87
|
+
"VpnInstruction",
|
|
88
|
+
"UnknownPolicyInstruction",
|
|
89
|
+
"RateLimitInstruction",
|
|
90
|
+
"PolicyKind",
|
|
91
|
+
"PolicyInstruction",
|
|
92
|
+
"GeoInstruction",
|
|
93
|
+
"EnforcementMode",
|
|
94
|
+
"CanonicalPolicyInstruction",
|
|
95
|
+
"AdaptiveInstruction",
|
|
96
|
+
"default_policy_evaluator_registry",
|
|
97
|
+
"VpnEvaluationHandler",
|
|
98
|
+
"RateLimitEvaluationHandler",
|
|
99
|
+
"PolicyEvaluatorRegistry",
|
|
100
|
+
"PolicyEvaluationHandler",
|
|
101
|
+
"GeoEvaluationHandler",
|
|
102
|
+
"EnforcementResultBuilder",
|
|
103
|
+
"EndpointCatalogueSyncResult",
|
|
104
|
+
"EndpointCatalogueError",
|
|
105
|
+
"EndpointCatalogueClient",
|
|
106
|
+
"EnforcementResult",
|
|
107
|
+
"PolicyEvaluator",
|
|
108
|
+
"PolicyEvaluation",
|
|
109
|
+
"EvaluationAction",
|
|
110
|
+
"PolicyManager",
|
|
111
|
+
"PolicyDecision",
|
|
112
|
+
"PolicyCacheKey",
|
|
113
|
+
"PolicyCache",
|
|
114
|
+
"EffectivePolicy",
|
|
115
|
+
"DecisionSource",
|
|
116
|
+
"route_matches",
|
|
117
|
+
"normalise_route_path",
|
|
118
|
+
"compile_route_pattern",
|
|
119
|
+
"RateLimitResult",
|
|
120
|
+
"RateLimitKey",
|
|
121
|
+
"InMemoryRateLimitStore",
|
|
122
|
+
"RuntimeEnforcer",
|
|
123
|
+
"RuntimeDecision",
|
|
124
|
+
"RuntimeAction",
|
|
125
|
+
"RuntimeRequestContext",
|
|
126
|
+
"RuntimeDiagnostics",
|
|
127
|
+
"RuntimeClient",
|
|
128
|
+
"RuntimeClientError",
|
|
129
|
+
"SignedTransport",
|
|
130
|
+
"TransportError",
|
|
131
|
+
"build_canonical_request",
|
|
132
|
+
"generate_nonce",
|
|
133
|
+
"sha256_hex",
|
|
134
|
+
"sign_request",
|
|
135
|
+
"verify_signature",
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from .models import BootstrapConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BootstrapError(RuntimeError):
|
|
12
|
+
"""Raised when Runtime Guard bootstrap cannot be completed."""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(slots=True)
|
|
16
|
+
class _CachedBootstrap:
|
|
17
|
+
config: BootstrapConfig
|
|
18
|
+
expires_at: float
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BootstrapClient:
|
|
22
|
+
"""
|
|
23
|
+
Resolves Runtime Guard configuration from a Zubbl API key.
|
|
24
|
+
|
|
25
|
+
Bootstrap results are cached in memory only. The signing secret is never
|
|
26
|
+
persisted to disk by this client.
|
|
27
|
+
|
|
28
|
+
A shared HTTP client may be supplied by RuntimeClient. When no client is
|
|
29
|
+
supplied, standalone use retains the original per-request client lifecycle.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
api_key: str,
|
|
35
|
+
*,
|
|
36
|
+
base_url: str = "https://api.zubbl.com",
|
|
37
|
+
timeout_seconds: float = 8.0,
|
|
38
|
+
http_client: httpx.AsyncClient | None = None,
|
|
39
|
+
) -> None:
|
|
40
|
+
cleaned_api_key = api_key.strip()
|
|
41
|
+
|
|
42
|
+
if not cleaned_api_key:
|
|
43
|
+
raise ValueError("api_key must not be empty")
|
|
44
|
+
|
|
45
|
+
self._api_key = cleaned_api_key
|
|
46
|
+
self._base_url = base_url.rstrip("/")
|
|
47
|
+
self._timeout_seconds = timeout_seconds
|
|
48
|
+
self._http_client = http_client
|
|
49
|
+
self._cache: _CachedBootstrap | None = None
|
|
50
|
+
|
|
51
|
+
async def _post_bootstrap(
|
|
52
|
+
self,
|
|
53
|
+
url: str,
|
|
54
|
+
*,
|
|
55
|
+
headers: dict[str, str],
|
|
56
|
+
) -> httpx.Response:
|
|
57
|
+
if self._http_client is not None:
|
|
58
|
+
return await self._http_client.post(
|
|
59
|
+
url,
|
|
60
|
+
headers=headers,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Backwards-compatible standalone behaviour.
|
|
64
|
+
async with httpx.AsyncClient(
|
|
65
|
+
timeout=self._timeout_seconds,
|
|
66
|
+
) as client:
|
|
67
|
+
return await client.post(
|
|
68
|
+
url,
|
|
69
|
+
headers=headers,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
async def bootstrap(
|
|
73
|
+
self,
|
|
74
|
+
*,
|
|
75
|
+
force_refresh: bool = False,
|
|
76
|
+
) -> BootstrapConfig:
|
|
77
|
+
now = time.monotonic()
|
|
78
|
+
|
|
79
|
+
if (
|
|
80
|
+
not force_refresh
|
|
81
|
+
and self._cache is not None
|
|
82
|
+
and self._cache.expires_at > now
|
|
83
|
+
):
|
|
84
|
+
return self._cache.config
|
|
85
|
+
|
|
86
|
+
url = f"{self._base_url}/api/sdk/bootstrap"
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
response = await self._post_bootstrap(
|
|
90
|
+
url,
|
|
91
|
+
headers={
|
|
92
|
+
"Authorization": f"Bearer {self._api_key}",
|
|
93
|
+
"User-Agent": "zubbl-runtime/0.1.0",
|
|
94
|
+
},
|
|
95
|
+
)
|
|
96
|
+
except httpx.TimeoutException as exc:
|
|
97
|
+
raise BootstrapError(
|
|
98
|
+
"Zubbl bootstrap request timed out"
|
|
99
|
+
) from exc
|
|
100
|
+
except httpx.HTTPError as exc:
|
|
101
|
+
raise BootstrapError(
|
|
102
|
+
"Could not connect to Zubbl bootstrap endpoint"
|
|
103
|
+
) from exc
|
|
104
|
+
|
|
105
|
+
if response.status_code in {401, 403}:
|
|
106
|
+
raise BootstrapError(
|
|
107
|
+
"Zubbl API key is invalid, revoked or unavailable"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
if response.status_code != 200:
|
|
111
|
+
raise BootstrapError(
|
|
112
|
+
f"Zubbl bootstrap failed with HTTP "
|
|
113
|
+
f"{response.status_code}"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
payload = response.json()
|
|
118
|
+
|
|
119
|
+
config = BootstrapConfig(
|
|
120
|
+
tenant_id=str(payload["tenant_id"]),
|
|
121
|
+
app_id=str(payload["app_id"]),
|
|
122
|
+
signing_secret=str(payload["signing_secret"]),
|
|
123
|
+
runtime_version=str(payload["runtime_version"]),
|
|
124
|
+
telemetry_enabled=bool(payload["telemetry_enabled"]),
|
|
125
|
+
endpoint_discovery=bool(payload["endpoint_discovery"]),
|
|
126
|
+
cache_ttl_seconds=int(
|
|
127
|
+
payload["cache_ttl_seconds"]
|
|
128
|
+
),
|
|
129
|
+
polling_interval_seconds=int(
|
|
130
|
+
payload["polling_interval_seconds"]
|
|
131
|
+
),
|
|
132
|
+
protocol_version=int(payload["protocol_version"]),
|
|
133
|
+
runtime_fail_mode=str(
|
|
134
|
+
payload["runtime_fail_mode"]
|
|
135
|
+
),
|
|
136
|
+
)
|
|
137
|
+
except (KeyError, TypeError, ValueError) as exc:
|
|
138
|
+
raise BootstrapError(
|
|
139
|
+
"Zubbl bootstrap returned an invalid configuration"
|
|
140
|
+
) from exc
|
|
141
|
+
|
|
142
|
+
if (
|
|
143
|
+
not config.tenant_id
|
|
144
|
+
or not config.app_id
|
|
145
|
+
or not config.signing_secret
|
|
146
|
+
):
|
|
147
|
+
raise BootstrapError(
|
|
148
|
+
"Zubbl bootstrap configuration is incomplete"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
if config.runtime_fail_mode not in {"open", "closed"}:
|
|
152
|
+
raise BootstrapError(
|
|
153
|
+
"Zubbl bootstrap returned an invalid runtime_fail_mode"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
self._cache = _CachedBootstrap(
|
|
157
|
+
config=config,
|
|
158
|
+
expires_at=(
|
|
159
|
+
now + max(1, config.cache_ttl_seconds)
|
|
160
|
+
),
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
return config
|
|
164
|
+
|
|
165
|
+
def clear_cache(self) -> None:
|
|
166
|
+
self._cache = None
|