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,621 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Process-owned runtime for the durable public submission dispatcher.
|
|
3
|
+
|
|
4
|
+
The composition root validates the client, worker policy, and existing SQLite
|
|
5
|
+
generation before it creates or mutates durable state. One runtime owns one
|
|
6
|
+
connection, guards its file identity around every operation, serializes access,
|
|
7
|
+
and can supervise one non-daemon delivery worker.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import importlib.util
|
|
13
|
+
import math
|
|
14
|
+
import os
|
|
15
|
+
import sqlite3
|
|
16
|
+
import sys
|
|
17
|
+
import threading
|
|
18
|
+
import time
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any, Callable, Mapping
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _load_module(name: str, path: Path):
|
|
24
|
+
spec = importlib.util.spec_from_file_location(name, path)
|
|
25
|
+
if spec is None or spec.loader is None:
|
|
26
|
+
raise RuntimeError(f"cannot load submission runtime dependency: {path.name}")
|
|
27
|
+
module = importlib.util.module_from_spec(spec)
|
|
28
|
+
sys.modules[spec.name] = module
|
|
29
|
+
spec.loader.exec_module(module)
|
|
30
|
+
return module
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
_ROOT = Path(__file__).resolve().parents[1]
|
|
34
|
+
_DISPATCH = _load_module(
|
|
35
|
+
"blun_website_localization_submission_dispatch_runtime_store",
|
|
36
|
+
_ROOT
|
|
37
|
+
/ "integrations"
|
|
38
|
+
/ "website_localization_cms_source_delivery_submission_dispatch.py",
|
|
39
|
+
)
|
|
40
|
+
_FILE_RUNTIME = _load_module(
|
|
41
|
+
"blun_website_localization_submission_dispatch_runtime_file",
|
|
42
|
+
_ROOT / "integrations" / "website_localization_cms_source_delivery_runtime.py",
|
|
43
|
+
)
|
|
44
|
+
_HTTP = _load_module(
|
|
45
|
+
"blun_website_localization_submission_dispatch_runtime_http",
|
|
46
|
+
_ROOT
|
|
47
|
+
/ "integrations"
|
|
48
|
+
/ "website_localization_cms_source_delivery_submission_dispatch_http.py",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class CMSSourceDeliverySubmissionDispatchRuntimeBlocked(RuntimeError):
|
|
53
|
+
"""Stable runtime failure without paths, content, or private exceptions."""
|
|
54
|
+
|
|
55
|
+
def __init__(self, code: str):
|
|
56
|
+
super().__init__(code)
|
|
57
|
+
self.code = code
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _blocked(code: str) -> CMSSourceDeliverySubmissionDispatchRuntimeBlocked:
|
|
61
|
+
return CMSSourceDeliverySubmissionDispatchRuntimeBlocked(
|
|
62
|
+
"source_delivery_submission_dispatch_runtime." + code
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _store_failure(error: Exception) -> CMSSourceDeliverySubmissionDispatchRuntimeBlocked:
|
|
67
|
+
code = getattr(error, "code", "")
|
|
68
|
+
if code.endswith("idempotency_collision"):
|
|
69
|
+
return _blocked("idempotency_collision")
|
|
70
|
+
if code.endswith("submission_missing"):
|
|
71
|
+
return _blocked("submission_missing")
|
|
72
|
+
if code.endswith("lifecycle_not_accepted"):
|
|
73
|
+
return _blocked("lifecycle_not_accepted")
|
|
74
|
+
if code.endswith("commercial_profile_invalid"):
|
|
75
|
+
return _blocked("commercial_profile_invalid")
|
|
76
|
+
if code.endswith(("request_invalid", "attempts_invalid", "identity_invalid")):
|
|
77
|
+
return _blocked("request_invalid")
|
|
78
|
+
return _blocked("outbox_blocked")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _file_failure(error: Exception) -> CMSSourceDeliverySubmissionDispatchRuntimeBlocked:
|
|
82
|
+
code = getattr(error, "code", "")
|
|
83
|
+
suffix = code.rsplit(".", 1)[-1]
|
|
84
|
+
allowed = {
|
|
85
|
+
"database_path_invalid",
|
|
86
|
+
"database_parent_unavailable",
|
|
87
|
+
"database_parent_unsafe",
|
|
88
|
+
"database_unavailable",
|
|
89
|
+
"database_unsafe",
|
|
90
|
+
"database_create_failed",
|
|
91
|
+
"sqlite_timeout_invalid",
|
|
92
|
+
}
|
|
93
|
+
return _blocked(suffix if suffix in allowed else "database_blocked")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _duration(value: Any, code: str, *, maximum: float = 86_400) -> float:
|
|
97
|
+
if (
|
|
98
|
+
isinstance(value, bool)
|
|
99
|
+
or not isinstance(value, (int, float))
|
|
100
|
+
or not math.isfinite(float(value))
|
|
101
|
+
or not 0 < float(value) <= maximum
|
|
102
|
+
):
|
|
103
|
+
raise _blocked(code)
|
|
104
|
+
return float(value)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _now(clock: Callable[[], float | int]) -> float:
|
|
108
|
+
try:
|
|
109
|
+
value = clock()
|
|
110
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
111
|
+
raise ValueError
|
|
112
|
+
result = float(value)
|
|
113
|
+
if result < 0 or not math.isfinite(result):
|
|
114
|
+
raise ValueError
|
|
115
|
+
return result
|
|
116
|
+
except Exception:
|
|
117
|
+
raise _blocked("clock_invalid") from None
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _client_digest(client: Any) -> str:
|
|
121
|
+
value = getattr(client, "expected_capabilities_sha256", None)
|
|
122
|
+
if (
|
|
123
|
+
not callable(getattr(client, "submit_change", None))
|
|
124
|
+
or not callable(getattr(client, "submit_removal", None))
|
|
125
|
+
or not isinstance(value, str)
|
|
126
|
+
or _DISPATCH.SHA256.fullmatch(value) is None
|
|
127
|
+
):
|
|
128
|
+
raise _blocked("configuration_invalid")
|
|
129
|
+
return value
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _preflight(
|
|
133
|
+
client: Any,
|
|
134
|
+
worker_id: Any,
|
|
135
|
+
lease_seconds: Any,
|
|
136
|
+
clock: Callable[[], float | int],
|
|
137
|
+
options: Mapping[str, Any],
|
|
138
|
+
) -> tuple[str, float, tuple[tuple[Any, ...], ...]]:
|
|
139
|
+
if not callable(clock):
|
|
140
|
+
raise _blocked("configuration_invalid")
|
|
141
|
+
digest = _client_digest(client)
|
|
142
|
+
lease = _duration(lease_seconds, "lease_invalid")
|
|
143
|
+
now = _now(clock)
|
|
144
|
+
connection = sqlite3.connect(":memory:")
|
|
145
|
+
try:
|
|
146
|
+
dispatcher = _DISPATCH.DurableCMSSourceDeliverySubmissionDispatcher(
|
|
147
|
+
connection, digest, **dict(options)
|
|
148
|
+
)
|
|
149
|
+
dispatcher.run_once(client, worker_id, now=now, lease_seconds=lease)
|
|
150
|
+
dispatcher.health(now=now)
|
|
151
|
+
schema = tuple(tuple(row) for row in connection.execute(
|
|
152
|
+
"SELECT type, name, tbl_name, sql FROM sqlite_master "
|
|
153
|
+
"WHERE name NOT LIKE 'sqlite_%' ORDER BY type, name"
|
|
154
|
+
).fetchall())
|
|
155
|
+
except CMSSourceDeliverySubmissionDispatchRuntimeBlocked:
|
|
156
|
+
raise
|
|
157
|
+
except Exception as error:
|
|
158
|
+
raise _blocked("configuration_invalid") from error
|
|
159
|
+
finally:
|
|
160
|
+
connection.close()
|
|
161
|
+
return digest, lease, schema
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _preflight_existing(
|
|
165
|
+
path: str,
|
|
166
|
+
digest: str,
|
|
167
|
+
options: Mapping[str, Any],
|
|
168
|
+
expected_schema: tuple[tuple[Any, ...], ...],
|
|
169
|
+
now: float,
|
|
170
|
+
) -> None:
|
|
171
|
+
if path == ":memory:" or not os.path.exists(path):
|
|
172
|
+
return
|
|
173
|
+
connection = None
|
|
174
|
+
try:
|
|
175
|
+
connection = sqlite3.connect(
|
|
176
|
+
Path(path).as_uri() + "?mode=ro",
|
|
177
|
+
uri=True,
|
|
178
|
+
timeout=0,
|
|
179
|
+
isolation_level=None,
|
|
180
|
+
)
|
|
181
|
+
connection.row_factory = sqlite3.Row
|
|
182
|
+
actual = tuple(tuple(row) for row in connection.execute(
|
|
183
|
+
"SELECT type, name, tbl_name, sql FROM sqlite_master "
|
|
184
|
+
"WHERE name NOT LIKE 'sqlite_%' ORDER BY type, name"
|
|
185
|
+
).fetchall())
|
|
186
|
+
if not actual:
|
|
187
|
+
return
|
|
188
|
+
if actual != expected_schema:
|
|
189
|
+
if _DISPATCH._is_empty_legacy_v1(connection, digest):
|
|
190
|
+
return
|
|
191
|
+
raise _blocked("database_schema_altered")
|
|
192
|
+
dispatcher = object.__new__(
|
|
193
|
+
_DISPATCH.DurableCMSSourceDeliverySubmissionDispatcher
|
|
194
|
+
)
|
|
195
|
+
dispatcher.connection = connection
|
|
196
|
+
dispatcher.expected_capabilities_sha256 = digest
|
|
197
|
+
dispatcher.base_delay_seconds = float(options["base_delay_seconds"])
|
|
198
|
+
dispatcher.max_delay_seconds = float(options["max_delay_seconds"])
|
|
199
|
+
dispatcher.health(now=now)
|
|
200
|
+
except CMSSourceDeliverySubmissionDispatchRuntimeBlocked:
|
|
201
|
+
raise
|
|
202
|
+
except Exception as error:
|
|
203
|
+
if isinstance(error, _DISPATCH.CMSSourceDeliverySubmissionDispatchBlocked):
|
|
204
|
+
raise _blocked("database_generation_invalid") from error
|
|
205
|
+
raise _blocked("database_preflight_failed") from error
|
|
206
|
+
finally:
|
|
207
|
+
if connection is not None:
|
|
208
|
+
connection.close()
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class DurableCMSSourceDeliverySubmissionDispatchRuntime:
|
|
212
|
+
"""One process-owned, guarded, thread-safe caller submission runtime."""
|
|
213
|
+
|
|
214
|
+
def __init__(
|
|
215
|
+
self,
|
|
216
|
+
connection: sqlite3.Connection,
|
|
217
|
+
guard: Callable[[], None],
|
|
218
|
+
dispatcher: Any,
|
|
219
|
+
client: Any,
|
|
220
|
+
*,
|
|
221
|
+
worker_id: str,
|
|
222
|
+
lease_seconds: float,
|
|
223
|
+
clock: Callable[[], float | int],
|
|
224
|
+
expected_capabilities_sha256: str,
|
|
225
|
+
http_authenticator: Callable[[dict[str, Any]], Any] | None = None,
|
|
226
|
+
):
|
|
227
|
+
self._connection = connection
|
|
228
|
+
self._guard = guard
|
|
229
|
+
self._dispatcher = dispatcher
|
|
230
|
+
self._client = client
|
|
231
|
+
self._worker_id = worker_id
|
|
232
|
+
self._lease_seconds = lease_seconds
|
|
233
|
+
self._clock = clock
|
|
234
|
+
self._expected_capabilities_sha256 = expected_capabilities_sha256
|
|
235
|
+
self._lock = threading.RLock()
|
|
236
|
+
self._closed = False
|
|
237
|
+
self._owner_pid = os.getpid()
|
|
238
|
+
self._worker_stop = threading.Event()
|
|
239
|
+
self._worker_thread: threading.Thread | None = None
|
|
240
|
+
self._worker_state = "unmanaged"
|
|
241
|
+
self._worker_error_code: str | None = None
|
|
242
|
+
self.http = (
|
|
243
|
+
None
|
|
244
|
+
if http_authenticator is None
|
|
245
|
+
else _HTTP.build_submission_dispatch_http(self, http_authenticator)
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
def __repr__(self) -> str:
|
|
249
|
+
return f"DurableCMSSourceDeliverySubmissionDispatchRuntime(state={self.state!r})"
|
|
250
|
+
|
|
251
|
+
def __enter__(self) -> "DurableCMSSourceDeliverySubmissionDispatchRuntime":
|
|
252
|
+
self._assert_owner()
|
|
253
|
+
if self._closed:
|
|
254
|
+
raise _blocked("closed")
|
|
255
|
+
return self
|
|
256
|
+
|
|
257
|
+
def __exit__(self, *_exc: Any) -> None:
|
|
258
|
+
self.close()
|
|
259
|
+
|
|
260
|
+
def _assert_owner(self) -> None:
|
|
261
|
+
if os.getpid() != self._owner_pid:
|
|
262
|
+
raise _blocked("foreign_process")
|
|
263
|
+
|
|
264
|
+
def _guard_client(self) -> None:
|
|
265
|
+
if _client_digest(self._client) != self._expected_capabilities_sha256:
|
|
266
|
+
raise _blocked("capability_drift")
|
|
267
|
+
|
|
268
|
+
def _call(self, name: str, *args: Any, **kwargs: Any) -> Any:
|
|
269
|
+
self._assert_owner()
|
|
270
|
+
with self._lock:
|
|
271
|
+
if self._closed:
|
|
272
|
+
raise _blocked("closed")
|
|
273
|
+
try:
|
|
274
|
+
self._guard()
|
|
275
|
+
self._guard_client()
|
|
276
|
+
result = getattr(self._dispatcher, name)(*args, **kwargs)
|
|
277
|
+
self._guard()
|
|
278
|
+
return result
|
|
279
|
+
except CMSSourceDeliverySubmissionDispatchRuntimeBlocked:
|
|
280
|
+
raise
|
|
281
|
+
except _FILE_RUNTIME.DurableCMSSourceDeliveryRuntimeBlocked as error:
|
|
282
|
+
raise _file_failure(error) from error
|
|
283
|
+
except _DISPATCH.CMSSourceDeliverySubmissionDispatchBlocked as error:
|
|
284
|
+
raise _store_failure(error) from error
|
|
285
|
+
except Exception as error:
|
|
286
|
+
raise _blocked("outbox_blocked") from error
|
|
287
|
+
|
|
288
|
+
def _require_worker_locked(self) -> None:
|
|
289
|
+
if self._worker_state == "unmanaged":
|
|
290
|
+
return
|
|
291
|
+
if (
|
|
292
|
+
self._worker_state != "running"
|
|
293
|
+
or self._worker_error_code is not None
|
|
294
|
+
or self._worker_thread is None
|
|
295
|
+
or not self._worker_thread.is_alive()
|
|
296
|
+
):
|
|
297
|
+
raise _blocked("worker_not_ready")
|
|
298
|
+
|
|
299
|
+
def enqueue(
|
|
300
|
+
self,
|
|
301
|
+
payload: Mapping[str, Any],
|
|
302
|
+
*,
|
|
303
|
+
source_max_attempts: int = 5,
|
|
304
|
+
delivery_max_attempts: int = 5,
|
|
305
|
+
client_max_attempts: int = 5,
|
|
306
|
+
) -> Any:
|
|
307
|
+
self._assert_owner()
|
|
308
|
+
with self._lock:
|
|
309
|
+
self._require_worker_locked()
|
|
310
|
+
return self._call(
|
|
311
|
+
"enqueue",
|
|
312
|
+
payload,
|
|
313
|
+
source_max_attempts=source_max_attempts,
|
|
314
|
+
delivery_max_attempts=delivery_max_attempts,
|
|
315
|
+
client_max_attempts=client_max_attempts,
|
|
316
|
+
now=_now(self._clock),
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
def status(self, operation: str, request_id: str) -> Any:
|
|
320
|
+
return self._call("status", operation, request_id, now=_now(self._clock))
|
|
321
|
+
|
|
322
|
+
def lifecycle(self, operation: str, request_id: str) -> Any:
|
|
323
|
+
return self._call(
|
|
324
|
+
"lifecycle", self._client, operation, request_id,
|
|
325
|
+
now=_now(self._clock),
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
def commercial_profile(self) -> Any:
|
|
329
|
+
return self._call("commercial_profile", self._client)
|
|
330
|
+
|
|
331
|
+
def health(self) -> Any:
|
|
332
|
+
return self._call("health", now=_now(self._clock))
|
|
333
|
+
|
|
334
|
+
def run_once(self) -> Any:
|
|
335
|
+
return self._call(
|
|
336
|
+
"run_once",
|
|
337
|
+
self._client,
|
|
338
|
+
self._worker_id,
|
|
339
|
+
now=_now(self._clock),
|
|
340
|
+
lease_seconds=self._lease_seconds,
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
@property
|
|
344
|
+
def expected_capabilities_sha256(self) -> str:
|
|
345
|
+
"""Return the locally verified public website contract pin."""
|
|
346
|
+
|
|
347
|
+
self._assert_owner()
|
|
348
|
+
with self._lock:
|
|
349
|
+
if self._closed:
|
|
350
|
+
raise _blocked("closed")
|
|
351
|
+
try:
|
|
352
|
+
self._guard()
|
|
353
|
+
self._guard_client()
|
|
354
|
+
except _FILE_RUNTIME.DurableCMSSourceDeliveryRuntimeBlocked as error:
|
|
355
|
+
raise _file_failure(error) from error
|
|
356
|
+
return self._expected_capabilities_sha256
|
|
357
|
+
|
|
358
|
+
@staticmethod
|
|
359
|
+
def _loop_delays(
|
|
360
|
+
active_delay_seconds: Any,
|
|
361
|
+
idle_delay_seconds: Any,
|
|
362
|
+
blocked_delay_seconds: Any,
|
|
363
|
+
) -> tuple[float, float, float]:
|
|
364
|
+
return tuple(
|
|
365
|
+
_duration(value, "loop_invalid", maximum=300)
|
|
366
|
+
for value in (
|
|
367
|
+
active_delay_seconds,
|
|
368
|
+
idle_delay_seconds,
|
|
369
|
+
blocked_delay_seconds,
|
|
370
|
+
)
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
def _managed_worker(
|
|
374
|
+
self,
|
|
375
|
+
stop: threading.Event,
|
|
376
|
+
delays: tuple[float, float, float],
|
|
377
|
+
) -> None:
|
|
378
|
+
try:
|
|
379
|
+
active, idle, blocked = delays
|
|
380
|
+
while not stop.is_set():
|
|
381
|
+
outcome = self.run_once()
|
|
382
|
+
if outcome is None:
|
|
383
|
+
delay = idle
|
|
384
|
+
elif outcome.status in {"retry_wait", "failed"}:
|
|
385
|
+
delay = blocked
|
|
386
|
+
else:
|
|
387
|
+
delay = active
|
|
388
|
+
stop.wait(delay)
|
|
389
|
+
except Exception:
|
|
390
|
+
with self._lock:
|
|
391
|
+
self._worker_error_code = (
|
|
392
|
+
"source_delivery_submission_dispatch_runtime.worker_blocked"
|
|
393
|
+
)
|
|
394
|
+
self._worker_state = "failed"
|
|
395
|
+
return
|
|
396
|
+
with self._lock:
|
|
397
|
+
if self._worker_state in {"running", "stopping"}:
|
|
398
|
+
self._worker_state = "stopped"
|
|
399
|
+
|
|
400
|
+
def start_worker(
|
|
401
|
+
self,
|
|
402
|
+
*,
|
|
403
|
+
active_delay_seconds: float | int = 0.05,
|
|
404
|
+
idle_delay_seconds: float | int = 1,
|
|
405
|
+
blocked_delay_seconds: float | int = 5,
|
|
406
|
+
) -> None:
|
|
407
|
+
delays = self._loop_delays(
|
|
408
|
+
active_delay_seconds, idle_delay_seconds, blocked_delay_seconds
|
|
409
|
+
)
|
|
410
|
+
self._assert_owner()
|
|
411
|
+
with self._lock:
|
|
412
|
+
if self._closed:
|
|
413
|
+
raise _blocked("closed")
|
|
414
|
+
if self._worker_state == "running":
|
|
415
|
+
return
|
|
416
|
+
if self._worker_state in {"stopping", "failed"}:
|
|
417
|
+
raise _blocked("worker_blocked")
|
|
418
|
+
self._guard()
|
|
419
|
+
stop = threading.Event()
|
|
420
|
+
thread = threading.Thread(
|
|
421
|
+
target=self._managed_worker,
|
|
422
|
+
args=(stop, delays),
|
|
423
|
+
name="cms-public-submission-worker",
|
|
424
|
+
daemon=False,
|
|
425
|
+
)
|
|
426
|
+
self._worker_stop = stop
|
|
427
|
+
self._worker_thread = thread
|
|
428
|
+
self._worker_error_code = None
|
|
429
|
+
self._worker_state = "running"
|
|
430
|
+
try:
|
|
431
|
+
thread.start()
|
|
432
|
+
except Exception as error:
|
|
433
|
+
self._worker_thread = None
|
|
434
|
+
self._worker_state = "failed"
|
|
435
|
+
self._worker_error_code = (
|
|
436
|
+
"source_delivery_submission_dispatch_runtime.worker_blocked"
|
|
437
|
+
)
|
|
438
|
+
raise _blocked("worker_blocked") from error
|
|
439
|
+
|
|
440
|
+
def stop_worker(self, *, timeout_seconds: float | int = 30) -> None:
|
|
441
|
+
timeout = _duration(timeout_seconds, "worker_timeout_invalid", maximum=300)
|
|
442
|
+
self._assert_owner()
|
|
443
|
+
thread = self._worker_thread
|
|
444
|
+
state = self._worker_state
|
|
445
|
+
if thread is None or state in {"unmanaged", "stopped"}:
|
|
446
|
+
return
|
|
447
|
+
if thread is threading.current_thread():
|
|
448
|
+
raise _blocked("worker_stop_invalid")
|
|
449
|
+
if state != "failed":
|
|
450
|
+
self._worker_state = "stopping"
|
|
451
|
+
self._worker_stop.set()
|
|
452
|
+
thread.join(timeout)
|
|
453
|
+
if thread.is_alive():
|
|
454
|
+
raise _blocked("worker_stop_timeout")
|
|
455
|
+
with self._lock:
|
|
456
|
+
if self._worker_state == "stopping":
|
|
457
|
+
self._worker_state = "stopped"
|
|
458
|
+
|
|
459
|
+
def worker_readiness(self) -> dict[str, Any]:
|
|
460
|
+
self._assert_owner()
|
|
461
|
+
with self._lock:
|
|
462
|
+
state = "closed" if self._closed else self._worker_state
|
|
463
|
+
thread = self._worker_thread
|
|
464
|
+
alive = thread is not None and thread.is_alive()
|
|
465
|
+
error_code = self._worker_error_code
|
|
466
|
+
if state != "running" or not alive:
|
|
467
|
+
return {
|
|
468
|
+
"schema": "blun.cms-public-submission-worker-readiness.v1",
|
|
469
|
+
"status": "not_ready",
|
|
470
|
+
"worker_state": state,
|
|
471
|
+
"outbox_status": None,
|
|
472
|
+
"error_code": error_code or (
|
|
473
|
+
"source_delivery_submission_dispatch_runtime.worker_not_ready"
|
|
474
|
+
),
|
|
475
|
+
"capabilities_sha256": self._expected_capabilities_sha256,
|
|
476
|
+
}
|
|
477
|
+
try:
|
|
478
|
+
health = self.health()
|
|
479
|
+
except CMSSourceDeliverySubmissionDispatchRuntimeBlocked:
|
|
480
|
+
return {
|
|
481
|
+
"schema": "blun.cms-public-submission-worker-readiness.v1",
|
|
482
|
+
"status": "not_ready",
|
|
483
|
+
"worker_state": "running",
|
|
484
|
+
"outbox_status": "blocked",
|
|
485
|
+
"error_code": (
|
|
486
|
+
"source_delivery_submission_dispatch_runtime.outbox_blocked"
|
|
487
|
+
),
|
|
488
|
+
"capabilities_sha256": self._expected_capabilities_sha256,
|
|
489
|
+
}
|
|
490
|
+
ready = health.status == "ok"
|
|
491
|
+
return {
|
|
492
|
+
"schema": "blun.cms-public-submission-worker-readiness.v1",
|
|
493
|
+
"status": "ready" if ready else "not_ready",
|
|
494
|
+
"worker_state": "running",
|
|
495
|
+
"outbox_status": health.status,
|
|
496
|
+
"error_code": None if ready else (
|
|
497
|
+
"source_delivery_submission_dispatch_runtime.outbox_blocked"
|
|
498
|
+
),
|
|
499
|
+
"capabilities_sha256": self._expected_capabilities_sha256,
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
def close(self, *, worker_timeout_seconds: float | int = 30) -> None:
|
|
503
|
+
self._assert_owner()
|
|
504
|
+
self.stop_worker(timeout_seconds=worker_timeout_seconds)
|
|
505
|
+
with self._lock:
|
|
506
|
+
if self._closed:
|
|
507
|
+
return
|
|
508
|
+
try:
|
|
509
|
+
self._connection.close()
|
|
510
|
+
except sqlite3.Error as error:
|
|
511
|
+
raise _blocked("close_failed") from error
|
|
512
|
+
self._closed = True
|
|
513
|
+
|
|
514
|
+
@property
|
|
515
|
+
def state(self) -> str:
|
|
516
|
+
if os.getpid() != self._owner_pid:
|
|
517
|
+
return "foreign-process"
|
|
518
|
+
with self._lock:
|
|
519
|
+
return "closed" if self._closed else "open"
|
|
520
|
+
|
|
521
|
+
@property
|
|
522
|
+
def worker_state(self) -> str:
|
|
523
|
+
if os.getpid() != self._owner_pid:
|
|
524
|
+
return "foreign-process"
|
|
525
|
+
with self._lock:
|
|
526
|
+
return "closed" if self._closed else self._worker_state
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def open_durable_cms_source_delivery_submission_dispatch(
|
|
530
|
+
database: str | os.PathLike[str],
|
|
531
|
+
client: Any,
|
|
532
|
+
*,
|
|
533
|
+
worker_id: str,
|
|
534
|
+
clock: Callable[[], float | int] = time.time,
|
|
535
|
+
sqlite_timeout_seconds: float | int = 5,
|
|
536
|
+
lease_seconds: float | int = 600,
|
|
537
|
+
base_delay_seconds: float | int = 5,
|
|
538
|
+
max_delay_seconds: float | int = 300,
|
|
539
|
+
http_authenticator: Callable[[dict[str, Any]], Any] | None = None,
|
|
540
|
+
) -> DurableCMSSourceDeliverySubmissionDispatchRuntime:
|
|
541
|
+
"""Validate everything, then open one guarded caller-owned outbox."""
|
|
542
|
+
|
|
543
|
+
if http_authenticator is not None and not callable(http_authenticator):
|
|
544
|
+
raise _blocked("configuration_invalid")
|
|
545
|
+
options = {
|
|
546
|
+
"base_delay_seconds": base_delay_seconds,
|
|
547
|
+
"max_delay_seconds": max_delay_seconds,
|
|
548
|
+
}
|
|
549
|
+
digest, lease, schema = _preflight(
|
|
550
|
+
client, worker_id, lease_seconds, clock, options
|
|
551
|
+
)
|
|
552
|
+
try:
|
|
553
|
+
path = _FILE_RUNTIME._database_path(database)
|
|
554
|
+
timeout = _FILE_RUNTIME._sqlite_timeout(sqlite_timeout_seconds)
|
|
555
|
+
guard = _FILE_RUNTIME._prepare_database_file(path)
|
|
556
|
+
_preflight_existing(path, digest, options, schema, _now(clock))
|
|
557
|
+
guard()
|
|
558
|
+
except CMSSourceDeliverySubmissionDispatchRuntimeBlocked:
|
|
559
|
+
raise
|
|
560
|
+
except _FILE_RUNTIME.DurableCMSSourceDeliveryRuntimeBlocked as error:
|
|
561
|
+
raise _file_failure(error) from error
|
|
562
|
+
connection = None
|
|
563
|
+
try:
|
|
564
|
+
connection = sqlite3.connect(
|
|
565
|
+
path,
|
|
566
|
+
timeout=timeout,
|
|
567
|
+
isolation_level=None,
|
|
568
|
+
check_same_thread=False,
|
|
569
|
+
)
|
|
570
|
+
guard()
|
|
571
|
+
dispatcher = _DISPATCH.DurableCMSSourceDeliverySubmissionDispatcher(
|
|
572
|
+
connection, digest, **options
|
|
573
|
+
)
|
|
574
|
+
guard()
|
|
575
|
+
except Exception as error:
|
|
576
|
+
if connection is not None:
|
|
577
|
+
try:
|
|
578
|
+
connection.close()
|
|
579
|
+
except sqlite3.Error:
|
|
580
|
+
pass
|
|
581
|
+
if isinstance(error, _FILE_RUNTIME.DurableCMSSourceDeliveryRuntimeBlocked):
|
|
582
|
+
raise _file_failure(error) from error
|
|
583
|
+
if isinstance(error, _DISPATCH.CMSSourceDeliverySubmissionDispatchBlocked):
|
|
584
|
+
raise _store_failure(error) from error
|
|
585
|
+
raise _blocked("initialization_failed") from error
|
|
586
|
+
return DurableCMSSourceDeliverySubmissionDispatchRuntime(
|
|
587
|
+
connection,
|
|
588
|
+
guard,
|
|
589
|
+
dispatcher,
|
|
590
|
+
client,
|
|
591
|
+
worker_id=worker_id,
|
|
592
|
+
lease_seconds=lease,
|
|
593
|
+
clock=clock,
|
|
594
|
+
expected_capabilities_sha256=digest,
|
|
595
|
+
http_authenticator=http_authenticator,
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def open_hosted_cms_source_delivery_submission_dispatch(
|
|
600
|
+
*args: Any,
|
|
601
|
+
active_delay_seconds: float | int = 0.05,
|
|
602
|
+
idle_delay_seconds: float | int = 1,
|
|
603
|
+
blocked_delay_seconds: float | int = 5,
|
|
604
|
+
**kwargs: Any,
|
|
605
|
+
) -> DurableCMSSourceDeliverySubmissionDispatchRuntime:
|
|
606
|
+
"""Open the guarded caller outbox and start its supervised worker."""
|
|
607
|
+
|
|
608
|
+
delays = DurableCMSSourceDeliverySubmissionDispatchRuntime._loop_delays(
|
|
609
|
+
active_delay_seconds, idle_delay_seconds, blocked_delay_seconds
|
|
610
|
+
)
|
|
611
|
+
runtime = open_durable_cms_source_delivery_submission_dispatch(*args, **kwargs)
|
|
612
|
+
try:
|
|
613
|
+
runtime.start_worker(
|
|
614
|
+
active_delay_seconds=delays[0],
|
|
615
|
+
idle_delay_seconds=delays[1],
|
|
616
|
+
blocked_delay_seconds=delays[2],
|
|
617
|
+
)
|
|
618
|
+
except Exception:
|
|
619
|
+
runtime.close()
|
|
620
|
+
raise
|
|
621
|
+
return runtime
|