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
@@ -0,0 +1,826 @@
1
+ #!/usr/bin/env python3
2
+ """Contract-pinned HTTPS client for the durable CMS submission sidecar.
3
+
4
+ The adapter lets non-Python CMS backends enqueue and inspect caller-owned
5
+ submission records without sharing a process or database. It discovers the
6
+ exact sidecar contract before every operation, performs one transport attempt
7
+ per request, and binds every content-free response to both the sidecar and the
8
+ downstream public-submission capability generations. Retry scheduling remains
9
+ the caller's responsibility.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import hashlib
15
+ import importlib.util
16
+ import json
17
+ import math
18
+ import re
19
+ import socket
20
+ import sys
21
+ import unicodedata
22
+ import urllib.error
23
+ import urllib.parse
24
+ import urllib.request
25
+ from dataclasses import dataclass
26
+ from pathlib import Path
27
+ from typing import Any, Callable, Mapping, Protocol
28
+
29
+
30
+ MAX_ENDPOINT_LENGTH = 2_048
31
+ MAX_HEADER_VALUE_LENGTH = 4_096
32
+ MAX_AUTHENTICATION_HEADERS = 58
33
+ MAX_RESPONSE_BYTES = 4_000_000
34
+ SHA256 = re.compile(r"^[a-f0-9]{64}$")
35
+ TOKEN = re.compile(r"^[A-Za-z0-9_.:-]{1,256}$")
36
+ ERROR_CODE = re.compile(r"^[a-z][a-z0-9_.-]{0,127}$")
37
+ AUTH_CONTEXT_SCHEMA = "blun.cms-public-submission-dispatch-client-auth-context.v4"
38
+ RESERVED_HEADERS = {
39
+ "accept", "connection", "content-length", "content-type", "host",
40
+ "idempotency-key", "transfer-encoding",
41
+ "x-localization-source-payload-sha256",
42
+ "x-localization-capabilities-sha256",
43
+ }
44
+
45
+
46
+ def _load_module(name: str, path: Path):
47
+ spec = importlib.util.spec_from_file_location(name, path)
48
+ if spec is None or spec.loader is None:
49
+ raise RuntimeError(f"cannot load submission sidecar client dependency: {path.name}")
50
+ module = importlib.util.module_from_spec(spec)
51
+ sys.modules[spec.name] = module
52
+ spec.loader.exec_module(module)
53
+ return module
54
+
55
+
56
+ _ROOT = Path(__file__).resolve().parents[1]
57
+ _HTTP = _load_module(
58
+ "blun_website_localization_submission_dispatch_client_http",
59
+ _ROOT / "integrations" / "website_localization_cms_source_delivery_submission_dispatch_http.py",
60
+ )
61
+
62
+
63
+ class CMSSourceDeliverySubmissionDispatchClientBlocked(RuntimeError):
64
+ """Stable content-free client failure with an explicit retry decision."""
65
+
66
+ cms_source_delivery_submission_dispatch_client_failure = True
67
+
68
+ def __init__(
69
+ self, code: str, *, retryable: bool,
70
+ http_status: int | None = None, remote_error_code: str | None = None,
71
+ ):
72
+ remote_valid = (
73
+ remote_error_code is None
74
+ or isinstance(remote_error_code, str)
75
+ and ERROR_CODE.fullmatch(remote_error_code) is not None
76
+ )
77
+ status_valid = (
78
+ http_status is None
79
+ or isinstance(http_status, int) and not isinstance(http_status, bool)
80
+ and 400 <= http_status <= 599
81
+ )
82
+ if (
83
+ ERROR_CODE.fullmatch(code) is None or not isinstance(retryable, bool)
84
+ or not remote_valid or not status_valid
85
+ or (remote_error_code is None) != (http_status is None)
86
+ ):
87
+ raise ValueError("submission sidecar client failure is invalid")
88
+ super().__init__(code)
89
+ self.code = code
90
+ self.retryable = retryable
91
+ self.http_status = http_status
92
+ self.remote_error_code = remote_error_code
93
+
94
+
95
+ @dataclass(frozen=True)
96
+ class HTTPResult:
97
+ status: int
98
+ headers: tuple[tuple[str, str], ...]
99
+ body: bytes
100
+
101
+
102
+ class HTTPTransport(Protocol):
103
+ def request(
104
+ self, method: str, url: str, headers: Mapping[str, str],
105
+ body: bytes | None, *, timeout: float,
106
+ ) -> HTTPResult: ...
107
+
108
+
109
+ class _NoRedirect(urllib.request.HTTPRedirectHandler):
110
+ def redirect_request(self, req, fp, code, msg, headers, newurl):
111
+ return None
112
+
113
+
114
+ class URLTransport:
115
+ """Perform one bounded HTTPS request without following redirects."""
116
+
117
+ def __init__(self):
118
+ self._opener = urllib.request.build_opener(_NoRedirect)
119
+
120
+ def request(self, method, url, headers, body, *, timeout):
121
+ request = urllib.request.Request(
122
+ url, data=body, headers=dict(headers), method=method,
123
+ )
124
+ try:
125
+ response = self._opener.open(request, timeout=timeout)
126
+ except urllib.error.HTTPError as error:
127
+ response = error
128
+ except (urllib.error.URLError, TimeoutError, socket.timeout, OSError):
129
+ _fail("network", retryable=True)
130
+ try:
131
+ raw = response.read(MAX_RESPONSE_BYTES + 1)
132
+ return HTTPResult(
133
+ int(response.status), tuple(response.headers.items()), raw,
134
+ )
135
+ except (TimeoutError, socket.timeout, OSError):
136
+ _fail("network", retryable=True)
137
+ finally:
138
+ response.close()
139
+
140
+
141
+ def _fail(
142
+ code: str, *, retryable: bool = False,
143
+ http_status: int | None = None, remote_error_code: str | None = None,
144
+ ) -> None:
145
+ raise CMSSourceDeliverySubmissionDispatchClientBlocked(
146
+ "source_delivery_submission_dispatch_client." + code,
147
+ retryable=retryable,
148
+ http_status=http_status,
149
+ remote_error_code=remote_error_code,
150
+ )
151
+
152
+
153
+ def _canonical(value: Any) -> bytes:
154
+ try:
155
+ raw = json.dumps(
156
+ value, ensure_ascii=False, allow_nan=False, sort_keys=True,
157
+ separators=(",", ":"),
158
+ ).encode("utf-8")
159
+ except (TypeError, ValueError, RecursionError):
160
+ _fail("request_invalid")
161
+ if not raw or len(raw) > _HTTP.MAX_BODY_BYTES:
162
+ _fail("request_invalid")
163
+ return raw
164
+
165
+
166
+ def _pairs(items: list[tuple[str, Any]]) -> dict[str, Any]:
167
+ result: dict[str, Any] = {}
168
+ for key, value in items:
169
+ if key in result:
170
+ raise ValueError("duplicate key")
171
+ result[key] = value
172
+ return result
173
+
174
+
175
+ def _constant(_value: str) -> None:
176
+ raise ValueError("non-finite number")
177
+
178
+
179
+ def _sha256(value: Any) -> bool:
180
+ return isinstance(value, str) and SHA256.fullmatch(value) is not None
181
+
182
+
183
+ def _token(value: Any) -> bool:
184
+ return (
185
+ isinstance(value, str) and TOKEN.fullmatch(value) is not None
186
+ and unicodedata.is_normalized("NFC", value)
187
+ )
188
+
189
+
190
+ def _count(value: Any, *, minimum: int = 0, maximum: int = 20) -> bool:
191
+ return (
192
+ isinstance(value, int) and not isinstance(value, bool)
193
+ and minimum <= value <= maximum
194
+ )
195
+
196
+
197
+ def _timestamp(value: Any, *, nullable: bool = False) -> bool:
198
+ return (
199
+ nullable and value is None
200
+ or isinstance(value, (int, float)) and not isinstance(value, bool)
201
+ and math.isfinite(float(value)) and float(value) >= 0
202
+ )
203
+
204
+
205
+ def _endpoint(value: Any, allow_loopback_http: bool) -> str:
206
+ if (
207
+ not isinstance(value, str) or value != value.strip() or not value
208
+ or not value.isascii() or len(value) > MAX_ENDPOINT_LENGTH
209
+ or any(ord(character) <= 32 or ord(character) == 127 for character in value)
210
+ ):
211
+ raise ValueError("origin is invalid")
212
+ try:
213
+ parsed = urllib.parse.urlsplit(value)
214
+ hostname, port = parsed.hostname, parsed.port
215
+ except ValueError:
216
+ raise ValueError("origin is invalid") from None
217
+ secure = parsed.scheme == "https"
218
+ loopback = (
219
+ parsed.scheme == "http" and allow_loopback_http
220
+ and isinstance(hostname, str)
221
+ and hostname.lower() in {"localhost", "127.0.0.1", "::1"}
222
+ )
223
+ if (
224
+ not (secure or loopback) or not hostname
225
+ or parsed.username is not None or parsed.password is not None
226
+ or parsed.path or parsed.query or parsed.fragment
227
+ or port is not None and not 1 <= port <= 65_535
228
+ or value != f"{parsed.scheme}://{parsed.netloc}"
229
+ ):
230
+ raise ValueError("origin must be one exact HTTPS origin")
231
+ return value
232
+
233
+
234
+ def _authentication_headers(provider, context) -> dict[str, str]:
235
+ try:
236
+ supplied = provider(dict(context))
237
+ if not isinstance(supplied, Mapping):
238
+ raise TypeError
239
+ items = tuple(supplied.items())
240
+ except Exception:
241
+ _fail("authentication")
242
+ if not items or len(items) > MAX_AUTHENTICATION_HEADERS:
243
+ _fail("authentication")
244
+ result: dict[str, str] = {}
245
+ names: set[str] = set()
246
+ for name, value in items:
247
+ normalized = name.lower() if isinstance(name, str) else ""
248
+ if (
249
+ not isinstance(name, str) or _HTTP.HEADER_NAME.fullmatch(name) is None
250
+ or normalized in RESERVED_HEADERS or normalized in names
251
+ or not isinstance(value, str) or not value
252
+ or len(value) > MAX_HEADER_VALUE_LENGTH
253
+ or "\r" in value or "\n" in value
254
+ or any(ord(character) < 32 or ord(character) == 127 for character in value)
255
+ ):
256
+ _fail("authentication")
257
+ names.add(normalized)
258
+ result[name] = value
259
+ return result
260
+
261
+
262
+ def _remote_error(
263
+ value: Any, status: int, error_codes: Mapping[str, Any],
264
+ ) -> None:
265
+ allowed = error_codes.get(str(status))
266
+ code = value.get("error_code") if isinstance(value, Mapping) else None
267
+ if (
268
+ not isinstance(allowed, list) or not allowed
269
+ or not all(isinstance(item, str) for item in allowed)
270
+ or not isinstance(value, Mapping)
271
+ or set(value) != {"schema", "status", "error_code"}
272
+ or value.get("schema") != _HTTP.ERROR_SCHEMA
273
+ or value.get("status") != "BLOCK"
274
+ or not isinstance(code, str) or code not in allowed
275
+ ):
276
+ _fail("error_response", retryable=status >= 500)
277
+ _fail(
278
+ "http_status", retryable=status in {408, 425, 429} or status >= 500,
279
+ http_status=status, remote_error_code=code,
280
+ )
281
+
282
+
283
+ def _json_response(
284
+ result: Any, allowed_statuses: set[int], error_codes: Mapping[str, Any],
285
+ ) -> dict[str, Any]:
286
+ if (
287
+ not isinstance(result, HTTPResult) or isinstance(result.status, bool)
288
+ or not isinstance(result.status, int) or not 100 <= result.status <= 599
289
+ ):
290
+ _fail("transport_invalid", retryable=True)
291
+ if 300 <= result.status <= 399:
292
+ _fail("redirect")
293
+ known_error = (
294
+ result.status not in allowed_statuses
295
+ and str(result.status) in error_codes
296
+ )
297
+ if result.status not in allowed_statuses and not known_error:
298
+ _fail(
299
+ "http_status",
300
+ retryable=result.status in {408, 425, 429} or result.status >= 500,
301
+ )
302
+ if not isinstance(result.headers, tuple):
303
+ _fail("transport_invalid", retryable=True)
304
+ headers: dict[str, str] = {}
305
+ for item in result.headers:
306
+ if (
307
+ not isinstance(item, tuple) or len(item) != 2
308
+ or not all(isinstance(part, str) for part in item)
309
+ ):
310
+ _fail("transport_invalid", retryable=True)
311
+ name, content = item[0].lower(), item[1].strip()
312
+ if name in headers:
313
+ _fail("response_headers")
314
+ headers[name] = content
315
+ if headers.get("content-type", "").lower().replace(" ", "") not in {
316
+ "application/json", "application/json;charset=utf-8",
317
+ }:
318
+ _fail("response_content_type")
319
+ if (
320
+ not isinstance(result.body, bytes) or not result.body
321
+ or len(result.body) > MAX_RESPONSE_BYTES
322
+ ):
323
+ _fail("response_size")
324
+ declared = headers.get("content-length")
325
+ if declared is not None and (
326
+ not declared.isascii() or not declared.isdecimal()
327
+ or int(declared) != len(result.body)
328
+ ):
329
+ _fail("response_size")
330
+ try:
331
+ text = result.body.decode("utf-8")
332
+ if text.startswith("\ufeff"):
333
+ raise ValueError
334
+ value = json.loads(text, object_pairs_hook=_pairs, parse_constant=_constant)
335
+ except (UnicodeDecodeError, json.JSONDecodeError, ValueError, RecursionError):
336
+ _fail("response_json")
337
+ if not isinstance(value, dict):
338
+ _fail("response_binding")
339
+ if known_error:
340
+ _remote_error(value, result.status, error_codes)
341
+ return value
342
+
343
+
344
+ def _status(
345
+ value: Any, identity: Mapping[str, str],
346
+ expected_commercial_binding: Mapping[str, str],
347
+ ) -> dict[str, Any]:
348
+ fields = {
349
+ "operation", "request_id", "event_id", "site_id", "payload_sha256",
350
+ "commercial_contract_binding",
351
+ "source_max_attempts", "delivery_max_attempts", "status", "attempts",
352
+ "client_max_attempts", "next_attempt_at", "lease_expires_at",
353
+ "lease_expired", "last_error_code", "remote_status", "remote_attempts",
354
+ "remote_capabilities_sha256", "remote_binding_sha256",
355
+ "remote_website_capability_binding", "response_sha256",
356
+ }
357
+ if not isinstance(value, Mapping) or set(value) != fields:
358
+ _fail("status_binding")
359
+ payload = dict(value)
360
+ commercial_binding = payload["commercial_contract_binding"]
361
+ if commercial_binding is not None and (
362
+ commercial_binding != expected_commercial_binding
363
+ or payload["operation"] != "change"
364
+ ):
365
+ _fail("status_binding")
366
+ valid = (
367
+ payload["operation"] in _HTTP.OPERATIONS
368
+ and all(payload.get(name) == wanted for name, wanted in identity.items())
369
+ and all(_token(payload[name]) for name in ("request_id", "event_id", "site_id"))
370
+ and _sha256(payload["payload_sha256"])
371
+ and all(_count(payload[name], minimum=1) for name in (
372
+ "source_max_attempts", "delivery_max_attempts", "client_max_attempts",
373
+ ))
374
+ and _count(payload["attempts"])
375
+ and payload["attempts"] <= payload["client_max_attempts"]
376
+ and _timestamp(payload["next_attempt_at"])
377
+ and _timestamp(payload["lease_expires_at"], nullable=True)
378
+ and isinstance(payload["lease_expired"], bool)
379
+ and payload["status"] in _HTTP.STATUSES
380
+ and (payload["status"] == "leased") == (payload["lease_expires_at"] is not None)
381
+ and (payload["last_error_code"] is None or (
382
+ isinstance(payload["last_error_code"], str)
383
+ and ERROR_CODE.fullmatch(payload["last_error_code"]) is not None
384
+ ))
385
+ and payload["remote_status"] in {
386
+ None, "pending", "leased", "retry_wait", "succeeded", "failed",
387
+ }
388
+ and (payload["remote_attempts"] is None or _count(payload["remote_attempts"]))
389
+ and all(payload[name] is None or _sha256(payload[name]) for name in (
390
+ "remote_capabilities_sha256", "remote_binding_sha256", "response_sha256",
391
+ ))
392
+ )
393
+ binding = payload["remote_website_capability_binding"]
394
+ if binding is not None:
395
+ try:
396
+ normalized_binding = _HTTP._website_capability_binding(binding)
397
+ except Exception:
398
+ _fail("status_binding")
399
+ if normalized_binding != binding:
400
+ _fail("status_binding")
401
+ binding = normalized_binding
402
+ remote = (
403
+ payload["remote_status"], payload["remote_attempts"],
404
+ payload["remote_capabilities_sha256"], payload["remote_binding_sha256"],
405
+ binding, payload["response_sha256"],
406
+ )
407
+ accepted = payload["status"] == "accepted"
408
+ if (
409
+ not valid
410
+ or accepted != all(item is not None for item in remote)
411
+ or accepted and (
412
+ binding["delivery_capabilities_sha256"]
413
+ != payload["remote_capabilities_sha256"]
414
+ or hashlib.sha256(_canonical(binding)).hexdigest()
415
+ != payload["remote_binding_sha256"]
416
+ or commercial_binding is not None
417
+ and binding["commercial_rendering_registry_sha256"]
418
+ != commercial_binding["commercial_rendering_registry_sha256"]
419
+ )
420
+ ):
421
+ _fail("status_binding")
422
+ return payload
423
+
424
+
425
+ class CMSSourceDeliverySubmissionDispatchHTTPClient:
426
+ """Operate one durable CMS submission sidecar through its exact contract."""
427
+
428
+ def __init__(
429
+ self, origin: str, expected_capabilities_sha256: str,
430
+ expected_public_submission_capabilities_sha256: str,
431
+ authentication_headers: Callable[[Mapping[str, Any]], Mapping[str, str]],
432
+ *, transport: HTTPTransport | None = None, timeout: float | int = 30,
433
+ allow_loopback_http: bool = False,
434
+ ):
435
+ if not isinstance(allow_loopback_http, bool):
436
+ raise TypeError("allow_loopback_http must be boolean")
437
+ self.origin = _endpoint(origin, allow_loopback_http)
438
+ if not _sha256(expected_capabilities_sha256) or not _sha256(
439
+ expected_public_submission_capabilities_sha256
440
+ ):
441
+ raise ValueError("expected capability digest is invalid")
442
+ expected = _HTTP._capabilities_payload(
443
+ expected_public_submission_capabilities_sha256
444
+ )
445
+ if expected["sha256"] != expected_capabilities_sha256:
446
+ raise ValueError("sidecar and public capability pins are inconsistent")
447
+ if not callable(authentication_headers):
448
+ raise TypeError("authentication_headers must be callable")
449
+ if (
450
+ isinstance(timeout, bool) or not isinstance(timeout, (int, float))
451
+ or not math.isfinite(float(timeout)) or not 0 < float(timeout) <= 300
452
+ ):
453
+ raise ValueError("timeout is outside the supported range")
454
+ self.expected_capabilities_sha256 = expected_capabilities_sha256
455
+ self.expected_public_submission_capabilities_sha256 = (
456
+ expected_public_submission_capabilities_sha256
457
+ )
458
+ self._expected_capabilities = expected
459
+ self.authentication_headers = authentication_headers
460
+ self.transport = URLTransport() if transport is None else transport
461
+ if not callable(getattr(self.transport, "request", None)):
462
+ raise TypeError("transport must provide request")
463
+ self.timeout = float(timeout)
464
+
465
+ def __repr__(self) -> str:
466
+ return "CMSSourceDeliverySubmissionDispatchHTTPClient(configured=True)"
467
+
468
+ def _request(
469
+ self, method, path, scope, body, statuses, context,
470
+ error_codes, headers=None,
471
+ ):
472
+ authentication = {
473
+ "schema": AUTH_CONTEXT_SCHEMA, "method": method,
474
+ "origin": self.origin, "path": path, "scope": scope,
475
+ "body_sha256": hashlib.sha256(body or b"").hexdigest(),
476
+ **context,
477
+ }
478
+ if path != _HTTP.CAPABILITIES_PATH:
479
+ authentication["capabilities_sha256"] = (
480
+ self.expected_capabilities_sha256
481
+ )
482
+ request_headers = _authentication_headers(
483
+ self.authentication_headers, authentication,
484
+ )
485
+ request_headers["Accept"] = "application/json"
486
+ if body is not None:
487
+ request_headers["Content-Type"] = "application/json; charset=utf-8"
488
+ if path != _HTTP.CAPABILITIES_PATH:
489
+ request_headers[_HTTP.CAPABILITIES_PRECONDITION_HEADER] = (
490
+ self.expected_capabilities_sha256
491
+ )
492
+ if headers:
493
+ request_headers.update(headers)
494
+ try:
495
+ result = self.transport.request(
496
+ method, self.origin + path, request_headers, body,
497
+ timeout=self.timeout,
498
+ )
499
+ except CMSSourceDeliverySubmissionDispatchClientBlocked:
500
+ raise
501
+ except Exception:
502
+ _fail("network", retryable=True)
503
+ return _json_response(result, statuses, error_codes)
504
+
505
+ def capabilities(self) -> Mapping[str, Any]:
506
+ contract = self._expected_capabilities["operations"]["capabilities"]
507
+ response = self._request(
508
+ "GET", _HTTP.CAPABILITIES_PATH, _HTTP.SCOPES[_HTTP.CAPABILITIES_PATH],
509
+ None, {200}, {}, contract["error_codes"],
510
+ )
511
+ if (
512
+ set(response) != {"schema", "capabilities"}
513
+ or response.get("schema") != _HTTP.CAPABILITIES_RESPONSE_SCHEMA
514
+ or response.get("capabilities") != self._expected_capabilities
515
+ ):
516
+ _fail("capabilities_binding")
517
+ return response
518
+
519
+ def _contract(self, name: str) -> Mapping[str, Any]:
520
+ return self.capabilities()["capabilities"]["operations"][name]
521
+
522
+ def enqueue(
523
+ self, payload: Mapping[str, Any], *, source_max_attempts: int = 5,
524
+ delivery_max_attempts: int = 5, client_max_attempts: int = 5,
525
+ ) -> Mapping[str, Any]:
526
+ if not all(_count(value, minimum=1) for value in (
527
+ source_max_attempts, delivery_max_attempts, client_max_attempts,
528
+ )):
529
+ _fail("request_invalid")
530
+ try:
531
+ copied, identity, canonical = _HTTP._DISPATCH._payload(payload)
532
+ except Exception:
533
+ _fail("request_invalid")
534
+ contract = self._contract("enqueue")
535
+ request = {
536
+ "schema": contract["request_schema"], "payload": copied,
537
+ "source_max_attempts": source_max_attempts,
538
+ "delivery_max_attempts": delivery_max_attempts,
539
+ "client_max_attempts": client_max_attempts,
540
+ "commercial_contract_binding": (
541
+ self._expected_capabilities["commercial_contract_binding"]
542
+ if (
543
+ copied.get("schema") == _HTTP._DISPATCH._CLIENT._CMS.CHANGE_SCHEMA
544
+ and copied.get("localization", {}).get("content_type")
545
+ == "commercial"
546
+ ) else None
547
+ ),
548
+ }
549
+ body = _canonical(request)
550
+ response = self._request(
551
+ contract["method"], contract["path"], contract["scope"], body,
552
+ {contract["success_status"]}, identity,
553
+ contract["error_codes"],
554
+ {
555
+ "Idempotency-Key": identity["request_id"],
556
+ "X-Localization-Source-Payload-Sha256": hashlib.sha256(
557
+ canonical.encode("utf-8")
558
+ ).hexdigest(),
559
+ },
560
+ )
561
+ if (
562
+ set(response) != {
563
+ "schema", "api_schema", "status", "capabilities_sha256",
564
+ "accepted_implies_publication",
565
+ }
566
+ or response.get("schema") != contract["response_schema"]
567
+ or response.get("api_schema") != _HTTP.API_SCHEMA
568
+ or response.get("capabilities_sha256") != self.expected_capabilities_sha256
569
+ or response.get("accepted_implies_publication") is not False
570
+ ):
571
+ _fail("enqueue_binding")
572
+ status = _status(
573
+ response.get("status"), identity,
574
+ self._expected_capabilities["commercial_contract_binding"],
575
+ )
576
+ if (
577
+ status["source_max_attempts"] != source_max_attempts
578
+ or status["delivery_max_attempts"] != delivery_max_attempts
579
+ or status["client_max_attempts"] != client_max_attempts
580
+ or status["commercial_contract_binding"]
581
+ != request["commercial_contract_binding"]
582
+ ):
583
+ _fail("enqueue_binding")
584
+ return response
585
+
586
+ def status(
587
+ self, operation: str, request_id: str, event_id: str, site_id: str,
588
+ payload_sha256: str,
589
+ ) -> Mapping[str, Any]:
590
+ identity = {
591
+ "operation": operation, "request_id": request_id,
592
+ "event_id": event_id, "site_id": site_id,
593
+ "payload_sha256": payload_sha256,
594
+ }
595
+ if (
596
+ operation not in _HTTP.OPERATIONS
597
+ or not all(_token(identity[name]) for name in ("request_id", "event_id", "site_id"))
598
+ or operation == "change" and request_id != event_id
599
+ or not _sha256(payload_sha256)
600
+ ):
601
+ _fail("request_invalid")
602
+ contract = self._contract("status")
603
+ body = _canonical({"schema": contract["request_schema"], **identity})
604
+ response = self._request(
605
+ contract["method"], contract["path"], contract["scope"], body,
606
+ {contract["success_status"]}, identity, contract["error_codes"],
607
+ )
608
+ if (
609
+ set(response) != {
610
+ "schema", "api_schema", "status", "capabilities_sha256",
611
+ "accepted_implies_publication",
612
+ }
613
+ or response.get("schema") != contract["response_schema"]
614
+ or response.get("api_schema") != _HTTP.API_SCHEMA
615
+ or response.get("capabilities_sha256") != self.expected_capabilities_sha256
616
+ or response.get("accepted_implies_publication") is not False
617
+ ):
618
+ _fail("status_binding")
619
+ _status(
620
+ response.get("status"), identity,
621
+ self._expected_capabilities["commercial_contract_binding"],
622
+ )
623
+ return response
624
+
625
+ def lifecycle(
626
+ self, operation: str, request_id: str, event_id: str, site_id: str,
627
+ payload_sha256: str,
628
+ ) -> Mapping[str, Any]:
629
+ identity = {
630
+ "operation": operation, "request_id": request_id,
631
+ "event_id": event_id, "site_id": site_id,
632
+ "payload_sha256": payload_sha256,
633
+ }
634
+ if (
635
+ operation not in _HTTP.OPERATIONS
636
+ or not all(_token(identity[name]) for name in (
637
+ "request_id", "event_id", "site_id",
638
+ ))
639
+ or operation == "change" and request_id != event_id
640
+ or not _sha256(payload_sha256)
641
+ ):
642
+ _fail("request_invalid")
643
+ contract = self._contract("lifecycle")
644
+ body = _canonical({"schema": contract["request_schema"], **identity})
645
+ response = self._request(
646
+ contract["method"], contract["path"], contract["scope"], body,
647
+ {contract["success_status"]}, identity, contract["error_codes"],
648
+ )
649
+ if (
650
+ set(response) != {
651
+ "schema", "api_schema", "lifecycle", "capabilities_sha256",
652
+ "accepted_implies_publication",
653
+ }
654
+ or response.get("schema") != contract["response_schema"]
655
+ or response.get("api_schema") != _HTTP.API_SCHEMA
656
+ or response.get("capabilities_sha256")
657
+ != self.expected_capabilities_sha256
658
+ or response.get("accepted_implies_publication") is not False
659
+ or not isinstance(response.get("lifecycle"), Mapping)
660
+ ):
661
+ _fail("lifecycle_binding")
662
+ lifecycle = response["lifecycle"]
663
+ try:
664
+ normalized = _HTTP._lifecycle_payload(
665
+ _HTTP._DISPATCH.SubmissionDispatchLifecycle(
666
+ dispatch_status=_HTTP._DISPATCH.SubmissionDispatchStatus(
667
+ **lifecycle["dispatch_status"]
668
+ ),
669
+ source_lifecycle=lifecycle["source_lifecycle"],
670
+ ),
671
+ identity,
672
+ )
673
+ except Exception:
674
+ _fail("lifecycle_binding")
675
+ if normalized != lifecycle:
676
+ _fail("lifecycle_binding")
677
+ return response
678
+
679
+ def commercial_profile(self) -> Mapping[str, Any]:
680
+ """Return the exact live-bound, content-free commercial contract."""
681
+ contract = self._contract("commercial_profile")
682
+ response = self._request(
683
+ contract["method"], contract["path"], contract["scope"], None,
684
+ {contract["success_status"]}, {}, contract["error_codes"],
685
+ )
686
+ if (
687
+ set(response) != {
688
+ "schema", "api_schema", "commercial_profile",
689
+ "commercial_rendering_registry", "website_capability_binding",
690
+ "capabilities_sha256", "content_free",
691
+ "publication_authority",
692
+ }
693
+ or response.get("schema") != contract["response_schema"]
694
+ or response.get("api_schema") != _HTTP.API_SCHEMA
695
+ or response.get("capabilities_sha256")
696
+ != self.expected_capabilities_sha256
697
+ or response.get("content_free") is not True
698
+ or response.get("publication_authority") is not False
699
+ or response.get("commercial_profile")
700
+ != self._expected_capabilities["commercial_profile"]
701
+ or response.get("commercial_rendering_registry")
702
+ != self._expected_capabilities["commercial_rendering_registry"]
703
+ or not isinstance(response.get("website_capability_binding"), Mapping)
704
+ ):
705
+ _fail("commercial_profile_binding")
706
+ try:
707
+ value = _HTTP._DISPATCH.SubmissionDispatchCommercialProfile(
708
+ commercial_profile=response["commercial_profile"],
709
+ commercial_rendering_registry=response[
710
+ "commercial_rendering_registry"
711
+ ],
712
+ website_capability_binding=response[
713
+ "website_capability_binding"
714
+ ],
715
+ )
716
+ normalized = _HTTP._commercial_profile_payload(value)
717
+ except Exception:
718
+ _fail("commercial_profile_binding")
719
+ if (
720
+ normalized["commercial_profile"] != response["commercial_profile"]
721
+ or normalized["commercial_rendering_registry"]
722
+ != response["commercial_rendering_registry"]
723
+ or normalized["website_capability_binding"]
724
+ != response["website_capability_binding"]
725
+ ):
726
+ _fail("commercial_profile_binding")
727
+ return response
728
+
729
+ def health(self) -> Mapping[str, Any]:
730
+ return self._monitor("health")
731
+
732
+ def readiness(self) -> Mapping[str, Any]:
733
+ return self._monitor("readiness")
734
+
735
+ def openapi(self) -> Mapping[str, Any]:
736
+ """Return the exact origin-free API document for this pinned deployment."""
737
+ contract = self._contract("openapi")
738
+ response = self._request(
739
+ contract["method"], contract["path"], contract["scope"], None,
740
+ {contract["success_status"]}, {}, contract["error_codes"],
741
+ )
742
+ expected = _HTTP._OPENAPI.build_document(self._expected_capabilities)
743
+ if (
744
+ set(response) != {
745
+ "schema", "openapi", "openapi_sha256", "capabilities_sha256",
746
+ }
747
+ or response.get("schema") != contract["response_schema"]
748
+ or response.get("capabilities_sha256")
749
+ != self.expected_capabilities_sha256
750
+ or response.get("openapi") != expected
751
+ or response.get("openapi_sha256")
752
+ != _HTTP._OPENAPI.document_sha256(expected)
753
+ ):
754
+ _fail("openapi_binding")
755
+ return response
756
+
757
+ def _monitor(self, name: str) -> Mapping[str, Any]:
758
+ contract = self._contract(name)
759
+ response = self._request(
760
+ contract["method"], contract["path"], contract["scope"], None,
761
+ {contract["success_status"], 503}, {}, contract["error_codes"],
762
+ )
763
+ if response.get("schema") == _HTTP.ERROR_SCHEMA:
764
+ _remote_error(response, 503, contract["error_codes"])
765
+ key = name
766
+ if (
767
+ set(response) != {"schema", key, "capabilities_sha256"}
768
+ or response.get("schema") != contract["response_schema"]
769
+ or response.get("capabilities_sha256") != self.expected_capabilities_sha256
770
+ or not isinstance(response.get(key), Mapping)
771
+ ):
772
+ _fail(name + "_binding")
773
+ payload = response[key]
774
+ if name == "health":
775
+ valid = (
776
+ set(payload) == {
777
+ "status", "counts", "operations", "due", "expired_leases",
778
+ "failed", "expected_capabilities_sha256",
779
+ }
780
+ and payload.get("status") in {"ok", "blocked"}
781
+ and isinstance(payload.get("counts"), Mapping)
782
+ and set(payload["counts"]) == _HTTP.STATUSES
783
+ and isinstance(payload.get("operations"), Mapping)
784
+ and set(payload["operations"]) == _HTTP.OPERATIONS
785
+ and all(_count(value, maximum=1_000_000) for value in payload["counts"].values())
786
+ and all(_count(value, maximum=1_000_000) for value in payload["operations"].values())
787
+ and all(_count(payload[name], maximum=1_000_000) for name in (
788
+ "due", "expired_leases", "failed",
789
+ ))
790
+ and sum(payload["counts"].values()) == sum(payload["operations"].values())
791
+ and payload["due"] <= payload["counts"]["pending"] + payload["counts"]["retry_wait"]
792
+ and payload["expired_leases"] <= payload["counts"]["leased"]
793
+ and payload["failed"] == payload["counts"]["failed"]
794
+ and (payload["status"] == "ok") == (
795
+ payload["expired_leases"] == 0 and payload["failed"] == 0
796
+ )
797
+ and payload["expected_capabilities_sha256"]
798
+ == self.expected_public_submission_capabilities_sha256
799
+ )
800
+ else:
801
+ valid = (
802
+ set(payload) == {
803
+ "schema", "status", "worker_state", "outbox_status",
804
+ "error_code", "capabilities_sha256",
805
+ }
806
+ and payload.get("schema") == "blun.cms-public-submission-worker-readiness.v1"
807
+ and payload.get("status") in {"ready", "not_ready"}
808
+ and payload.get("worker_state") in _HTTP.WORKER_STATES
809
+ and payload.get("outbox_status") in {None, "ok", "blocked"}
810
+ and (payload.get("error_code") is None or (
811
+ isinstance(payload["error_code"], str)
812
+ and ERROR_CODE.fullmatch(payload["error_code"]) is not None
813
+ ))
814
+ and payload.get("capabilities_sha256")
815
+ == self.expected_public_submission_capabilities_sha256
816
+ )
817
+ ready = (
818
+ payload.get("worker_state") == "running"
819
+ and payload.get("outbox_status") == "ok"
820
+ and payload.get("error_code") is None
821
+ )
822
+ valid = valid and ready == (payload.get("status") == "ready")
823
+ valid = valid and (ready or payload.get("error_code") is not None)
824
+ if not valid:
825
+ _fail(name + "_binding")
826
+ return response