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
package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery.py
CHANGED
|
@@ -25,6 +25,56 @@ from typing import Any, Callable, Iterator, Mapping
|
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
SCHEMA_VERSION = 1
|
|
28
|
+
CAPABILITY_BINDING_SCHEMA = (
|
|
29
|
+
"blun.cms-source-delivery-runtime-capability-binding.v2"
|
|
30
|
+
)
|
|
31
|
+
LEGACY_CAPABILITY_BINDING_SCHEMA = (
|
|
32
|
+
"blun.cms-source-delivery-runtime-capability-binding.v1"
|
|
33
|
+
)
|
|
34
|
+
CAPABILITY_BINDING_TABLE = "cms_source_delivery_runtime_capability_binding"
|
|
35
|
+
CAPABILITY_DATABASE_ROLE = "source_delivery"
|
|
36
|
+
CAPABILITY_BINDING_SQL = (
|
|
37
|
+
"CREATE TABLE cms_source_delivery_runtime_capability_binding ("
|
|
38
|
+
"singleton INTEGER PRIMARY KEY CHECK (singleton = 1), "
|
|
39
|
+
"schema TEXT NOT NULL CHECK (schema = "
|
|
40
|
+
"'blun.cms-source-delivery-runtime-capability-binding.v2'), "
|
|
41
|
+
"database_role TEXT NOT NULL CHECK (database_role = 'source_delivery'), "
|
|
42
|
+
"delivery_capabilities_sha256 TEXT NOT NULL, "
|
|
43
|
+
"runtime_capabilities_sha256 TEXT NOT NULL, "
|
|
44
|
+
"commercial_rendering_registry_sha256 TEXT NOT NULL, "
|
|
45
|
+
"terminal_receiver_capabilities_sha256 TEXT NOT NULL, "
|
|
46
|
+
"binding_sha256 TEXT NOT NULL)"
|
|
47
|
+
)
|
|
48
|
+
LEGACY_CAPABILITY_BINDING_SQL = (
|
|
49
|
+
"CREATE TABLE cms_source_delivery_runtime_capability_binding ("
|
|
50
|
+
"singleton INTEGER PRIMARY KEY CHECK (singleton = 1), "
|
|
51
|
+
"schema TEXT NOT NULL CHECK (schema = "
|
|
52
|
+
"'blun.cms-source-delivery-runtime-capability-binding.v1'), "
|
|
53
|
+
"database_role TEXT NOT NULL CHECK (database_role = 'source_delivery'), "
|
|
54
|
+
"delivery_capabilities_sha256 TEXT NOT NULL, "
|
|
55
|
+
"runtime_capabilities_sha256 TEXT NOT NULL, "
|
|
56
|
+
"commercial_rendering_registry_sha256 TEXT NOT NULL, "
|
|
57
|
+
"binding_sha256 TEXT NOT NULL)"
|
|
58
|
+
)
|
|
59
|
+
CAPABILITY_BINDING_COLUMNS = (
|
|
60
|
+
("singleton", "INTEGER", 0, 1),
|
|
61
|
+
("schema", "TEXT", 1, 0),
|
|
62
|
+
("database_role", "TEXT", 1, 0),
|
|
63
|
+
("delivery_capabilities_sha256", "TEXT", 1, 0),
|
|
64
|
+
("runtime_capabilities_sha256", "TEXT", 1, 0),
|
|
65
|
+
("commercial_rendering_registry_sha256", "TEXT", 1, 0),
|
|
66
|
+
("terminal_receiver_capabilities_sha256", "TEXT", 1, 0),
|
|
67
|
+
("binding_sha256", "TEXT", 1, 0),
|
|
68
|
+
)
|
|
69
|
+
LEGACY_CAPABILITY_BINDING_COLUMNS = (
|
|
70
|
+
("singleton", "INTEGER", 0, 1),
|
|
71
|
+
("schema", "TEXT", 1, 0),
|
|
72
|
+
("database_role", "TEXT", 1, 0),
|
|
73
|
+
("delivery_capabilities_sha256", "TEXT", 1, 0),
|
|
74
|
+
("runtime_capabilities_sha256", "TEXT", 1, 0),
|
|
75
|
+
("commercial_rendering_registry_sha256", "TEXT", 1, 0),
|
|
76
|
+
("binding_sha256", "TEXT", 1, 0),
|
|
77
|
+
)
|
|
28
78
|
MAX_ATTEMPTS = 20
|
|
29
79
|
MAX_LEASE_SECONDS = 86_400.0
|
|
30
80
|
MAX_DELAY_SECONDS = 86_400.0
|
|
@@ -54,6 +104,59 @@ def _load_module(name: str, path: Path):
|
|
|
54
104
|
return module
|
|
55
105
|
|
|
56
106
|
|
|
107
|
+
def _capability_binding_row(
|
|
108
|
+
delivery_capabilities_sha256: str,
|
|
109
|
+
runtime_capabilities_sha256: str,
|
|
110
|
+
commercial_rendering_registry_sha256: str,
|
|
111
|
+
terminal_receiver_capabilities_sha256: str,
|
|
112
|
+
) -> tuple[Any, ...]:
|
|
113
|
+
"""Return the one canonical durable generation-binding row."""
|
|
114
|
+
|
|
115
|
+
binding_hash = hashlib.sha256("\x00".join((
|
|
116
|
+
CAPABILITY_BINDING_SCHEMA,
|
|
117
|
+
CAPABILITY_DATABASE_ROLE,
|
|
118
|
+
delivery_capabilities_sha256,
|
|
119
|
+
runtime_capabilities_sha256,
|
|
120
|
+
commercial_rendering_registry_sha256,
|
|
121
|
+
terminal_receiver_capabilities_sha256,
|
|
122
|
+
)).encode("utf-8")).hexdigest()
|
|
123
|
+
return (
|
|
124
|
+
1,
|
|
125
|
+
CAPABILITY_BINDING_SCHEMA,
|
|
126
|
+
CAPABILITY_DATABASE_ROLE,
|
|
127
|
+
delivery_capabilities_sha256,
|
|
128
|
+
runtime_capabilities_sha256,
|
|
129
|
+
commercial_rendering_registry_sha256,
|
|
130
|
+
terminal_receiver_capabilities_sha256,
|
|
131
|
+
binding_hash,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _legacy_capability_binding_row(
|
|
136
|
+
delivery_capabilities_sha256: str,
|
|
137
|
+
runtime_capabilities_sha256: str,
|
|
138
|
+
commercial_rendering_registry_sha256: str,
|
|
139
|
+
) -> tuple[Any, ...]:
|
|
140
|
+
"""Return the exact prior binding accepted only for an empty migration."""
|
|
141
|
+
|
|
142
|
+
binding_hash = hashlib.sha256("\x00".join((
|
|
143
|
+
LEGACY_CAPABILITY_BINDING_SCHEMA,
|
|
144
|
+
CAPABILITY_DATABASE_ROLE,
|
|
145
|
+
delivery_capabilities_sha256,
|
|
146
|
+
runtime_capabilities_sha256,
|
|
147
|
+
commercial_rendering_registry_sha256,
|
|
148
|
+
)).encode("utf-8")).hexdigest()
|
|
149
|
+
return (
|
|
150
|
+
1,
|
|
151
|
+
LEGACY_CAPABILITY_BINDING_SCHEMA,
|
|
152
|
+
CAPABILITY_DATABASE_ROLE,
|
|
153
|
+
delivery_capabilities_sha256,
|
|
154
|
+
runtime_capabilities_sha256,
|
|
155
|
+
commercial_rendering_registry_sha256,
|
|
156
|
+
binding_hash,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
57
160
|
_ROOT = Path(__file__).resolve().parents[1]
|
|
58
161
|
_CLIENT = _load_module(
|
|
59
162
|
"blun_website_localization_cms_source_delivery_client",
|
|
@@ -278,11 +381,38 @@ class DurableCMSSourceDeliveryOutbox:
|
|
|
278
381
|
if not callable(clock):
|
|
279
382
|
raise TypeError("clock must be callable")
|
|
280
383
|
digest = getattr(client, "expected_capabilities_sha256", None)
|
|
384
|
+
legacy_digest = getattr(
|
|
385
|
+
client, "legacy_expected_capabilities_sha256", digest,
|
|
386
|
+
)
|
|
387
|
+
runtime_digest = getattr(
|
|
388
|
+
client, "expected_runtime_capabilities_sha256", None,
|
|
389
|
+
)
|
|
390
|
+
rendering_digest = getattr(
|
|
391
|
+
client, "expected_commercial_rendering_registry_sha256", None,
|
|
392
|
+
)
|
|
393
|
+
terminal_receiver_digest = getattr(
|
|
394
|
+
client, "expected_terminal_receiver_capabilities_sha256", None,
|
|
395
|
+
)
|
|
281
396
|
if (
|
|
282
397
|
not callable(getattr(client, "submit_change", None))
|
|
283
398
|
or not callable(getattr(client, "submit_removal", None))
|
|
284
399
|
or not isinstance(digest, str)
|
|
285
400
|
or SHA256.fullmatch(digest) is None
|
|
401
|
+
or not isinstance(legacy_digest, str)
|
|
402
|
+
or SHA256.fullmatch(legacy_digest) is None
|
|
403
|
+
or len({
|
|
404
|
+
runtime_digest is None,
|
|
405
|
+
rendering_digest is None,
|
|
406
|
+
terminal_receiver_digest is None,
|
|
407
|
+
}) != 1
|
|
408
|
+
or runtime_digest is not None and (
|
|
409
|
+
not isinstance(runtime_digest, str)
|
|
410
|
+
or SHA256.fullmatch(runtime_digest) is None
|
|
411
|
+
or not isinstance(rendering_digest, str)
|
|
412
|
+
or SHA256.fullmatch(rendering_digest) is None
|
|
413
|
+
or not isinstance(terminal_receiver_digest, str)
|
|
414
|
+
or SHA256.fullmatch(terminal_receiver_digest) is None
|
|
415
|
+
)
|
|
286
416
|
):
|
|
287
417
|
raise TypeError("client must provide the pinned source operations")
|
|
288
418
|
timeout = getattr(client, "timeout", None)
|
|
@@ -297,6 +427,10 @@ class DurableCMSSourceDeliveryOutbox:
|
|
|
297
427
|
self.connection.row_factory = sqlite3.Row
|
|
298
428
|
self.client = client
|
|
299
429
|
self.capabilities_sha256 = digest
|
|
430
|
+
self.legacy_capabilities_sha256 = legacy_digest
|
|
431
|
+
self.runtime_capabilities_sha256 = runtime_digest
|
|
432
|
+
self.commercial_rendering_registry_sha256 = rendering_digest
|
|
433
|
+
self.terminal_receiver_capabilities_sha256 = terminal_receiver_digest
|
|
300
434
|
self.timeout = float(timeout)
|
|
301
435
|
self.clock = clock
|
|
302
436
|
self.base_delay_seconds = _duration(
|
|
@@ -311,6 +445,157 @@ class DurableCMSSourceDeliveryOutbox:
|
|
|
311
445
|
raise CMSSourceDeliveryBlocked("source_delivery.delay_invalid")
|
|
312
446
|
self._initialize()
|
|
313
447
|
|
|
448
|
+
def _capability_binding_row(self) -> tuple[Any, ...]:
|
|
449
|
+
if (
|
|
450
|
+
self.runtime_capabilities_sha256 is None
|
|
451
|
+
or self.commercial_rendering_registry_sha256 is None
|
|
452
|
+
):
|
|
453
|
+
raise CMSSourceDeliveryBlocked(
|
|
454
|
+
"source_delivery.capability_binding_required"
|
|
455
|
+
)
|
|
456
|
+
return _capability_binding_row(
|
|
457
|
+
self.capabilities_sha256,
|
|
458
|
+
self.runtime_capabilities_sha256,
|
|
459
|
+
self.commercial_rendering_registry_sha256,
|
|
460
|
+
self.terminal_receiver_capabilities_sha256,
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
def _capability_table_state(self) -> str:
|
|
464
|
+
try:
|
|
465
|
+
rows = self.connection.execute(
|
|
466
|
+
"SELECT type, name, tbl_name, sql FROM sqlite_master "
|
|
467
|
+
"WHERE name = ?",
|
|
468
|
+
(CAPABILITY_BINDING_TABLE,),
|
|
469
|
+
).fetchall()
|
|
470
|
+
except sqlite3.Error as error:
|
|
471
|
+
raise CMSSourceDeliveryBlocked(
|
|
472
|
+
"source_delivery.capability_database_invalid"
|
|
473
|
+
) from error
|
|
474
|
+
if not rows:
|
|
475
|
+
return "missing"
|
|
476
|
+
if len(rows) != 1:
|
|
477
|
+
raise CMSSourceDeliveryBlocked(
|
|
478
|
+
"source_delivery.capability_database_invalid"
|
|
479
|
+
)
|
|
480
|
+
row = tuple(rows[0])
|
|
481
|
+
current = (
|
|
482
|
+
"table", CAPABILITY_BINDING_TABLE,
|
|
483
|
+
CAPABILITY_BINDING_TABLE, CAPABILITY_BINDING_SQL,
|
|
484
|
+
)
|
|
485
|
+
legacy = (
|
|
486
|
+
"table", CAPABILITY_BINDING_TABLE,
|
|
487
|
+
CAPABILITY_BINDING_TABLE, LEGACY_CAPABILITY_BINDING_SQL,
|
|
488
|
+
)
|
|
489
|
+
if row == current:
|
|
490
|
+
return "current"
|
|
491
|
+
if row == legacy:
|
|
492
|
+
return "legacy"
|
|
493
|
+
raise CMSSourceDeliveryBlocked(
|
|
494
|
+
"source_delivery.capability_database_invalid"
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
def _validate_legacy_capability_database(self) -> None:
|
|
498
|
+
try:
|
|
499
|
+
columns = tuple(
|
|
500
|
+
(row[1], row[2], row[3], row[5])
|
|
501
|
+
for row in self.connection.execute(
|
|
502
|
+
f"PRAGMA table_info({CAPABILITY_BINDING_TABLE})"
|
|
503
|
+
).fetchall()
|
|
504
|
+
)
|
|
505
|
+
rows = self.connection.execute(
|
|
506
|
+
f"SELECT singleton, schema, database_role, "
|
|
507
|
+
f"delivery_capabilities_sha256, "
|
|
508
|
+
f"runtime_capabilities_sha256, "
|
|
509
|
+
f"commercial_rendering_registry_sha256, binding_sha256 "
|
|
510
|
+
f"FROM {CAPABILITY_BINDING_TABLE}"
|
|
511
|
+
).fetchall()
|
|
512
|
+
except sqlite3.Error as error:
|
|
513
|
+
raise CMSSourceDeliveryBlocked(
|
|
514
|
+
"source_delivery.capability_database_invalid"
|
|
515
|
+
) from error
|
|
516
|
+
expected = _legacy_capability_binding_row(
|
|
517
|
+
self.legacy_capabilities_sha256,
|
|
518
|
+
self.runtime_capabilities_sha256,
|
|
519
|
+
self.commercial_rendering_registry_sha256,
|
|
520
|
+
)
|
|
521
|
+
if (
|
|
522
|
+
columns != LEGACY_CAPABILITY_BINDING_COLUMNS
|
|
523
|
+
or len(rows) != 1
|
|
524
|
+
or tuple(rows[0]) != expected
|
|
525
|
+
):
|
|
526
|
+
raise CMSSourceDeliveryBlocked(
|
|
527
|
+
"source_delivery.capability_database_mismatch"
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
def _validate_capability_database(self) -> None:
|
|
531
|
+
configured = self.runtime_capabilities_sha256 is not None
|
|
532
|
+
state = self._capability_table_state()
|
|
533
|
+
if state == "missing":
|
|
534
|
+
if configured:
|
|
535
|
+
raise CMSSourceDeliveryBlocked(
|
|
536
|
+
"source_delivery.capability_database_unbound"
|
|
537
|
+
)
|
|
538
|
+
return
|
|
539
|
+
if state != "current":
|
|
540
|
+
raise CMSSourceDeliveryBlocked(
|
|
541
|
+
"source_delivery.capability_database_unbound"
|
|
542
|
+
)
|
|
543
|
+
if not configured:
|
|
544
|
+
raise CMSSourceDeliveryBlocked(
|
|
545
|
+
"source_delivery.capability_binding_required"
|
|
546
|
+
)
|
|
547
|
+
try:
|
|
548
|
+
columns = tuple(
|
|
549
|
+
(row[1], row[2], row[3], row[5])
|
|
550
|
+
for row in self.connection.execute(
|
|
551
|
+
f"PRAGMA table_info({CAPABILITY_BINDING_TABLE})"
|
|
552
|
+
).fetchall()
|
|
553
|
+
)
|
|
554
|
+
rows = self.connection.execute(
|
|
555
|
+
f"SELECT singleton, schema, database_role, "
|
|
556
|
+
f"delivery_capabilities_sha256, "
|
|
557
|
+
f"runtime_capabilities_sha256, "
|
|
558
|
+
f"commercial_rendering_registry_sha256, "
|
|
559
|
+
f"terminal_receiver_capabilities_sha256, binding_sha256 "
|
|
560
|
+
f"FROM {CAPABILITY_BINDING_TABLE}"
|
|
561
|
+
).fetchall()
|
|
562
|
+
except sqlite3.Error as error:
|
|
563
|
+
raise CMSSourceDeliveryBlocked(
|
|
564
|
+
"source_delivery.capability_database_invalid"
|
|
565
|
+
) from error
|
|
566
|
+
if columns != CAPABILITY_BINDING_COLUMNS or len(rows) != 1:
|
|
567
|
+
raise CMSSourceDeliveryBlocked(
|
|
568
|
+
"source_delivery.capability_database_invalid"
|
|
569
|
+
)
|
|
570
|
+
if tuple(rows[0]) != self._capability_binding_row():
|
|
571
|
+
raise CMSSourceDeliveryBlocked(
|
|
572
|
+
"source_delivery.capability_database_mismatch"
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
def _guard_capability_binding(self) -> None:
|
|
576
|
+
if self.runtime_capabilities_sha256 is not None and (
|
|
577
|
+
getattr(
|
|
578
|
+
self.client, "expected_runtime_capabilities_sha256", None,
|
|
579
|
+
) != self.runtime_capabilities_sha256
|
|
580
|
+
or getattr(
|
|
581
|
+
self.client,
|
|
582
|
+
"expected_commercial_rendering_registry_sha256",
|
|
583
|
+
None,
|
|
584
|
+
) != self.commercial_rendering_registry_sha256
|
|
585
|
+
or getattr(
|
|
586
|
+
self.client, "expected_capabilities_sha256", None,
|
|
587
|
+
) != self.capabilities_sha256
|
|
588
|
+
or getattr(
|
|
589
|
+
self.client,
|
|
590
|
+
"expected_terminal_receiver_capabilities_sha256",
|
|
591
|
+
None,
|
|
592
|
+
) != self.terminal_receiver_capabilities_sha256
|
|
593
|
+
):
|
|
594
|
+
raise CMSSourceDeliveryBlocked(
|
|
595
|
+
"source_delivery.capability_binding_changed"
|
|
596
|
+
)
|
|
597
|
+
self._validate_capability_database()
|
|
598
|
+
|
|
314
599
|
def _now(self, value: float | int | None = None) -> float:
|
|
315
600
|
try:
|
|
316
601
|
supplied = self.clock() if value is None else value
|
|
@@ -381,6 +666,33 @@ class DurableCMSSourceDeliveryOutbox:
|
|
|
381
666
|
status, next_attempt_at, created_at, operation, request_id
|
|
382
667
|
)
|
|
383
668
|
""")
|
|
669
|
+
if self.runtime_capabilities_sha256 is not None:
|
|
670
|
+
state = self._capability_table_state()
|
|
671
|
+
has_work = self.connection.execute(
|
|
672
|
+
"SELECT 1 FROM cms_source_delivery_outbox LIMIT 1"
|
|
673
|
+
).fetchone()
|
|
674
|
+
if state == "missing":
|
|
675
|
+
if has_work is not None:
|
|
676
|
+
raise CMSSourceDeliveryBlocked(
|
|
677
|
+
"source_delivery.capability_database_unbound"
|
|
678
|
+
)
|
|
679
|
+
self.connection.execute(CAPABILITY_BINDING_SQL)
|
|
680
|
+
elif state == "legacy":
|
|
681
|
+
if has_work is not None:
|
|
682
|
+
raise CMSSourceDeliveryBlocked(
|
|
683
|
+
"source_delivery.capability_database_unbound"
|
|
684
|
+
)
|
|
685
|
+
self._validate_legacy_capability_database()
|
|
686
|
+
self.connection.execute(
|
|
687
|
+
f"DROP TABLE {CAPABILITY_BINDING_TABLE}"
|
|
688
|
+
)
|
|
689
|
+
self.connection.execute(CAPABILITY_BINDING_SQL)
|
|
690
|
+
self.connection.execute(
|
|
691
|
+
f"INSERT OR IGNORE INTO {CAPABILITY_BINDING_TABLE} "
|
|
692
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
|
693
|
+
self._capability_binding_row(),
|
|
694
|
+
)
|
|
695
|
+
self._guard_capability_binding()
|
|
384
696
|
self._validate_schema()
|
|
385
697
|
|
|
386
698
|
def _validate_schema(self) -> None:
|
|
@@ -404,6 +716,54 @@ class DurableCMSSourceDeliveryOutbox:
|
|
|
404
716
|
or columns != _COLUMNS
|
|
405
717
|
):
|
|
406
718
|
raise CMSSourceDeliveryBlocked("source_delivery.schema_altered")
|
|
719
|
+
self._guard_capability_binding()
|
|
720
|
+
|
|
721
|
+
def capability_binding(self) -> dict[str, Any]:
|
|
722
|
+
"""Return the content-free durable generation bound to this outbox."""
|
|
723
|
+
|
|
724
|
+
self._validate_schema()
|
|
725
|
+
if self.runtime_capabilities_sha256 is None:
|
|
726
|
+
return {
|
|
727
|
+
"schema": CAPABILITY_BINDING_SCHEMA,
|
|
728
|
+
"status": "not_configured",
|
|
729
|
+
"database_role": CAPABILITY_DATABASE_ROLE,
|
|
730
|
+
"delivery_capabilities_sha256": self.capabilities_sha256,
|
|
731
|
+
"runtime_capabilities_sha256": None,
|
|
732
|
+
"commercial_rendering_registry_sha256": None,
|
|
733
|
+
"terminal_receiver_capabilities_sha256": None,
|
|
734
|
+
"binding_sha256": None,
|
|
735
|
+
}
|
|
736
|
+
row = self._capability_binding_row()
|
|
737
|
+
return {
|
|
738
|
+
"schema": row[1],
|
|
739
|
+
"status": "verified",
|
|
740
|
+
"database_role": row[2],
|
|
741
|
+
"delivery_capabilities_sha256": row[3],
|
|
742
|
+
"runtime_capabilities_sha256": row[4],
|
|
743
|
+
"commercial_rendering_registry_sha256": row[5],
|
|
744
|
+
"terminal_receiver_capabilities_sha256": row[6],
|
|
745
|
+
"binding_sha256": row[7],
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
def _verified_source_binding(self, value: Any) -> dict[str, Any]:
|
|
749
|
+
try:
|
|
750
|
+
binding = _CLIENT._HTTP._capability_binding_payload(value)
|
|
751
|
+
except Exception:
|
|
752
|
+
raise CMSSourceDeliveryBlocked(
|
|
753
|
+
"source_delivery.source_capability_binding_invalid"
|
|
754
|
+
) from None
|
|
755
|
+
if (
|
|
756
|
+
self.runtime_capabilities_sha256 is None
|
|
757
|
+
or self.commercial_rendering_registry_sha256 is None
|
|
758
|
+
or binding["capabilities_sha256"]
|
|
759
|
+
!= self.runtime_capabilities_sha256
|
|
760
|
+
or binding["commercial_rendering_registry_sha256"]
|
|
761
|
+
!= self.commercial_rendering_registry_sha256
|
|
762
|
+
):
|
|
763
|
+
raise CMSSourceDeliveryBlocked(
|
|
764
|
+
"source_delivery.source_capability_binding_invalid"
|
|
765
|
+
)
|
|
766
|
+
return binding
|
|
407
767
|
|
|
408
768
|
def enqueue_change(
|
|
409
769
|
self,
|
|
@@ -622,6 +982,164 @@ class DurableCMSSourceDeliveryOutbox:
|
|
|
622
982
|
response_sha256=row["response_sha256"],
|
|
623
983
|
)
|
|
624
984
|
|
|
985
|
+
def source_status(
|
|
986
|
+
self,
|
|
987
|
+
event_id: str,
|
|
988
|
+
site_id: str,
|
|
989
|
+
payload_sha256: str,
|
|
990
|
+
) -> Mapping[str, Any]:
|
|
991
|
+
"""Read downstream lifecycle only for an exact accepted submission."""
|
|
992
|
+
|
|
993
|
+
self._validate_schema()
|
|
994
|
+
event_id = _token(event_id, "source_delivery.status_invalid")
|
|
995
|
+
site_id = _token(site_id, "source_delivery.status_invalid")
|
|
996
|
+
payload_sha256 = _sha256(
|
|
997
|
+
payload_sha256, "source_delivery.status_invalid",
|
|
998
|
+
)
|
|
999
|
+
rows = self.connection.execute("""
|
|
1000
|
+
SELECT * FROM cms_source_delivery_outbox
|
|
1001
|
+
WHERE event_id = ? AND site_id = ? AND payload_sha256 = ?
|
|
1002
|
+
ORDER BY created_at, operation, request_id
|
|
1003
|
+
""", (event_id, site_id, payload_sha256)).fetchall()
|
|
1004
|
+
if not rows:
|
|
1005
|
+
raise CMSSourceDeliveryBlocked(
|
|
1006
|
+
"source_delivery.status_not_found"
|
|
1007
|
+
)
|
|
1008
|
+
for row in rows:
|
|
1009
|
+
self._validated_row(row)
|
|
1010
|
+
if len(rows) != 1 or rows[0]["status"] != "succeeded":
|
|
1011
|
+
raise CMSSourceDeliveryBlocked(
|
|
1012
|
+
"source_delivery.source_status_unavailable"
|
|
1013
|
+
)
|
|
1014
|
+
client_status = getattr(self.client, "status", None)
|
|
1015
|
+
if not callable(client_status):
|
|
1016
|
+
raise CMSSourceDeliveryBlocked(
|
|
1017
|
+
"source_delivery.client_failure"
|
|
1018
|
+
)
|
|
1019
|
+
try:
|
|
1020
|
+
response = client_status(event_id, site_id)
|
|
1021
|
+
if (
|
|
1022
|
+
not isinstance(response, Mapping)
|
|
1023
|
+
or set(response)
|
|
1024
|
+
!= {
|
|
1025
|
+
"schema", "status", "capabilities_sha256",
|
|
1026
|
+
"capability_binding",
|
|
1027
|
+
}
|
|
1028
|
+
or response.get("schema")
|
|
1029
|
+
!= _CLIENT._HTTP.STATUS_RESPONSE_SCHEMA
|
|
1030
|
+
or response.get("capabilities_sha256")
|
|
1031
|
+
!= self.capabilities_sha256
|
|
1032
|
+
):
|
|
1033
|
+
raise ValueError
|
|
1034
|
+
normalized = _CLIENT._HTTP._source_status_payload(
|
|
1035
|
+
_CLIENT._PayloadView(response["status"]),
|
|
1036
|
+
expected_event_id=event_id,
|
|
1037
|
+
expected_site_id=site_id,
|
|
1038
|
+
)
|
|
1039
|
+
if normalized != response["status"]:
|
|
1040
|
+
raise ValueError
|
|
1041
|
+
binding = self._verified_source_binding(
|
|
1042
|
+
response["capability_binding"]
|
|
1043
|
+
)
|
|
1044
|
+
except Exception as error:
|
|
1045
|
+
if getattr(error, "cms_source_client_failure", False) is True:
|
|
1046
|
+
raise
|
|
1047
|
+
raise CMSSourceDeliveryBlocked(
|
|
1048
|
+
"source_delivery.source_status_invalid"
|
|
1049
|
+
) from None
|
|
1050
|
+
return {
|
|
1051
|
+
"schema": response["schema"],
|
|
1052
|
+
"status": dict(normalized),
|
|
1053
|
+
"capabilities_sha256": response["capabilities_sha256"],
|
|
1054
|
+
"capability_binding": binding,
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
def source_readiness(self) -> Mapping[str, Any]:
|
|
1058
|
+
"""Read the pinned source-service readiness without changing work."""
|
|
1059
|
+
|
|
1060
|
+
self._validate_schema()
|
|
1061
|
+
client_readiness = getattr(self.client, "readiness", None)
|
|
1062
|
+
if not callable(client_readiness):
|
|
1063
|
+
raise CMSSourceDeliveryBlocked(
|
|
1064
|
+
"source_delivery.client_failure"
|
|
1065
|
+
)
|
|
1066
|
+
try:
|
|
1067
|
+
response = client_readiness()
|
|
1068
|
+
if (
|
|
1069
|
+
not isinstance(response, Mapping)
|
|
1070
|
+
or set(response)
|
|
1071
|
+
!= {
|
|
1072
|
+
"schema", "readiness", "capabilities_sha256",
|
|
1073
|
+
"capability_binding",
|
|
1074
|
+
}
|
|
1075
|
+
or response.get("schema")
|
|
1076
|
+
!= _CLIENT._HTTP.READINESS_RESPONSE_SCHEMA
|
|
1077
|
+
or response.get("capabilities_sha256")
|
|
1078
|
+
!= self.capabilities_sha256
|
|
1079
|
+
):
|
|
1080
|
+
raise ValueError
|
|
1081
|
+
normalized = _CLIENT._HTTP._readiness_payload(
|
|
1082
|
+
response["readiness"]
|
|
1083
|
+
)
|
|
1084
|
+
if normalized != response["readiness"]:
|
|
1085
|
+
raise ValueError
|
|
1086
|
+
binding = self._verified_source_binding(
|
|
1087
|
+
response["capability_binding"]
|
|
1088
|
+
)
|
|
1089
|
+
except Exception as error:
|
|
1090
|
+
if getattr(error, "cms_source_client_failure", False) is True:
|
|
1091
|
+
raise
|
|
1092
|
+
raise CMSSourceDeliveryBlocked(
|
|
1093
|
+
"source_delivery.source_readiness_invalid"
|
|
1094
|
+
) from None
|
|
1095
|
+
return {
|
|
1096
|
+
"schema": response["schema"],
|
|
1097
|
+
"readiness": dict(normalized),
|
|
1098
|
+
"capabilities_sha256": response["capabilities_sha256"],
|
|
1099
|
+
"capability_binding": binding,
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
def source_health(self) -> Mapping[str, Any]:
|
|
1103
|
+
"""Read the pinned source-service queue health without changing work."""
|
|
1104
|
+
|
|
1105
|
+
self._validate_schema()
|
|
1106
|
+
client_health = getattr(self.client, "health", None)
|
|
1107
|
+
if not callable(client_health):
|
|
1108
|
+
raise CMSSourceDeliveryBlocked("source_delivery.client_failure")
|
|
1109
|
+
try:
|
|
1110
|
+
response = client_health()
|
|
1111
|
+
if (
|
|
1112
|
+
not isinstance(response, Mapping)
|
|
1113
|
+
or set(response) != {
|
|
1114
|
+
"schema", "health", "capabilities_sha256",
|
|
1115
|
+
"capability_binding",
|
|
1116
|
+
}
|
|
1117
|
+
or response.get("schema") != _CLIENT._HTTP.HEALTH_RESPONSE_SCHEMA
|
|
1118
|
+
or response.get("capabilities_sha256")
|
|
1119
|
+
!= self.capabilities_sha256
|
|
1120
|
+
):
|
|
1121
|
+
raise ValueError
|
|
1122
|
+
normalized = _CLIENT._HTTP._health_payload(
|
|
1123
|
+
_CLIENT._PayloadView(response["health"])
|
|
1124
|
+
)
|
|
1125
|
+
if normalized != response["health"]:
|
|
1126
|
+
raise ValueError
|
|
1127
|
+
binding = self._verified_source_binding(
|
|
1128
|
+
response["capability_binding"]
|
|
1129
|
+
)
|
|
1130
|
+
except Exception as error:
|
|
1131
|
+
if getattr(error, "cms_source_client_failure", False) is True:
|
|
1132
|
+
raise
|
|
1133
|
+
raise CMSSourceDeliveryBlocked(
|
|
1134
|
+
"source_delivery.source_health_invalid"
|
|
1135
|
+
) from None
|
|
1136
|
+
return {
|
|
1137
|
+
"schema": response["schema"],
|
|
1138
|
+
"health": normalized,
|
|
1139
|
+
"capabilities_sha256": response["capabilities_sha256"],
|
|
1140
|
+
"capability_binding": binding,
|
|
1141
|
+
}
|
|
1142
|
+
|
|
625
1143
|
def claim(
|
|
626
1144
|
self,
|
|
627
1145
|
worker_id: str,
|
|
@@ -771,12 +1289,15 @@ class DurableCMSSourceDeliveryOutbox:
|
|
|
771
1289
|
if claim.operation == "change"
|
|
772
1290
|
else _CLIENT._HTTP.REMOVAL_RESPONSE_SCHEMA
|
|
773
1291
|
)
|
|
1292
|
+
expected_fields = {
|
|
1293
|
+
"schema", "operation", "request_id", "event_id",
|
|
1294
|
+
"payload_sha256", "status", "attempts", "max_attempts",
|
|
1295
|
+
"capabilities_sha256",
|
|
1296
|
+
}
|
|
1297
|
+
if self.runtime_capabilities_sha256 is not None:
|
|
1298
|
+
expected_fields.add("capability_binding")
|
|
774
1299
|
if (
|
|
775
|
-
set(response) !=
|
|
776
|
-
"schema", "operation", "request_id", "event_id",
|
|
777
|
-
"payload_sha256", "status", "attempts", "max_attempts",
|
|
778
|
-
"capabilities_sha256",
|
|
779
|
-
}
|
|
1300
|
+
set(response) != expected_fields
|
|
780
1301
|
or response.get("schema") != expected_schema
|
|
781
1302
|
or response.get("operation") != claim.operation
|
|
782
1303
|
or response.get("request_id") != claim.request_id
|
|
@@ -791,6 +1312,8 @@ class DurableCMSSourceDeliveryOutbox:
|
|
|
791
1312
|
or response.get("max_attempts") != claim.source_max_attempts
|
|
792
1313
|
):
|
|
793
1314
|
raise CMSSourceDeliveryBlocked("source_delivery.response_invalid")
|
|
1315
|
+
if self.runtime_capabilities_sha256 is not None:
|
|
1316
|
+
self._verified_source_binding(response["capability_binding"])
|
|
794
1317
|
return response_json
|
|
795
1318
|
|
|
796
1319
|
def complete(
|