|
1 | 1 | from __future__ import annotations |
2 | 2 |
|
3 | | -from collections.abc import Iterable, Mapping, Sequence |
4 | | -from contextlib import nullcontext |
5 | | -from copy import deepcopy |
6 | 3 | import hashlib |
7 | 4 | import json |
8 | 5 | import os |
| 6 | +from collections.abc import Iterable, Mapping, Sequence |
| 7 | +from contextlib import nullcontext |
| 8 | +from copy import deepcopy |
9 | 9 | from pathlib import Path |
10 | 10 | from typing import Any |
11 | 11 |
|
12 | 12 | from ..file_lock import exclusive_file_lock |
13 | | -from .process_runtime import run_capped_process |
14 | 13 | from .manifest import load_extension_manifest |
| 14 | +from .process_runtime import run_capped_process |
15 | 15 | from .readiness import ( |
16 | 16 | EXTENSION_DOCTOR_SCHEMA_VERSION, |
17 | 17 | ResolvedRuntimeEntrypoint, |
|
20 | 20 | resolve_runtime_entrypoint, |
21 | 21 | ) |
22 | 22 |
|
23 | | - |
24 | 23 | EXTENSION_STATE_SCHEMA_VERSION = "loopx_extension_state_v0" |
25 | 24 | EXTENSION_OPERATION_SCHEMA_VERSION = "loopx_extension_operation_v0" |
26 | 25 | EXTENSION_BINDING_SCHEMA_VERSION = "loopx_extension_runtime_binding_v0" |
27 | 26 | EXTENSION_ACTIVATION_SCHEMA_VERSION = "loopx_extension_activation_v0" |
28 | 27 | EXTENSION_RUN_SCHEMA_VERSION = "loopx_extension_run_receipt_v0" |
| 28 | +EXTENSION_DOCTOR_BATCH_SCHEMA_VERSION = "loopx_extension_doctor_batch_v0" |
29 | 29 | MAX_REVISIONS = 5 |
30 | 30 | MAX_EXTENSION_REQUEST_BYTES = 1_000_000 |
31 | 31 | MAX_EXTENSION_RESPONSE_BYTES = 1_000_000 |
@@ -443,6 +443,57 @@ def doctor_installed_extension( |
443 | 443 | return doctor |
444 | 444 |
|
445 | 445 |
|
| 446 | +def doctor_enabled_extensions( |
| 447 | + *, |
| 448 | + state_file: str | Path, |
| 449 | + execute: bool = False, |
| 450 | +) -> dict[str, Any]: |
| 451 | + """Revalidate every enabled extension after a runtime release transition. |
| 452 | +
|
| 453 | + Extension doctors are declared read-only provider probes. Running them as |
| 454 | + one bounded batch lets installers and rollback paths refresh the runtime |
| 455 | + identity without weakening the fail-closed activation fence. |
| 456 | + """ |
| 457 | + |
| 458 | + path = Path(state_file).expanduser() |
| 459 | + state = _read_state(path) |
| 460 | + extension_ids = sorted( |
| 461 | + str(extension_id) |
| 462 | + for extension_id, entry in state["extensions"].items() |
| 463 | + if isinstance(entry, Mapping) and entry.get("enabled") |
| 464 | + ) |
| 465 | + doctors = [ |
| 466 | + doctor_installed_extension( |
| 467 | + extension_id, |
| 468 | + state_file=path, |
| 469 | + execute=execute, |
| 470 | + ) |
| 471 | + for extension_id in extension_ids |
| 472 | + ] |
| 473 | + verified_count = sum(bool(doctor.get("verified")) for doctor in doctors) |
| 474 | + blocked_count = len(doctors) - verified_count if execute else 0 |
| 475 | + return { |
| 476 | + "ok": blocked_count == 0, |
| 477 | + "schema_version": EXTENSION_DOCTOR_BATCH_SCHEMA_VERSION, |
| 478 | + "status": ( |
| 479 | + "ready" |
| 480 | + if execute and blocked_count == 0 |
| 481 | + else "blocked" |
| 482 | + if execute |
| 483 | + else "probe_required" |
| 484 | + if doctors |
| 485 | + else "ready" |
| 486 | + ), |
| 487 | + "execute": execute, |
| 488 | + "enabled_count": len(doctors), |
| 489 | + "verified_count": verified_count, |
| 490 | + "blocked_count": blocked_count, |
| 491 | + "extensions": doctors, |
| 492 | + "local_state_write_performed": execute and bool(doctors), |
| 493 | + "external_writes_performed": False, |
| 494 | + } |
| 495 | + |
| 496 | + |
446 | 497 | def _verified_entrypoint( |
447 | 498 | entry: Mapping[str, Any], |
448 | 499 | ) -> ResolvedRuntimeEntrypoint | None: |
@@ -577,7 +628,10 @@ def _resolved_active_extension( |
577 | 628 | active_revision = str(entry.get("active_revision") or "") |
578 | 629 | verified_entrypoint = _verified_entrypoint(entry) |
579 | 630 | if verified_entrypoint is None: |
580 | | - raise ValueError(f"extension `{extension_id}` doctor readiness is stale") |
| 631 | + raise ValueError( |
| 632 | + f"extension `{extension_id}` doctor readiness is stale; run " |
| 633 | + f"`loopx extension doctor {extension_id} --execute`" |
| 634 | + ) |
581 | 635 | manifest = _active_manifest(entry) |
582 | 636 | return active_revision, verified_entrypoint, manifest |
583 | 637 |
|
|
0 commit comments