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
|
@@ -14,7 +14,7 @@ from dataclasses import dataclass
|
|
|
14
14
|
from typing import Any, Callable, Iterator, Mapping
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
SCHEMA_VERSION =
|
|
17
|
+
SCHEMA_VERSION = 2
|
|
18
18
|
STATUS_SCHEMA = "blun.cms-terminal-receiver-status-response.v1"
|
|
19
19
|
NOTIFICATION_SCHEMA = "blun.cms-source-terminal-notification.v1"
|
|
20
20
|
NOTIFICATION_FIELDS = {
|
|
@@ -87,6 +87,7 @@ class TerminalProcessingHealth:
|
|
|
87
87
|
due: int
|
|
88
88
|
expired_leases: int
|
|
89
89
|
failed: int
|
|
90
|
+
expected_capabilities_sha256: str
|
|
90
91
|
|
|
91
92
|
|
|
92
93
|
def _blocked(code: str) -> None:
|
|
@@ -169,6 +170,7 @@ class DurableTerminalProcessingMonitor:
|
|
|
169
170
|
def __init__(
|
|
170
171
|
self,
|
|
171
172
|
connection: sqlite3.Connection,
|
|
173
|
+
expected_capabilities_sha256: str,
|
|
172
174
|
*,
|
|
173
175
|
poll_interval_seconds: float | int = 30,
|
|
174
176
|
base_delay_seconds: float | int = 5,
|
|
@@ -178,6 +180,9 @@ class DurableTerminalProcessingMonitor:
|
|
|
178
180
|
raise TypeError("connection must be sqlite3.Connection")
|
|
179
181
|
self.connection = connection
|
|
180
182
|
self.connection.row_factory = sqlite3.Row
|
|
183
|
+
if not _sha(expected_capabilities_sha256):
|
|
184
|
+
_blocked("capabilities_invalid")
|
|
185
|
+
self.expected_capabilities_sha256 = expected_capabilities_sha256
|
|
181
186
|
self.poll_interval_seconds = _duration(
|
|
182
187
|
poll_interval_seconds, "poll_interval_invalid"
|
|
183
188
|
)
|
|
@@ -189,15 +194,68 @@ class DurableTerminalProcessingMonitor:
|
|
|
189
194
|
|
|
190
195
|
def _initialize(self) -> None:
|
|
191
196
|
with _transaction(self.connection):
|
|
197
|
+
tables = {
|
|
198
|
+
row[0] for row in self.connection.execute(
|
|
199
|
+
"SELECT name FROM sqlite_master WHERE type = 'table' "
|
|
200
|
+
"AND name IN ('cms_source_terminal_processing_meta', "
|
|
201
|
+
"'cms_source_terminal_processing')"
|
|
202
|
+
).fetchall()
|
|
203
|
+
}
|
|
204
|
+
if len(tables) == 1:
|
|
205
|
+
_blocked("schema_altered")
|
|
206
|
+
if tables:
|
|
207
|
+
meta_columns = tuple(
|
|
208
|
+
row["name"] for row in self.connection.execute(
|
|
209
|
+
"PRAGMA table_info(cms_source_terminal_processing_meta)"
|
|
210
|
+
).fetchall()
|
|
211
|
+
)
|
|
212
|
+
columns = tuple(
|
|
213
|
+
row["name"] for row in self.connection.execute(
|
|
214
|
+
"PRAGMA table_info(cms_source_terminal_processing)"
|
|
215
|
+
).fetchall()
|
|
216
|
+
)
|
|
217
|
+
meta = self.connection.execute(
|
|
218
|
+
"SELECT * FROM cms_source_terminal_processing_meta"
|
|
219
|
+
).fetchall()
|
|
220
|
+
legacy = (
|
|
221
|
+
meta_columns == ("singleton", "schema_version")
|
|
222
|
+
and columns == _COLUMNS
|
|
223
|
+
and len(meta) == 1
|
|
224
|
+
and tuple(meta[0]) == (1, 1)
|
|
225
|
+
)
|
|
226
|
+
current = (
|
|
227
|
+
meta_columns == (
|
|
228
|
+
"singleton", "schema_version",
|
|
229
|
+
"expected_capabilities_sha256",
|
|
230
|
+
)
|
|
231
|
+
and columns == _COLUMNS
|
|
232
|
+
and len(meta) == 1
|
|
233
|
+
and tuple(meta[0]) == (
|
|
234
|
+
1, SCHEMA_VERSION,
|
|
235
|
+
self.expected_capabilities_sha256,
|
|
236
|
+
)
|
|
237
|
+
)
|
|
238
|
+
if legacy:
|
|
239
|
+
if self.connection.execute(
|
|
240
|
+
"SELECT 1 FROM cms_source_terminal_processing LIMIT 1"
|
|
241
|
+
).fetchone() is not None:
|
|
242
|
+
_blocked("legacy_unbound")
|
|
243
|
+
self.connection.execute(
|
|
244
|
+
"DROP TABLE cms_source_terminal_processing_meta"
|
|
245
|
+
)
|
|
246
|
+
elif not current:
|
|
247
|
+
_blocked("schema_altered")
|
|
192
248
|
self.connection.execute("""
|
|
193
249
|
CREATE TABLE IF NOT EXISTS cms_source_terminal_processing_meta (
|
|
194
250
|
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
195
|
-
schema_version INTEGER NOT NULL CHECK (schema_version =
|
|
251
|
+
schema_version INTEGER NOT NULL CHECK (schema_version = 2),
|
|
252
|
+
expected_capabilities_sha256 TEXT NOT NULL
|
|
196
253
|
)
|
|
197
254
|
""")
|
|
198
255
|
self.connection.execute("""
|
|
199
|
-
INSERT OR IGNORE INTO cms_source_terminal_processing_meta
|
|
200
|
-
|
|
256
|
+
INSERT OR IGNORE INTO cms_source_terminal_processing_meta
|
|
257
|
+
VALUES (1, 2, ?)
|
|
258
|
+
""", (self.expected_capabilities_sha256,))
|
|
201
259
|
self.connection.execute("""
|
|
202
260
|
CREATE TABLE IF NOT EXISTS cms_source_terminal_processing (
|
|
203
261
|
event_id TEXT PRIMARY KEY, notification_id TEXT NOT NULL UNIQUE,
|
|
@@ -236,12 +294,17 @@ class DurableTerminalProcessingMonitor:
|
|
|
236
294
|
"PRAGMA table_info(cms_source_terminal_processing)"
|
|
237
295
|
).fetchall())
|
|
238
296
|
meta = self.connection.execute(
|
|
239
|
-
"SELECT singleton, schema_version
|
|
297
|
+
"SELECT singleton, schema_version, expected_capabilities_sha256 "
|
|
298
|
+
"FROM cms_source_terminal_processing_meta"
|
|
240
299
|
).fetchall()
|
|
241
300
|
if (
|
|
242
|
-
meta_columns != (
|
|
301
|
+
meta_columns != (
|
|
302
|
+
"singleton", "schema_version", "expected_capabilities_sha256",
|
|
303
|
+
)
|
|
243
304
|
or columns != _COLUMNS or len(meta) != 1
|
|
244
|
-
or tuple(meta[0]) != (
|
|
305
|
+
or tuple(meta[0]) != (
|
|
306
|
+
1, SCHEMA_VERSION, self.expected_capabilities_sha256,
|
|
307
|
+
)
|
|
245
308
|
):
|
|
246
309
|
_blocked("schema_altered")
|
|
247
310
|
|
|
@@ -417,6 +480,8 @@ class DurableTerminalProcessingMonitor:
|
|
|
417
480
|
and (error is None or _error(error))
|
|
418
481
|
and _valid_time(value.get("processed_at"), nullable=True)
|
|
419
482
|
and _sha(value.get("capabilities_sha256"))
|
|
483
|
+
and value.get("capabilities_sha256")
|
|
484
|
+
== self.expected_capabilities_sha256
|
|
420
485
|
and (remote == "leased") == (value.get("lease_expires_at") is not None)
|
|
421
486
|
and (remote == "succeeded") == (value.get("processed_at") is not None)
|
|
422
487
|
and (error is None or remote in {"retry_wait", "failed"})
|
|
@@ -532,6 +597,7 @@ class DurableTerminalProcessingMonitor:
|
|
|
532
597
|
return TerminalProcessingHealth(
|
|
533
598
|
"blocked" if expired or counts["failed"] else "ok",
|
|
534
599
|
counts, due, expired, counts["failed"],
|
|
600
|
+
self.expected_capabilities_sha256,
|
|
535
601
|
)
|
|
536
602
|
|
|
537
603
|
def _require_claim(self, claim: sqlite3.Row, now: float) -> sqlite3.Row:
|
|
@@ -4,23 +4,28 @@
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
6
|
import hashlib
|
|
7
|
+
import importlib.util
|
|
7
8
|
import json
|
|
8
9
|
import math
|
|
9
10
|
import re
|
|
10
11
|
import socket
|
|
12
|
+
import sys
|
|
11
13
|
import urllib.error
|
|
12
14
|
import urllib.parse
|
|
13
15
|
import urllib.request
|
|
14
16
|
from dataclasses import dataclass
|
|
17
|
+
from pathlib import Path
|
|
15
18
|
from typing import Any, Callable, Mapping, Protocol
|
|
16
19
|
|
|
17
20
|
|
|
18
21
|
AUTH_SCHEMA = "blun.cms-source-terminal-notification-http-auth.v1"
|
|
19
|
-
API_SCHEMA = "blun.cms-terminal-receiver-api.
|
|
20
|
-
CAPABILITIES_SCHEMA = "blun.cms-terminal-receiver-capabilities.
|
|
22
|
+
API_SCHEMA = "blun.cms-terminal-receiver-api.v3"
|
|
23
|
+
CAPABILITIES_SCHEMA = "blun.cms-terminal-receiver-capabilities.v3"
|
|
21
24
|
CAPABILITIES_RESPONSE_SCHEMA = (
|
|
22
|
-
"blun.cms-terminal-receiver-capabilities-response.
|
|
25
|
+
"blun.cms-terminal-receiver-capabilities-response.v3"
|
|
23
26
|
)
|
|
27
|
+
OPENAPI_DOCUMENT_SCHEMA = "blun.cms-terminal-receiver-openapi.v2"
|
|
28
|
+
OPENAPI_RESPONSE_SCHEMA = "blun.cms-terminal-receiver-openapi-response.v1"
|
|
24
29
|
HEALTH_SCHEMA = "blun.cms-terminal-receiver-health.v1"
|
|
25
30
|
READINESS_SCHEMA = "blun.cms-terminal-receiver-readiness.v1"
|
|
26
31
|
STATUS_REQUEST_SCHEMA = "blun.cms-terminal-receiver-status-request.v1"
|
|
@@ -31,10 +36,13 @@ NOTIFICATION_SCHEMA = "blun.cms-source-terminal-notification.v1"
|
|
|
31
36
|
ACK_SCHEMA = "blun.cms-source-terminal-notification-ack.v1"
|
|
32
37
|
BASE_PATH = "/v1/localization/terminal-notifications"
|
|
33
38
|
CAPABILITIES_PATH = BASE_PATH + "/capabilities"
|
|
39
|
+
OPENAPI_PATH = BASE_PATH + "/openapi"
|
|
34
40
|
HEALTH_PATH = BASE_PATH + "/health"
|
|
35
41
|
READINESS_PATH = BASE_PATH + "/readiness"
|
|
36
42
|
STATUS_PATH = BASE_PATH + "/status"
|
|
37
|
-
|
|
43
|
+
CAPABILITIES_PRECONDITION_HEADER = "X-Localization-Capabilities-SHA256"
|
|
44
|
+
MAX_REQUEST_BYTES = 16_384
|
|
45
|
+
MAX_RESPONSE_BYTES = 1_000_000
|
|
38
46
|
MAX_ENDPOINT_LENGTH = 2_048
|
|
39
47
|
MAX_HEADER_VALUE_LENGTH = 4_096
|
|
40
48
|
EMPTY_SHA256 = hashlib.sha256(b"").hexdigest()
|
|
@@ -57,6 +65,7 @@ RESERVED_HEADERS = {
|
|
|
57
65
|
"x-localization-terminal-notification-id",
|
|
58
66
|
"x-localization-terminal-notification-sha256",
|
|
59
67
|
"x-localization-terminal-status-sha256",
|
|
68
|
+
"x-localization-capabilities-sha256",
|
|
60
69
|
}
|
|
61
70
|
NOTIFICATION_FIELDS = {
|
|
62
71
|
"schema", "notification_id", "event_id", "site_id", "plan_id",
|
|
@@ -69,6 +78,26 @@ ACK_FIELDS = {
|
|
|
69
78
|
}
|
|
70
79
|
|
|
71
80
|
|
|
81
|
+
def _load_module(name: str, path: Path):
|
|
82
|
+
spec = importlib.util.spec_from_file_location(name, path)
|
|
83
|
+
if spec is None or spec.loader is None:
|
|
84
|
+
raise RuntimeError(
|
|
85
|
+
"cannot load terminal receiver client dependency: " + path.name
|
|
86
|
+
)
|
|
87
|
+
module = importlib.util.module_from_spec(spec)
|
|
88
|
+
sys.modules[spec.name] = module
|
|
89
|
+
spec.loader.exec_module(module)
|
|
90
|
+
return module
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
_OPENAPI = _load_module(
|
|
94
|
+
"blun_website_localization_terminal_receiver_client_openapi",
|
|
95
|
+
Path(__file__).resolve().with_name(
|
|
96
|
+
"website_localization_cms_terminal_notification_receiver_openapi.py"
|
|
97
|
+
),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
72
101
|
class TerminalReceiverClientBlocked(RuntimeError):
|
|
73
102
|
"""Stable, content-free control-plane failure."""
|
|
74
103
|
|
|
@@ -147,7 +176,7 @@ def _canonical(value: Any) -> bytes:
|
|
|
147
176
|
).encode("utf-8")
|
|
148
177
|
except (TypeError, ValueError, RecursionError):
|
|
149
178
|
_fail("request_invalid")
|
|
150
|
-
if not raw or len(raw) >
|
|
179
|
+
if not raw or len(raw) > MAX_REQUEST_BYTES:
|
|
151
180
|
_fail("request_invalid")
|
|
152
181
|
return raw
|
|
153
182
|
|
|
@@ -373,6 +402,10 @@ def _operation(name, method, path, scope, request_schema, request_fields,
|
|
|
373
402
|
"request_fields": request_fields,
|
|
374
403
|
"response_schema": response_schema,
|
|
375
404
|
"response_fields": response_fields,
|
|
405
|
+
"capabilities_precondition_header": (
|
|
406
|
+
None if name == "capabilities"
|
|
407
|
+
else CAPABILITIES_PRECONDITION_HEADER
|
|
408
|
+
),
|
|
376
409
|
"success_status": 200,
|
|
377
410
|
}
|
|
378
411
|
|
|
@@ -381,7 +414,8 @@ def _valid_capabilities(value: Any, expected_sha256: str) -> bool:
|
|
|
381
414
|
if not isinstance(value, dict) or set(value) != {
|
|
382
415
|
"schema", "api_schema", "authentication_request_schema",
|
|
383
416
|
"principal_schema", "error_schema", "limits", "processing_statuses",
|
|
384
|
-
"terminal_statuses", "operations", "sha256",
|
|
417
|
+
"terminal_statuses", "operations", "openapi_document_schema", "sha256",
|
|
418
|
+
"semantics",
|
|
385
419
|
}:
|
|
386
420
|
return False
|
|
387
421
|
digest = value.get("sha256")
|
|
@@ -395,8 +429,12 @@ def _valid_capabilities(value: Any, expected_sha256: str) -> bool:
|
|
|
395
429
|
or value.get("authentication_request_schema") != AUTH_SCHEMA
|
|
396
430
|
or value.get("principal_schema") != PRINCIPAL_SCHEMA
|
|
397
431
|
or value.get("error_schema") != ERROR_SCHEMA
|
|
432
|
+
or value.get("openapi_document_schema") != OPENAPI_DOCUMENT_SCHEMA
|
|
398
433
|
or value.get("processing_statuses") != list(PROCESSING_STATUSES)
|
|
399
434
|
or value.get("terminal_statuses") != sorted(TERMINAL_STATUSES)
|
|
435
|
+
or value.get("semantics") != {
|
|
436
|
+
"non_discovery_operations_require_exact_capability_precondition": True,
|
|
437
|
+
}
|
|
400
438
|
):
|
|
401
439
|
return False
|
|
402
440
|
limits = value.get("limits")
|
|
@@ -404,12 +442,14 @@ def _valid_capabilities(value: Any, expected_sha256: str) -> bool:
|
|
|
404
442
|
not isinstance(limits, dict)
|
|
405
443
|
or set(limits) != {
|
|
406
444
|
"max_body_bytes", "max_headers", "max_header_value_bytes",
|
|
407
|
-
"
|
|
445
|
+
"max_response_bytes", "processing_max_attempts_min",
|
|
446
|
+
"processing_max_attempts_max",
|
|
408
447
|
}
|
|
409
448
|
or limits != {
|
|
410
449
|
"max_body_bytes": 16_384,
|
|
411
450
|
"max_headers": 64,
|
|
412
451
|
"max_header_value_bytes": 4_096,
|
|
452
|
+
"max_response_bytes": 1_000_000,
|
|
413
453
|
"processing_max_attempts_min": 1,
|
|
414
454
|
"processing_max_attempts_max": 20,
|
|
415
455
|
}
|
|
@@ -417,7 +457,8 @@ def _valid_capabilities(value: Any, expected_sha256: str) -> bool:
|
|
|
417
457
|
return False
|
|
418
458
|
operations = value.get("operations")
|
|
419
459
|
if not isinstance(operations, dict) or set(operations) != {
|
|
420
|
-
"capabilities", "health", "notification", "
|
|
460
|
+
"capabilities", "health", "notification", "openapi", "readiness",
|
|
461
|
+
"status",
|
|
421
462
|
}:
|
|
422
463
|
return False
|
|
423
464
|
expected = {
|
|
@@ -436,6 +477,11 @@ def _valid_capabilities(value: Any, expected_sha256: str) -> bool:
|
|
|
436
477
|
"capabilities_sha256",
|
|
437
478
|
],
|
|
438
479
|
),
|
|
480
|
+
"openapi": _operation(
|
|
481
|
+
"openapi", "GET", OPENAPI_PATH, "terminal-notification-openapi:read",
|
|
482
|
+
None, [], OPENAPI_RESPONSE_SCHEMA,
|
|
483
|
+
["schema", "openapi", "openapi_sha256", "capabilities_sha256"],
|
|
484
|
+
),
|
|
439
485
|
"readiness": _operation(
|
|
440
486
|
"readiness", "GET", READINESS_PATH,
|
|
441
487
|
"terminal-notification-readiness:read", None, [], READINESS_SCHEMA,
|
|
@@ -466,7 +512,10 @@ def _valid_capabilities(value: Any, expected_sha256: str) -> bool:
|
|
|
466
512
|
not isinstance(path, str)
|
|
467
513
|
or not path.startswith("/")
|
|
468
514
|
or path.startswith("//")
|
|
469
|
-
or path in {
|
|
515
|
+
or path in {
|
|
516
|
+
CAPABILITIES_PATH, HEALTH_PATH, OPENAPI_PATH, READINESS_PATH,
|
|
517
|
+
STATUS_PATH,
|
|
518
|
+
}
|
|
470
519
|
):
|
|
471
520
|
return False
|
|
472
521
|
expected["notification"] = _operation(
|
|
@@ -536,10 +585,18 @@ class HTTPTerminalReceiverClient:
|
|
|
536
585
|
"body_sha256": body_sha256,
|
|
537
586
|
**context,
|
|
538
587
|
}
|
|
588
|
+
if path != CAPABILITIES_PATH:
|
|
589
|
+
authentication["capabilities_sha256"] = (
|
|
590
|
+
self.expected_capabilities_sha256
|
|
591
|
+
)
|
|
539
592
|
headers = _authentication_headers(
|
|
540
593
|
self.authentication_headers, authentication,
|
|
541
594
|
)
|
|
542
595
|
headers["Accept"] = "application/json"
|
|
596
|
+
if path != CAPABILITIES_PATH:
|
|
597
|
+
headers[CAPABILITIES_PRECONDITION_HEADER] = (
|
|
598
|
+
self.expected_capabilities_sha256
|
|
599
|
+
)
|
|
543
600
|
if body is not None:
|
|
544
601
|
headers["Content-Type"] = "application/json; charset=utf-8"
|
|
545
602
|
headers["X-Localization-Terminal-Status-SHA256"] = body_sha256
|
|
@@ -572,6 +629,29 @@ class HTTPTerminalReceiverClient:
|
|
|
572
629
|
def _verify_contract(self) -> None:
|
|
573
630
|
self.capabilities()
|
|
574
631
|
|
|
632
|
+
def openapi(self) -> Mapping[str, Any]:
|
|
633
|
+
"""Return only the exact API document for the freshly pinned contract."""
|
|
634
|
+
|
|
635
|
+
capabilities = self.capabilities()["capabilities"]
|
|
636
|
+
_result, response = self._request(
|
|
637
|
+
"GET", OPENAPI_PATH, None, {200}, {},
|
|
638
|
+
)
|
|
639
|
+
expected = _OPENAPI.build_document(capabilities)
|
|
640
|
+
if (
|
|
641
|
+
set(response) != {
|
|
642
|
+
"schema", "openapi", "openapi_sha256",
|
|
643
|
+
"capabilities_sha256",
|
|
644
|
+
}
|
|
645
|
+
or response.get("schema") != OPENAPI_RESPONSE_SCHEMA
|
|
646
|
+
or response.get("capabilities_sha256")
|
|
647
|
+
!= self.expected_capabilities_sha256
|
|
648
|
+
or response.get("openapi") != expected
|
|
649
|
+
or response.get("openapi_sha256")
|
|
650
|
+
!= _OPENAPI.document_sha256(expected)
|
|
651
|
+
):
|
|
652
|
+
_fail("openapi_binding")
|
|
653
|
+
return response
|
|
654
|
+
|
|
575
655
|
def notify(self, notification: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
576
656
|
"""Send one immutable notification through its freshly pinned route."""
|
|
577
657
|
|
|
@@ -590,6 +670,7 @@ class HTTPTerminalReceiverClient:
|
|
|
590
670
|
"path": path,
|
|
591
671
|
"body_sha256": body_sha256,
|
|
592
672
|
**context,
|
|
673
|
+
"capabilities_sha256": self.expected_capabilities_sha256,
|
|
593
674
|
}
|
|
594
675
|
headers = _authentication_headers(
|
|
595
676
|
self.authentication_headers, authentication,
|
|
@@ -602,6 +683,9 @@ class HTTPTerminalReceiverClient:
|
|
|
602
683
|
payload["notification_id"]
|
|
603
684
|
),
|
|
604
685
|
"X-Localization-Terminal-Notification-Sha256": body_sha256,
|
|
686
|
+
CAPABILITIES_PRECONDITION_HEADER: (
|
|
687
|
+
self.expected_capabilities_sha256
|
|
688
|
+
),
|
|
605
689
|
})
|
|
606
690
|
try:
|
|
607
691
|
result = self.transport.request(
|