← Files VeraARCHIVED FILE
modules/browser-automation/scripts/check_installation.py
4.56 KB · Oct 2, 2026 · 00:29 UTC
#!/usr/bin/env python3
"""Resolve and validate the Browser Automation installation under test."""
from __future__ import annotations
import argparse
import json
import logging
import re
import sys
from pathlib import Path
from typing import Any
__all__ = ["inspect_installation", "main", "resolve_plugin_manifest"]
LOGGER = logging.getLogger(__name__)
SUPPORTED_PLUGIN_NAMES = {"browser-automation", "vera"}
VERSION_PATTERN = re.compile(r"^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$")
REQUIRED_COMPONENT_PATHS = (
"requirements.txt",
"scripts/acceptance_fixture.py",
"scripts/check_dependencies.py",
"scripts/capability_pipeline.py",
"scripts/capability_runtime.mjs",
"scripts/browser_session.mjs",
"scripts/agenzia_download.mjs",
"scripts/agenzia_acquisition.mjs",
"scripts/agenzia_artifacts.mjs",
"references/browser-session.md",
"references/agenzia-download.md",
"references/agenzia-acquisition.md",
"scripts/download_directory.mjs",
"scripts/discovery_pack.py",
"scripts/discovery_runtime.mjs",
"scripts/teaching_checkpoint.py",
"scripts/batch_review.py",
"scripts/econs_review.mjs",
"scripts/econs_setup.mjs",
"references/econs-review.md",
"scripts/development_request.py",
"scripts/process_lifecycle.py",
"scripts/process_runtime.mjs",
"scripts/process_skills.py",
"references/production-skills.md",
"references/process-lifecycle.md",
"references/ordinary-use.md",
"references/development-request.md",
"references/batch-review.md",
"references/teaching-checkpoint.md",
"skills/browser-automation/SKILL.md",
)
def resolve_plugin_manifest(component_root: Path) -> Path:
"""Return the manifest owning this exact component without scanning caches."""
root = component_root.resolve()
candidates = (
root.parent.parent / ".codex-plugin" / "plugin.json",
root / ".codex-plugin" / "plugin.json",
)
for candidate in candidates:
if candidate.is_file():
return candidate
raise ValueError("No owning .codex-plugin/plugin.json found for this component.")
def inspect_installation(component_root: Path) -> dict[str, Any]:
"""Report compatibility from the active manifest and required local files.
This check is deterministic because manifest parsing and required-file
presence are mechanically verifiable. It deliberately does not compare the
observed version with a historical release number.
"""
root = component_root.resolve()
manifest_path = resolve_plugin_manifest(root)
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
name = manifest.get("name")
version = manifest.get("version")
if name not in SUPPORTED_PLUGIN_NAMES:
raise ValueError(f"Unsupported owning plugin name: {name!r}.")
if not isinstance(version, str) or VERSION_PATTERN.fullmatch(version) is None:
raise ValueError("Owning plugin manifest has an invalid version.")
missing = [
relative_path
for relative_path in REQUIRED_COMPONENT_PATHS
if not (root / relative_path).is_file()
]
if missing:
raise ValueError(
"Browser Automation installation is incomplete: " + ", ".join(missing)
)
return {
"schema_version": "browser-automation-installation/v1",
"status": "compatible",
"plugin": {
"name": name,
"version": version,
"manifest_path": str(manifest_path),
},
"component_root": str(root),
"version_source": "active_plugin_manifest",
"compatibility_basis": [
"manifest_shape",
"supported_plugin_name",
"required_component_files",
],
}
def main(argv: list[str] | None = None) -> int:
"""Print one structured preflight result for the installation being run."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--component-root",
type=Path,
default=Path(__file__).resolve().parents[1],
help="Browser Automation module root; defaults to this script's module.",
)
args = parser.parse_args(argv)
try:
result = inspect_installation(args.component_root)
except (OSError, ValueError, json.JSONDecodeError) as error:
LOGGER.error("Browser Automation installation preflight failed: %s", error)
return 1
sys.stdout.write(json.dumps(result, ensure_ascii=False, sort_keys=True) + "\n")
return 0
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO, format="%(message)s")
raise SystemExit(main())
SHA-256: 5692e46244d6999f860fdb1aa3907d1a7a95dc6363038d07176acaee9ad63ded