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
@@ -30,11 +30,13 @@ PROCESSING_ACK_SCHEMA = "blun.cms-terminal-notification-processing-ack.v1"
30
30
  AUTH_SCHEMA = "blun.cms-source-terminal-notification-http-auth.v1"
31
31
  PRINCIPAL_SCHEMA = "blun.cms-source-terminal-notification-principal.v1"
32
32
  ERROR_SCHEMA = "blun.cms-source-terminal-notification-http-error.v1"
33
- API_SCHEMA = "blun.cms-terminal-receiver-api.v1"
34
- CAPABILITIES_SCHEMA = "blun.cms-terminal-receiver-capabilities.v1"
33
+ API_SCHEMA = "blun.cms-terminal-receiver-api.v3"
34
+ CAPABILITIES_SCHEMA = "blun.cms-terminal-receiver-capabilities.v3"
35
35
  CAPABILITIES_RESPONSE_SCHEMA = (
36
- "blun.cms-terminal-receiver-capabilities-response.v1"
36
+ "blun.cms-terminal-receiver-capabilities-response.v3"
37
37
  )
38
+ OPENAPI_RESPONSE_SCHEMA = "blun.cms-terminal-receiver-openapi-response.v1"
39
+ OPENAPI_DOCUMENT_SCHEMA = "blun.cms-terminal-receiver-openapi.v2"
38
40
  HEALTH_RESPONSE_SCHEMA = "blun.cms-terminal-receiver-health.v1"
39
41
  HEALTH_RESPONSE_FIELDS = (
40
42
  "schema", "status", "runtime_state", "worker_state", "inbox_status",
@@ -45,16 +47,20 @@ WRITE_SCOPE = "terminal-notification:write"
45
47
  STATUS_SCOPE = "terminal-notification-status:read"
46
48
  READINESS_SCOPE = "terminal-notification-readiness:read"
47
49
  CAPABILITIES_SCOPE = "terminal-notification-capabilities:read"
50
+ OPENAPI_SCOPE = "terminal-notification-openapi:read"
48
51
  HEALTH_SCOPE = "terminal-notification-health:read"
49
52
  DEFAULT_PATH = "/v1/localization/terminal-notifications"
50
53
  STATUS_PATH = DEFAULT_PATH + "/status"
51
54
  READINESS_PATH = DEFAULT_PATH + "/readiness"
52
55
  CAPABILITIES_PATH = DEFAULT_PATH + "/capabilities"
56
+ OPENAPI_PATH = DEFAULT_PATH + "/openapi"
53
57
  HEALTH_PATH = DEFAULT_PATH + "/health"
58
+ CAPABILITIES_PRECONDITION_HEADER = "X-Localization-Capabilities-SHA256"
54
59
  STATUS_REQUEST_SCHEMA = "blun.cms-terminal-receiver-status-request.v1"
55
60
  STATUS_RESPONSE_SCHEMA = "blun.cms-terminal-receiver-status-response.v1"
56
61
  READINESS_RESPONSE_SCHEMA = "blun.cms-terminal-receiver-readiness.v1"
57
62
  MAX_BODY_BYTES = 16_384
63
+ MAX_RESPONSE_BYTES = 1_000_000
58
64
  MAX_HEADERS = 64
59
65
  MAX_HEADER_VALUE_LENGTH = 4_096
60
66
  TOKEN = re.compile(r"^[A-Za-z0-9_.:-]{1,256}$")
@@ -174,7 +180,7 @@ def _blocked(code: str, status: int) -> None:
174
180
  raise TerminalNotificationReceiverBlocked("notification_receiver." + code, status)
175
181
 
176
182
 
177
- def _canonical(value: Any) -> bytes:
183
+ def _canonical(value: Any, *, max_bytes: int = MAX_BODY_BYTES) -> bytes:
178
184
  try:
179
185
  result = json.dumps(
180
186
  value, ensure_ascii=False, allow_nan=False, sort_keys=True,
@@ -182,7 +188,7 @@ def _canonical(value: Any) -> bytes:
182
188
  ).encode("utf-8")
183
189
  except (TypeError, ValueError, RecursionError):
184
190
  _blocked("request_invalid", 400)
185
- if not result or len(result) > MAX_BODY_BYTES:
191
+ if not result or len(result) > max_bytes:
186
192
  _blocked("request_invalid", 400)
187
193
  return result
188
194
 
@@ -359,6 +365,19 @@ def capabilities_payload(notification_path: str = DEFAULT_PATH) -> dict[str, Any
359
365
  ],
360
366
  "success_status": 200,
361
367
  },
