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.
- package/CHANGELOG.md +39 -0
- package/README.md +17 -1
- package/agent-spine-plugin/CHANGELOG.md +1 -1
- package/agent-spine-plugin/docs/host-integration.md +1 -15
- package/agent-spine-plugin/docs/preflight-recall.md +1 -1
- package/agent-spine-plugin/scripts/check-install.js +1 -6
- package/agent-spine-plugin/scripts/hermetic-process.js +8 -0
- package/agent-spine-plugin/scripts/release-check.js +2 -3
- package/agent-spine-plugin/scripts/run-tests-hermetic.js +8 -11
- package/agent-spine-plugin/src/hook.js +40 -40
- package/agent-spine-plugin/src/lib/briefing.js +6 -3
- package/agent-spine-plugin/src/lib/hook-context.js +63 -64
- package/agent-spine-plugin/src/lib/hook-output.js +115 -94
- package/agent-spine-plugin/src/lib/hook-timeline.js +20 -11
- package/agent-spine-plugin/src/lib/host-instruction-budget.js +8 -14
- package/agent-spine-plugin/src/lib/mcp-timeline-tools.js +0 -7
- package/agent-spine-plugin/src/lib/pre-answer-timeline-recall.js +104 -0
- package/agent-spine-plugin/src/lib/session-timeline-codex.js +0 -4
- package/agent-spine-plugin/src/lib/session-timeline-enrollment-source.js +0 -3
- package/agent-spine-plugin/src/lib/session-timeline-enrollment-storage.js +0 -4
- package/agent-spine-plugin/src/lib/session-timeline-enrollment-transport.js +0 -2
- package/agent-spine-plugin/src/lib/session-timeline-enrollment.js +0 -22
- package/agent-spine-plugin/src/lib/session-timeline-host-origin.js +0 -4
- package/agent-spine-plugin/src/lib/session-timeline-prior.js +42 -50
- package/agent-spine-plugin/src/lib/session-timeline-provider.js +0 -3
- package/agent-spine-plugin/src/lib/session-timeline-query.js +0 -1
- package/agent-spine-plugin/src/lib/session-timeline-sid-acl.js +0 -4
- package/agent-spine-plugin/src/lib/session-timeline-source.js +0 -3
- package/agent-spine-plugin/src/lib/session-timeline-transport.js +0 -8
- package/agent-spine-plugin/src/lib/session-timeline-windows-acl.js +0 -3
- package/agent-spine-plugin/src/lib/session-timeline.js +370 -408
- package/agent-spine-plugin/src/lib/source-roots.js +26 -16
- package/agent-spine-plugin/src/lib/timeline-user-feedback.js +0 -1
- package/bin/core-bootstrap.js +18 -10
- package/bin/launcher-mode.js +5 -0
- package/bin/launcher-runtime.js +27 -0
- package/bin/verify-bundled-agent-sources.cjs +28 -2
- package/blun.mjs +798 -143
- package/bundled-agent-sources.json +192 -155
- package/package.json +1 -1
- package/standard-skills/translate-native/README.md +1334 -1
- package/standard-skills/translate-native/SKILL.md +1 -1
- package/standard-skills/translate-native/VERSION +1 -1
- package/standard-skills/translate-native/docs/PREMORTEM.md +901 -0
- package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION.md +208 -41
- package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_API.md +1319 -77
- package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_EVIDENCE_HTTP.md +66 -11
- package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_HTTP_PROVIDER.md +19 -1
- package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_RECEIPT_VERIFIER_HTTP.md +24 -6
- package/standard-skills/translate-native/integrations/commercial_localization_profile.py +23 -0
- package/standard-skills/translate-native/integrations/website_localization.py +234 -10
- package/standard-skills/translate-native/integrations/website_localization_benchmark.py +39 -3
- package/standard-skills/translate-native/integrations/website_localization_cms.py +512 -10
- package/standard-skills/translate-native/integrations/website_localization_cms_client.py +116 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_receiver.py +101 -11
- package/standard-skills/translate-native/integrations/website_localization_cms_receiver_runtime.py +12 -1
- package/standard-skills/translate-native/integrations/website_localization_cms_receiver_store.py +186 -24
- package/standard-skills/translate-native/integrations/website_localization_cms_source_client.py +64 -1
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery.py +528 -5
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_auth.py +918 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_auth_runtime.py +609 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_client.py +885 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_http.py +1244 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_runtime.py +220 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_sidecar_adapter.py +331 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_capabilities_http.py +444 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_client.py +673 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_dispatch.py +1317 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_dispatch_client.py +826 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_dispatch_http.py +1152 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_dispatch_openapi.py +896 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_dispatch_runtime.py +621 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_http.py +1180 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_runtime.py +1479 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_http.py +133 -17
- package/standard-skills/translate-native/integrations/website_localization_cms_source_runtime.py +319 -4
- package/standard-skills/translate-native/integrations/website_localization_cms_source_service.py +72 -2
- package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_http.py +13 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_receiver.py +56 -13
- package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_receiver_openapi.py +335 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_receiver_runtime.py +66 -1
- package/standard-skills/translate-native/integrations/website_localization_cms_terminal_processing_monitor.py +73 -7
- package/standard-skills/translate-native/integrations/website_localization_cms_terminal_receiver_client.py +93 -9
- package/standard-skills/translate-native/integrations/website_localization_evidence_http.py +221 -11
- package/standard-skills/translate-native/integrations/website_localization_health.py +6 -0
- package/standard-skills/translate-native/integrations/website_localization_http_provider.py +1 -1
- package/standard-skills/translate-native/integrations/website_localization_quality_profiles.py +350 -0
- package/standard-skills/translate-native/integrations/website_localization_queue.py +126 -56
- package/standard-skills/translate-native/integrations/website_localization_receipt_verifier_http.py +166 -13
- package/standard-skills/translate-native/integrations/website_localization_release.py +423 -17
- package/standard-skills/translate-native/integrations/website_localization_release_coordinator.py +168 -13
- package/standard-skills/translate-native/integrations/website_localization_runner.py +10 -0
- package/standard-skills/translate-native/integrations/website_localization_worker.py +213 -9
- package/standard-skills/translate-native/provenance.json +2 -2
- package/standard-skills/translate-native/references/commercial-localization.md +96 -17
- package/standard-skills/translate-native/scripts/blun_language_guard.py +3 -1
- package/standard-skills/translate-native/scripts/check_commercial_review.py +69 -9
- package/standard-skills/translate-native/scripts/commercial_localization_profile.py +829 -31
- package/standard-tools/language-guard/VERSION +1 -1
- package/standard-tools/language-guard/blun_language_guard.py +3 -1
- package/standard-tools/language-guard/check_commercial_review.py +69 -9
- package/standard-tools/language-guard/commercial_localization_profile.py +829 -31
- package/standard-tools/language-guard/provenance.json +2 -2
- package/standard-tools/manifest.json +1 -1
- package/worker-host.mjs +215 -82
- 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.
|
|
34
|
-
CAPABILITIES_SCHEMA = "blun.cms-terminal-receiver-capabilities.
|
|
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.
|
|
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) >
|
|
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,
|
|
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
|
-
}) !=
|
|
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", "
|
|
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,
|
|
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",
|
|
1387
|
-
|
|
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.
|
|
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],
|