blun-king-cli 9.1.597 → 9.1.600
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.
- package/CHANGELOG.md +39 -0
- package/README.md +17 -1
- package/agent-spine-plugin/CHANGELOG.md +1 -1
- package/agent-spine-plugin/docs/host-integration.md +1 -15
- package/agent-spine-plugin/docs/preflight-recall.md +1 -1
- package/agent-spine-plugin/scripts/check-install.js +1 -6
- package/agent-spine-plugin/scripts/hermetic-process.js +8 -0
- package/agent-spine-plugin/scripts/release-check.js +2 -3
- package/agent-spine-plugin/scripts/run-tests-hermetic.js +8 -11
- package/agent-spine-plugin/src/hook.js +40 -40
- package/agent-spine-plugin/src/lib/briefing.js +6 -3
- package/agent-spine-plugin/src/lib/hook-context.js +63 -64
- package/agent-spine-plugin/src/lib/hook-output.js +115 -94
- package/agent-spine-plugin/src/lib/hook-timeline.js +20 -11
- package/agent-spine-plugin/src/lib/host-instruction-budget.js +8 -14
- package/agent-spine-plugin/src/lib/mcp-timeline-tools.js +0 -7
- package/agent-spine-plugin/src/lib/pre-answer-timeline-recall.js +104 -0
- package/agent-spine-plugin/src/lib/session-timeline-codex.js +0 -4
- package/agent-spine-plugin/src/lib/session-timeline-enrollment-source.js +0 -3
- package/agent-spine-plugin/src/lib/session-timeline-enrollment-storage.js +0 -4
- package/agent-spine-plugin/src/lib/session-timeline-enrollment-transport.js +0 -2
- package/agent-spine-plugin/src/lib/session-timeline-enrollment.js +0 -22
- package/agent-spine-plugin/src/lib/session-timeline-host-origin.js +0 -4
- package/agent-spine-plugin/src/lib/session-timeline-prior.js +42 -50
- package/agent-spine-plugin/src/lib/session-timeline-provider.js +0 -3
- package/agent-spine-plugin/src/lib/session-timeline-query.js +0 -1
- package/agent-spine-plugin/src/lib/session-timeline-sid-acl.js +0 -4
- package/agent-spine-plugin/src/lib/session-timeline-source.js +0 -3
- package/agent-spine-plugin/src/lib/session-timeline-transport.js +0 -8
- package/agent-spine-plugin/src/lib/session-timeline-windows-acl.js +0 -3
- package/agent-spine-plugin/src/lib/session-timeline.js +370 -408
- package/agent-spine-plugin/src/lib/source-roots.js +26 -16
- package/agent-spine-plugin/src/lib/timeline-user-feedback.js +0 -1
- package/bin/core-bootstrap.js +18 -10
- package/bin/launcher-mode.js +5 -0
- package/bin/launcher-runtime.js +27 -0
- package/bin/verify-bundled-agent-sources.cjs +28 -2
- package/blun.mjs +798 -143
- package/bundled-agent-sources.json +192 -155
- package/package.json +1 -1
- package/standard-skills/translate-native/README.md +1334 -1
- package/standard-skills/translate-native/SKILL.md +1 -1
- package/standard-skills/translate-native/VERSION +1 -1
- package/standard-skills/translate-native/docs/PREMORTEM.md +901 -0
- package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION.md +208 -41
- package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_API.md +1319 -77
- package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_EVIDENCE_HTTP.md +66 -11
- package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_HTTP_PROVIDER.md +19 -1
- package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_RECEIPT_VERIFIER_HTTP.md +24 -6
- package/standard-skills/translate-native/integrations/commercial_localization_profile.py +23 -0
- package/standard-skills/translate-native/integrations/website_localization.py +234 -10
- package/standard-skills/translate-native/integrations/website_localization_benchmark.py +39 -3
- package/standard-skills/translate-native/integrations/website_localization_cms.py +512 -10
- package/standard-skills/translate-native/integrations/website_localization_cms_client.py +116 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_receiver.py +101 -11
- package/standard-skills/translate-native/integrations/website_localization_cms_receiver_runtime.py +12 -1
- package/standard-skills/translate-native/integrations/website_localization_cms_receiver_store.py +186 -24
- package/standard-skills/translate-native/integrations/website_localization_cms_source_client.py +64 -1
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery.py +528 -5
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_auth.py +918 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_auth_runtime.py +609 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_client.py +885 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_http.py +1244 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_runtime.py +220 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_sidecar_adapter.py +331 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_capabilities_http.py +444 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_client.py +673 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_dispatch.py +1317 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_dispatch_client.py +826 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_dispatch_http.py +1152 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_dispatch_openapi.py +896 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_dispatch_runtime.py +621 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_http.py +1180 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_runtime.py +1479 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_http.py +133 -17
- package/standard-skills/translate-native/integrations/website_localization_cms_source_runtime.py +319 -4
- package/standard-skills/translate-native/integrations/website_localization_cms_source_service.py +72 -2
- package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_http.py +13 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_receiver.py +56 -13
- package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_receiver_openapi.py +335 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_receiver_runtime.py +66 -1
- package/standard-skills/translate-native/integrations/website_localization_cms_terminal_processing_monitor.py +73 -7
- package/standard-skills/translate-native/integrations/website_localization_cms_terminal_receiver_client.py +93 -9
- package/standard-skills/translate-native/integrations/website_localization_evidence_http.py +221 -11
- package/standard-skills/translate-native/integrations/website_localization_health.py +6 -0
- package/standard-skills/translate-native/integrations/website_localization_http_provider.py +1 -1
- package/standard-skills/translate-native/integrations/website_localization_quality_profiles.py +350 -0
- package/standard-skills/translate-native/integrations/website_localization_queue.py +126 -56
- package/standard-skills/translate-native/integrations/website_localization_receipt_verifier_http.py +166 -13
- package/standard-skills/translate-native/integrations/website_localization_release.py +423 -17
- package/standard-skills/translate-native/integrations/website_localization_release_coordinator.py +168 -13
- package/standard-skills/translate-native/integrations/website_localization_runner.py +10 -0
- package/standard-skills/translate-native/integrations/website_localization_worker.py +213 -9
- package/standard-skills/translate-native/provenance.json +2 -2
- package/standard-skills/translate-native/references/commercial-localization.md +96 -17
- package/standard-skills/translate-native/scripts/blun_language_guard.py +3 -1
- package/standard-skills/translate-native/scripts/check_commercial_review.py +69 -9
- package/standard-skills/translate-native/scripts/commercial_localization_profile.py +829 -31
- package/standard-tools/language-guard/VERSION +1 -1
- package/standard-tools/language-guard/blun_language_guard.py +3 -1
- package/standard-tools/language-guard/check_commercial_review.py +69 -9
- package/standard-tools/language-guard/commercial_localization_profile.py +829 -31
- package/standard-tools/language-guard/provenance.json +2 -2
- package/standard-tools/manifest.json +1 -1
- package/worker-host.mjs +215 -82
- package/agent-spine-plugin/scripts/check-install-king.js +0 -37
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Protected runtime and hosted composition for source-delivery HMAC auth."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import importlib.util
|
|
7
|
+
import math
|
|
8
|
+
import os
|
|
9
|
+
import sqlite3
|
|
10
|
+
import stat
|
|
11
|
+
import sys
|
|
12
|
+
import threading
|
|
13
|
+
import time
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Callable, Iterable, Mapping
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _load_module(name: str, path: Path):
|
|
19
|
+
spec = importlib.util.spec_from_file_location(name, path)
|
|
20
|
+
if spec is None or spec.loader is None:
|
|
21
|
+
raise RuntimeError(
|
|
22
|
+
f"cannot load source-delivery HMAC runtime dependency: {path.name}"
|
|
23
|
+
)
|
|
24
|
+
module = importlib.util.module_from_spec(spec)
|
|
25
|
+
sys.modules[spec.name] = module
|
|
26
|
+
spec.loader.exec_module(module)
|
|
27
|
+
return module
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
_ROOT = Path(__file__).resolve().parents[1]
|
|
31
|
+
_AUTH = _load_module(
|
|
32
|
+
"blun_website_localization_cms_source_delivery_auth_runtime_auth",
|
|
33
|
+
_ROOT / "integrations" / "website_localization_cms_source_delivery_auth.py",
|
|
34
|
+
)
|
|
35
|
+
_DELIVERY_RUNTIME = _load_module(
|
|
36
|
+
"blun_website_localization_cms_source_delivery_auth_runtime_delivery",
|
|
37
|
+
_ROOT / "integrations" / "website_localization_cms_source_delivery_runtime.py",
|
|
38
|
+
)
|
|
39
|
+
HMACCredential = _AUTH.HMACCredential
|
|
40
|
+
RotatingSourceDeliveryHMACSigner = _AUTH.RotatingSourceDeliveryHMACSigner
|
|
41
|
+
RotatingHMACCMSSourceDeliveryClient = (
|
|
42
|
+
_AUTH.RotatingHMACCMSSourceDeliveryClient
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class SourceDeliveryHMACRuntimeBlocked(RuntimeError):
|
|
47
|
+
"""Stable runtime failure without paths, secrets, or private exceptions."""
|
|
48
|
+
|
|
49
|
+
def __init__(self, code: str):
|
|
50
|
+
super().__init__(code)
|
|
51
|
+
self.code = code
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _blocked(code: str) -> SourceDeliveryHMACRuntimeBlocked:
|
|
55
|
+
return SourceDeliveryHMACRuntimeBlocked("source_delivery_hmac_runtime." + code)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _database_path(value: Any) -> str:
|
|
59
|
+
try:
|
|
60
|
+
result = os.fspath(value)
|
|
61
|
+
except TypeError:
|
|
62
|
+
raise _blocked("database_path_invalid") from None
|
|
63
|
+
if isinstance(result, bytes):
|
|
64
|
+
raise _blocked("database_path_invalid")
|
|
65
|
+
if (
|
|
66
|
+
not isinstance(result, str)
|
|
67
|
+
or not result
|
|
68
|
+
or "\x00" in result
|
|
69
|
+
or result.startswith("file:")
|
|
70
|
+
):
|
|
71
|
+
raise _blocked("database_path_invalid")
|
|
72
|
+
if result == ":memory:":
|
|
73
|
+
return result
|
|
74
|
+
if (
|
|
75
|
+
os.name != "posix"
|
|
76
|
+
or not os.path.isabs(result)
|
|
77
|
+
or os.path.normpath(result) != result
|
|
78
|
+
):
|
|
79
|
+
raise _blocked("database_path_invalid")
|
|
80
|
+
return result
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _sqlite_timeout(value: Any) -> float:
|
|
84
|
+
if (
|
|
85
|
+
isinstance(value, bool)
|
|
86
|
+
or not isinstance(value, (int, float))
|
|
87
|
+
or not math.isfinite(float(value))
|
|
88
|
+
or not 0 < float(value) <= 60
|
|
89
|
+
):
|
|
90
|
+
raise _blocked("sqlite_timeout_invalid")
|
|
91
|
+
return float(value)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _validate_parent(database_path: str) -> None:
|
|
95
|
+
current = os.path.dirname(database_path)
|
|
96
|
+
first = True
|
|
97
|
+
while True:
|
|
98
|
+
try:
|
|
99
|
+
current_stat = os.lstat(current)
|
|
100
|
+
except OSError:
|
|
101
|
+
raise _blocked("database_parent_unavailable") from None
|
|
102
|
+
mode = stat.S_IMODE(current_stat.st_mode)
|
|
103
|
+
sticky_root_directory = (
|
|
104
|
+
current_stat.st_uid == 0
|
|
105
|
+
and bool(mode & stat.S_ISVTX)
|
|
106
|
+
and bool(mode & 0o002)
|
|
107
|
+
)
|
|
108
|
+
if (
|
|
109
|
+
not stat.S_ISDIR(current_stat.st_mode)
|
|
110
|
+
or current_stat.st_uid not in {0, os.geteuid()}
|
|
111
|
+
or (first and current_stat.st_uid != os.geteuid())
|
|
112
|
+
or (mode & 0o022 and not sticky_root_directory)
|
|
113
|
+
):
|
|
114
|
+
raise _blocked("database_parent_unsafe")
|
|
115
|
+
parent = os.path.dirname(current)
|
|
116
|
+
if parent == current:
|
|
117
|
+
break
|
|
118
|
+
current = parent
|
|
119
|
+
first = False
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _validate_file(
|
|
123
|
+
database_path: str,
|
|
124
|
+
expected_identity: tuple[int, int] | None = None,
|
|
125
|
+
) -> tuple[int, int]:
|
|
126
|
+
try:
|
|
127
|
+
database_stat = os.lstat(database_path)
|
|
128
|
+
except FileNotFoundError:
|
|
129
|
+
raise
|
|
130
|
+
except OSError:
|
|
131
|
+
raise _blocked("database_unavailable") from None
|
|
132
|
+
identity = (database_stat.st_dev, database_stat.st_ino)
|
|
133
|
+
if (
|
|
134
|
+
not stat.S_ISREG(database_stat.st_mode)
|
|
135
|
+
or database_stat.st_uid != os.geteuid()
|
|
136
|
+
or database_stat.st_nlink != 1
|
|
137
|
+
or stat.S_IMODE(database_stat.st_mode) != 0o600
|
|
138
|
+
or (expected_identity is not None and identity != expected_identity)
|
|
139
|
+
):
|
|
140
|
+
raise _blocked("database_unsafe")
|
|
141
|
+
return identity
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _prepare_database_file(database_path: str) -> Callable[[], None]:
|
|
145
|
+
if database_path == ":memory:":
|
|
146
|
+
return lambda: None
|
|
147
|
+
_validate_parent(database_path)
|
|
148
|
+
try:
|
|
149
|
+
identity = _validate_file(database_path)
|
|
150
|
+
except FileNotFoundError:
|
|
151
|
+
flags = os.O_CREAT | os.O_EXCL | os.O_RDWR
|
|
152
|
+
flags |= getattr(os, "O_CLOEXEC", 0)
|
|
153
|
+
flags |= getattr(os, "O_NOFOLLOW", 0)
|
|
154
|
+
try:
|
|
155
|
+
descriptor = os.open(database_path, flags, 0o600)
|
|
156
|
+
except FileExistsError:
|
|
157
|
+
identity = _validate_file(database_path)
|
|
158
|
+
except OSError:
|
|
159
|
+
raise _blocked("database_create_failed") from None
|
|
160
|
+
else:
|
|
161
|
+
try:
|
|
162
|
+
os.fchmod(descriptor, 0o600)
|
|
163
|
+
created_stat = os.fstat(descriptor)
|
|
164
|
+
identity = (created_stat.st_dev, created_stat.st_ino)
|
|
165
|
+
if (
|
|
166
|
+
not stat.S_ISREG(created_stat.st_mode)
|
|
167
|
+
or created_stat.st_uid != os.geteuid()
|
|
168
|
+
or created_stat.st_nlink != 1
|
|
169
|
+
or stat.S_IMODE(created_stat.st_mode) != 0o600
|
|
170
|
+
):
|
|
171
|
+
raise _blocked("database_create_failed")
|
|
172
|
+
finally:
|
|
173
|
+
os.close(descriptor)
|
|
174
|
+
|
|
175
|
+
def guard() -> None:
|
|
176
|
+
_validate_parent(database_path)
|
|
177
|
+
try:
|
|
178
|
+
_validate_file(database_path, identity)
|
|
179
|
+
except FileNotFoundError:
|
|
180
|
+
raise _blocked("database_unavailable") from None
|
|
181
|
+
|
|
182
|
+
guard()
|
|
183
|
+
return guard
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _same_database_identity(first_path: str, second_path: str) -> bool:
|
|
187
|
+
if ":memory:" in {first_path, second_path}:
|
|
188
|
+
return False
|
|
189
|
+
try:
|
|
190
|
+
first = os.lstat(first_path)
|
|
191
|
+
second = os.lstat(second_path)
|
|
192
|
+
except OSError:
|
|
193
|
+
raise _blocked("database_identity_unavailable") from None
|
|
194
|
+
return (first.st_dev, first.st_ino) == (second.st_dev, second.st_ino)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _credential_values(
|
|
198
|
+
credentials: Iterable[Any],
|
|
199
|
+
) -> tuple[Any, ...]:
|
|
200
|
+
try:
|
|
201
|
+
values = tuple(credentials)
|
|
202
|
+
except Exception:
|
|
203
|
+
raise _blocked("configuration_invalid") from None
|
|
204
|
+
if not values:
|
|
205
|
+
raise _blocked("configuration_invalid")
|
|
206
|
+
return values
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _preflight_authentication(
|
|
210
|
+
credentials: tuple[Any, ...],
|
|
211
|
+
*,
|
|
212
|
+
origin: str,
|
|
213
|
+
sidecar_capabilities_sha256: str,
|
|
214
|
+
remote_capabilities_sha256: str,
|
|
215
|
+
clock: Callable[[], float | int],
|
|
216
|
+
max_age_seconds: float | int,
|
|
217
|
+
future_skew_seconds: float | int,
|
|
218
|
+
allow_loopback_http: bool,
|
|
219
|
+
) -> None:
|
|
220
|
+
connection = sqlite3.connect(":memory:", check_same_thread=False)
|
|
221
|
+
try:
|
|
222
|
+
store = _AUTH.DurableHMACReplayStore(connection)
|
|
223
|
+
_AUTH.SourceDeliveryHMACVerifier(
|
|
224
|
+
credentials,
|
|
225
|
+
store,
|
|
226
|
+
origin=origin,
|
|
227
|
+
sidecar_capabilities_sha256=sidecar_capabilities_sha256,
|
|
228
|
+
remote_capabilities_sha256=remote_capabilities_sha256,
|
|
229
|
+
clock=clock,
|
|
230
|
+
max_age_seconds=max_age_seconds,
|
|
231
|
+
future_skew_seconds=future_skew_seconds,
|
|
232
|
+
allow_loopback_http=allow_loopback_http,
|
|
233
|
+
)
|
|
234
|
+
except Exception as error:
|
|
235
|
+
raise _blocked("configuration_invalid") from error
|
|
236
|
+
finally:
|
|
237
|
+
connection.close()
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class DurableSourceDeliveryHMACRuntime:
|
|
241
|
+
"""Own one guarded replay ledger and its request verifier."""
|
|
242
|
+
|
|
243
|
+
def __init__(
|
|
244
|
+
self,
|
|
245
|
+
connection: sqlite3.Connection,
|
|
246
|
+
guard: Callable[[], None],
|
|
247
|
+
store: Any,
|
|
248
|
+
verifier: Any,
|
|
249
|
+
verifier_configuration: Mapping[str, Any],
|
|
250
|
+
):
|
|
251
|
+
self._connection = connection
|
|
252
|
+
self._guard = guard
|
|
253
|
+
self._store = store
|
|
254
|
+
self._verifier = verifier
|
|
255
|
+
self._verifier_configuration = dict(verifier_configuration)
|
|
256
|
+
self._lock = threading.RLock()
|
|
257
|
+
self._owner_pid = os.getpid()
|
|
258
|
+
self._closed = False
|
|
259
|
+
|
|
260
|
+
def __repr__(self) -> str:
|
|
261
|
+
return f"DurableSourceDeliveryHMACRuntime(state={self.state!r})"
|
|
262
|
+
|
|
263
|
+
def __enter__(self) -> "DurableSourceDeliveryHMACRuntime":
|
|
264
|
+
self._assert_owner()
|
|
265
|
+
if self._closed:
|
|
266
|
+
raise _blocked("closed")
|
|
267
|
+
return self
|
|
268
|
+
|
|
269
|
+
def __exit__(self, *_exc: Any) -> None:
|
|
270
|
+
self.close()
|
|
271
|
+
|
|
272
|
+
def _assert_owner(self) -> None:
|
|
273
|
+
if os.getpid() != self._owner_pid:
|
|
274
|
+
raise _blocked("foreign_process")
|
|
275
|
+
|
|
276
|
+
def _call(self, callback: Callable[[], Any]) -> Any:
|
|
277
|
+
self._assert_owner()
|
|
278
|
+
with self._lock:
|
|
279
|
+
if self._closed:
|
|
280
|
+
raise _blocked("closed")
|
|
281
|
+
self._guard()
|
|
282
|
+
try:
|
|
283
|
+
return callback()
|
|
284
|
+
finally:
|
|
285
|
+
self._guard()
|
|
286
|
+
|
|
287
|
+
def __call__(self, request: Mapping[str, Any]) -> Mapping[str, str] | None:
|
|
288
|
+
return self._call(lambda: self._verifier(request))
|
|
289
|
+
|
|
290
|
+
def replace_credentials(self, credentials: Iterable[Any]) -> None:
|
|
291
|
+
"""Atomically replace accepted generations without resetting replay state."""
|
|
292
|
+
|
|
293
|
+
self._assert_owner()
|
|
294
|
+
values = _credential_values(credentials)
|
|
295
|
+
with self._lock:
|
|
296
|
+
if self._closed:
|
|
297
|
+
raise _blocked("closed")
|
|
298
|
+
self._guard()
|
|
299
|
+
try:
|
|
300
|
+
replacement = _AUTH.SourceDeliveryHMACVerifier(
|
|
301
|
+
values,
|
|
302
|
+
self._store,
|
|
303
|
+
**self._verifier_configuration,
|
|
304
|
+
)
|
|
305
|
+
except Exception as error:
|
|
306
|
+
raise _blocked("configuration_invalid") from error
|
|
307
|
+
self._guard()
|
|
308
|
+
self._verifier = replacement
|
|
309
|
+
|
|
310
|
+
def health(self) -> Mapping[str, Any]:
|
|
311
|
+
try:
|
|
312
|
+
store_health = self._call(self._store.health)
|
|
313
|
+
if (
|
|
314
|
+
not isinstance(store_health, Mapping)
|
|
315
|
+
or store_health.get("status") != "ok"
|
|
316
|
+
or not isinstance(store_health.get("consumed_nonces"), int)
|
|
317
|
+
or isinstance(store_health.get("consumed_nonces"), bool)
|
|
318
|
+
or store_health["consumed_nonces"] < 0
|
|
319
|
+
):
|
|
320
|
+
raise _blocked("replay_store_blocked")
|
|
321
|
+
return {
|
|
322
|
+
"schema": "blun.cms-source-delivery-hmac-runtime-health.v1",
|
|
323
|
+
"status": "ok",
|
|
324
|
+
"runtime_state": "open",
|
|
325
|
+
"consumed_nonces": store_health["consumed_nonces"],
|
|
326
|
+
"error_code": None,
|
|
327
|
+
}
|
|
328
|
+
except Exception as error:
|
|
329
|
+
code = getattr(error, "code", None)
|
|
330
|
+
if not (
|
|
331
|
+
isinstance(code, str)
|
|
332
|
+
and code.startswith((
|
|
333
|
+
"source_delivery_hmac_runtime.",
|
|
334
|
+
"source_delivery_hmac.",
|
|
335
|
+
))
|
|
336
|
+
):
|
|
337
|
+
code = "source_delivery_hmac_runtime.health_unavailable"
|
|
338
|
+
return {
|
|
339
|
+
"schema": "blun.cms-source-delivery-hmac-runtime-health.v1",
|
|
340
|
+
"status": "blocked",
|
|
341
|
+
"runtime_state": self.state,
|
|
342
|
+
"consumed_nonces": None,
|
|
343
|
+
"error_code": code,
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
def close(self) -> None:
|
|
347
|
+
self._assert_owner()
|
|
348
|
+
with self._lock:
|
|
349
|
+
if self._closed:
|
|
350
|
+
return
|
|
351
|
+
try:
|
|
352
|
+
self._connection.close()
|
|
353
|
+
except sqlite3.Error as error:
|
|
354
|
+
raise _blocked("close_failed") from error
|
|
355
|
+
self._closed = True
|
|
356
|
+
|
|
357
|
+
@property
|
|
358
|
+
def state(self) -> str:
|
|
359
|
+
if os.getpid() != self._owner_pid:
|
|
360
|
+
return "foreign-process"
|
|
361
|
+
with self._lock:
|
|
362
|
+
return "closed" if self._closed else "open"
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def open_durable_source_delivery_hmac_runtime(
|
|
366
|
+
replay_database: str | os.PathLike[str],
|
|
367
|
+
credentials: Iterable[Any],
|
|
368
|
+
*,
|
|
369
|
+
origin: str,
|
|
370
|
+
sidecar_capabilities_sha256: str,
|
|
371
|
+
remote_capabilities_sha256: str,
|
|
372
|
+
clock: Callable[[], float | int] = time.time,
|
|
373
|
+
max_age_seconds: float | int = 60,
|
|
374
|
+
future_skew_seconds: float | int = 5,
|
|
375
|
+
sqlite_timeout_seconds: float | int = 5,
|
|
376
|
+
allow_loopback_http: bool = False,
|
|
377
|
+
) -> DurableSourceDeliveryHMACRuntime:
|
|
378
|
+
"""Preflight configuration, then open one private replay ledger."""
|
|
379
|
+
|
|
380
|
+
if not isinstance(allow_loopback_http, bool):
|
|
381
|
+
raise _blocked("configuration_invalid")
|
|
382
|
+
path = _database_path(replay_database)
|
|
383
|
+
timeout = _sqlite_timeout(sqlite_timeout_seconds)
|
|
384
|
+
values = _credential_values(credentials)
|
|
385
|
+
_preflight_authentication(
|
|
386
|
+
values,
|
|
387
|
+
origin=origin,
|
|
388
|
+
sidecar_capabilities_sha256=sidecar_capabilities_sha256,
|
|
389
|
+
remote_capabilities_sha256=remote_capabilities_sha256,
|
|
390
|
+
clock=clock,
|
|
391
|
+
max_age_seconds=max_age_seconds,
|
|
392
|
+
future_skew_seconds=future_skew_seconds,
|
|
393
|
+
allow_loopback_http=allow_loopback_http,
|
|
394
|
+
)
|
|
395
|
+
guard = _prepare_database_file(path)
|
|
396
|
+
connection = None
|
|
397
|
+
try:
|
|
398
|
+
connection = sqlite3.connect(
|
|
399
|
+
path,
|
|
400
|
+
timeout=timeout,
|
|
401
|
+
isolation_level=None,
|
|
402
|
+
check_same_thread=False,
|
|
403
|
+
)
|
|
404
|
+
guard()
|
|
405
|
+
store = _AUTH.DurableHMACReplayStore(connection)
|
|
406
|
+
verifier_configuration = {
|
|
407
|
+
"origin": origin,
|
|
408
|
+
"sidecar_capabilities_sha256": sidecar_capabilities_sha256,
|
|
409
|
+
"remote_capabilities_sha256": remote_capabilities_sha256,
|
|
410
|
+
"clock": clock,
|
|
411
|
+
"max_age_seconds": max_age_seconds,
|
|
412
|
+
"future_skew_seconds": future_skew_seconds,
|
|
413
|
+
"allow_loopback_http": allow_loopback_http,
|
|
414
|
+
}
|
|
415
|
+
verifier = _AUTH.SourceDeliveryHMACVerifier(
|
|
416
|
+
values, store, **verifier_configuration,
|
|
417
|
+
)
|
|
418
|
+
guard()
|
|
419
|
+
except Exception as error:
|
|
420
|
+
if connection is not None:
|
|
421
|
+
try:
|
|
422
|
+
connection.close()
|
|
423
|
+
except sqlite3.Error:
|
|
424
|
+
pass
|
|
425
|
+
if isinstance(error, SourceDeliveryHMACRuntimeBlocked):
|
|
426
|
+
raise
|
|
427
|
+
raise _blocked("initialization_failed") from error
|
|
428
|
+
return DurableSourceDeliveryHMACRuntime(
|
|
429
|
+
connection, guard, store, verifier, verifier_configuration,
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
class HostedHMACAuthenticatedCMSSourceDelivery:
|
|
434
|
+
"""Own the hosted delivery runtime and its HMAC replay runtime."""
|
|
435
|
+
|
|
436
|
+
def __init__(self, delivery: Any, authentication: Any):
|
|
437
|
+
self.delivery = delivery
|
|
438
|
+
self.authentication = authentication
|
|
439
|
+
self.http = delivery.http
|
|
440
|
+
|
|
441
|
+
def __repr__(self) -> str:
|
|
442
|
+
return (
|
|
443
|
+
"HostedHMACAuthenticatedCMSSourceDelivery("
|
|
444
|
+
f"state={self.state!r}, worker_state={self.worker_state!r})"
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
def __enter__(self) -> "HostedHMACAuthenticatedCMSSourceDelivery":
|
|
448
|
+
if self.state != "open":
|
|
449
|
+
raise _blocked("closed")
|
|
450
|
+
return self
|
|
451
|
+
|
|
452
|
+
def __exit__(self, *_exc: Any) -> None:
|
|
453
|
+
self.close()
|
|
454
|
+
|
|
455
|
+
def enqueue_change(self, *args: Any, **kwargs: Any) -> Any:
|
|
456
|
+
return self.delivery.enqueue_change(*args, **kwargs)
|
|
457
|
+
|
|
458
|
+
def enqueue_removal(self, *args: Any, **kwargs: Any) -> Any:
|
|
459
|
+
return self.delivery.enqueue_removal(*args, **kwargs)
|
|
460
|
+
|
|
461
|
+
def status(self, *args: Any, **kwargs: Any) -> Any:
|
|
462
|
+
return self.delivery.status(*args, **kwargs)
|
|
463
|
+
|
|
464
|
+
def health(self) -> Any:
|
|
465
|
+
return self.delivery.health()
|
|
466
|
+
|
|
467
|
+
def worker_readiness(self) -> Mapping[str, Any]:
|
|
468
|
+
return self.delivery.worker_readiness()
|
|
469
|
+
|
|
470
|
+
def authentication_health(self) -> Mapping[str, Any]:
|
|
471
|
+
return self.authentication.health()
|
|
472
|
+
|
|
473
|
+
def replace_credentials(self, credentials: Iterable[Any]) -> None:
|
|
474
|
+
self.authentication.replace_credentials(credentials)
|
|
475
|
+
|
|
476
|
+
def close(self, *, worker_timeout_seconds: float | int = 30) -> None:
|
|
477
|
+
self.delivery.close(worker_timeout_seconds=worker_timeout_seconds)
|
|
478
|
+
self.authentication.close()
|
|
479
|
+
|
|
480
|
+
@property
|
|
481
|
+
def state(self) -> str:
|
|
482
|
+
states = {self.delivery.state, self.authentication.state}
|
|
483
|
+
if "foreign-process" in states:
|
|
484
|
+
return "foreign-process"
|
|
485
|
+
if states == {"closed"}:
|
|
486
|
+
return "closed"
|
|
487
|
+
if states == {"open"}:
|
|
488
|
+
return "open"
|
|
489
|
+
return "blocked"
|
|
490
|
+
|
|
491
|
+
@property
|
|
492
|
+
def worker_state(self) -> str:
|
|
493
|
+
return self.delivery.worker_state
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def open_hosted_hmac_authenticated_cms_source_delivery(
|
|
497
|
+
outbox_database: str | os.PathLike[str],
|
|
498
|
+
replay_database: str | os.PathLike[str],
|
|
499
|
+
client: Any,
|
|
500
|
+
credentials: Iterable[Any],
|
|
501
|
+
*,
|
|
502
|
+
worker_id: str,
|
|
503
|
+
origin: str,
|
|
504
|
+
sidecar_capabilities_sha256: str,
|
|
505
|
+
remote_capabilities_sha256: str,
|
|
506
|
+
clock: Callable[[], float | int] = time.time,
|
|
507
|
+
sqlite_timeout_seconds: float | int = 5,
|
|
508
|
+
replay_sqlite_timeout_seconds: float | int = 5,
|
|
509
|
+
lease_seconds: float | int = 600,
|
|
510
|
+
base_delay_seconds: float | int = 5,
|
|
511
|
+
max_delay_seconds: float | int = 300,
|
|
512
|
+
max_age_seconds: float | int = 60,
|
|
513
|
+
future_skew_seconds: float | int = 5,
|
|
514
|
+
active_delay_seconds: float | int = 0.05,
|
|
515
|
+
idle_delay_seconds: float | int = 1,
|
|
516
|
+
blocked_delay_seconds: float | int = 5,
|
|
517
|
+
allow_loopback_http: bool = False,
|
|
518
|
+
) -> HostedHMACAuthenticatedCMSSourceDelivery:
|
|
519
|
+
"""Open one hosted sidecar with a separately guarded replay ledger."""
|
|
520
|
+
|
|
521
|
+
if not isinstance(allow_loopback_http, bool):
|
|
522
|
+
raise _blocked("configuration_invalid")
|
|
523
|
+
try:
|
|
524
|
+
outbox_path = _DELIVERY_RUNTIME._database_path(outbox_database)
|
|
525
|
+
except Exception as error:
|
|
526
|
+
raise _blocked("configuration_invalid") from error
|
|
527
|
+
replay_path = _database_path(replay_database)
|
|
528
|
+
if outbox_path == replay_path:
|
|
529
|
+
raise _blocked("database_paths_conflict")
|
|
530
|
+
values = _credential_values(credentials)
|
|
531
|
+
_sqlite_timeout(replay_sqlite_timeout_seconds)
|
|
532
|
+
_preflight_authentication(
|
|
533
|
+
values,
|
|
534
|
+
origin=origin,
|
|
535
|
+
sidecar_capabilities_sha256=sidecar_capabilities_sha256,
|
|
536
|
+
remote_capabilities_sha256=remote_capabilities_sha256,
|
|
537
|
+
clock=clock,
|
|
538
|
+
max_age_seconds=max_age_seconds,
|
|
539
|
+
future_skew_seconds=future_skew_seconds,
|
|
540
|
+
allow_loopback_http=allow_loopback_http,
|
|
541
|
+
)
|
|
542
|
+
try:
|
|
543
|
+
_DELIVERY_RUNTIME._sqlite_timeout(sqlite_timeout_seconds)
|
|
544
|
+
_DELIVERY_RUNTIME.DurableCMSSourceDeliveryRuntime._loop_delays(
|
|
545
|
+
active_delay_seconds,
|
|
546
|
+
idle_delay_seconds,
|
|
547
|
+
blocked_delay_seconds,
|
|
548
|
+
)
|
|
549
|
+
_DELIVERY_RUNTIME._preflight(
|
|
550
|
+
client,
|
|
551
|
+
worker_id,
|
|
552
|
+
lease_seconds,
|
|
553
|
+
{
|
|
554
|
+
"clock": clock,
|
|
555
|
+
"base_delay_seconds": base_delay_seconds,
|
|
556
|
+
"max_delay_seconds": max_delay_seconds,
|
|
557
|
+
},
|
|
558
|
+
)
|
|
559
|
+
except Exception as error:
|
|
560
|
+
raise _blocked("configuration_invalid") from error
|
|
561
|
+
|
|
562
|
+
authentication = open_durable_source_delivery_hmac_runtime(
|
|
563
|
+
replay_path,
|
|
564
|
+
values,
|
|
565
|
+
origin=origin,
|
|
566
|
+
sidecar_capabilities_sha256=sidecar_capabilities_sha256,
|
|
567
|
+
remote_capabilities_sha256=remote_capabilities_sha256,
|
|
568
|
+
clock=clock,
|
|
569
|
+
max_age_seconds=max_age_seconds,
|
|
570
|
+
future_skew_seconds=future_skew_seconds,
|
|
571
|
+
sqlite_timeout_seconds=replay_sqlite_timeout_seconds,
|
|
572
|
+
allow_loopback_http=allow_loopback_http,
|
|
573
|
+
)
|
|
574
|
+
delivery = None
|
|
575
|
+
try:
|
|
576
|
+
delivery = _DELIVERY_RUNTIME.open_durable_cms_source_delivery(
|
|
577
|
+
outbox_path,
|
|
578
|
+
client,
|
|
579
|
+
worker_id=worker_id,
|
|
580
|
+
clock=clock,
|
|
581
|
+
sqlite_timeout_seconds=sqlite_timeout_seconds,
|
|
582
|
+
lease_seconds=lease_seconds,
|
|
583
|
+
base_delay_seconds=base_delay_seconds,
|
|
584
|
+
max_delay_seconds=max_delay_seconds,
|
|
585
|
+
http_authenticator=authentication,
|
|
586
|
+
)
|
|
587
|
+
if _same_database_identity(outbox_path, replay_path):
|
|
588
|
+
delivery.close()
|
|
589
|
+
authentication.close()
|
|
590
|
+
raise _blocked("database_paths_conflict")
|
|
591
|
+
delivery.start_worker(
|
|
592
|
+
active_delay_seconds=active_delay_seconds,
|
|
593
|
+
idle_delay_seconds=idle_delay_seconds,
|
|
594
|
+
blocked_delay_seconds=blocked_delay_seconds,
|
|
595
|
+
)
|
|
596
|
+
except Exception as error:
|
|
597
|
+
if delivery is not None and delivery.state == "open":
|
|
598
|
+
try:
|
|
599
|
+
delivery.close()
|
|
600
|
+
except Exception:
|
|
601
|
+
pass
|
|
602
|
+
if authentication.state == "open":
|
|
603
|
+
authentication.close()
|
|
604
|
+
if isinstance(error, SourceDeliveryHMACRuntimeBlocked):
|
|
605
|
+
raise
|
|
606
|
+
raise _blocked("delivery_initialization_failed") from error
|
|
607
|
+
return HostedHMACAuthenticatedCMSSourceDelivery(
|
|
608
|
+
delivery, authentication,
|
|
609
|
+
)
|