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
@@ -14,7 +14,7 @@ from dataclasses import dataclass
14
14
  from typing import Any, Callable, Iterator, Mapping
15
15
 
16
16
 
17
- SCHEMA_VERSION = 1
17
+ SCHEMA_VERSION = 2
18
18
  STATUS_SCHEMA = "blun.cms-terminal-receiver-status-response.v1"
19
19
  NOTIFICATION_SCHEMA = "blun.cms-source-terminal-notification.v1"
20
20
  NOTIFICATION_FIELDS = {
@@ -87,6 +87,7 @@ class TerminalProcessingHealth:
87
87
  due: int
88
88
  expired_leases: int
89
89
  failed: int
90
+ expected_capabilities_sha256: str
90
91
 
91
92
 
92
93
  def _blocked(code: str) -> None:
@@ -169,6 +170,7 @@ class DurableTerminalProcessingMonitor:
169
170
  def __init__(
170
171
  self,
171
172
  connection: sqlite3.Connection,
173
+ expected_capabilities_sha256: str,
172
174
  *,
173
175
  poll_interval_seconds: float | int = 30,
174
176
  base_delay_seconds: float | int = 5,
@@ -178,6 +180,9 @@ class DurableTerminalProcessingMonitor:
178
180
  raise TypeError("connection must be sqlite3.Connection")
179
181
  self.connection = connection
180
182
  self.connection.row_factory = sqlite3.Row
183
+ if not _sha(expected_capabilities_sha256):
184
+ _blocked("capabilities_invalid")
185
+ self.expected_capabilities_sha256 = expected_capabilities_sha256
181
186
  self.poll_interval_seconds = _duration(
182
187
  poll_interval_seconds, "poll_interval_invalid"
183
188
  )
@@ -189,15 +194,68 @@ class DurableTerminalProcessingMonitor:
189
194
 
190
195
  def _initialize(self) -> None:
191
196
  with _transaction(self.connection):
197
+ tables = {
198
+ row[0] for row in self.connection.execute(
199
+ "SELECT name FROM sqlite_master WHERE type = 'table' "
200
+ "AND name IN ('cms_source_terminal_processing_meta', "
201
+ "'cms_source_terminal_processing')"
202
+ ).fetchall()
203
+ }
204
+ if len(tables) == 1:
205
+ _blocked("schema_altered")
206
+ if tables:
207
+ meta_columns = tuple(
208
+ row["name"] for row in self.connection.execute(
209
+ "PRAGMA table_info(cms_source_terminal_processing_meta)"
210
+ ).fetchall()
211
+ )
212
+ columns = tuple(
213
+ row["name"] for row in self.connection.execute(
214
+ "PRAGMA table_info(cms_source_terminal_processing)"
215
+ ).fetchall()
216
+ )
217
+ meta = self.connection.execute(
218
+ "SELECT * FROM cms_source_terminal_processing_meta"
219
+ ).fetchall()
220
+ legacy = (
221
+ meta_columns == ("singleton", "schema_version")
222
+ and columns == _COLUMNS
223
+ and len(meta) == 1
224
+ and tuple(meta[0]) == (1, 1)
225
+ )
226
+ current = (
227
+ meta_columns == (
228
+ "singleton", "schema_version",
229
+ "expected_capabilities_sha256",
230
+ )
231
+ and columns == _COLUMNS
232
+ and len(meta) == 1
233
+ and tuple(meta[0]) == (
234
+ 1, SCHEMA_VERSION,
235
+ self.expected_capabilities_sha256,
236
+ )
237
+ )
238
+ if legacy:
239
+ if self.connection.execute(
240
+ "SELECT 1 FROM cms_source_terminal_processing LIMIT 1"
241
+ ).fetchone() is not None:
242
+ _blocked("legacy_unbound")
243
+ self.connection.execute(
244
+ "DROP TABLE cms_source_terminal_processing_meta"
245
+ )
246
+ elif not current:
247
+ _blocked("schema_altered")
192
248
  self.connection.execute("""
193
249
  CREATE TABLE IF NOT EXISTS cms_source_terminal_processing_meta (
194
250
  singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
195
- schema_version INTEGER NOT NULL CHECK (schema_version = 1)
251
+ schema_version INTEGER NOT NULL CHECK (schema_version = 2),
252
+ expected_capabilities_sha256 TEXT NOT NULL
196
253
  )
197
254
  """)
198
255
  self.connection.execute("""
199
- INSERT OR IGNORE INTO cms_source_terminal_processing_meta VALUES (1, 1)
200
- """)
256
+ INSERT OR IGNORE INTO cms_source_terminal_processing_meta
257
+ VALUES (1, 2, ?)
258
+ """, (self.expected_capabilities_sha256,))
201
259
  self.connection.execute("""
202
260
  CREATE TABLE IF NOT EXISTS cms_source_terminal_processing (
203
261
  event_id TEXT PRIMARY KEY, notification_id TEXT NOT NULL UNIQUE,
@@ -236,12 +294,17 @@ class DurableTerminalProcessingMonitor:
236
294
  "PRAGMA table_info(cms_source_terminal_processing)"
237
295
  ).fetchall())
238
296
  meta = self.connection.execute(
239
- "SELECT singleton, schema_version FROM cms_source_terminal_processing_meta"
297
+ "SELECT singleton, schema_version, expected_capabilities_sha256 "
298
+ "FROM cms_source_terminal_processing_meta"
240
299
  ).fetchall()
241
300
  if (
242
- meta_columns != ("singleton", "schema_version")
301
+ meta_columns != (
302
+ "singleton", "schema_version", "expected_capabilities_sha256",
303
+ )
243
304
  or columns != _COLUMNS or len(meta) != 1
244
- or tuple(meta[0]) != (1, SCHEMA_VERSION)
305
+ or tuple(meta[0]) != (
306
+ 1, SCHEMA_VERSION, self.expected_capabilities_sha256,
307
+ )
245
308
  ):
246
309
  _blocked("schema_altered")
247
310
 
@@ -417,6 +480,8 @@ class DurableTerminalProcessingMonitor:
417
480
  and (error is None or _error(error))
418
481
  and _valid_time(value.get("processed_at"), nullable=True)
419
482
  and _sha(value.get("capabilities_sha256"))
483
+ and value.get("capabilities_sha256")
484
+ == self.expected_capabilities_sha256
420
485
  and (remote == "leased") == (value.get("lease_expires_at") is not None)
421
486
  and (remote == "succeeded") == (value.get("processed_at") is not None)
422
487
  and (error is None or remote in {"retry_wait", "failed"})
@@ -532,6 +597,7 @@ class DurableTerminalProcessingMonitor:
532
597
  return TerminalProcessingHealth(
533
598
  "blocked" if expired or counts["failed"] else "ok",
534
599
  counts, due, expired, counts["failed"],
600
+ self.expected_capabilities_sha256,
535
601
  )
536
602
 
537
603
  def _require_claim(self, claim: sqlite3.Row, now: float) -> sqlite3.Row:
@@ -4,23 +4,28 @@
4
4
  from __future__ import annotations
5
5
 
6
6
  import hashlib
7
+ import importlib.util
7
8
  import json
8
9
  import math
9
10
  import re
10
11
  import socket
12
+ import sys
11
13
  import urllib.error
12
14
  import urllib.parse
13
15
  import urllib.request
14
16
  from dataclasses import dataclass
17
+ from pathlib import Path
15
18
  from typing import Any, Callable, Mapping, Protocol
16
19
 
17
20
 
18
21
  AUTH_SCHEMA = "blun.cms-source-terminal-notification-http-auth.v1"
19
- API_SCHEMA = "blun.cms-terminal-receiver-api.v1"
20
- CAPABILITIES_SCHEMA = "blun.cms-terminal-receiver-capabilities.v1"
22
+ API_SCHEMA = "blun.cms-terminal-receiver-api.v3"
23
+ CAPABILITIES_SCHEMA = "blun.cms-terminal-receiver-capabilities.v3"
21
24
  CAPABILITIES_RESPONSE_SCHEMA = (
22
- "blun.cms-terminal-receiver-capabilities-response.v1"
25
+ "blun.cms-terminal-receiver-capabilities-response.v3"
23
26
  )
27
+ OPENAPI_DOCUMENT_SCHEMA = "blun.cms-terminal-receiver-openapi.v2"
28
+ OPENAPI_RESPONSE_SCHEMA = "blun.cms-terminal-receiver-openapi-response.v1"
24
29
  HEALTH_SCHEMA = "blun.cms-terminal-receiver-health.v1"
25
30
  READINESS_SCHEMA = "blun.cms-terminal-receiver-readiness.v1"
26
31
  STATUS_REQUEST_SCHEMA = "blun.cms-terminal-receiver-status-request.v1"
@@ -31,10 +36,13 @@ NOTIFICATION_SCHEMA = "blun.cms-source-terminal-notification.v1"
31
36
  ACK_SCHEMA = "blun.cms-source-terminal-notification-ack.v1"
32
37
  BASE_PATH = "/v1/localization/terminal-notifications"
33
38
  CAPABILITIES_PATH = BASE_PATH + "/capabilities"
39
+ OPENAPI_PATH = BASE_PATH + "/openapi"
34
40
  HEALTH_PATH = BASE_PATH + "/health"
35
41
  READINESS_PATH = BASE_PATH + "/readiness"
36
42
  STATUS_PATH = BASE_PATH + "/status"
37
- MAX_RESPONSE_BYTES = 16_384
43
+ CAPABILITIES_PRECONDITION_HEADER = "X-Localization-Capabilities-SHA256"
44
+ MAX_REQUEST_BYTES = 16_384
45
+ MAX_RESPONSE_BYTES = 1_000_000
38
46
  MAX_ENDPOINT_LENGTH = 2_048
39
47
  MAX_HEADER_VALUE_LENGTH = 4_096
40
48
  EMPTY_SHA256 = hashlib.sha256(b"").hexdigest()
@@ -57,6 +65,7 @@ RESERVED_HEADERS = {
57
65
  "x-localization-terminal-notification-id",
58
66
  "x-localization-terminal-notification-sha256",
59
67
  "x-localization-terminal-status-sha256",
68
+ "x-localization-capabilities-sha256",
60
69
  }
61
70
  NOTIFICATION_FIELDS = {
62
71
  "schema", "notification_id", "event_id", "site_id", "plan_id",
@@ -69,6 +78,26 @@ ACK_FIELDS = {
69
78
  }
70
79
 
71
80
 
81
+ def _load_module(name: str, path: Path):
82
+ spec = importlib.util.spec_from_file_location(name, path)
83
+ if spec is None or spec.loader is None:
84
+ raise RuntimeError(
85
+ "cannot load terminal receiver client dependency: " + path.name
86
+ )
87
+ module = importlib.util.module_from_spec(spec)
88
+ sys.modules[spec.name] = module
89
+ spec.loader.exec_module(module)
90
+ return module
91
+
92
+
93
+ _OPENAPI = _load_module(
94
+ "blun_website_localization_terminal_receiver_client_openapi",
95
+ Path(__file__).resolve().with_name(
96
+ "website_localization_cms_terminal_notification_receiver_openapi.py"
97
+ ),
98
+ )
99
+
100
+
72
101
  class TerminalReceiverClientBlocked(RuntimeError):
73
102
  """Stable, content-free control-plane failure."""
74
103
 
@@ -147,7 +176,7 @@ def _canonical(value: Any) -> bytes:
147
176
  ).encode("utf-8")
148
177
  except (TypeError, ValueError, RecursionError):
149
178
  _fail("request_invalid")
150
- if not raw or len(raw) > MAX_RESPONSE_BYTES:
179
+ if not raw or len(raw) > MAX_REQUEST_BYTES:
151
180
  _fail("request_invalid")
152
181
  return raw
153
182
 
@@ -373,6 +402,10 @@ def _operation(name, method, path, scope, request_schema, request_fields,
373
402
  "request_fields": request_fields,
374
403
  "response_schema": response_schema,
375
404
  "response_fields": response_fields,
405
+ "capabilities_precondition_header": (
406
+ None if name == "capabilities"
407
+ else CAPABILITIES_PRECONDITION_HEADER
408
+ ),
376
409
  "success_status": 200,
377
410
  }
378
411
 
@@ -381,7 +414,8 @@ def _valid_capabilities(value: Any, expected_sha256: str) -> bool:
381
414
  if not isinstance(value, dict) or set(value) != {
382
415
  "schema", "api_schema", "authentication_request_schema",
383
416
  "principal_schema", "error_schema", "limits", "processing_statuses",
384
- "terminal_statuses", "operations", "sha256",
417
+ "terminal_statuses", "operations", "openapi_document_schema", "sha256",
418
+ "semantics",
385
419
  }:
386
420
  return False
387
421
  digest = value.get("sha256")
@@ -395,8 +429,12 @@ def _valid_capabilities(value: Any, expected_sha256: str) -> bool:
395
429
  or value.get("authentication_request_schema") != AUTH_SCHEMA
396
430
  or value.get("principal_schema") != PRINCIPAL_SCHEMA
397
431
  or value.get("error_schema") != ERROR_SCHEMA
432
+ or value.get("openapi_document_schema") != OPENAPI_DOCUMENT_SCHEMA
398
433
  or value.get("processing_statuses") != list(PROCESSING_STATUSES)
399
434
  or value.get("terminal_statuses") != sorted(TERMINAL_STATUSES)
435
+ or value.get("semantics") != {
436
+ "non_discovery_operations_require_exact_capability_precondition": True,
437
+ }
400
438
  ):