368
+ "openapi": {
369
+ "method": "GET",
370
+ "path": OPENAPI_PATH,
371
+ "scope": OPENAPI_SCOPE,
372
+ "request_schema": None,
373
+ "request_fields": [],
374
+ "response_schema": OPENAPI_RESPONSE_SCHEMA,
375
+ "response_fields": [
376
+ "schema", "openapi", "openapi_sha256",
377
+ "capabilities_sha256",
378
+ ],
379
+ "success_status": 200,
380
+ },
362
381
  "readiness": {
363
382
  "method": "GET",
364
383
  "path": READINESS_PATH,
@@ -393,12 +412,13 @@ def capabilities_payload(notification_path: str = DEFAULT_PATH) -> dict[str, Any
393
412
  if (
394
413
  not isinstance(notification_path, str)
395
414
  or notification_path in {
396
- STATUS_PATH, READINESS_PATH, CAPABILITIES_PATH, HEALTH_PATH,
415
+ STATUS_PATH, READINESS_PATH, CAPABILITIES_PATH, OPENAPI_PATH,
416
+ HEALTH_PATH,
397
417
  }
398
418
  or len({
399
419
  WRITE_SCOPE, STATUS_SCOPE, READINESS_SCOPE, CAPABILITIES_SCOPE,
400
- HEALTH_SCOPE,
401
- }) != 5
420
+ OPENAPI_SCOPE, HEALTH_SCOPE,
421
+ }) != 6
402
422
  ):
403
423
  _blocked("capabilities_invalid", 503)
404
424
  operations = {
@@ -422,14 +442,21 @@ def capabilities_payload(notification_path: str = DEFAULT_PATH) -> dict[str, Any
422
442
  "success_status": 200,
423
443
  },
424
444
  }
