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
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Authenticated, content-free HTTP discovery for the website submission edge.
|
|
3
|
+
|
|
4
|
+
The WSGI boundary exposes only the complete capability projection of an owned
|
|
5
|
+
website submission runtime. Authentication receives canonical request metadata
|
|
6
|
+
and the empty-body digest before the runtime or its downstream sidecar is read.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
import importlib.util
|
|
13
|
+
import json
|
|
14
|
+
import re
|
|
15
|
+
import sys
|
|
16
|
+
import unicodedata
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, Callable, Mapping
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _load_module(name: str, path: Path):
|
|
22
|
+
spec = importlib.util.spec_from_file_location(name, path)
|
|
23
|
+
if spec is None or spec.loader is None:
|
|
24
|
+
raise RuntimeError(
|
|
25
|
+
"cannot load submission-capabilities HTTP dependency"
|
|
26
|
+
)
|
|
27
|
+
module = importlib.util.module_from_spec(spec)
|
|
28
|
+
sys.modules[spec.name] = module
|
|
29
|
+
spec.loader.exec_module(module)
|
|
30
|
+
return module
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
_ROOT = Path(__file__).resolve().parents[1]
|
|
34
|
+
_SUBMISSION = _load_module(
|
|
35
|
+
"blun_website_localization_submission_capabilities_http_runtime",
|
|
36
|
+
_ROOT
|
|
37
|
+
/ "integrations"
|
|
38
|
+
/ "website_localization_cms_source_delivery_submission_runtime.py",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
API_SCHEMA = (
|
|
42
|
+
"blun.cms-source-delivery-submission-capabilities-http.v1"
|
|
43
|
+
)
|
|
44
|
+
ERROR_SCHEMA = (
|
|
45
|
+
"blun.cms-source-delivery-submission-capabilities-http-error.v1"
|
|
46
|
+
)
|
|
47
|
+
AUTH_REQUEST_SCHEMA = _SUBMISSION.CAPABILITIES_HTTP_AUTH_REQUEST_SCHEMA
|
|
48
|
+
PRINCIPAL_SCHEMA = _SUBMISSION.CAPABILITIES_HTTP_PRINCIPAL_SCHEMA
|
|
49
|
+
CAPABILITIES_RESPONSE_SCHEMA = _SUBMISSION.CAPABILITIES_HTTP_RESPONSE_SCHEMA
|
|
50
|
+
CAPABILITIES_PATH = _SUBMISSION.CAPABILITIES_HTTP_PATH
|
|
51
|
+
CAPABILITIES_SCOPE = _SUBMISSION.CAPABILITIES_HTTP_SCOPE
|
|
52
|
+
|
|
53
|
+
MAX_HEADERS = 64
|
|
54
|
+
MAX_HEADER_VALUE = 4096
|
|
55
|
+
TOKEN = re.compile(r"^[A-Za-z0-9_.:-]{1,256}$")
|
|
56
|
+
SHA256 = re.compile(r"^[0-9a-f]{64}$")
|
|
57
|
+
HEADER_NAME = re.compile(r"^[!#$%&'*+.^_`|~0-9A-Za-z-]{1,128}$")
|
|
58
|
+
ERROR_CODE = re.compile(r"^[a-z][a-z0-9_.-]{0,127}$")
|
|
59
|
+
EMPTY_SHA256 = hashlib.sha256(b"").hexdigest()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class SubmissionCapabilitiesHTTPBlocked(RuntimeError):
|
|
63
|
+
"""Stable, content-free public boundary failure."""
|
|
64
|
+
|
|
65
|
+
def __init__(self, code: str, status: int):
|
|
66
|
+
if ERROR_CODE.fullmatch(code) is None or status not in {
|
|
67
|
+
400, 401, 403, 404, 405, 503,
|
|
68
|
+
}:
|
|
69
|
+
raise ValueError("invalid submission-capabilities HTTP failure")
|
|
70
|
+
super().__init__(code)
|
|
71
|
+
self.code = code
|
|
72
|
+
self.status = status
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _blocked(code: str, status: int) -> SubmissionCapabilitiesHTTPBlocked:
|
|
76
|
+
return SubmissionCapabilitiesHTTPBlocked(
|
|
77
|
+
"submission_capabilities_http." + code, status,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _canonical(value: Any) -> bytes:
|
|
82
|
+
try:
|
|
83
|
+
result = json.dumps(
|
|
84
|
+
value,
|
|
85
|
+
ensure_ascii=False,
|
|
86
|
+
allow_nan=False,
|
|
87
|
+
sort_keys=True,
|
|
88
|
+
separators=(",", ":"),
|
|
89
|
+
).encode("utf-8")
|
|
90
|
+
except (TypeError, ValueError, RecursionError):
|
|
91
|
+
raise _blocked("response_invalid", 503) from None
|
|
92
|
+
if not result or len(result) > 1_000_000:
|
|
93
|
+
raise _blocked("response_invalid", 503)
|
|
94
|
+
return result
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _token(value: Any) -> str:
|
|
98
|
+
if (
|
|
99
|
+
not isinstance(value, str)
|
|
100
|
+
or TOKEN.fullmatch(value) is None
|
|
101
|
+
or not unicodedata.is_normalized("NFC", value)
|
|
102
|
+
):
|
|
103
|
+
raise ValueError
|
|
104
|
+
return value
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _sha256(value: Any) -> str:
|
|
108
|
+
if not isinstance(value, str) or SHA256.fullmatch(value) is None:
|
|
109
|
+
raise ValueError
|
|
110
|
+
return value
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _principal(value: Any) -> dict[str, str]:
|
|
114
|
+
fields = {
|
|
115
|
+
"schema", "principal_id", "credential_id", "credential_version",
|
|
116
|
+
"scope",
|
|
117
|
+
}
|
|
118
|
+
if not isinstance(value, Mapping) or set(value) != fields:
|
|
119
|
+
raise _blocked("authentication_failed", 401)
|
|
120
|
+
try:
|
|
121
|
+
if value["scope"] != CAPABILITIES_SCOPE:
|
|
122
|
+
raise _blocked("scope_rejected", 403)
|
|
123
|
+
if value["schema"] != PRINCIPAL_SCHEMA:
|
|
124
|
+
raise ValueError
|
|
125
|
+
for name in fields - {"schema"}:
|
|
126
|
+
_token(value[name])
|
|
127
|
+
except SubmissionCapabilitiesHTTPBlocked:
|
|
128
|
+
raise
|
|
129
|
+
except (KeyError, TypeError, ValueError):
|
|
130
|
+
raise _blocked("authentication_failed", 401) from None
|
|
131
|
+
return dict(value)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _headers(environ: Mapping[str, Any]) -> tuple[tuple[str, str], ...]:
|
|
135
|
+
headers = []
|
|
136
|
+
for key, value in environ.items():
|
|
137
|
+
if key.startswith("HTTP_"):
|
|
138
|
+
name = key[5:].replace("_", "-").lower()
|
|
139
|
+
elif key in {"CONTENT_TYPE", "CONTENT_LENGTH"}:
|
|
140
|
+
name = key.replace("_", "-").lower()
|
|
141
|
+
else:
|
|
142
|
+
continue
|
|
143
|
+
if (
|
|
144
|
+
HEADER_NAME.fullmatch(name) is None
|
|
145
|
+
or not isinstance(value, str)
|
|
146
|
+
or len(value) > MAX_HEADER_VALUE
|
|
147
|
+
or "\r" in value
|
|
148
|
+
or "\n" in value
|
|
149
|
+
):
|
|
150
|
+
raise _blocked("headers_invalid", 400)
|
|
151
|
+
headers.append((name, value))
|
|
152
|
+
headers.sort()
|
|
153
|
+
if (
|
|
154
|
+
len(headers) > MAX_HEADERS
|
|
155
|
+
or len({name for name, _ in headers}) != len(headers)
|
|
156
|
+
):
|
|
157
|
+
raise _blocked("headers_invalid", 400)
|
|
158
|
+
return tuple(headers)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _binding(value: Any) -> dict[str, Any]:
|
|
162
|
+
fields = {
|
|
163
|
+
"schema", "status", "database_role",
|
|
164
|
+
"delivery_capabilities_sha256", "runtime_capabilities_sha256",
|
|
165
|
+
"commercial_rendering_registry_sha256",
|
|
166
|
+
"terminal_receiver_capabilities_sha256", "binding_sha256",
|
|
167
|
+
}
|
|
168
|
+
if not isinstance(value, Mapping) or set(value) != fields:
|
|
169
|
+
raise ValueError
|
|
170
|
+
if (
|
|
171
|
+
value["schema"]
|
|
172
|
+
!= "blun.cms-source-delivery-runtime-capability-binding.v2"
|
|
173
|
+
or value["status"] != "verified"
|
|
174
|
+
or value["database_role"] != "source_delivery"
|
|
175
|
+
):
|
|
176
|
+
raise ValueError
|
|
177
|
+
for name in fields - {"schema", "status", "database_role"}:
|
|
178
|
+
_sha256(value[name])
|
|
179
|
+
expected_binding_sha256 = hashlib.sha256("\x00".join((
|
|
180
|
+
value["schema"],
|
|
181
|
+
value["database_role"],
|
|
182
|
+
value["delivery_capabilities_sha256"],
|
|
183
|
+
value["runtime_capabilities_sha256"],
|
|
184
|
+
value["commercial_rendering_registry_sha256"],
|
|
185
|
+
value["terminal_receiver_capabilities_sha256"],
|
|
186
|
+
)).encode("utf-8")).hexdigest()
|
|
187
|
+
if value["binding_sha256"] != expected_binding_sha256:
|
|
188
|
+
raise ValueError
|
|
189
|
+
return dict(value)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _capabilities(value: Any) -> dict[str, Any]:
|
|
193
|
+
try:
|
|
194
|
+
payload = value.as_payload()
|
|
195
|
+
fields = {
|
|
196
|
+
"schema", "operations", "retry_budgets", "semantics",
|
|
197
|
+
"sidecar_capabilities_sha256", "source_capabilities_sha256",
|
|
198
|
+
"website_capability_binding", "sha256",
|
|
199
|
+
}
|
|
200
|
+
if not isinstance(payload, Mapping) or set(payload) != fields:
|
|
201
|
+
raise ValueError
|
|
202
|
+
if payload["schema"] != _SUBMISSION.CAPABILITIES_SCHEMA:
|
|
203
|
+
raise ValueError
|
|
204
|
+
operations = payload["operations"]
|
|
205
|
+
expected_operations = {
|
|
206
|
+
"capabilities_http": {
|
|
207
|
+
"kind": "read",
|
|
208
|
+
"method": "GET",
|
|
209
|
+
"path": CAPABILITIES_PATH,
|
|
210
|
+
"scope": CAPABILITIES_SCOPE,
|
|
211
|
+
"principal_schema": PRINCIPAL_SCHEMA,
|
|
212
|
+
"request_schema": None,
|
|
213
|
+
"response_schema": CAPABILITIES_RESPONSE_SCHEMA,
|
|
214
|
+
},
|
|
215
|
+
"enqueue_change": {
|
|
216
|
+
"kind": "write",
|
|
217
|
+
"method": "POST",
|
|
218
|
+
"path": _SUBMISSION.CHANGE_HTTP_PATH,
|
|
219
|
+
"scope": _SUBMISSION.CHANGE_HTTP_SCOPE,
|
|
220
|
+
"principal_schema": _SUBMISSION.WRITE_HTTP_PRINCIPAL_SCHEMA,
|
|
221
|
+
"request_schema": _SUBMISSION.CHANGE_HTTP_REQUEST_SCHEMA,
|
|
222
|
+
"payload_schemas": [_SUBMISSION._ADAPTER.CHANGE_SCHEMA],
|
|
223
|
+
"response_schema": _SUBMISSION.CHANGE_HTTP_RESPONSE_SCHEMA,
|
|
224
|
+
},
|
|
225
|
+
"enqueue_removal": {
|
|
226
|
+
"kind": "write",
|
|
227
|
+
"method": "POST",
|
|
228
|
+
"path": _SUBMISSION.REMOVAL_HTTP_PATH,
|
|
229
|
+
"scope": _SUBMISSION.REMOVAL_HTTP_SCOPE,
|
|
230
|
+
"principal_schema": _SUBMISSION.WRITE_HTTP_PRINCIPAL_SCHEMA,
|
|
231
|
+
"request_schema": _SUBMISSION.REMOVAL_HTTP_REQUEST_SCHEMA,
|
|
232
|
+
"payload_schemas": [
|
|
233
|
+
_SUBMISSION._ADAPTER.CANCELLATION_SCHEMA,
|
|
234
|
+
_SUBMISSION._ADAPTER.TOMBSTONE_SCHEMA,
|
|
235
|
+
],
|
|
236
|
+
"response_schema": _SUBMISSION.REMOVAL_HTTP_RESPONSE_SCHEMA,
|
|
237
|
+
},
|
|
238
|
+
"submission_status": {
|
|
239
|
+
"kind": "read",
|
|
240
|
+
"method": "POST",
|
|
241
|
+
"path": _SUBMISSION.STATUS_HTTP_PATH,
|
|
242
|
+
"scope": _SUBMISSION.STATUS_HTTP_SCOPE,
|
|
243
|
+
"principal_schema": _SUBMISSION.READ_HTTP_PRINCIPAL_SCHEMA,
|
|
244
|
+
"request_schema": _SUBMISSION.STATUS_HTTP_REQUEST_SCHEMA,
|
|
245
|
+
"result_schema": _SUBMISSION.STATUS_SCHEMA,
|
|
246
|
+
"response_schema": _SUBMISSION.STATUS_HTTP_RESPONSE_SCHEMA,
|
|
247
|
+
},
|
|
248
|
+
"submission_lifecycle": {
|
|
249
|
+
"kind": "read",
|
|
250
|
+
"method": "POST",
|
|
251
|
+
"path": _SUBMISSION.LIFECYCLE_HTTP_PATH,
|
|
252
|
+
"scope": _SUBMISSION.LIFECYCLE_HTTP_SCOPE,
|
|
253
|
+
"principal_schema": _SUBMISSION.READ_HTTP_PRINCIPAL_SCHEMA,
|
|
254
|
+
"request_schema": _SUBMISSION.LIFECYCLE_HTTP_REQUEST_SCHEMA,
|
|
255
|
+
"result_schema": _SUBMISSION.LIFECYCLE_SCHEMA,
|
|
256
|
+
"response_schema": _SUBMISSION.LIFECYCLE_HTTP_RESPONSE_SCHEMA,
|
|
257
|
+
},
|
|
258
|
+
"submission_health": {
|
|
259
|
+
"kind": "read",
|
|
260
|
+
"response_schema": _SUBMISSION.HEALTH_SCHEMA,
|
|
261
|
+
},
|
|
262
|
+
"submission_pipeline_health": {
|
|
263
|
+
"kind": "read",
|
|
264
|
+
"method": "GET",
|
|
265
|
+
"path": _SUBMISSION.PIPELINE_HEALTH_HTTP_PATH,
|
|
266
|
+
"scope": _SUBMISSION.PIPELINE_HEALTH_HTTP_SCOPE,
|
|
267
|
+
"principal_schema": _SUBMISSION.OPERATOR_HTTP_PRINCIPAL_SCHEMA,
|
|
268
|
+
"request_schema": None,
|
|
269
|
+
"result_schema": _SUBMISSION.PIPELINE_HEALTH_SCHEMA,
|
|
270
|
+
"response_schema": (
|
|
271
|
+
_SUBMISSION.PIPELINE_HEALTH_HTTP_RESPONSE_SCHEMA
|
|
272
|
+
),
|
|
273
|
+
},
|
|
274
|
+
"submission_readiness": {
|
|
275
|
+
"kind": "read",
|
|
276
|
+
"response_schema": _SUBMISSION.READINESS_SCHEMA,
|
|
277
|
+
},
|
|
278
|
+
"submission_pipeline_readiness": {
|
|
279
|
+
"kind": "read",
|
|
280
|
+
"method": "GET",
|
|
281
|
+
"path": _SUBMISSION.PIPELINE_READINESS_HTTP_PATH,
|
|
282
|
+
"scope": _SUBMISSION.PIPELINE_READINESS_HTTP_SCOPE,
|
|
283
|
+
"principal_schema": _SUBMISSION.OPERATOR_HTTP_PRINCIPAL_SCHEMA,
|
|
284
|
+
"request_schema": None,
|
|
285
|
+
"result_schema": _SUBMISSION.PIPELINE_READINESS_SCHEMA,
|
|
286
|
+
"response_schema": (
|
|
287
|
+
_SUBMISSION.PIPELINE_READINESS_HTTP_RESPONSE_SCHEMA
|
|
288
|
+
),
|
|
289
|
+
},
|
|
290
|
+
}
|
|
291
|
+
if not isinstance(operations, Mapping) or operations != expected_operations:
|
|
292
|
+
raise ValueError
|
|
293
|
+
retry_budgets = payload["retry_budgets"]
|
|
294
|
+
if not isinstance(retry_budgets, Mapping):
|
|
295
|
+
raise ValueError
|
|
296
|
+
middle = retry_budgets.get("sidecar_to_source")
|
|
297
|
+
if (
|
|
298
|
+
not isinstance(middle, Mapping)
|
|
299
|
+
or set(middle) != {"configured_per_runtime", "maximum_attempts"}
|
|
300
|
+
or middle["configured_per_runtime"] is not True
|
|
301
|
+
or isinstance(middle["maximum_attempts"], bool)
|
|
302
|
+
or not isinstance(middle["maximum_attempts"], int)
|
|
303
|
+
or not 1 <= middle["maximum_attempts"] <= 20
|
|
304
|
+
or retry_budgets != {
|
|
305
|
+
"website_to_sidecar": {
|
|
306
|
+
"configured_per_request": True,
|
|
307
|
+
"minimum": 1,
|
|
308
|
+
"maximum": 20,
|
|
309
|
+
},
|
|
310
|
+
"sidecar_to_source": dict(middle),
|
|
311
|
+
"source_processing": {
|
|
312
|
+
"configured_per_request": True,
|
|
313
|
+
"minimum": 1,
|
|
314
|
+
"maximum": 20,
|
|
315
|
+
},
|
|
316
|
+
}
|
|
317
|
+
):
|
|
318
|
+
raise ValueError
|
|
319
|
+
if payload["semantics"] != {
|
|
320
|
+
"content_free": True,
|
|
321
|
+
"accepted_means": "durable_website_outbox_acceptance",
|
|
322
|
+
"accepted_implies_publication": False,
|
|
323
|
+
"translation_generation": False,
|
|
324
|
+
"publication_authority": False,
|
|
325
|
+
}:
|
|
326
|
+
raise ValueError
|
|
327
|
+
_sha256(payload["sidecar_capabilities_sha256"])
|
|
328
|
+
_sha256(payload["source_capabilities_sha256"])
|
|
329
|
+
payload = dict(payload)
|
|
330
|
+
payload["website_capability_binding"] = _binding(
|
|
331
|
+
payload["website_capability_binding"]
|
|
332
|
+
)
|
|
333
|
+
claimed = _sha256(payload.pop("sha256"))
|
|
334
|
+
actual = hashlib.sha256(_canonical(payload)).hexdigest()
|
|
335
|
+
if claimed != actual:
|
|
336
|
+
raise ValueError
|
|
337
|
+
payload["sha256"] = claimed
|
|
338
|
+
return payload
|
|
339
|
+
except SubmissionCapabilitiesHTTPBlocked:
|
|
340
|
+
raise
|
|
341
|
+
except Exception:
|
|
342
|
+
raise _blocked("runtime_response_invalid", 503) from None
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
class SubmissionCapabilitiesHTTPApplication:
|
|
346
|
+
"""Strict authenticated WSGI projection of one live website edge."""
|
|
347
|
+
|
|
348
|
+
def __init__(
|
|
349
|
+
self,
|
|
350
|
+
runtime: Any,
|
|
351
|
+
authenticator: Callable[[dict[str, Any]], Any],
|
|
352
|
+
):
|
|
353
|
+
if not callable(getattr(runtime, "submission_capabilities", None)):
|
|
354
|
+
raise TypeError("runtime must provide submission_capabilities")
|
|
355
|
+
if not callable(authenticator):
|
|
356
|
+
raise TypeError("authenticator must be callable")
|
|
357
|
+
self.runtime = runtime
|
|
358
|
+
self.authenticator = authenticator
|
|
359
|
+
|
|
360
|
+
@staticmethod
|
|
361
|
+
def _send(start_response, status: int, payload: Mapping[str, Any]):
|
|
362
|
+
body = _canonical(dict(payload))
|
|
363
|
+
phrases = {
|
|
364
|
+
200: "OK", 400: "Bad Request", 401: "Unauthorized",
|
|
365
|
+
403: "Forbidden", 404: "Not Found",
|
|
366
|
+
405: "Method Not Allowed", 503: "Service Unavailable",
|
|
367
|
+
}
|
|
368
|
+
start_response(f"{status} {phrases[status]}", [
|
|
369
|
+
("Content-Type", "application/json; charset=utf-8"),
|
|
370
|
+
("Content-Length", str(len(body))),
|
|
371
|
+
("Cache-Control", "no-store"),
|
|
372
|
+
("X-Content-Type-Options", "nosniff"),
|
|
373
|
+
("Referrer-Policy", "no-referrer"),
|
|
374
|
+
])
|
|
375
|
+
return [body]
|
|
376
|
+
|
|
377
|
+
@classmethod
|
|
378
|
+
def _error(cls, start_response, failure):
|
|
379
|
+
return cls._send(start_response, failure.status, {
|
|
380
|
+
"schema": ERROR_SCHEMA,
|
|
381
|
+
"status": "BLOCK",
|
|
382
|
+
"error_code": failure.code,
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
def __call__(
|
|
386
|
+
self,
|
|
387
|
+
environ: Mapping[str, Any],
|
|
388
|
+
start_response: Callable[..., Any],
|
|
389
|
+
):
|
|
390
|
+
try:
|
|
391
|
+
if not isinstance(environ, Mapping):
|
|
392
|
+
raise _blocked("environment_invalid", 400)
|
|
393
|
+
if environ.get("PATH_INFO") != CAPABILITIES_PATH:
|
|
394
|
+
raise _blocked("route_not_found", 404)
|
|
395
|
+
if environ.get("REQUEST_METHOD") != "GET":
|
|
396
|
+
raise _blocked("method_not_allowed", 405)
|
|
397
|
+
if environ.get("wsgi.url_scheme") != "https":
|
|
398
|
+
raise _blocked("https_required", 400)
|
|
399
|
+
if environ.get("QUERY_STRING") not in {None, ""}:
|
|
400
|
+
raise _blocked("query_rejected", 400)
|
|
401
|
+
if environ.get("HTTP_TRANSFER_ENCODING") not in {None, ""}:
|
|
402
|
+
raise _blocked("transfer_encoding_rejected", 400)
|
|
403
|
+
if environ.get("CONTENT_LENGTH") not in {None, "", "0"}:
|
|
404
|
+
raise _blocked("body_not_allowed", 400)
|
|
405
|
+
if environ.get("CONTENT_TYPE") not in {None, ""}:
|
|
406
|
+
raise _blocked("body_not_allowed", 400)
|
|
407
|
+
|
|
408
|
+
request = {
|
|
409
|
+
"schema": AUTH_REQUEST_SCHEMA,
|
|
410
|
+
"method": "GET",
|
|
411
|
+
"path": CAPABILITIES_PATH,
|
|
412
|
+
"headers": [list(item) for item in _headers(environ)],
|
|
413
|
+
"body_sha256": EMPTY_SHA256,
|
|
414
|
+
}
|
|
415
|
+
try:
|
|
416
|
+
principal = self.authenticator(request)
|
|
417
|
+
except Exception:
|
|
418
|
+
raise _blocked("authentication_unavailable", 503) from None
|
|
419
|
+
_principal(principal)
|
|
420
|
+
|
|
421
|
+
try:
|
|
422
|
+
capabilities = _capabilities(
|
|
423
|
+
self.runtime.submission_capabilities()
|
|
424
|
+
)
|
|
425
|
+
except SubmissionCapabilitiesHTTPBlocked:
|
|
426
|
+
raise
|
|
427
|
+
except Exception:
|
|
428
|
+
raise _blocked("runtime_blocked", 503) from None
|
|
429
|
+
return self._send(start_response, 200, {
|
|
430
|
+
"schema": CAPABILITIES_RESPONSE_SCHEMA,
|
|
431
|
+
"api_schema": API_SCHEMA,
|
|
432
|
+
"capabilities": capabilities,
|
|
433
|
+
})
|
|
434
|
+
except SubmissionCapabilitiesHTTPBlocked as failure:
|
|
435
|
+
return self._error(start_response, failure)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def build_submission_capabilities_http(
|
|
439
|
+
runtime: Any,
|
|
440
|
+
authenticator: Callable[[dict[str, Any]], Any],
|
|
441
|
+
) -> SubmissionCapabilitiesHTTPApplication:
|
|
442
|
+
"""Build the public read-only adapter without altering runtime ownership."""
|
|
443
|
+
|
|
444
|
+
return SubmissionCapabilitiesHTTPApplication(runtime, authenticator)
|