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
@@ -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(