445
+ for name, operation in operations.items():
446
+ operation["capabilities_precondition_header"] = (
447
+ None if name == "capabilities"
448
+ else CAPABILITIES_PRECONDITION_HEADER
449
+ )
425
450
  capabilities = {
426
451
  "schema": CAPABILITIES_SCHEMA,
427
452
  "api_schema": API_SCHEMA,
428
453
  "authentication_request_schema": AUTH_SCHEMA,
429
454
  "principal_schema": PRINCIPAL_SCHEMA,
430
455
  "error_schema": ERROR_SCHEMA,
456
+ "openapi_document_schema": OPENAPI_DOCUMENT_SCHEMA,
431
457
  "limits": {
432
458
  "max_body_bytes": MAX_BODY_BYTES,
459
+ "max_response_bytes": MAX_RESPONSE_BYTES,
433
460
  "max_headers": MAX_HEADERS,
434
461
  "max_header_value_bytes": MAX_HEADER_VALUE_LENGTH,
435
462
  "processing_max_attempts_min": 1,
@@ -438,10 +465,14 @@ def capabilities_payload(notification_path: str = DEFAULT_PATH) -> dict[str, Any
438
465
  "processing_statuses": list(PROCESSING_STATUSES),
439
466
  "terminal_statuses": sorted(TERMINAL_STATUSES),
440
467
  "operations": operations,
468
+ "semantics": {
469
+ "non_discovery_operations_require_exact_capability_precondition": True,
470
+ },
441
471
  }
442
472
  try:
443
473
  if set(operations) != {
444
- "capabilities", "health", "notification", "readiness", "status",
474
+ "capabilities", "health", "notification", "openapi",
475
+ "readiness", "status",
445
476
  }:
446
477
  raise ValueError
447
478
  if set(NOTIFICATION_FIELDS) != set(
@@ -1338,7 +1369,8 @@ class CMSTerminalNotificationReceiverApplication:
1338
1369
  or "#" in path
1339
1370
  or len(path) > 256
1340
1371
  or path in {
1341
- STATUS_PATH, READINESS_PATH, CAPABILITIES_PATH, HEALTH_PATH,
1372
+ STATUS_PATH, READINESS_PATH, CAPABILITIES_PATH, OPENAPI_PATH,
1373
+ HEALTH_PATH,
1342
1374
  }
1343
1375
  ):
1344
1376
  raise ValueError("path is invalid")
@@ -1379,12 +1411,14 @@ class CMSTerminalNotificationReceiverApplication:
1379
1411
 
1380
1412
  @staticmethod
1381
1413
  def _send(start_response: Callable[..., Any], status: int, value: Any):
1382
- body = _canonical(value)
1414
+ body = _canonical(value, max_bytes=MAX_RESPONSE_BYTES)
1383
1415
  phrases = {
1384
1416
  200: "OK", 400: "Bad Request", 401: "Unauthorized",
1385
1417
  403: "Forbidden", 404: "Not Found", 405: "Method Not Allowed",
1386
- 409: "Conflict", 411: "Length Required", 413: "Content Too Large",
1387
- 415: "Unsupported Media Type", 503: "Service Unavailable",
1418
+ 409: "Conflict", 411: "Length Required",
1419
+ 412: "Precondition Failed", 413: "Content Too Large",
1420
+ 415: "Unsupported Media Type", 428: "Precondition Required",
1421
+ 503: "Service Unavailable",
1388
1422
  }
1389
1423
  start_response(f"{status} {phrases[status]}", (
1390
1424
  ("Content-Type", "application/json; charset=utf-8"),
@@ -1445,12 +1479,21 @@ class CMSTerminalNotificationReceiverApplication:
1445
1479
  "event_id": payload["event_id"],
1446
1480
  "site_id": payload["site_id"],
1447
1481
  "body_sha256": body_sha256,
1482
+ "capabilities_sha256": headers.get(
1483
+ "x-localization-capabilities-sha256"
1484
+ ),
1448
1485
  }
1449
1486
  try:
1450
1487
  principal = self.authenticate(dict(request), dict(headers))
1451
1488
  except Exception:
1452
1489
  _blocked("authentication_unavailable", 503)
1453
1490
  _principal(principal, payload["site_id"])
1491
+ expected_capabilities = capabilities_payload(self.path)["sha256"]
1492
+ supplied_capabilities = request["capabilities_sha256"]
1493
+ if supplied_capabilities is None:
1494
+ _blocked("capabilities_precondition_required", 428)
1495
+ if supplied_capabilities != expected_capabilities:
1496
+ _blocked("capabilities_precondition_failed", 412)
1454
1497
  acknowledgement = self.inbox.accept(
1455
1498
  payload, body, body_sha256, now=self.clock(),
1456
1499
  )
@@ -0,0 +1,335 @@
1
+ #!/usr/bin/env python3
2
+ """Canonical OpenAPI 3.1 profile for the terminal-notification receiver."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import hashlib
7
+ import json
8
+ from typing import Any, Mapping
9
+
10
+
11
+ DOCUMENT_SCHEMA = "blun.cms-terminal-receiver-openapi.v2"
12
+ RESPONSE_SCHEMA = "blun.cms-terminal-receiver-openapi-response.v1"
13
+
14
+
15
+ def _canonical(value: Any) -> bytes:
16
+ return json.dumps(
17
+ value, ensure_ascii=False, allow_nan=False, sort_keys=True,
18
+ separators=(",", ":"),
19
+ ).encode("utf-8")
20
+
21
+
22
+ def _ref(name: str) -> dict[str, str]:
23
+ return {"$ref": "#/components/schemas/" + name}
24
+
25
+
26
+ def _object(
27
+ properties: Mapping[str, Any], *, optional: tuple[str, ...] = (),
28
+ ) -> dict[str, Any]:
29
+ return {
30
+ "type": "object",
31
+ "additionalProperties": False,
32
+ "required": sorted(set(properties) - set(optional)),
33
+ "properties": dict(properties),
34
+ }
35
+
36
+
37
+ def _exact(value: Any) -> dict[str, Any]:
38
+ if isinstance(value, Mapping):
39
+ return _object({str(key): _exact(item) for key, item in value.items()})
40
+ if value is None:
41
+ return {"type": "null"}
42
+ if isinstance(value, bool):
43
+ return {"type": "boolean", "const": value}
44
+ if isinstance(value, int):
45
+ return {"type": "integer", "const": value}
46
+ if isinstance(value, str):
47
+ return {"type": "string", "const": value}
48
+ if isinstance(value, (list, tuple)):
49
+ items = [_exact(item) for item in value]
50
+ return {
51
+ "type": "array", "prefixItems": items, "items": False,
52
+ "minItems": len(items), "maxItems": len(items),
53
+ }
54
+ raise TypeError("unsupported exact capability value")
55
+
56
+
57
+ def _error_response(status: int, codes: tuple[str, ...]) -> dict[str, Any]:
58
+ return {
59
+ "description": "Fail-closed, content-free receiver error.",
60
+ "content": {"application/json": {"schema": _object({
61
+ "schema": {"const": "blun.cms-source-terminal-notification-http-error.v1"},
62
+ "status": {"const": "BLOCK"},
63
+ "error_code": {"type": "string", "enum": list(codes)},
64
+ })}},
65
+ "x-error-codes": list(codes),
66
+ "x-http-status": status,
67
+ }
68
+
69
+
70
+ _COMMON = {
71
+ 400: (
72
+ "notification_receiver.body_invalid",
73
+ "notification_receiver.framing_invalid",
74
+ "notification_receiver.headers_invalid",
75
+ "notification_receiver.https_required",
76
+ "notification_receiver.query_rejected",
77
+ ),
78
+ 401: ("notification_receiver.authentication_failed",),
79
+ 403: ("notification_receiver.authorization_failed",),
80
+ 405: ("notification_receiver.method_not_allowed",),
81
+ 503: (
82
+ "notification_receiver.authentication_unavailable",
83
+ "notification_receiver.capabilities_invalid",
84
+ "notification_receiver.internal",
85
+ "notification_receiver.runtime_unavailable",
86
+ ),
87
+ }
88
+
89
+
90
+ def _errors(name: str) -> dict[int, tuple[str, ...]]:
91
+ result = {status: tuple(codes) for status, codes in _COMMON.items()}
92
+ if name != "capabilities":
93
+ result[412] = ("notification_receiver.capabilities_precondition_failed",)
94
+ result[428] = ("notification_receiver.capabilities_precondition_required",)
95
+ if name in {"capabilities", "health", "openapi", "readiness"}:
96
+ result[415] = ("notification_receiver.content_type",)
97
+ if name == "health":
98
+ result[503] += ("notification_receiver.health_invalid",)
99
+ if name == "notification":
100
+ result[400] += (
101
+ "notification_receiver.canonical_body_required",
102
+ "notification_receiver.header_binding_invalid",
103
+ "notification_receiver.json_invalid",
104
+ "notification_receiver.request_invalid",
105
+ )
106
+ result[409] = ("notification_receiver.idempotency_collision",)
107
+ result[411] = ("notification_receiver.content_length_required",)
108
+ result[413] = ("notification_receiver.body_too_large",)
109
+ result[415] = ("notification_receiver.content_type",)
110
+ result[503] += (
111
+ "notification_receiver.clock_invalid",
112
+ "notification_receiver.database_unsafe",
113
+ "notification_receiver.foreign_process",
114
+ "notification_receiver.integrity_failed",
115
+ "notification_receiver.schema_altered",
116
+ "notification_receiver.storage_unavailable",
117
+ "notification_receiver.stored_binding_invalid",
118
+ "notification_receiver.transaction_nested",
119
+ )
120
+ if name == "status":
121
+ result[400] += (
122
+ "notification_receiver.header_binding_invalid",
123
+ "notification_receiver.json_invalid",
124
+ "notification_receiver.request_invalid",
125
+ )
126
+ result[404] = ("notification_receiver.not_found",)
127
+ result[411] = ("notification_receiver.content_length_required",)
128
+ result[413] = ("notification_receiver.body_too_large",)
129
+ result[415] = ("notification_receiver.content_type",)
130
+ return {status: tuple(sorted(set(codes))) for status, codes in result.items()}
131
+
132
+
133
+ def _operation(
134
+ name: str, contract: Mapping[str, Any], *, response: str,
135
+ request: str | None = None, degraded: bool = False,
136
+ ) -> dict[str, Any]:
137
+ responses: dict[str, Any] = {
138
+ str(contract["success_status"]): {
139
+ "description": "Exact validated content-free response.",
140
+ "content": {"application/json": {"schema": _ref(response)}},
141
+ },
142
+ }
143
+ for status, codes in _errors(name).items():
144
+ error = _error_response(status, codes)
145
+ if degraded and status == 503:
146
+ error["content"]["application/json"]["schema"] = {
147
+ "oneOf": [_ref(response), error["content"]["application/json"]["schema"]],
148
+ }
149
+ responses[str(status)] = error
150
+ operation: dict[str, Any] = {
151
+ "operationId": {
152
+ "capabilities": "discoverTerminalReceiverCapabilities",
153
+ "health": "readTerminalReceiverHealth",
154
+ "notification": "acceptTerminalNotification",
155
+ "openapi": "readTerminalReceiverOpenApi",
156
+ "readiness": "readTerminalReceiverReadiness",
157
+ "status": "readTerminalNotificationStatus",
158
+ }[name],
159
+ "security": [{"hostAuthentication": []}],
160
+ "x-authentication-scope": contract["scope"],
161
+ "responses": dict(sorted(responses.items(), key=lambda item: int(item[0]))),
162
+ }
163
+ if request is not None:
164
+ operation["requestBody"] = {
165
+ "required": True,
166
+ "content": {"application/json": {"schema": _ref(request)}},
167
+ }
168
+ parameters = []
169
+ if name != "capabilities":
170
+ parameters.append({
171
+ "name": contract["capabilities_precondition_header"],
172
+ "in": "header", "required": True, "schema": _ref("Sha256"),
173
+ })
174
+ if name == "notification":
175
+ parameters.extend([
176
+ {"name": "Idempotency-Key", "in": "header", "required": True,
177
+ "schema": _ref("NotificationId")},
178
+ {"name": "X-Localization-Terminal-Notification-Id", "in": "header",
179
+ "required": True, "schema": _ref("NotificationId")},
180
+ {"name": "X-Localization-Terminal-Notification-Sha256", "in": "header",
181
+ "required": True, "schema": _ref("Sha256")},
182
+ ])
183
+ elif name == "status":
184
+ parameters.append({
185
+ "name": "X-Localization-Terminal-Status-SHA256", "in": "header",
186
+ "required": True, "schema": _ref("Sha256"),
187
+ })
188
+ if parameters:
189
+ operation["parameters"] = parameters
190
+ return operation
191
+
192
+
193
+ def build_document(capabilities: Mapping[str, Any]) -> dict[str, Any]:
194
+ """Build the origin-free document for one exact capability generation."""
195
+ operations = capabilities["operations"]
196
+ sha256 = {"type": "string", "pattern": "^[a-f0-9]{64}$"}
197
+ token = {"type": "string", "pattern": "^[A-Za-z0-9_.:-]{1,256}$"}
198
+ nullable_time = {"oneOf": [{"type": "number", "minimum": 0}, {"type": "null"}]}
199
+ nullable_error = {"oneOf": [_ref("ErrorCode"), {"type": "null"}]}
200
+ count = {"type": "integer", "minimum": 0}
201
+ processing_statuses = capabilities["processing_statuses"]
202
+ terminal_statuses = capabilities["terminal_statuses"]
203
+ notification = _object({
204
+ "schema": {"const": operations["notification"]["request_schema"]},
205
+ "notification_id": _ref("NotificationId"),
206
+ "event_id": _ref("Token"), "site_id": _ref("Token"),
207
+ "plan_id": _ref("Token"), "website_version": _ref("Token"),
208
+ "source_sequence": {"type": "integer", "minimum": 1},
209
+ "job_count": {"type": "integer", "minimum": 1},
210
+ "change_sha256": _ref("Sha256"),
211
+ "lifecycle_binding_sha256": _ref("Sha256"),
212
+ "terminal_status": {"type": "string", "enum": terminal_statuses},
213
+ "lifecycle_sha256": {"oneOf": [_ref("Sha256"), {"type": "null"}]},
214
+ })
215
+ notification["allOf"] = [{
216
+ "if": {"properties": {"terminal_status": {"enum": ["cancelled", "superseded"]}}},
217
+ "then": {"properties": {"lifecycle_sha256": {"type": "null"}}},
218
+ "else": {"properties": {"lifecycle_sha256": _ref("Sha256")}},
219
+ }]
220
+ status_response = _object({
221
+ "schema": {"const": operations["status"]["response_schema"]},
222
+ "notification_id": _ref("NotificationId"),
223
+ "event_id": _ref("Token"), "site_id": _ref("Token"),
224
+ "terminal_status": {"type": "string", "enum": terminal_statuses},
225
+ "notification_sha256": _ref("Sha256"),
226
+ "processing_status": {"type": "string", "enum": processing_statuses},
227
+ "attempts": {"type": "integer", "minimum": 0, "maximum": 20},
228
+ "max_attempts": {"type": "integer", "minimum": 1, "maximum": 20},
229
+ "next_attempt_at": {"type": "number", "minimum": 0},
230
+ "lease_expires_at": nullable_time, "lease_expired": {"type": "boolean"},
231
+ "last_error_code": nullable_error, "processed_at": nullable_time,
232
+ "capabilities_sha256": {"const": capabilities["sha256"]},
233
+ })
234
+ count_properties = {name: count for name in processing_statuses}
235
+ health = _object({
236
+ "schema": {"const": operations["health"]["response_schema"]},
237
+ "status": {"type": "string", "enum": ["ok", "blocked"]},
238
+ "runtime_state": {"const": "open"},
239
+ "worker_state": {"type": "string", "enum": [
240
+ "unmanaged", "starting", "running", "stopping", "stopped", "failed",
241
+ ]},
242
+ "inbox_status": {"type": "string", "enum": ["ok", "blocked"]},
243
+ "received": {"oneOf": [count, {"type": "null"}]},
244
+ "processing_counts": {"oneOf": [_object(count_properties), {"type": "null"}]},
245
+ "processing_due": {"oneOf": [count, {"type": "null"}]},
246
+ "expired_leases": {"oneOf": [count, {"type": "null"}]},
247
+ "failed": {"oneOf": [count, {"type": "null"}]},
248
+ "error_code": nullable_error,
249
+ "capabilities_sha256": {"const": capabilities["sha256"]},
250
+ })
251
+ readiness = _object({
252
+ "schema": {"const": operations["readiness"]["response_schema"]},
253
+ "status": {"type": "string", "enum": ["ready", "not_ready"]},
254
+ "worker_state": {"type": "string", "enum": [
255
+ "unmanaged", "running", "stopping", "stopped", "failed", "closed",
256
+ ]},
257
+ "inbox_status": {"oneOf": [
258
+ {"type": "string", "enum": ["ok", "blocked"]}, {"type": "null"},
259
+ ]},
260
+ "error_code": nullable_error,
261
+ "capabilities_sha256": {"const": capabilities["sha256"]},
262
+ })
263
+ schemas = {
264
+ "Sha256": sha256, "Token": token,
265
+ "NotificationId": {"type": "string", "pattern": "^terminal-[a-f0-9]{64}$"},
266
+ "ErrorCode": {"type": "string", "pattern": "^[a-z][a-z0-9_.-]{0,127}$"},
267
+ "Capabilities": {**_exact(capabilities), "x-capabilities-sha256": capabilities["sha256"]},
268
+ "CapabilitiesResponse": _object({
269
+ "schema": {"const": operations["capabilities"]["response_schema"]},
270
+ "capabilities": _ref("Capabilities"),
271
+ }),
272
+ "TerminalNotification": notification,
273
+ "NotificationAck": _object({
274
+ "schema": {"const": operations["notification"]["response_schema"]},
275
+ "notification_id": _ref("NotificationId"), "event_id": _ref("Token"),
276
+ "site_id": _ref("Token"), "status": {"const": "accepted"},
277
+ "notification_sha256": _ref("Sha256"),
278
+ }),
279
+ "StatusRequest": _object({
280
+ "schema": {"const": operations["status"]["request_schema"]},
281
+ "event_id": _ref("Token"), "site_id": _ref("Token"),
282
+ }),
283
+ "StatusResponse": status_response,
284
+ "HealthResponse": health,
285
+ "ReadinessResponse": readiness,
286
+ "OpenApiResponse": _object({
287
+ "schema": {"const": RESPONSE_SCHEMA}, "openapi": {"type": "object"},
288
+ "openapi_sha256": _ref("Sha256"),
289
+ "capabilities_sha256": {"const": capabilities["sha256"]},
290
+ }),
291
+ }
292
+ paths = {}
293
+ definitions = {
294
+ "capabilities": (None, "CapabilitiesResponse", False),
295
+ "health": (None, "HealthResponse", True),
296
+ "notification": ("TerminalNotification", "NotificationAck", False),
297
+ "openapi": (None, "OpenApiResponse", False),
298
+ "readiness": (None, "ReadinessResponse", True),
299
+ "status": ("StatusRequest", "StatusResponse", False),
300
+ }
301
+ for name, (request, response, degraded) in definitions.items():
302
+ contract = operations[name]
303
+ paths[contract["path"]] = {
304
+ contract["method"].lower(): _operation(
305
+ name, contract, request=request, response=response, degraded=degraded,
306
+ ),
307
+ }
308
+ return {
309
+ "openapi": "3.1.0",
310
+ "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
311
+ "info": {
312
+ "title": "CMS Terminal Notification Receiver API",
313
+ "version": capabilities["api_schema"],
314
+ "description": (
315
+ "Provider-neutral, fail-closed terminal callback receiver. "
316
+ "Acknowledgement means durable intake only."
317
+ ),
318
+ },
319
+ "paths": dict(sorted(paths.items())),
320
+ "components": {
321
+ "securitySchemes": {"hostAuthentication": {
322
+ "type": "apiKey", "in": "header", "name": "Authorization",
323
+ "x-host-defined": True,
324
+ }},
325
+ "schemas": schemas,
326
+ },
327
+ "x-schema": DOCUMENT_SCHEMA,
328
+ "x-capabilities-sha256": capabilities["sha256"],
329
+ "x-content-free-responses": True,
330
+ "x-servers-omitted-intentionally": True,
331
+ }
332
+
333
+
334
+ def document_sha256(document: Mapping[str, Any]) -> str:
335
+ return hashlib.sha256(_canonical(dict(document))).hexdigest()
@@ -35,6 +35,12 @@ _RECEIVER = _load_module(
35
35
  / "integrations"
36
36
  / "website_localization_cms_terminal_notification_receiver.py",
37
37
  )
38
+ _OPENAPI = _load_module(
39
+ "blun_website_localization_terminal_receiver_openapi_runtime_dependency",
40
+ _ROOT
41
+ / "integrations"
42
+ / "website_localization_cms_terminal_notification_receiver_openapi.py",
43
+ )
38
44
 
39
45
 
40
46
  class DurableTerminalReceiverRuntimeBlocked(RuntimeError):
@@ -313,7 +319,8 @@ class DurableTerminalNotificationReceiverRuntime:
313
319
  self._require_open()
314
320
  if isinstance(environ, Mapping) and environ.get("PATH_INFO") in {
315
321
  _RECEIVER.STATUS_PATH, _RECEIVER.READINESS_PATH,
316
- _RECEIVER.CAPABILITIES_PATH, _RECEIVER.HEALTH_PATH,
322
+ _RECEIVER.CAPABILITIES_PATH, _RECEIVER.OPENAPI_PATH,
323
+ _RECEIVER.HEALTH_PATH,
317
324
  }:
318
325
  return self._control_request(environ, start_response)
319
326
  self.require_worker_ready()
@@ -345,6 +352,10 @@ class DurableTerminalNotificationReceiverRuntime:
345
352
  return self._capabilities_request(
346
353
  environ, headers, start_response,
347
354
  )
355
+ if path == _RECEIVER.OPENAPI_PATH:
356
+ return self._openapi_request(
357
+ environ, headers, start_response,
358
+ )
348
359
  if path == _RECEIVER.HEALTH_PATH:
349
360
  return self._health_request(environ, headers, start_response)
350
361
  if path == _RECEIVER.READINESS_PATH:
@@ -370,6 +381,10 @@ class DurableTerminalNotificationReceiverRuntime:
370
381
  site_id: str | None,
371
382
  scope: str,
372
383
  ) -> None:
384
+ if request["path"] != _RECEIVER.CAPABILITIES_PATH:
385
+ request["capabilities_sha256"] = headers.get(
386
+ "x-localization-capabilities-sha256"
387
+ )
373
388
  try:
374
389
  principal = self.application.authenticate(
375
390
  dict(request), dict(headers)
@@ -377,6 +392,19 @@ class DurableTerminalNotificationReceiverRuntime:
377
392
  except Exception:
378
393
  _RECEIVER._blocked("authentication_unavailable", 503)
379
394
  _RECEIVER._principal(principal, site_id, scope)
395
+ if request["path"] != _RECEIVER.CAPABILITIES_PATH:
396
+ supplied = request["capabilities_sha256"]
397
+ expected = _RECEIVER.capabilities_payload(
398
+ self.application.path
399
+ )["sha256"]
400
+ if supplied is None:
401
+ _RECEIVER._blocked(
402
+ "capabilities_precondition_required", 428
403
+ )
404
+ if supplied != expected:
405
+ _RECEIVER._blocked(
406
+ "capabilities_precondition_failed", 412
407
+ )
380
408
 
381
409
  def _capabilities_request(
382
410
  self,
@@ -440,6 +468,43 @@ class DurableTerminalNotificationReceiverRuntime:
440
468
  status = 200 if report["status"] == "ready" else 503
441
469
  return self.application._send(start_response, status, report)
442
470
 
471
+ def _openapi_request(
472
+ self,
473
+ environ: Mapping[str, Any],
474
+ headers: dict[str, str],
475
+ start_response: Callable[..., Any],
476
+ ):
477
+ if environ.get("REQUEST_METHOD") != "GET":
478
+ _RECEIVER._blocked("method_not_allowed", 405)
479
+ if headers.get("content-length") not in {None, "0"}:
480
+ _RECEIVER._blocked("body_invalid", 400)
481
+ if "content-type" in headers:
482
+ _RECEIVER._blocked("content_type", 415)
483
+ body_sha256 = hashlib.sha256(b"").hexdigest()
484
+ request = {
485
+ "schema": _RECEIVER.AUTH_SCHEMA,
486
+ "method": "GET",
487
+ "origin": self.application.origin,
488
+ "path": _RECEIVER.OPENAPI_PATH,
489
+ "body_sha256": body_sha256,
490
+ }
491
+ self._authenticate_control(
492
+ request, headers, None, _RECEIVER.OPENAPI_SCOPE,
493
+ )
494
+ capabilities = _RECEIVER.capabilities_payload(self.application.path)
495
+ document = _OPENAPI.build_document(capabilities)
496
+ if (
497
+ document.get("x-schema") != capabilities["openapi_document_schema"]
498
+ or document.get("x-capabilities-sha256") != capabilities["sha256"]
499
+ ):
500
+ _RECEIVER._blocked("capabilities_invalid", 503)
501
+ return self.application._send(start_response, 200, {
502
+ "schema": _RECEIVER.OPENAPI_RESPONSE_SCHEMA,
503
+ "openapi": document,
504
+ "openapi_sha256": _OPENAPI.document_sha256(document),
505
+ "capabilities_sha256": capabilities["sha256"],
506
+ })
507
+
443
508
  def _health_request(
444
509
  self,
445
510
  environ: Mapping[str, Any],