401
439
  return False
402
440
  limits = value.get("limits")
@@ -404,12 +442,14 @@ def _valid_capabilities(value: Any, expected_sha256: str) -> bool:
404
442
  not isinstance(limits, dict)
405
443
  or set(limits) != {
406
444
  "max_body_bytes", "max_headers", "max_header_value_bytes",
407
- "processing_max_attempts_min", "processing_max_attempts_max",
445
+ "max_response_bytes", "processing_max_attempts_min",
446
+ "processing_max_attempts_max",
408
447
  }
409
448
  or limits != {
410
449
  "max_body_bytes": 16_384,
411
450
  "max_headers": 64,
412
451
  "max_header_value_bytes": 4_096,
452
+ "max_response_bytes": 1_000_000,
413
453
  "processing_max_attempts_min": 1,
414
454
  "processing_max_attempts_max": 20,
415
455
  }
@@ -417,7 +457,8 @@ def _valid_capabilities(value: Any, expected_sha256: str) -> bool:
417
457
  return False
418
458
  operations = value.get("operations")
419
459
  if not isinstance(operations, dict) or set(operations) != {
420
- "capabilities", "health", "notification", "readiness", "status",
460
+ "capabilities", "health", "notification", "openapi", "readiness",
461
+ "status",
421
462
  }:
422
463
  return False
423
464
  expected = {
@@ -436,6 +477,11 @@ def _valid_capabilities(value: Any, expected_sha256: str) -> bool:
436
477
  "capabilities_sha256",
437
478
  ],
438
479
  ),
480
+ "openapi": _operation(
481
+ "openapi", "GET", OPENAPI_PATH, "terminal-notification-openapi:read",
482
+ None, [], OPENAPI_RESPONSE_SCHEMA,
483
+ ["schema", "openapi", "openapi_sha256", "capabilities_sha256"],
484
+ ),
439
485
  "readiness": _operation(
440
486
  "readiness", "GET", READINESS_PATH,
441
487
  "terminal-notification-readiness:read", None, [], READINESS_SCHEMA,
@@ -466,7 +512,10 @@ def _valid_capabilities(value: Any, expected_sha256: str) -> bool:
466
512
  not isinstance(path, str)
467
513
  or not path.startswith("/")
468
514
  or path.startswith("//")
469
- or path in {CAPABILITIES_PATH, HEALTH_PATH, READINESS_PATH, STATUS_PATH}
515
+ or path in {
516
+ CAPABILITIES_PATH, HEALTH_PATH, OPENAPI_PATH, READINESS_PATH,
517
+ STATUS_PATH,
518
+ }
470
519
  ):
