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.
Files changed (106) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +17 -1
  3. package/agent-spine-plugin/CHANGELOG.md +1 -1
  4. package/agent-spine-plugin/docs/host-integration.md +1 -15
  5. package/agent-spine-plugin/docs/preflight-recall.md +1 -1
  6. package/agent-spine-plugin/scripts/check-install.js +1 -6
  7. package/agent-spine-plugin/scripts/hermetic-process.js +8 -0
  8. package/agent-spine-plugin/scripts/release-check.js +2 -3
  9. package/agent-spine-plugin/scripts/run-tests-hermetic.js +8 -11
  10. package/agent-spine-plugin/src/hook.js +40 -40
  11. package/agent-spine-plugin/src/lib/briefing.js +6 -3
  12. package/agent-spine-plugin/src/lib/hook-context.js +63 -64
  13. package/agent-spine-plugin/src/lib/hook-output.js +115 -94
  14. package/agent-spine-plugin/src/lib/hook-timeline.js +20 -11
  15. package/agent-spine-plugin/src/lib/host-instruction-budget.js +8 -14
  16. package/agent-spine-plugin/src/lib/mcp-timeline-tools.js +0 -7
  17. package/agent-spine-plugin/src/lib/pre-answer-timeline-recall.js +104 -0
  18. package/agent-spine-plugin/src/lib/session-timeline-codex.js +0 -4
  19. package/agent-spine-plugin/src/lib/session-timeline-enrollment-source.js +0 -3
  20. package/agent-spine-plugin/src/lib/session-timeline-enrollment-storage.js +0 -4
  21. package/agent-spine-plugin/src/lib/session-timeline-enrollment-transport.js +0 -2
  22. package/agent-spine-plugin/src/lib/session-timeline-enrollment.js +0 -22
  23. package/agent-spine-plugin/src/lib/session-timeline-host-origin.js +0 -4
  24. package/agent-spine-plugin/src/lib/session-timeline-prior.js +42 -50
  25. package/agent-spine-plugin/src/lib/session-timeline-provider.js +0 -3
  26. package/agent-spine-plugin/src/lib/session-timeline-query.js +0 -1
  27. package/agent-spine-plugin/src/lib/session-timeline-sid-acl.js +0 -4
  28. package/agent-spine-plugin/src/lib/session-timeline-source.js +0 -3
  29. package/agent-spine-plugin/src/lib/session-timeline-transport.js +0 -8
  30. package/agent-spine-plugin/src/lib/session-timeline-windows-acl.js +0 -3
  31. package/agent-spine-plugin/src/lib/session-timeline.js +370 -408
  32. package/agent-spine-plugin/src/lib/source-roots.js +26 -16
  33. package/agent-spine-plugin/src/lib/timeline-user-feedback.js +0 -1
  34. package/bin/core-bootstrap.js +18 -10
  35. package/bin/launcher-mode.js +5 -0
  36. package/bin/launcher-runtime.js +27 -0
  37. package/bin/verify-bundled-agent-sources.cjs +28 -2
  38. package/blun.mjs +798 -143
  39. package/bundled-agent-sources.json +192 -155
  40. package/package.json +1 -1
  41. package/standard-skills/translate-native/README.md +1334 -1
  42. package/standard-skills/translate-native/SKILL.md +1 -1
  43. package/standard-skills/translate-native/VERSION +1 -1
  44. package/standard-skills/translate-native/docs/PREMORTEM.md +901 -0
  45. package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION.md +208 -41
  46. package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_API.md +1319 -77
  47. package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_EVIDENCE_HTTP.md +66 -11
  48. package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_HTTP_PROVIDER.md +19 -1
  49. package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_RECEIPT_VERIFIER_HTTP.md +24 -6
  50. package/standard-skills/translate-native/integrations/commercial_localization_profile.py +23 -0
  51. package/standard-skills/translate-native/integrations/website_localization.py +234 -10
  52. package/standard-skills/translate-native/integrations/website_localization_benchmark.py +39 -3
  53. package/standard-skills/translate-native/integrations/website_localization_cms.py +512 -10
  54. package/standard-skills/translate-native/integrations/website_localization_cms_client.py +116 -0
  55. package/standard-skills/translate-native/integrations/website_localization_cms_receiver.py +101 -11
  56. package/standard-skills/translate-native/integrations/website_localization_cms_receiver_runtime.py +12 -1
  57. package/standard-skills/translate-native/integrations/website_localization_cms_receiver_store.py +186 -24
  58. package/standard-skills/translate-native/integrations/website_localization_cms_source_client.py +64 -1
  59. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery.py +528 -5
  60. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_auth.py +918 -0
  61. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_auth_runtime.py +609 -0
  62. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_client.py +885 -0
  63. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_http.py +1244 -0
  64. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_runtime.py +220 -0
  65. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_sidecar_adapter.py +331 -0
  66. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_capabilities_http.py +444 -0
  67. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_client.py +673 -0
  68. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_dispatch.py +1317 -0
  69. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_dispatch_client.py +826 -0
  70. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_dispatch_http.py +1152 -0
  71. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_dispatch_openapi.py +896 -0
  72. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_dispatch_runtime.py +621 -0
  73. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_http.py +1180 -0
  74. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_runtime.py +1479 -0
  75. package/standard-skills/translate-native/integrations/website_localization_cms_source_http.py +133 -17
  76. package/standard-skills/translate-native/integrations/website_localization_cms_source_runtime.py +319 -4
  77. package/standard-skills/translate-native/integrations/website_localization_cms_source_service.py +72 -2
  78. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_http.py +13 -0
  79. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_receiver.py +56 -13
  80. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_receiver_openapi.py +335 -0
  81. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_receiver_runtime.py +66 -1
  82. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_processing_monitor.py +73 -7
  83. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_receiver_client.py +93 -9
  84. package/standard-skills/translate-native/integrations/website_localization_evidence_http.py +221 -11
  85. package/standard-skills/translate-native/integrations/website_localization_health.py +6 -0
  86. package/standard-skills/translate-native/integrations/website_localization_http_provider.py +1 -1
  87. package/standard-skills/translate-native/integrations/website_localization_quality_profiles.py +350 -0
  88. package/standard-skills/translate-native/integrations/website_localization_queue.py +126 -56
  89. package/standard-skills/translate-native/integrations/website_localization_receipt_verifier_http.py +166 -13
  90. package/standard-skills/translate-native/integrations/website_localization_release.py +423 -17
  91. package/standard-skills/translate-native/integrations/website_localization_release_coordinator.py +168 -13
  92. package/standard-skills/translate-native/integrations/website_localization_runner.py +10 -0
  93. package/standard-skills/translate-native/integrations/website_localization_worker.py +213 -9
  94. package/standard-skills/translate-native/provenance.json +2 -2
  95. package/standard-skills/translate-native/references/commercial-localization.md +96 -17
  96. package/standard-skills/translate-native/scripts/blun_language_guard.py +3 -1
  97. package/standard-skills/translate-native/scripts/check_commercial_review.py +69 -9
  98. package/standard-skills/translate-native/scripts/commercial_localization_profile.py +829 -31
  99. package/standard-tools/language-guard/VERSION +1 -1
  100. package/standard-tools/language-guard/blun_language_guard.py +3 -1
  101. package/standard-tools/language-guard/check_commercial_review.py +69 -9
  102. package/standard-tools/language-guard/commercial_localization_profile.py +829 -31
  103. package/standard-tools/language-guard/provenance.json +2 -2
  104. package/standard-tools/manifest.json +1 -1
  105. package/worker-host.mjs +215 -82
  106. package/agent-spine-plugin/scripts/check-install-king.js +0 -37
@@ -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
- Every callback rechecks the current binding inside one SQLite write transaction
7
- so a concurrent source change cannot publish or delete the wrong generation.
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 = 1
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 _verify_schema(self) -> None:
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 != SCHEMA_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, row: sqlite3.Row,
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
- payload = _decode_json(row["payload_json"], field="payload_json")
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
- payload: Mapping[str, Any], expectation: Mapping[str, Any],
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, updated_at = ?
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
- payload = self._validate_publication_row(publication)
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(publication)
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(publication) is not None
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(publication) is None:
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, status = 'deleted',
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()
@@ -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) != {"schema", "capabilities"}
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: