blun-king-cli 9.1.597 → 9.1.600

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +17 -1
  3. package/agent-spine-plugin/CHANGELOG.md +1 -1
  4. package/agent-spine-plugin/docs/host-integration.md +1 -15
  5. package/agent-spine-plugin/docs/preflight-recall.md +1 -1
  6. package/agent-spine-plugin/scripts/check-install.js +1 -6
  7. package/agent-spine-plugin/scripts/hermetic-process.js +8 -0
  8. package/agent-spine-plugin/scripts/release-check.js +2 -3
  9. package/agent-spine-plugin/scripts/run-tests-hermetic.js +8 -11
  10. package/agent-spine-plugin/src/hook.js +40 -40
  11. package/agent-spine-plugin/src/lib/briefing.js +6 -3
  12. package/agent-spine-plugin/src/lib/hook-context.js +63 -64
  13. package/agent-spine-plugin/src/lib/hook-output.js +115 -94
  14. package/agent-spine-plugin/src/lib/hook-timeline.js +20 -11
  15. package/agent-spine-plugin/src/lib/host-instruction-budget.js +8 -14
  16. package/agent-spine-plugin/src/lib/mcp-timeline-tools.js +0 -7
  17. package/agent-spine-plugin/src/lib/pre-answer-timeline-recall.js +104 -0
  18. package/agent-spine-plugin/src/lib/session-timeline-codex.js +0 -4
  19. package/agent-spine-plugin/src/lib/session-timeline-enrollment-source.js +0 -3
  20. package/agent-spine-plugin/src/lib/session-timeline-enrollment-storage.js +0 -4
  21. package/agent-spine-plugin/src/lib/session-timeline-enrollment-transport.js +0 -2
  22. package/agent-spine-plugin/src/lib/session-timeline-enrollment.js +0 -22
  23. package/agent-spine-plugin/src/lib/session-timeline-host-origin.js +0 -4
  24. package/agent-spine-plugin/src/lib/session-timeline-prior.js +42 -50
  25. package/agent-spine-plugin/src/lib/session-timeline-provider.js +0 -3
  26. package/agent-spine-plugin/src/lib/session-timeline-query.js +0 -1
  27. package/agent-spine-plugin/src/lib/session-timeline-sid-acl.js +0 -4
  28. package/agent-spine-plugin/src/lib/session-timeline-source.js +0 -3
  29. package/agent-spine-plugin/src/lib/session-timeline-transport.js +0 -8
  30. package/agent-spine-plugin/src/lib/session-timeline-windows-acl.js +0 -3
  31. package/agent-spine-plugin/src/lib/session-timeline.js +370 -408
  32. package/agent-spine-plugin/src/lib/source-roots.js +26 -16
  33. package/agent-spine-plugin/src/lib/timeline-user-feedback.js +0 -1
  34. package/bin/core-bootstrap.js +18 -10
  35. package/bin/launcher-mode.js +5 -0
  36. package/bin/launcher-runtime.js +27 -0
  37. package/bin/verify-bundled-agent-sources.cjs +28 -2
  38. package/blun.mjs +798 -143
  39. package/bundled-agent-sources.json +192 -155
  40. package/package.json +1 -1
  41. package/standard-skills/translate-native/README.md +1334 -1
  42. package/standard-skills/translate-native/SKILL.md +1 -1
  43. package/standard-skills/translate-native/VERSION +1 -1
  44. package/standard-skills/translate-native/docs/PREMORTEM.md +901 -0
  45. package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION.md +208 -41
  46. package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_API.md +1319 -77
  47. package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_EVIDENCE_HTTP.md +66 -11
  48. package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_HTTP_PROVIDER.md +19 -1
  49. package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_RECEIPT_VERIFIER_HTTP.md +24 -6
  50. package/standard-skills/translate-native/integrations/commercial_localization_profile.py +23 -0
  51. package/standard-skills/translate-native/integrations/website_localization.py +234 -10
  52. package/standard-skills/translate-native/integrations/website_localization_benchmark.py +39 -3
  53. package/standard-skills/translate-native/integrations/website_localization_cms.py +512 -10
  54. package/standard-skills/translate-native/integrations/website_localization_cms_client.py +116 -0
  55. package/standard-skills/translate-native/integrations/website_localization_cms_receiver.py +101 -11
  56. package/standard-skills/translate-native/integrations/website_localization_cms_receiver_runtime.py +12 -1
  57. package/standard-skills/translate-native/integrations/website_localization_cms_receiver_store.py +186 -24
  58. package/standard-skills/translate-native/integrations/website_localization_cms_source_client.py +64 -1
  59. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery.py +528 -5
  60. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_auth.py +918 -0
  61. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_auth_runtime.py +609 -0
  62. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_client.py +885 -0
  63. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_http.py +1244 -0
  64. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_runtime.py +220 -0
  65. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_sidecar_adapter.py +331 -0
  66. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_capabilities_http.py +444 -0
  67. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_client.py +673 -0
  68. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_dispatch.py +1317 -0
  69. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_dispatch_client.py +826 -0
  70. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_dispatch_http.py +1152 -0
  71. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_dispatch_openapi.py +896 -0
  72. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_dispatch_runtime.py +621 -0
  73. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_http.py +1180 -0
  74. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_runtime.py +1479 -0
  75. package/standard-skills/translate-native/integrations/website_localization_cms_source_http.py +133 -17
  76. package/standard-skills/translate-native/integrations/website_localization_cms_source_runtime.py +319 -4
  77. package/standard-skills/translate-native/integrations/website_localization_cms_source_service.py +72 -2
  78. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_http.py +13 -0
  79. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_receiver.py +56 -13
  80. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_receiver_openapi.py +335 -0
  81. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_receiver_runtime.py +66 -1
  82. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_processing_monitor.py +73 -7
  83. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_receiver_client.py +93 -9
  84. package/standard-skills/translate-native/integrations/website_localization_evidence_http.py +221 -11
  85. package/standard-skills/translate-native/integrations/website_localization_health.py +6 -0
  86. package/standard-skills/translate-native/integrations/website_localization_http_provider.py +1 -1
  87. package/standard-skills/translate-native/integrations/website_localization_quality_profiles.py +350 -0
  88. package/standard-skills/translate-native/integrations/website_localization_queue.py +126 -56
  89. package/standard-skills/translate-native/integrations/website_localization_receipt_verifier_http.py +166 -13
  90. package/standard-skills/translate-native/integrations/website_localization_release.py +423 -17
  91. package/standard-skills/translate-native/integrations/website_localization_release_coordinator.py +168 -13
  92. package/standard-skills/translate-native/integrations/website_localization_runner.py +10 -0
  93. package/standard-skills/translate-native/integrations/website_localization_worker.py +213 -9
  94. package/standard-skills/translate-native/provenance.json +2 -2
  95. package/standard-skills/translate-native/references/commercial-localization.md +96 -17
  96. package/standard-skills/translate-native/scripts/blun_language_guard.py +3 -1
  97. package/standard-skills/translate-native/scripts/check_commercial_review.py +69 -9
  98. package/standard-skills/translate-native/scripts/commercial_localization_profile.py +829 -31
  99. package/standard-tools/language-guard/VERSION +1 -1
  100. package/standard-tools/language-guard/blun_language_guard.py +3 -1
  101. package/standard-tools/language-guard/check_commercial_review.py +69 -9
  102. package/standard-tools/language-guard/commercial_localization_profile.py +829 -31
  103. package/standard-tools/language-guard/provenance.json +2 -2
  104. package/standard-tools/manifest.json +1 -1
  105. package/worker-host.mjs +215 -82
  106. package/agent-spine-plugin/scripts/check-install-king.js +0 -37
@@ -0,0 +1,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)