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_receiver_store.py
CHANGED
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
|
|
4
4
|
The trusted host registers its current source and tombstone expectations. The
|
|
5
5
|
receiver invokes this store only after transport and signature verification.
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
The store retains and continuously rechecks that publisher signature together
|
|
7
|
+
with the current binding, so restart or database tampering cannot silently
|
|
8
|
+
authorize different content. Every write callback also rechecks the binding in
|
|
9
|
+
one SQLite transaction so a concurrent source change cannot affect the wrong
|
|
10
|
+
generation.
|
|
8
11
|
"""
|
|
9
12
|
|
|
10
13
|
from __future__ import annotations
|
|
@@ -19,10 +22,13 @@ from contextlib import contextmanager
|
|
|
19
22
|
from typing import Any, Callable, Iterator, Mapping
|
|
20
23
|
|
|
21
24
|
|
|
22
|
-
SCHEMA_VERSION =
|
|
25
|
+
SCHEMA_VERSION = 2
|
|
26
|
+
LEGACY_SCHEMA_VERSION = 1
|
|
23
27
|
MAX_JSON_BYTES = 4_000_000
|
|
24
28
|
TOKEN = re.compile(r"^[A-Za-z0-9_.:-]{1,256}$")
|
|
25
29
|
SHA256 = re.compile(r"^[0-9a-f]{64}$")
|
|
30
|
+
SIGNATURE_VALUE = re.compile(r"^[A-Za-z0-9_.:/+=-]{1,4096}$")
|
|
31
|
+
SIGNATURE_FIELDS = ("algorithm", "key_id", "signature")
|
|
26
32
|
CONTENT_TYPES = {
|
|
27
33
|
"headline", "cta", "marketing", "ui", "documentation", "seo", "legal",
|
|
28
34
|
"commercial",
|
|
@@ -101,6 +107,31 @@ def _sha256(value: Any, *, field: str) -> str:
|
|
|
101
107
|
return value
|
|
102
108
|
|
|
103
109
|
|
|
110
|
+
def _message_signature(value: Any) -> dict[str, str]:
|
|
111
|
+
if isinstance(value, Mapping):
|
|
112
|
+
result = dict(value)
|
|
113
|
+
else:
|
|
114
|
+
missing = object()
|
|
115
|
+
result = {
|
|
116
|
+
field: getattr(value, field, missing)
|
|
117
|
+
for field in SIGNATURE_FIELDS
|
|
118
|
+
}
|
|
119
|
+
if any(item is missing for item in result.values()):
|
|
120
|
+
raise CMSReceiverStoreBlocked("publication signature is invalid")
|
|
121
|
+
if set(result) != set(SIGNATURE_FIELDS):
|
|
122
|
+
raise CMSReceiverStoreBlocked("publication signature is invalid")
|
|
123
|
+
if (
|
|
124
|
+
not isinstance(result["algorithm"], str)
|
|
125
|
+
or TOKEN.fullmatch(result["algorithm"]) is None
|
|
126
|
+
or not isinstance(result["key_id"], str)
|
|
127
|
+
or TOKEN.fullmatch(result["key_id"]) is None
|
|
128
|
+
or not isinstance(result["signature"], str)
|
|
129
|
+
or SIGNATURE_VALUE.fullmatch(result["signature"]) is None
|
|
130
|
+
):
|
|
131
|
+
raise CMSReceiverStoreBlocked("publication signature is invalid")
|
|
132
|
+
return result
|
|
133
|
+
|
|
134
|
+
|
|
104
135
|
def _sequence(value: Any) -> int:
|
|
105
136
|
if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
|
|
106
137
|
raise CMSReceiverStoreBlocked("source_sequence is invalid")
|
|
@@ -216,13 +247,23 @@ class DurableCMSReceiverStore:
|
|
|
216
247
|
self,
|
|
217
248
|
connection: sqlite3.Connection,
|
|
218
249
|
*,
|
|
250
|
+
release_evidence_validator: Callable[..., bool],
|
|
251
|
+
publication_signature_validator: Callable[
|
|
252
|
+
[bytes, Mapping[str, str]], bool
|
|
253
|
+
],
|
|
219
254
|
clock: Callable[[], float | int] = time.time,
|
|
220
255
|
):
|
|
221
256
|
if not isinstance(connection, sqlite3.Connection) or connection.in_transaction:
|
|
222
257
|
raise CMSReceiverStoreBlocked("connection must be an idle SQLite connection")
|
|
258
|
+
if not callable(release_evidence_validator):
|
|
259
|
+
raise CMSReceiverStoreBlocked("release evidence validator is required")
|
|
260
|
+
if not callable(publication_signature_validator):
|
|
261
|
+
raise CMSReceiverStoreBlocked("publication signature validator is required")
|
|
223
262
|
if not callable(clock):
|
|
224
263
|
raise CMSReceiverStoreBlocked("clock must be callable")
|
|
225
264
|
self.connection = connection
|
|
265
|
+
self._release_evidence_validator = release_evidence_validator
|
|
266
|
+
self._publication_signature_validator = publication_signature_validator
|
|
226
267
|
self.clock = clock
|
|
227
268
|
self.connection.row_factory = sqlite3.Row
|
|
228
269
|
self.connection.execute("PRAGMA foreign_keys = ON")
|
|
@@ -231,10 +272,12 @@ class DurableCMSReceiverStore:
|
|
|
231
272
|
if int(self.connection.execute("PRAGMA secure_delete").fetchone()[0]) != 1:
|
|
232
273
|
raise CMSReceiverStoreBlocked("secure deletion is unavailable")
|
|
233
274
|
version = int(self.connection.execute("PRAGMA user_version").fetchone()[0])
|
|
234
|
-
if version not in {0, SCHEMA_VERSION}:
|
|
275
|
+
if version not in {0, LEGACY_SCHEMA_VERSION, SCHEMA_VERSION}:
|
|
235
276
|
raise CMSReceiverStoreBlocked("unsupported CMS receiver store schema")
|
|
236
277
|
if version == 0:
|
|
237
278
|
self._create_schema()
|
|
279
|
+
elif version == LEGACY_SCHEMA_VERSION:
|
|
280
|
+
self._migrate_schema()
|
|
238
281
|
self._verify_schema()
|
|
239
282
|
|
|
240
283
|
def _create_schema(self) -> None:
|
|
@@ -282,6 +325,7 @@ class DurableCMSReceiverStore:
|
|
|
282
325
|
tombstone_payload_sha256 TEXT,
|
|
283
326
|
created_at REAL NOT NULL,
|
|
284
327
|
updated_at REAL NOT NULL,
|
|
328
|
+
signature_json TEXT,
|
|
285
329
|
UNIQUE (site_id, source_id, source_sequence)
|
|
286
330
|
)
|
|
287
331
|
""")
|
|
@@ -326,7 +370,24 @@ class DurableCMSReceiverStore:
|
|
|
326
370
|
)
|
|
327
371
|
self.connection.execute(f"PRAGMA user_version = {SCHEMA_VERSION}")
|
|
328
372
|
|
|
329
|
-
def
|
|
373
|
+
def _migrate_schema(self) -> None:
|
|
374
|
+
with _transaction(self.connection):
|
|
375
|
+
self._verify_schema(
|
|
376
|
+
expected_version=LEGACY_SCHEMA_VERSION,
|
|
377
|
+
legacy=True,
|
|
378
|
+
)
|
|
379
|
+
self.connection.execute(
|
|
380
|
+
"ALTER TABLE cms_receiver_publications "
|
|
381
|
+
"ADD COLUMN signature_json TEXT"
|
|
382
|
+
)
|
|
383
|
+
self.connection.execute(f"PRAGMA user_version = {SCHEMA_VERSION}")
|
|
384
|
+
|
|
385
|
+
def _verify_schema(
|
|
386
|
+
self,
|
|
387
|
+
*,
|
|
388
|
+
expected_version: int = SCHEMA_VERSION,
|
|
389
|
+
legacy: bool = False,
|
|
390
|
+
) -> None:
|
|
330
391
|
if int(self.connection.execute("PRAGMA secure_delete").fetchone()[0]) != 1:
|
|
331
392
|
raise CMSReceiverStoreBlocked("secure deletion was disabled")
|
|
332
393
|
expected = {
|
|
@@ -342,7 +403,7 @@ class DurableCMSReceiverStore:
|
|
|
342
403
|
"source_sequence", "source_revision", "payload_json", "status",
|
|
343
404
|
"tombstone_delivery_id", "tombstone_payload_sha256", "created_at",
|
|
344
405
|
"updated_at",
|
|
345
|
-
),
|
|
406
|
+
) + (() if legacy else ("signature_json",)),
|
|
346
407
|
"cms_receiver_localizations": (
|
|
347
408
|
"delivery_id", "locale", "target_text", "target_sha256",
|
|
348
409
|
"approval_id", "approval_expires_at", "release_evidence_json",
|
|
@@ -356,7 +417,7 @@ class DurableCMSReceiverStore:
|
|
|
356
417
|
),
|
|
357
418
|
}
|
|
358
419
|
version = int(self.connection.execute("PRAGMA user_version").fetchone()[0])
|
|
359
|
-
if version !=
|
|
420
|
+
if version != expected_version:
|
|
360
421
|
raise CMSReceiverStoreBlocked("CMS receiver store schema version is invalid")
|
|
361
422
|
for table, columns in expected.items():
|
|
362
423
|
actual = tuple(
|
|
@@ -408,8 +469,30 @@ class DurableCMSReceiverStore:
|
|
|
408
469
|
raise CMSReceiverStoreBlocked("tombstone expectation hash is invalid")
|
|
409
470
|
return result
|
|
410
471
|
|
|
472
|
+
def _verify_publication_signature(
|
|
473
|
+
self,
|
|
474
|
+
payload_json: str,
|
|
475
|
+
signature: Any,
|
|
476
|
+
) -> dict[str, str]:
|
|
477
|
+
normalized = _message_signature(signature)
|
|
478
|
+
try:
|
|
479
|
+
valid = self._publication_signature_validator(
|
|
480
|
+
payload_json.encode("utf-8"), normalized,
|
|
481
|
+
) is True
|
|
482
|
+
except Exception:
|
|
483
|
+
valid = False
|
|
484
|
+
if not valid:
|
|
485
|
+
raise CMSReceiverStoreBlocked(
|
|
486
|
+
"publication signature is no longer valid"
|
|
487
|
+
)
|
|
488
|
+
return normalized
|
|
489
|
+
|
|
411
490
|
def _validate_publication_row(
|
|
412
|
-
self,
|
|
491
|
+
self,
|
|
492
|
+
row: sqlite3.Row,
|
|
493
|
+
*,
|
|
494
|
+
require_current_authorization: bool = True,
|
|
495
|
+
now: float | int | None = None,
|
|
413
496
|
) -> dict[str, Any] | None:
|
|
414
497
|
delivery_id = _token(row["delivery_id"], field="delivery_id")
|
|
415
498
|
payload_sha256 = _sha256(row["payload_sha256"], field="payload_sha256")
|
|
@@ -426,6 +509,7 @@ class DurableCMSReceiverStore:
|
|
|
426
509
|
if row["status"] in {"deleted", "superseded"}:
|
|
427
510
|
if (
|
|
428
511
|
row["payload_json"] is not None
|
|
512
|
+
or row["signature_json"] is not None
|
|
429
513
|
or localizations
|
|
430
514
|
):
|
|
431
515
|
raise CMSReceiverStoreBlocked(
|
|
@@ -457,7 +541,8 @@ class DurableCMSReceiverStore:
|
|
|
457
541
|
or row["tombstone_payload_sha256"] is not None
|
|
458
542
|
):
|
|
459
543
|
raise CMSReceiverStoreBlocked("publication state is invalid")
|
|
460
|
-
|
|
544
|
+
payload_json = row["payload_json"]
|
|
545
|
+
payload = _decode_json(payload_json, field="payload_json")
|
|
461
546
|
if (
|
|
462
547
|
not isinstance(payload, dict)
|
|
463
548
|
or payload.get("delivery_id") != delivery_id
|
|
@@ -479,6 +564,15 @@ class DurableCMSReceiverStore:
|
|
|
479
564
|
raise CMSReceiverStoreBlocked("stored locale bundle is incomplete")
|
|
480
565
|
if len(localizations) != len(expected) or locales != sorted(set(locales)):
|
|
481
566
|
raise CMSReceiverStoreBlocked("stored locale bundle is incomplete")
|
|
567
|
+
checked_at = (
|
|
568
|
+
_timestamp(self.clock() if now is None else now)
|
|
569
|
+
if require_current_authorization else None
|
|
570
|
+
)
|
|
571
|
+
if require_current_authorization:
|
|
572
|
+
signature = _decode_json(
|
|
573
|
+
row["signature_json"], field="signature_json",
|
|
574
|
+
)
|
|
575
|
+
self._verify_publication_signature(payload_json, signature)
|
|
482
576
|
for stored, item in zip(localizations, expected):
|
|
483
577
|
if not isinstance(item, dict):
|
|
484
578
|
raise CMSReceiverStoreBlocked("stored locale is invalid")
|
|
@@ -498,11 +592,29 @@ class DurableCMSReceiverStore:
|
|
|
498
592
|
!= stored["target_sha256"]
|
|
499
593
|
):
|
|
500
594
|
raise CMSReceiverStoreBlocked("stored locale binding is invalid")
|
|
595
|
+
if require_current_authorization:
|
|
596
|
+
approval_expires_at = _timestamp(stored["approval_expires_at"])
|
|
597
|
+
try:
|
|
598
|
+
evidence_valid = self._release_evidence_validator(
|
|
599
|
+
evidence,
|
|
600
|
+
locale=stored["locale"],
|
|
601
|
+
target_sha256=stored["target_sha256"],
|
|
602
|
+
approval_id=stored["approval_id"],
|
|
603
|
+
) is True
|
|
604
|
+
except Exception:
|
|
605
|
+
evidence_valid = False
|
|
606
|
+
if approval_expires_at <= checked_at or not evidence_valid:
|
|
607
|
+
raise CMSReceiverStoreBlocked(
|
|
608
|
+
"stored release authorization is no longer current"
|
|
609
|
+
)
|
|
501
610
|
return payload
|
|
502
611
|
|
|
503
|
-
@staticmethod
|
|
504
612
|
def _assert_publication_binding(
|
|
505
|
-
|
|
613
|
+
self,
|
|
614
|
+
payload: Mapping[str, Any],
|
|
615
|
+
expectation: Mapping[str, Any],
|
|
616
|
+
*,
|
|
617
|
+
now: float | int,
|
|
506
618
|
) -> None:
|
|
507
619
|
for field in (
|
|
508
620
|
"event_id", "site_id", "website_version", "plan_id", "source_id",
|
|
@@ -516,15 +628,49 @@ class DurableCMSReceiverStore:
|
|
|
516
628
|
locales = tuple(item.get("locale") for item in localizations if isinstance(item, dict))
|
|
517
629
|
if locales != expectation["required_locales"] or len(locales) != len(localizations):
|
|
518
630
|
raise CMSReceiverStoreBlocked("publication locale set is no longer current")
|
|
631
|
+
checked_at = _timestamp(now)
|
|
519
632
|
for item in localizations:
|
|
520
|
-
evidence = item.get("release_evidence")
|
|
633
|
+
evidence = item.get("release_evidence") if isinstance(item, Mapping) else None
|
|
634
|
+
commercial_quality = (
|
|
635
|
+
evidence.get("commercial_quality_profile")
|
|
636
|
+
if isinstance(evidence, dict)
|
|
637
|
+
else None
|
|
638
|
+
)
|
|
639
|
+
if expectation["content_type"] == "commercial":
|
|
640
|
+
quality_valid = (
|
|
641
|
+
isinstance(commercial_quality, dict)
|
|
642
|
+
and set(commercial_quality) == {"profile", "version", "sha256"}
|
|
643
|
+
and commercial_quality.get("profile")
|
|
644
|
+
== expectation["commercial_profile"]
|
|
645
|
+
and isinstance(commercial_quality.get("version"), str)
|
|
646
|
+
and TOKEN.fullmatch(commercial_quality["version"]) is not None
|
|
647
|
+
and isinstance(commercial_quality.get("sha256"), str)
|
|
648
|
+
and SHA256.fullmatch(commercial_quality["sha256"]) is not None
|
|
649
|
+
)
|
|
650
|
+
else:
|
|
651
|
+
quality_valid = commercial_quality is None
|
|
521
652
|
if (
|
|
522
653
|
not isinstance(evidence, dict)
|
|
654
|
+
or _timestamp(item.get("approval_expires_at")) <= checked_at
|
|
523
655
|
or evidence.get("content_type") != expectation["content_type"]
|
|
524
656
|
or evidence.get("commercial_profile")
|
|
525
657
|
!= expectation["commercial_profile"]
|
|
658
|
+
or not quality_valid
|
|
526
659
|
):
|
|
527
660
|
raise CMSReceiverStoreBlocked("publication scope is no longer current")
|
|
661
|
+
try:
|
|
662
|
+
evidence_valid = self._release_evidence_validator(
|
|
663
|
+
evidence,
|
|
664
|
+
locale=item.get("locale"),
|
|
665
|
+
target_sha256=item.get("target_sha256"),
|
|
666
|
+
approval_id=item.get("approval_id"),
|
|
667
|
+
) is True
|
|
668
|
+
except Exception:
|
|
669
|
+
evidence_valid = False
|
|
670
|
+
if not evidence_valid:
|
|
671
|
+
raise CMSReceiverStoreBlocked(
|
|
672
|
+
"publication release authorization is no longer current"
|
|
673
|
+
)
|
|
528
674
|
|
|
529
675
|
def register_source(self, expectation: Any) -> Mapping[str, Any]:
|
|
530
676
|
"""Register or monotonically advance one trusted current source."""
|
|
@@ -623,6 +769,11 @@ class DurableCMSReceiverStore:
|
|
|
623
769
|
delivery_id, payload_sha256, payload, payload_json = _verified(
|
|
624
770
|
publication, kind="publication",
|
|
625
771
|
)
|
|
772
|
+
signature = self._verify_publication_signature(
|
|
773
|
+
payload_json,
|
|
774
|
+
getattr(publication, "signature", None),
|
|
775
|
+
)
|
|
776
|
+
signature_json = _canonical_json(signature)
|
|
626
777
|
now = _timestamp(self.clock())
|
|
627
778
|
with _transaction(self.connection):
|
|
628
779
|
self._verify_schema()
|
|
@@ -633,7 +784,7 @@ class DurableCMSReceiverStore:
|
|
|
633
784
|
if row is None:
|
|
634
785
|
raise CMSReceiverStoreBlocked("source expectation is missing")
|
|
635
786
|
current = self._source_from_row(row)
|
|
636
|
-
self._assert_publication_binding(payload, current)
|
|
787
|
+
self._assert_publication_binding(payload, current, now=now)
|
|
637
788
|
existing = self.connection.execute(
|
|
638
789
|
"SELECT * FROM cms_receiver_publications WHERE delivery_id = ?",
|
|
639
790
|
(delivery_id,),
|
|
@@ -643,7 +794,7 @@ class DurableCMSReceiverStore:
|
|
|
643
794
|
existing["payload_sha256"] == payload_sha256
|
|
644
795
|
and existing["status"] == "active"
|
|
645
796
|
and row["active_delivery_id"] == delivery_id
|
|
646
|
-
and self._validate_publication_row(existing) == payload
|
|
797
|
+
and self._validate_publication_row(existing, now=now) == payload
|
|
647
798
|
):
|
|
648
799
|
return {
|
|
649
800
|
"delivery_id": delivery_id,
|
|
@@ -672,12 +823,13 @@ class DurableCMSReceiverStore:
|
|
|
672
823
|
delivery_id, payload_sha256, site_id, source_id,
|
|
673
824
|
source_sequence, source_revision, payload_json, status,
|
|
674
825
|
tombstone_delivery_id, tombstone_payload_sha256,
|
|
675
|
-
created_at, updated_at
|
|
676
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, 'active', NULL, NULL, ?, ?)
|
|
826
|
+
created_at, updated_at, signature_json
|
|
827
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, 'active', NULL, NULL, ?, ?, ?)
|
|
677
828
|
""", (
|
|
678
829
|
delivery_id, payload_sha256, current["site_id"],
|
|
679
830
|
current["source_id"], current["source_sequence"],
|
|
680
831
|
current["source_revision"], payload_json, now, now,
|
|
832
|
+
signature_json,
|
|
681
833
|
))
|
|
682
834
|
for item in payload["localizations"]:
|
|
683
835
|
self.connection.execute("""
|
|
@@ -713,7 +865,8 @@ class DurableCMSReceiverStore:
|
|
|
713
865
|
)
|
|
714
866
|
changed = self.connection.execute("""
|
|
715
867
|
UPDATE cms_receiver_publications
|
|
716
|
-
SET payload_json = NULL,
|
|
868
|
+
SET payload_json = NULL, signature_json = NULL,
|
|
869
|
+
updated_at = ?
|
|
717
870
|
WHERE delivery_id = ? AND status = 'superseded'
|
|
718
871
|
""", (now, old_delivery_id)).rowcount
|
|
719
872
|
except sqlite3.Error as error:
|
|
@@ -758,10 +911,11 @@ class DurableCMSReceiverStore:
|
|
|
758
911
|
or publication["source_id"] != source_id
|
|
759
912
|
):
|
|
760
913
|
raise CMSReceiverStoreBlocked("active publication is invalid")
|
|
761
|
-
|
|
914
|
+
now = _timestamp(self.clock())
|
|
915
|
+
payload = self._validate_publication_row(publication, now=now)
|
|
762
916
|
if payload is None:
|
|
763
917
|
raise CMSReceiverStoreBlocked("active publication was deleted")
|
|
764
|
-
self._assert_publication_binding(payload, expected)
|
|
918
|
+
self._assert_publication_binding(payload, expected, now=now)
|
|
765
919
|
return payload
|
|
766
920
|
|
|
767
921
|
def register_tombstone(self, expectation: Any) -> Mapping[str, Any]:
|
|
@@ -806,7 +960,9 @@ class DurableCMSReceiverStore:
|
|
|
806
960
|
or source["active_delivery_id"] != value["publication_delivery_id"]
|
|
807
961
|
):
|
|
808
962
|
raise CMSReceiverStoreBlocked("tombstone publication is not active")
|
|
809
|
-
payload = self._validate_publication_row(
|
|
963
|
+
payload = self._validate_publication_row(
|
|
964
|
+
publication, require_current_authorization=False,
|
|
965
|
+
)
|
|
810
966
|
if (
|
|
811
967
|
not isinstance(payload, dict)
|
|
812
968
|
or payload.get("event_id") != value["event_id"]
|
|
@@ -885,7 +1041,9 @@ class DurableCMSReceiverStore:
|
|
|
885
1041
|
).fetchone()
|
|
886
1042
|
if (
|
|
887
1043
|
publication is None
|
|
888
|
-
or self._validate_publication_row(
|
|
1044
|
+
or self._validate_publication_row(
|
|
1045
|
+
publication, require_current_authorization=False,
|
|
1046
|
+
) is not None
|
|
889
1047
|
or publication["status"] != "deleted"
|
|
890
1048
|
or publication["payload_sha256"]
|
|
891
1049
|
!= expected["publication_payload_sha256"]
|
|
@@ -923,7 +1081,9 @@ class DurableCMSReceiverStore:
|
|
|
923
1081
|
!= expected["publication_delivery_id"]
|
|
924
1082
|
):
|
|
925
1083
|
raise CMSReceiverStoreBlocked("tombstone publication changed")
|
|
926
|
-
if self._validate_publication_row(
|
|
1084
|
+
if self._validate_publication_row(
|
|
1085
|
+
publication, require_current_authorization=False,
|
|
1086
|
+
) is None:
|
|
927
1087
|
raise CMSReceiverStoreBlocked("tombstone publication was deleted")
|
|
928
1088
|
self.connection.execute(
|
|
929
1089
|
"DELETE FROM cms_receiver_localizations WHERE delivery_id = ?",
|
|
@@ -931,7 +1091,8 @@ class DurableCMSReceiverStore:
|
|
|
931
1091
|
)
|
|
932
1092
|
changed = self.connection.execute("""
|
|
933
1093
|
UPDATE cms_receiver_publications
|
|
934
|
-
SET payload_json = NULL,
|
|
1094
|
+
SET payload_json = NULL, signature_json = NULL,
|
|
1095
|
+
status = 'deleted',
|
|
935
1096
|
tombstone_delivery_id = ?, tombstone_payload_sha256 = ?,
|
|
936
1097
|
updated_at = ?
|
|
937
1098
|
WHERE delivery_id = ? AND status = 'active'
|
|
@@ -974,6 +1135,7 @@ class DurableCMSReceiverStore:
|
|
|
974
1135
|
contract_sha256 = _sha256(
|
|
975
1136
|
getattr(probe, "contract_sha256", None), field="contract_sha256",
|
|
976
1137
|
)
|
|
1138
|
+
now = _timestamp(self.clock())
|
|
977
1139
|
self._verify_schema()
|
|
978
1140
|
if self.connection.in_transaction:
|
|
979
1141
|
raise CMSReceiverStoreBlocked("health cannot join an external transaction")
|
|
@@ -1017,7 +1179,7 @@ class DurableCMSReceiverStore:
|
|
|
1017
1179
|
"SELECT * FROM cms_receiver_publications ORDER BY delivery_id"
|
|
1018
1180
|
).fetchall()
|
|
1019
1181
|
for publication in publications:
|
|
1020
|
-
self._validate_publication_row(publication)
|
|
1182
|
+
self._validate_publication_row(publication, now=now)
|
|
1021
1183
|
tombstones = self.connection.execute(
|
|
1022
1184
|
"SELECT * FROM cms_receiver_tombstones ORDER BY tombstone_id"
|
|
1023
1185
|
).fetchall()
|
package/standard-skills/translate-native/integrations/website_localization_cms_source_client.py
CHANGED
|
@@ -346,6 +346,9 @@ class CMSLocalizationSourceHTTPClient:
|
|
|
346
346
|
expected_capabilities_sha256: str,
|
|
347
347
|
authentication_headers: Callable[[Mapping[str, Any]], Mapping[str, str]],
|
|
348
348
|
*,
|
|
349
|
+
expected_runtime_capabilities_sha256: str,
|
|
350
|
+
expected_commercial_rendering_registry_sha256: str,
|
|
351
|
+
expected_terminal_receiver_capabilities_sha256: str | None = None,
|
|
349
352
|
transport: HTTPTransport | None = None,
|
|
350
353
|
timeout: float | int = 30,
|
|
351
354
|
allow_loopback_http: bool = False,
|
|
@@ -360,6 +363,29 @@ class CMSLocalizationSourceHTTPClient:
|
|
|
360
363
|
raise ValueError("expected capability digest is invalid")
|
|
361
364
|
if not callable(authentication_headers):
|
|
362
365
|
raise TypeError("authentication_headers must be callable")
|
|
366
|
+
if (
|
|
367
|
+
not isinstance(expected_runtime_capabilities_sha256, str)
|
|
368
|
+
or SHA256.fullmatch(expected_runtime_capabilities_sha256) is None
|
|
369
|
+
or not isinstance(
|
|
370
|
+
expected_commercial_rendering_registry_sha256, str,
|
|
371
|
+
)
|
|
372
|
+
or SHA256.fullmatch(
|
|
373
|
+
expected_commercial_rendering_registry_sha256
|
|
374
|
+
) is None
|
|
375
|
+
):
|
|
376
|
+
raise ValueError("expected runtime capability binding is invalid")
|
|
377
|
+
if (
|
|
378
|
+
expected_terminal_receiver_capabilities_sha256 is not None
|
|
379
|
+
and (
|
|
380
|
+
not isinstance(
|
|
381
|
+
expected_terminal_receiver_capabilities_sha256, str,
|
|
382
|
+
)
|
|
383
|
+
or SHA256.fullmatch(
|
|
384
|
+
expected_terminal_receiver_capabilities_sha256
|
|
385
|
+
) is None
|
|
386
|
+
)
|
|
387
|
+
):
|
|
388
|
+
raise ValueError("expected terminal receiver capability is invalid")
|
|
363
389
|
if (
|
|
364
390
|
isinstance(timeout, bool)
|
|
365
391
|
or not isinstance(timeout, (int, float))
|
|
@@ -368,6 +394,15 @@ class CMSLocalizationSourceHTTPClient:
|
|
|
368
394
|
):
|
|
369
395
|
raise ValueError("timeout is outside the supported range")
|
|
370
396
|
self.expected_capabilities_sha256 = expected_capabilities_sha256
|
|
397
|
+
self.expected_runtime_capabilities_sha256 = (
|
|
398
|
+
expected_runtime_capabilities_sha256
|
|
399
|
+
)
|
|
400
|
+
self.expected_commercial_rendering_registry_sha256 = (
|
|
401
|
+
expected_commercial_rendering_registry_sha256
|
|
402
|
+
)
|
|
403
|
+
self.expected_terminal_receiver_capabilities_sha256 = (
|
|
404
|
+
expected_terminal_receiver_capabilities_sha256
|
|
405
|
+
)
|
|
371
406
|
self.authentication_headers = authentication_headers
|
|
372
407
|
self.transport = URLTransport() if transport is None else transport
|
|
373
408
|
if not callable(getattr(self.transport, "request", None)):
|
|
@@ -417,14 +452,31 @@ class CMSLocalizationSourceHTTPClient:
|
|
|
417
452
|
except Exception:
|
|
418
453
|
_fail("local_contract_invalid")
|
|
419
454
|
if (
|
|
420
|
-
set(response) != {
|
|
455
|
+
set(response) != {
|
|
456
|
+
"schema", "capabilities", "capability_binding",
|
|
457
|
+
}
|
|
421
458
|
or response.get("schema") != _HTTP.CAPABILITIES_RESPONSE_SCHEMA
|
|
422
459
|
or response.get("capabilities") != expected
|
|
423
460
|
or expected.get("sha256") != self.expected_capabilities_sha256
|
|
424
461
|
):
|
|
425
462
|
_fail("capabilities_binding")
|
|
463
|
+
self._verified_runtime_binding(response.get("capability_binding"))
|
|
426
464
|
return response
|
|
427
465
|
|
|
466
|
+
def _verified_runtime_binding(self, value: Any) -> Mapping[str, Any]:
|
|
467
|
+
try:
|
|
468
|
+
binding = _HTTP._capability_binding_payload(value)
|
|
469
|
+
except Exception:
|
|
470
|
+
_fail("runtime_capability_binding")
|
|
471
|
+
if (
|
|
472
|
+
binding["capabilities_sha256"]
|
|
473
|
+
!= self.expected_runtime_capabilities_sha256
|
|
474
|
+
or binding["commercial_rendering_registry_sha256"]
|
|
475
|
+
!= self.expected_commercial_rendering_registry_sha256
|
|
476
|
+
):
|
|
477
|
+
_fail("runtime_capability_binding")
|
|
478
|
+
return binding
|
|
479
|
+
|
|
428
480
|
def _contract(self, operation: str) -> Mapping[str, Any]:
|
|
429
481
|
capabilities = self.capabilities()["capabilities"]
|
|
430
482
|
return capabilities["operations"][operation]
|
|
@@ -498,6 +550,7 @@ class CMSLocalizationSourceHTTPClient:
|
|
|
498
550
|
!= self.expected_capabilities_sha256
|
|
499
551
|
):
|
|
500
552
|
_fail("enqueue_binding")
|
|
553
|
+
self._verified_runtime_binding(response.get("capability_binding"))
|
|
501
554
|
return response
|
|
502
555
|
|
|
503
556
|
def status(self, event_id: str, site_id: str) -> Mapping[str, Any]:
|
|
@@ -522,6 +575,7 @@ class CMSLocalizationSourceHTTPClient:
|
|
|
522
575
|
!= self.expected_capabilities_sha256
|
|
523
576
|
):
|
|
524
577
|
_fail("status_binding")
|
|
578
|
+
self._verified_runtime_binding(response.get("capability_binding"))
|
|
525
579
|
try:
|
|
526
580
|
normalized = _HTTP._source_status_payload(
|
|
527
581
|
_PayloadView(response["status"]),
|
|
@@ -532,6 +586,11 @@ class CMSLocalizationSourceHTTPClient:
|
|
|
532
586
|
_fail("status_binding")
|
|
533
587
|
if normalized != response["status"]:
|
|
534
588
|
_fail("status_binding")
|
|
589
|
+
if (
|
|
590
|
+
normalized["terminal_receiver_capabilities_sha256"]
|
|
591
|
+
!= self.expected_terminal_receiver_capabilities_sha256
|
|
592
|
+
):
|
|
593
|
+
_fail("status_binding")
|
|
535
594
|
return response
|
|
536
595
|
|
|
537
596
|
def health(self) -> Mapping[str, Any]:
|
|
@@ -547,6 +606,7 @@ class CMSLocalizationSourceHTTPClient:
|
|
|
547
606
|
!= self.expected_capabilities_sha256
|
|
548
607
|
):
|
|
549
608
|
_fail("health_binding")
|
|
609
|
+
self._verified_runtime_binding(response.get("capability_binding"))
|
|
550
610
|
try:
|
|
551
611
|
normalized = _HTTP._health_payload(_PayloadView(response["health"]))
|
|
552
612
|
except Exception:
|
|
@@ -554,6 +614,8 @@ class CMSLocalizationSourceHTTPClient:
|
|
|
554
614
|
if (
|
|
555
615
|
normalized != response["health"]
|
|
556
616
|
or (result.status == 503) != (normalized["status"] == "blocked")
|
|
617
|
+
or normalized["terminal_receiver_capabilities_sha256"]
|
|
618
|
+
!= self.expected_terminal_receiver_capabilities_sha256
|
|
557
619
|
):
|
|
558
620
|
_fail("health_binding")
|
|
559
621
|
return response
|
|
@@ -571,6 +633,7 @@ class CMSLocalizationSourceHTTPClient:
|
|
|
571
633
|
!= self.expected_capabilities_sha256
|
|
572
634
|
):
|
|
573
635
|
_fail("readiness_binding")
|
|
636
|
+
self._verified_runtime_binding(response.get("capability_binding"))
|
|
574
637
|
try:
|
|
575
638
|
normalized = _HTTP._readiness_payload(response["readiness"])
|
|
576
639
|
except Exception:
|