471
520
  return False
472
521
  expected["notification"] = _operation(
@@ -536,10 +585,18 @@ class HTTPTerminalReceiverClient:
536
585
  "body_sha256": body_sha256,
537
586
  **context,
538
587
  }
588
+ if path != CAPABILITIES_PATH:
589
+ authentication["capabilities_sha256"] = (
590
+ self.expected_capabilities_sha256
591
+ )
539
592
  headers = _authentication_headers(
540
593
  self.authentication_headers, authentication,
541
594
  )
542
595
  headers["Accept"] = "application/json"
596
+ if path != CAPABILITIES_PATH:
597
+ headers[CAPABILITIES_PRECONDITION_HEADER] = (
598
+ self.expected_capabilities_sha256
599
+ )
543
600
  if body is not None:
544
601
  headers["Content-Type"] = "application/json; charset=utf-8"
545
602
  headers["X-Localization-Terminal-Status-SHA256"] = body_sha256
@@ -572,6 +629,29 @@ class HTTPTerminalReceiverClient:
572
629
  def _verify_contract(self) -> None:
573
630
  self.capabilities()
574
631
 
632
+ def openapi(self) -> Mapping[str, Any]:
633
+ """Return only the exact API document for the freshly pinned contract."""
634
+
635
+ capabilities = self.capabilities()["capabilities"]
636
+ _result, response = self._request(
637
+ "GET", OPENAPI_PATH, None, {200}, {},
638
+ )
639
+ expected = _OPENAPI.build_document(capabilities)
640
+ if (
641
+ set(response) != {
642
+ "schema", "openapi", "openapi_sha256",
643
+ "capabilities_sha256",
644
+ }
645
+ or response.get("schema") != OPENAPI_RESPONSE_SCHEMA
646
+ or response.get("capabilities_sha256")
647
+ != self.expected_capabilities_sha256
648
+ or response.get("openapi") != expected
649
+ or response.get("openapi_sha256")
650
+ != _OPENAPI.document_sha256(expected)
651
+ ):
652
+ _fail("openapi_binding")
653
+ return response
654
+
575
655
  def notify(self, notification: Mapping[str, Any]) -> Mapping[str, Any]:
576
656
  """Send one immutable notification through its freshly pinned route."""
577
657
 
@@ -590,6 +670,7 @@ class HTTPTerminalReceiverClient:
590
670
  "path": path,
591
671
  "body_sha256": body_sha256,
592
672
  **context,
673
+ "capabilities_sha256": self.expected_capabilities_sha256,
593
674
  }
594
675
  headers = _authentication_headers(
595
676
  self.authentication_headers, authentication,
@@ -602,6 +683,9 @@ class HTTPTerminalReceiverClient:
602
683
  payload["notification_id"]
603
684
  ),
604
685
  "X-Localization-Terminal-Notification-Sha256": body_sha256,
686
+ CAPABILITIES_PRECONDITION_HEADER: (
687
+ self.expected_capabilities_sha256
688
+ ),
605
689
  })
606
690
  try:
607
691
  result = self.transport.request(