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
package/standard-skills/translate-native/integrations/website_localization_cms_source_runtime.py
CHANGED
|
@@ -2,13 +2,16 @@
|
|
|
2
2
|
"""Owned, durable runtime for the source side of the CMS localization API.
|
|
3
3
|
|
|
4
4
|
The composition root validates the complete service configuration before it
|
|
5
|
-
opens three private SQLite databases.
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
opens three private SQLite databases. An explicit production preflight can
|
|
6
|
+
also verify both CMS capability pins before persistence and durably bind the
|
|
7
|
+
verified pair plus each database role. One runtime belongs to one process and
|
|
8
|
+
serializes every local state transition. Separate processes may safely open the
|
|
9
|
+
same files because the underlying outboxes and monitor use durable leases.
|
|
8
10
|
"""
|
|
9
11
|
|
|
10
12
|
from __future__ import annotations
|
|
11
13
|
|
|
14
|
+
import hashlib
|
|
12
15
|
import importlib.util
|
|
13
16
|
import math
|
|
14
17
|
import os
|
|
@@ -42,6 +45,30 @@ _HTTP = _load_module(
|
|
|
42
45
|
)
|
|
43
46
|
|
|
44
47
|
|
|
48
|
+
CAPABILITY_BINDING_SCHEMA = "blun.cms-source-runtime-capability-binding.v1"
|
|
49
|
+
CAPABILITY_BINDING_TABLE = "cms_source_runtime_capability_binding"
|
|
50
|
+
CAPABILITY_DATABASE_ROLES = ("changes", "removals", "lifecycle")
|
|
51
|
+
CAPABILITY_BINDING_SQL = (
|
|
52
|
+
"CREATE TABLE cms_source_runtime_capability_binding ("
|
|
53
|
+
"singleton INTEGER PRIMARY KEY CHECK (singleton = 1), "
|
|
54
|
+
"schema TEXT NOT NULL CHECK ("
|
|
55
|
+
"schema = 'blun.cms-source-runtime-capability-binding.v1'), "
|
|
56
|
+
"database_role TEXT NOT NULL CHECK ("
|
|
57
|
+
"database_role IN ('changes', 'removals', 'lifecycle')), "
|
|
58
|
+
"capabilities_sha256 TEXT NOT NULL, "
|
|
59
|
+
"commercial_rendering_registry_sha256 TEXT NOT NULL, "
|
|
60
|
+
"binding_sha256 TEXT NOT NULL)"
|
|
61
|
+
)
|
|
62
|
+
CAPABILITY_BINDING_COLUMNS = (
|
|
63
|
+
("singleton", "INTEGER", 0, 1),
|
|
64
|
+
("schema", "TEXT", 1, 0),
|
|
65
|
+
("database_role", "TEXT", 1, 0),
|
|
66
|
+
("capabilities_sha256", "TEXT", 1, 0),
|
|
67
|
+
("commercial_rendering_registry_sha256", "TEXT", 1, 0),
|
|
68
|
+
("binding_sha256", "TEXT", 1, 0),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
45
72
|
class DurableCMSSourceRuntimeBlocked(RuntimeError):
|
|
46
73
|
"""Stable runtime failure that contains no path, credential, or content."""
|
|
47
74
|
|
|
@@ -240,6 +267,204 @@ def _preflight_service(
|
|
|
240
267
|
connection.close()
|
|
241
268
|
|
|
242
269
|
|
|
270
|
+
def _sha256(value: Any) -> str | None:
|
|
271
|
+
if (
|
|
272
|
+
isinstance(value, str)
|
|
273
|
+
and len(value) == 64
|
|
274
|
+
and all(character in "0123456789abcdef" for character in value)
|
|
275
|
+
):
|
|
276
|
+
return value
|
|
277
|
+
return None
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _preflight_capabilities(client: Any) -> tuple[str, str]:
|
|
281
|
+
"""Verify both deployment pins through one bounded capability request."""
|
|
282
|
+
|
|
283
|
+
try:
|
|
284
|
+
capabilities_sha256 = _sha256(
|
|
285
|
+
getattr(client, "capabilities_sha256", None),
|
|
286
|
+
)
|
|
287
|
+
rendering_sha256 = _sha256(
|
|
288
|
+
getattr(client, "commercial_rendering_registry_sha256", None),
|
|
289
|
+
)
|
|
290
|
+
capabilities = getattr(client, "capabilities", None)
|
|
291
|
+
except Exception as error:
|
|
292
|
+
raise _blocked("source_runtime.capability_preflight_failed") from error
|
|
293
|
+
if (
|
|
294
|
+
capabilities_sha256 is None
|
|
295
|
+
or rendering_sha256 is None
|
|
296
|
+
or not callable(capabilities)
|
|
297
|
+
):
|
|
298
|
+
raise _blocked("source_runtime.capability_pins_required")
|
|
299
|
+
try:
|
|
300
|
+
response = capabilities()
|
|
301
|
+
except Exception as error:
|
|
302
|
+
if getattr(error, "code", None) in {
|
|
303
|
+
"capabilities_pin_mismatch",
|
|
304
|
+
"commercial_rendering_registry_pin_mismatch",
|
|
305
|
+
}:
|
|
306
|
+
raise _blocked("source_runtime.capability_pin_mismatch") from error
|
|
307
|
+
raise _blocked("source_runtime.capability_preflight_failed") from error
|
|
308
|
+
try:
|
|
309
|
+
capability_payload = response["capabilities"]
|
|
310
|
+
client_type = _SERVICE._DISPATCH._CLIENT.CMSLocalizationHTTPClient
|
|
311
|
+
valid = (
|
|
312
|
+
isinstance(response, Mapping)
|
|
313
|
+
and client_type._valid_capabilities(response)
|
|
314
|
+
and capability_payload["sha256"] == capabilities_sha256
|
|
315
|
+
and capability_payload["commercial_rendering_registry"]["sha256"]
|
|
316
|
+
== rendering_sha256
|
|
317
|
+
)
|
|
318
|
+
except Exception:
|
|
319
|
+
valid = False
|
|
320
|
+
if not valid:
|
|
321
|
+
raise _blocked("source_runtime.capability_preflight_failed")
|
|
322
|
+
return capabilities_sha256, rendering_sha256
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _capability_binding_row(
|
|
326
|
+
role: str,
|
|
327
|
+
binding: tuple[str, str],
|
|
328
|
+
) -> tuple[Any, ...]:
|
|
329
|
+
capabilities_sha256, rendering_sha256 = binding
|
|
330
|
+
digest = hashlib.sha256("\x00".join((
|
|
331
|
+
CAPABILITY_BINDING_SCHEMA,
|
|
332
|
+
role,
|
|
333
|
+
capabilities_sha256,
|
|
334
|
+
rendering_sha256,
|
|
335
|
+
)).encode("utf-8")).hexdigest()
|
|
336
|
+
return (
|
|
337
|
+
1,
|
|
338
|
+
CAPABILITY_BINDING_SCHEMA,
|
|
339
|
+
role,
|
|
340
|
+
capabilities_sha256,
|
|
341
|
+
rendering_sha256,
|
|
342
|
+
digest,
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _capability_table_exists(connection: sqlite3.Connection) -> bool:
|
|
347
|
+
rows = connection.execute(
|
|
348
|
+
"SELECT type, name, tbl_name, sql FROM sqlite_master WHERE name = ?",
|
|
349
|
+
(CAPABILITY_BINDING_TABLE,),
|
|
350
|
+
).fetchall()
|
|
351
|
+
if not rows:
|
|
352
|
+
return False
|
|
353
|
+
if len(rows) != 1 or tuple(rows[0]) != (
|
|
354
|
+
"table",
|
|
355
|
+
CAPABILITY_BINDING_TABLE,
|
|
356
|
+
CAPABILITY_BINDING_TABLE,
|
|
357
|
+
CAPABILITY_BINDING_SQL,
|
|
358
|
+
):
|
|
359
|
+
raise _blocked("source_runtime.capability_database_invalid")
|
|
360
|
+
return True
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _validate_capability_database(
|
|
364
|
+
connection: sqlite3.Connection,
|
|
365
|
+
role: str,
|
|
366
|
+
binding: tuple[str, str],
|
|
367
|
+
) -> None:
|
|
368
|
+
try:
|
|
369
|
+
if not _capability_table_exists(connection):
|
|
370
|
+
raise _blocked("source_runtime.capability_database_unbound")
|
|
371
|
+
columns = tuple(
|
|
372
|
+
(row[1], row[2], row[3], row[5])
|
|
373
|
+
for row in connection.execute(
|
|
374
|
+
f"PRAGMA table_info({CAPABILITY_BINDING_TABLE})"
|
|
375
|
+
).fetchall()
|
|
376
|
+
)
|
|
377
|
+
rows = connection.execute(
|
|
378
|
+
f"SELECT singleton, schema, database_role, capabilities_sha256, "
|
|
379
|
+
f"commercial_rendering_registry_sha256, binding_sha256 "
|
|
380
|
+
f"FROM {CAPABILITY_BINDING_TABLE}"
|
|
381
|
+
).fetchall()
|
|
382
|
+
except DurableCMSSourceRuntimeBlocked:
|
|
383
|
+
raise
|
|
384
|
+
except sqlite3.Error as error:
|
|
385
|
+
raise _blocked("source_runtime.capability_database_invalid") from error
|
|
386
|
+
if columns != CAPABILITY_BINDING_COLUMNS or len(rows) != 1:
|
|
387
|
+
raise _blocked("source_runtime.capability_database_invalid")
|
|
388
|
+
if tuple(rows[0]) != _capability_binding_row(role, binding):
|
|
389
|
+
raise _blocked("source_runtime.capability_database_mismatch")
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _unbound_database_has_work(connection: sqlite3.Connection) -> bool:
|
|
393
|
+
try:
|
|
394
|
+
tables = connection.execute(
|
|
395
|
+
"SELECT name FROM sqlite_master "
|
|
396
|
+
"WHERE type = 'table' AND name LIKE 'cms_source_%' "
|
|
397
|
+
"AND name <> ? ORDER BY name",
|
|
398
|
+
(CAPABILITY_BINDING_TABLE,),
|
|
399
|
+
).fetchall()
|
|
400
|
+
for (name,) in tables:
|
|
401
|
+
if (
|
|
402
|
+
not isinstance(name, str)
|
|
403
|
+
or not name.startswith("cms_source_")
|
|
404
|
+
or any(
|
|
405
|
+
character not in "abcdefghijklmnopqrstuvwxyz_"
|
|
406
|
+
for character in name
|
|
407
|
+
)
|
|
408
|
+
):
|
|
409
|
+
raise _blocked("source_runtime.capability_database_invalid")
|
|
410
|
+
if name.endswith("_meta"):
|
|
411
|
+
continue
|
|
412
|
+
if connection.execute(
|
|
413
|
+
f'SELECT 1 FROM "{name}" LIMIT 1'
|
|
414
|
+
).fetchone() is not None:
|
|
415
|
+
return True
|
|
416
|
+
except DurableCMSSourceRuntimeBlocked:
|
|
417
|
+
raise
|
|
418
|
+
except sqlite3.Error as error:
|
|
419
|
+
raise _blocked("source_runtime.capability_database_invalid") from error
|
|
420
|
+
return False
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _precheck_capability_databases(
|
|
424
|
+
connections: tuple[sqlite3.Connection, ...],
|
|
425
|
+
binding: tuple[str, str],
|
|
426
|
+
) -> None:
|
|
427
|
+
for role, connection in zip(CAPABILITY_DATABASE_ROLES, connections):
|
|
428
|
+
try:
|
|
429
|
+
exists = _capability_table_exists(connection)
|
|
430
|
+
except sqlite3.Error as error:
|
|
431
|
+
raise _blocked(
|
|
432
|
+
"source_runtime.capability_database_invalid",
|
|
433
|
+
) from error
|
|
434
|
+
if exists:
|
|
435
|
+
_validate_capability_database(connection, role, binding)
|
|
436
|
+
elif _unbound_database_has_work(connection):
|
|
437
|
+
raise _blocked("source_runtime.capability_database_unbound")
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _bind_capability_databases(
|
|
441
|
+
connections: tuple[sqlite3.Connection, ...],
|
|
442
|
+
binding: tuple[str, str],
|
|
443
|
+
) -> None:
|
|
444
|
+
for role, connection in zip(CAPABILITY_DATABASE_ROLES, connections):
|
|
445
|
+
try:
|
|
446
|
+
connection.execute("BEGIN IMMEDIATE")
|
|
447
|
+
if not _capability_table_exists(connection):
|
|
448
|
+
connection.execute(CAPABILITY_BINDING_SQL)
|
|
449
|
+
connection.execute(
|
|
450
|
+
f"INSERT OR IGNORE INTO {CAPABILITY_BINDING_TABLE} VALUES "
|
|
451
|
+
"(?, ?, ?, ?, ?, ?)",
|
|
452
|
+
_capability_binding_row(role, binding),
|
|
453
|
+
)
|
|
454
|
+
_validate_capability_database(connection, role, binding)
|
|
455
|
+
connection.commit()
|
|
456
|
+
except DurableCMSSourceRuntimeBlocked:
|
|
457
|
+
if connection.in_transaction:
|
|
458
|
+
connection.rollback()
|
|
459
|
+
raise
|
|
460
|
+
except sqlite3.Error as error:
|
|
461
|
+
if connection.in_transaction:
|
|
462
|
+
connection.rollback()
|
|
463
|
+
raise _blocked(
|
|
464
|
+
"source_runtime.capability_database_invalid",
|
|
465
|
+
) from error
|
|
466
|
+
|
|
467
|
+
|
|
243
468
|
class DurableCMSSourceRuntime:
|
|
244
469
|
"""One process-owned and thread-safe source-CMS service runtime."""
|
|
245
470
|
|
|
@@ -249,10 +474,12 @@ class DurableCMSSourceRuntime:
|
|
|
249
474
|
guards: tuple[Callable[[], None], Callable[[], None], Callable[[], None]],
|
|
250
475
|
service: Any,
|
|
251
476
|
http_authenticator: Callable[[dict[str, Any]], Any] | None,
|
|
477
|
+
capability_binding: tuple[str, str] | None,
|
|
252
478
|
):
|
|
253
479
|
self._connections = connections
|
|
254
480
|
self._guards = guards
|
|
255
481
|
self._service = service
|
|
482
|
+
self._capability_binding = capability_binding
|
|
256
483
|
self._lock = threading.RLock()
|
|
257
484
|
self._closed = False
|
|
258
485
|
self._owner_pid = os.getpid()
|
|
@@ -288,11 +515,46 @@ class DurableCMSSourceRuntime:
|
|
|
288
515
|
for guard in self._guards:
|
|
289
516
|
guard()
|
|
290
517
|
|
|
518
|
+
def _guard_capability_binding(self) -> None:
|
|
519
|
+
if self._capability_binding is None:
|
|
520
|
+
return
|
|
521
|
+
expected_capabilities, expected_rendering = self._capability_binding
|
|
522
|
+
try:
|
|
523
|
+
current_capabilities = _sha256(getattr(
|
|
524
|
+
self._service.client,
|
|
525
|
+
"capabilities_sha256",
|
|
526
|
+
None,
|
|
527
|
+
))
|
|
528
|
+
current_rendering = _sha256(getattr(
|
|
529
|
+
self._service.client,
|
|
530
|
+
"commercial_rendering_registry_sha256",
|
|
531
|
+
None,
|
|
532
|
+
))
|
|
533
|
+
except Exception as error:
|
|
534
|
+
raise _blocked(
|
|
535
|
+
"source_runtime.capability_binding_changed",
|
|
536
|
+
) from error
|
|
537
|
+
if (
|
|
538
|
+
current_capabilities != expected_capabilities
|
|
539
|
+
or current_rendering != expected_rendering
|
|
540
|
+
):
|
|
541
|
+
raise _blocked("source_runtime.capability_binding_changed")
|
|
542
|
+
for role, connection in zip(
|
|
543
|
+
CAPABILITY_DATABASE_ROLES,
|
|
544
|
+
self._connections,
|
|
545
|
+
):
|
|
546
|
+
_validate_capability_database(
|
|
547
|
+
connection,
|
|
548
|
+
role,
|
|
549
|
+
self._capability_binding,
|
|
550
|
+
)
|
|
551
|
+
|
|
291
552
|
def _call(self, name: str, *args: Any, **kwargs: Any) -> Any:
|
|
292
553
|
self._assert_owner()
|
|
293
554
|
with self._lock:
|
|
294
555
|
if self._closed:
|
|
295
556
|
raise _blocked("source_runtime.closed")
|
|
557
|
+
self._guard_capability_binding()
|
|
296
558
|
self._guard_databases()
|
|
297
559
|
try:
|
|
298
560
|
result = getattr(self._service, name)(*args, **kwargs)
|
|
@@ -321,6 +583,31 @@ class DurableCMSSourceRuntime:
|
|
|
321
583
|
def health(self) -> Any:
|
|
322
584
|
return self._call("health")
|
|
323
585
|
|
|
586
|
+
def capability_binding(self) -> dict[str, Any]:
|
|
587
|
+
"""Return the content-free startup capability verification state."""
|
|
588
|
+
|
|
589
|
+
self._assert_owner()
|
|
590
|
+
with self._lock:
|
|
591
|
+
if self._closed:
|
|
592
|
+
raise _blocked("source_runtime.closed")
|
|
593
|
+
self._guard_capability_binding()
|
|
594
|
+
if self._capability_binding is None:
|
|
595
|
+
return {
|
|
596
|
+
"schema": "blun.cms-source-capability-binding.v2",
|
|
597
|
+
"status": "not_configured",
|
|
598
|
+
"capabilities_sha256": None,
|
|
599
|
+
"commercial_rendering_registry_sha256": None,
|
|
600
|
+
"database_roles": [],
|
|
601
|
+
}
|
|
602
|
+
capabilities_sha256, rendering_sha256 = self._capability_binding
|
|
603
|
+
return {
|
|
604
|
+
"schema": "blun.cms-source-capability-binding.v2",
|
|
605
|
+
"status": "verified",
|
|
606
|
+
"capabilities_sha256": capabilities_sha256,
|
|
607
|
+
"commercial_rendering_registry_sha256": rendering_sha256,
|
|
608
|
+
"database_roles": list(CAPABILITY_DATABASE_ROLES),
|
|
609
|
+
}
|
|
610
|
+
|
|
324
611
|
@staticmethod
|
|
325
612
|
def _loop_delays(
|
|
326
613
|
active_delay_seconds: float | int,
|
|
@@ -402,6 +689,7 @@ class DurableCMSSourceRuntime:
|
|
|
402
689
|
return
|
|
403
690
|
if self._worker_state in {"stopping", "failed"}:
|
|
404
691
|
raise _blocked("source_runtime.worker_blocked")
|
|
692
|
+
self._guard_capability_binding()
|
|
405
693
|
self._guard_databases()
|
|
406
694
|
stop = threading.Event()
|
|
407
695
|
thread = threading.Thread(
|
|
@@ -554,6 +842,7 @@ def open_durable_cms_source(
|
|
|
554
842
|
lifecycle_worker_id: str,
|
|
555
843
|
terminal_notifier: Callable[[Mapping[str, Any]], Any] | None = None,
|
|
556
844
|
terminal_status_reader: Callable[[str, str], Any] | None = None,
|
|
845
|
+
terminal_receiver_capabilities_sha256: str | None = None,
|
|
557
846
|
notification_worker_id: str | None = None,
|
|
558
847
|
http_authenticator: Callable[[dict[str, Any]], Any] | None = None,
|
|
559
848
|
clock: Callable[[], float | int] = time.time,
|
|
@@ -576,21 +865,33 @@ def open_durable_cms_source(
|
|
|
576
865
|
terminal_processing_poll_interval_seconds: float | int = 30,
|
|
577
866
|
terminal_processing_base_delay_seconds: float | int = 5,
|
|
578
867
|
terminal_processing_max_delay_seconds: float | int = 300,
|
|
868
|
+
capability_preflight: bool = False,
|
|
579
869
|
) -> DurableCMSSourceRuntime:
|
|
580
|
-
"""Validate and open the complete durable source-side CMS worker.
|
|
870
|
+
"""Validate and open the complete durable source-side CMS worker.
|
|
871
|
+
|
|
872
|
+
Set ``capability_preflight`` to require both client deployment pins and one
|
|
873
|
+
successful, exact capability read before any persistent database exists.
|
|
874
|
+
"""
|
|
581
875
|
|
|
582
876
|
paths = tuple(_database_path(value) for value in (
|
|
583
877
|
change_database, removal_database, lifecycle_database,
|
|
584
878
|
))
|
|
585
879
|
timeout = _sqlite_timeout(sqlite_timeout_seconds)
|
|
880
|
+
if not isinstance(capability_preflight, bool):
|
|
881
|
+
raise _blocked("source_runtime.capability_preflight_invalid")
|
|
586
882
|
if http_authenticator is not None and not callable(http_authenticator):
|
|
587
883
|
raise _blocked("source_runtime.http_authenticator_invalid")
|
|
884
|
+
if http_authenticator is not None and not capability_preflight:
|
|
885
|
+
raise _blocked("source_runtime.capability_preflight_required")
|
|
588
886
|
service_options = {
|
|
589
887
|
"change_worker_id": change_worker_id,
|
|
590
888
|
"removal_worker_id": removal_worker_id,
|
|
591
889
|
"lifecycle_worker_id": lifecycle_worker_id,
|
|
592
890
|
"terminal_notifier": terminal_notifier,
|
|
593
891
|
"terminal_status_reader": terminal_status_reader,
|
|
892
|
+
"terminal_receiver_capabilities_sha256": (
|
|
893
|
+
terminal_receiver_capabilities_sha256
|
|
894
|
+
),
|
|
594
895
|
"notification_worker_id": notification_worker_id,
|
|
595
896
|
"clock": clock,
|
|
596
897
|
"change_lease_seconds": change_lease_seconds,
|
|
@@ -620,6 +921,9 @@ def open_durable_cms_source(
|
|
|
620
921
|
}
|
|
621
922
|
_preflight_service(client, service_options)
|
|
622
923
|
_validate_paths(paths)
|
|
924
|
+
capability_binding = (
|
|
925
|
+
_preflight_capabilities(client) if capability_preflight else None
|
|
926
|
+
)
|
|
623
927
|
guards = tuple(_prepare_database_file(path) for path in paths)
|
|
624
928
|
connections: list[sqlite3.Connection] = []
|
|
625
929
|
try:
|
|
@@ -632,11 +936,21 @@ def open_durable_cms_source(
|
|
|
632
936
|
))
|
|
633
937
|
for guard in guards:
|
|
634
938
|
guard()
|
|
939
|
+
if capability_binding is not None:
|
|
940
|
+
_precheck_capability_databases(
|
|
941
|
+
tuple(connections),
|
|
942
|
+
capability_binding,
|
|
943
|
+
)
|
|
635
944
|
service = _SERVICE.CMSLocalizationSourceService(
|
|
636
945
|
*connections,
|
|
637
946
|
client,
|
|
638
947
|
**service_options,
|
|
639
948
|
)
|
|
949
|
+
if capability_binding is not None:
|
|
950
|
+
_bind_capability_databases(
|
|
951
|
+
tuple(connections),
|
|
952
|
+
capability_binding,
|
|
953
|
+
)
|
|
640
954
|
for guard in guards:
|
|
641
955
|
guard()
|
|
642
956
|
except Exception as error:
|
|
@@ -650,6 +964,7 @@ def open_durable_cms_source(
|
|
|
650
964
|
raise _blocked("source_runtime.initialization_failed") from error
|
|
651
965
|
return DurableCMSSourceRuntime(
|
|
652
966
|
tuple(connections), guards, service, http_authenticator,
|
|
967
|
+
capability_binding,
|
|
653
968
|
)
|
|
654
969
|
|
|
655
970
|
|
package/standard-skills/translate-native/integrations/website_localization_cms_source_service.py
CHANGED
|
@@ -23,8 +23,8 @@ from typing import Any, Callable, Mapping
|
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
SCHEMA = "blun.cms-source-service-tick.v1"
|
|
26
|
-
HEALTH_SCHEMA = "blun.cms-source-service-health.
|
|
27
|
-
STATUS_SCHEMA = "blun.cms-source-service-status.
|
|
26
|
+
HEALTH_SCHEMA = "blun.cms-source-service-health.v4"
|
|
27
|
+
STATUS_SCHEMA = "blun.cms-source-service-status.v4"
|
|
28
28
|
TOKEN = re.compile(r"^[A-Za-z0-9_.:-]{1,256}$")
|
|
29
29
|
ERROR_CODE = re.compile(r"^[a-z][a-z0-9_.-]{0,127}$")
|
|
30
30
|
|
|
@@ -96,6 +96,7 @@ class CMSSourceServiceHealth:
|
|
|
96
96
|
lifecycle: Mapping[str, Any]
|
|
97
97
|
notifications: Mapping[str, Any]
|
|
98
98
|
terminal_processing: Mapping[str, Any]
|
|
99
|
+
terminal_receiver_capabilities_sha256: str | None
|
|
99
100
|
error_code: str | None = None
|
|
100
101
|
|
|
101
102
|
def as_payload(self) -> dict[str, Any]:
|
|
@@ -114,6 +115,9 @@ class CMSSourceServiceHealth:
|
|
|
114
115
|
"lifecycle": dict(self.lifecycle),
|
|
115
116
|
"notifications": dict(self.notifications),
|
|
116
117
|
"terminal_processing": dict(self.terminal_processing),
|
|
118
|
+
"terminal_receiver_capabilities_sha256": (
|
|
119
|
+
self.terminal_receiver_capabilities_sha256
|
|
120
|
+
),
|
|
117
121
|
"error_code": self.error_code,
|
|
118
122
|
}
|
|
119
123
|
|
|
@@ -147,6 +151,7 @@ class CMSSourceServiceStatus:
|
|
|
147
151
|
notification_attempts: int
|
|
148
152
|
notification_max_attempts: int | None
|
|
149
153
|
notification_error_code: str | None
|
|
154
|
+
terminal_receiver_capabilities_sha256: str | None
|
|
150
155
|
terminal_processing_state: str
|
|
151
156
|
terminal_processing_poll_attempts: int
|
|
152
157
|
terminal_processing_failures: int
|
|
@@ -186,6 +191,9 @@ class CMSSourceServiceStatus:
|
|
|
186
191
|
"notification_attempts": self.notification_attempts,
|
|
187
192
|
"notification_max_attempts": self.notification_max_attempts,
|
|
188
193
|
"notification_error_code": self.notification_error_code,
|
|
194
|
+
"terminal_receiver_capabilities_sha256": (
|
|
195
|
+
self.terminal_receiver_capabilities_sha256
|
|
196
|
+
),
|
|
189
197
|
"terminal_processing_state": self.terminal_processing_state,
|
|
190
198
|
"terminal_processing_poll_attempts": (
|
|
191
199
|
self.terminal_processing_poll_attempts
|
|
@@ -263,6 +271,7 @@ class CMSLocalizationSourceService:
|
|
|
263
271
|
lifecycle_worker_id: str,
|
|
264
272
|
terminal_notifier: Callable[[Mapping[str, Any]], Any] | None = None,
|
|
265
273
|
terminal_status_reader: Callable[[str, str], Any] | None = None,
|
|
274
|
+
terminal_receiver_capabilities_sha256: str | None = None,
|
|
266
275
|
notification_worker_id: str | None = None,
|
|
267
276
|
clock: Callable[[], float | int] = time.time,
|
|
268
277
|
change_lease_seconds: float | int = 600,
|
|
@@ -305,6 +314,33 @@ class CMSLocalizationSourceService:
|
|
|
305
314
|
terminal_notifier is None or not callable(terminal_status_reader)
|
|
306
315
|
):
|
|
307
316
|
raise CMSSourceServiceBlocked("source_service.processing_monitor_invalid")
|
|
317
|
+
discovered_receiver_capabilities = None
|
|
318
|
+
if terminal_status_reader is not None:
|
|
319
|
+
try:
|
|
320
|
+
discovered_receiver_capabilities = getattr(
|
|
321
|
+
terminal_notifier, "expected_capabilities_sha256", None,
|
|
322
|
+
)
|
|
323
|
+
except Exception as error:
|
|
324
|
+
raise CMSSourceServiceBlocked(
|
|
325
|
+
"source_service.processing_capabilities_invalid"
|
|
326
|
+
) from error
|
|
327
|
+
if terminal_receiver_capabilities_sha256 is None:
|
|
328
|
+
terminal_receiver_capabilities_sha256 = (
|
|
329
|
+
discovered_receiver_capabilities
|
|
330
|
+
)
|
|
331
|
+
if (
|
|
332
|
+
not _PROCESSING._sha(terminal_receiver_capabilities_sha256)
|
|
333
|
+
or discovered_receiver_capabilities is not None
|
|
334
|
+
and discovered_receiver_capabilities
|
|
335
|
+
!= terminal_receiver_capabilities_sha256
|
|
336
|
+
):
|
|
337
|
+
raise CMSSourceServiceBlocked(
|
|
338
|
+
"source_service.processing_capabilities_invalid"
|
|
339
|
+
)
|
|
340
|
+
elif terminal_receiver_capabilities_sha256 is not None:
|
|
341
|
+
raise CMSSourceServiceBlocked(
|
|
342
|
+
"source_service.processing_capabilities_invalid"
|
|
343
|
+
)
|
|
308
344
|
if not callable(clock):
|
|
309
345
|
raise CMSSourceServiceBlocked("source_service.clock_invalid")
|
|
310
346
|
timeout = getattr(client, "timeout", None)
|
|
@@ -379,6 +415,13 @@ class CMSLocalizationSourceService:
|
|
|
379
415
|
self.client = client
|
|
380
416
|
self.terminal_notifier = terminal_notifier
|
|
381
417
|
self.terminal_status_reader = terminal_status_reader
|
|
418
|
+
self.terminal_receiver_capabilities_sha256 = (
|
|
419
|
+
terminal_receiver_capabilities_sha256
|
|
420
|
+
)
|
|
421
|
+
self.terminal_receiver_capabilities_exposed = (
|
|
422
|
+
terminal_status_reader is not None
|
|
423
|
+
and discovered_receiver_capabilities is not None
|
|
424
|
+
)
|
|
382
425
|
|
|
383
426
|
# All configuration is validated before the first schema write.
|
|
384
427
|
for value, code in (
|
|
@@ -436,6 +479,7 @@ class CMSLocalizationSourceService:
|
|
|
436
479
|
if terminal_status_reader is None
|
|
437
480
|
else _PROCESSING.DurableTerminalProcessingMonitor(
|
|
438
481
|
lifecycle_connection,
|
|
482
|
+
terminal_receiver_capabilities_sha256,
|
|
439
483
|
poll_interval_seconds=terminal_processing_poll_interval_seconds,
|
|
440
484
|
base_delay_seconds=terminal_processing_base_delay_seconds,
|
|
441
485
|
max_delay_seconds=terminal_processing_max_delay_seconds,
|
|
@@ -459,6 +503,24 @@ class CMSLocalizationSourceService:
|
|
|
459
503
|
raise CMSSourceServiceBlocked("source_service.clock_invalid")
|
|
460
504
|
return float(value)
|
|
461
505
|
|
|
506
|
+
def _guard_terminal_receiver_capabilities(self) -> None:
|
|
507
|
+
if self.terminal_processing is None:
|
|
508
|
+
return
|
|
509
|
+
try:
|
|
510
|
+
current = getattr(
|
|
511
|
+
self.terminal_notifier, "expected_capabilities_sha256", None,
|
|
512
|
+
)
|
|
513
|
+
except Exception as error:
|
|
514
|
+
raise CMSSourceServiceBlocked(
|
|
515
|
+
"source_service.processing_capabilities_changed"
|
|
516
|
+
) from error
|
|
517
|
+
if self.terminal_receiver_capabilities_exposed and (
|
|
518
|
+
current != self.terminal_receiver_capabilities_sha256
|
|
519
|
+
):
|
|
520
|
+
raise CMSSourceServiceBlocked(
|
|
521
|
+
"source_service.processing_capabilities_changed"
|
|
522
|
+
)
|
|
523
|
+
|
|
462
524
|
def enqueue_change(
|
|
463
525
|
self,
|
|
464
526
|
change: Mapping[str, Any],
|
|
@@ -481,6 +543,7 @@ class CMSLocalizationSourceService:
|
|
|
481
543
|
|
|
482
544
|
def status(self, event_id: str, site_id: str) -> CMSSourceServiceStatus:
|
|
483
545
|
"""Return a content-free snapshot without repairing or leasing work."""
|
|
546
|
+
self._guard_terminal_receiver_capabilities()
|
|
484
547
|
event_id = _identifier(event_id, "source_service.status_invalid")
|
|
485
548
|
site_id = _identifier(site_id, "source_service.status_invalid")
|
|
486
549
|
now = self._now()
|
|
@@ -629,6 +692,9 @@ class CMSLocalizationSourceService:
|
|
|
629
692
|
notification_error_code=(
|
|
630
693
|
None if notification is None else notification.last_error_code
|
|
631
694
|
),
|
|
695
|
+
terminal_receiver_capabilities_sha256=(
|
|
696
|
+
self.terminal_receiver_capabilities_sha256
|
|
697
|
+
),
|
|
632
698
|
terminal_processing_state=terminal_processing_state,
|
|
633
699
|
terminal_processing_poll_attempts=(
|
|
634
700
|
0 if terminal_processing is None
|
|
@@ -820,6 +886,7 @@ class CMSLocalizationSourceService:
|
|
|
820
886
|
def run_once(self) -> CMSSourceTickOutcome:
|
|
821
887
|
"""Advance at most one external operation and one local handoff."""
|
|
822
888
|
try:
|
|
889
|
+
self._guard_terminal_receiver_capabilities()
|
|
823
890
|
now = self._now()
|
|
824
891
|
except Exception as error:
|
|
825
892
|
return self._outcome(
|
|
@@ -1037,6 +1104,7 @@ class CMSLocalizationSourceService:
|
|
|
1037
1104
|
|
|
1038
1105
|
def health(self) -> CMSSourceServiceHealth:
|
|
1039
1106
|
try:
|
|
1107
|
+
self._guard_terminal_receiver_capabilities()
|
|
1040
1108
|
now = self._now()
|
|
1041
1109
|
changes = _health_payload(self.changes.health(now=now))
|
|
1042
1110
|
removals = _health_payload(self.removals.health(now=now))
|
|
@@ -1080,6 +1148,7 @@ class CMSLocalizationSourceService:
|
|
|
1080
1148
|
{},
|
|
1081
1149
|
{},
|
|
1082
1150
|
{},
|
|
1151
|
+
self.terminal_receiver_capabilities_sha256,
|
|
1083
1152
|
_safe_code(error, "source_service.health_blocked"),
|
|
1084
1153
|
)
|
|
1085
1154
|
blocked = any(
|
|
@@ -1121,5 +1190,6 @@ class CMSLocalizationSourceService:
|
|
|
1121
1190
|
lifecycle,
|
|
1122
1191
|
notifications,
|
|
1123
1192
|
terminal_processing,
|
|
1193
|
+
self.terminal_receiver_capabilities_sha256,
|
|
1124
1194
|
error_code,
|
|
1125
1195
|
)
|
|
@@ -18,6 +18,7 @@ from typing import Any, Callable, Mapping, Protocol
|
|
|
18
18
|
NOTIFICATION_SCHEMA = "blun.cms-source-terminal-notification.v1"
|
|
19
19
|
ACK_SCHEMA = "blun.cms-source-terminal-notification-ack.v1"
|
|
20
20
|
AUTH_SCHEMA = "blun.cms-source-terminal-notification-http-auth.v1"
|
|
21
|
+
CAPABILITIES_PRECONDITION_HEADER = "X-Localization-Capabilities-SHA256"
|
|
21
22
|
MAX_BODY_BYTES = 16_384
|
|
22
23
|
MAX_RESPONSE_BYTES = 8_192
|
|
23
24
|
MAX_ENDPOINT_LENGTH = 2_048
|
|
@@ -35,6 +36,7 @@ RESERVED_HEADERS = {
|
|
|
35
36
|
"idempotency-key", "transfer-encoding",
|
|
36
37
|
"x-localization-terminal-notification-id",
|
|
37
38
|
"x-localization-terminal-notification-sha256",
|
|
39
|
+
"x-localization-capabilities-sha256",
|
|
38
40
|
}
|
|
39
41
|
NOTIFICATION_FIELDS = {
|
|
40
42
|
"schema", "notification_id", "event_id", "site_id", "plan_id",
|
|
@@ -326,6 +328,7 @@ class HTTPTerminalNotifierAdapter:
|
|
|
326
328
|
def __init__(
|
|
327
329
|
self,
|
|
328
330
|
endpoint: str,
|
|
331
|
+
expected_capabilities_sha256: str,
|
|
329
332
|
authentication_headers: Callable[
|
|
330
333
|
[Mapping[str, Any]], Mapping[str, str]
|
|
331
334
|
],
|
|
@@ -339,6 +342,11 @@ class HTTPTerminalNotifierAdapter:
|
|
|
339
342
|
self.endpoint, self.origin, self.path = _endpoint(
|
|
340
343
|
endpoint, allow_loopback_http,
|
|
341
344
|
)
|
|
345
|
+
if (
|
|
346
|
+
not isinstance(expected_capabilities_sha256, str)
|
|
347
|
+
or SHA256.fullmatch(expected_capabilities_sha256) is None
|
|
348
|
+
):
|
|
349
|
+
raise ValueError("expected capability digest is invalid")
|
|
342
350
|
if not callable(authentication_headers):
|
|
343
351
|
raise TypeError("authentication_headers must be callable")
|
|
344
352
|
if (
|
|
@@ -349,6 +357,7 @@ class HTTPTerminalNotifierAdapter:
|
|
|
349
357
|
):
|
|
350
358
|
raise ValueError("timeout is outside the supported range")
|
|
351
359
|
self.authentication_headers = authentication_headers
|
|
360
|
+
self.expected_capabilities_sha256 = expected_capabilities_sha256
|
|
352
361
|
self.transport = URLTransport() if transport is None else transport
|
|
353
362
|
if not callable(getattr(self.transport, "post", None)):
|
|
354
363
|
raise TypeError("transport must provide post")
|
|
@@ -368,6 +377,7 @@ class HTTPTerminalNotifierAdapter:
|
|
|
368
377
|
"event_id": payload["event_id"],
|
|
369
378
|
"site_id": payload["site_id"],
|
|
370
379
|
"body_sha256": body_sha256,
|
|
380
|
+
"capabilities_sha256": self.expected_capabilities_sha256,
|
|
371
381
|
}
|
|
372
382
|
headers = _authentication_headers(
|
|
373
383
|
self.authentication_headers, authentication,
|
|
@@ -378,6 +388,9 @@ class HTTPTerminalNotifierAdapter:
|
|
|
378
388
|
"Idempotency-Key": payload["notification_id"],
|
|
379
389
|
"X-Localization-Terminal-Notification-Id": payload["notification_id"],
|
|
380
390
|
"X-Localization-Terminal-Notification-Sha256": body_sha256,
|
|
391
|
+
CAPABILITIES_PRECONDITION_HEADER: (
|
|
392
|
+
self.expected_capabilities_sha256
|
|
393
|
+
),
|
|
381
394
|
})
|
|
382
395
|
try:
|
|
383
396
|
result = self.transport.post(
|