Files
svrnty-vision/tools/validate_vision_host_adapter_candidates.py
T
2026-06-06 08:25:14 -04:00

120 lines
4.3 KiB
Python

#!/usr/bin/env python3
"""Validate VISION host adapter candidate parity."""
from __future__ import annotations
import json
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
HOSTS = {
"claude-code": "candidate-manifests/host-adapters/vision-claude-code-adapter.json",
"codex-cli": "candidate-manifests/host-adapters/vision-codex-cli-adapter.json",
"pi-code": "candidate-manifests/host-adapters/vision-pi-code-adapter.json",
}
CURRENT_TOOLS = [
"vision.image_analyze",
"vision.image_generate",
"vision.palette_extract",
"vision.background_cutout",
]
PLANNED_TOOLS = [
"vision.ocr_read",
"vision.screenshot_observe",
"vision.browser_observe",
"vision.document_layout_read",
"vision.chart_read",
"vision.table_read",
"vision.diagram_read",
"vision.object_detect",
"vision.visual_ground",
"vision.segment",
"vision.video_read",
"vision.image_edit",
]
def main() -> int:
errors: list[str] = []
checked = list(HOSTS.values()) + ["docs/VISION-HOST-ADAPTER-CANDIDATES.md"]
manifests: dict[str, dict] = {}
for host, rel in HOSTS.items():
manifest = _read_json(ROOT / rel, errors)
if manifest:
manifests[host] = manifest
for host, manifest in manifests.items():
_expect(manifest.get("host_runtime") == host, f"host_runtime_mismatch:{host}", errors)
_expect(manifest.get("package_id") == "visual-perception-package-candidate", f"package_mismatch:{host}", errors)
_expect(manifest.get("adapter_role") == "thin_access_adapter", f"adapter_role_mismatch:{host}", errors)
_expect(manifest.get("wildcard_grant_allowed") is False, f"wildcard_grant_allowed:{host}", errors)
_expect(manifest.get("current_tool_candidates") == CURRENT_TOOLS, f"current_tools_mismatch:{host}", errors)
_expect(manifest.get("planned_tool_candidates") == PLANNED_TOOLS, f"planned_tools_mismatch:{host}", errors)
disclosures = manifest.get("disclosure_contract", {})
for key in ("provider_mode_required", "retention_required", "visual_evidence_required"):
if disclosures.get(key) is not True:
errors.append(f"disclosure_missing:{host}:{key}")
forbidden = set(manifest.get("forbidden_effects", []))
for effect in ("research_synthesis", "textual_web_search", "runtime_start", "wildcard_tool_exposure"):
if effect not in forbidden:
errors.append(f"forbidden_effect_missing:{host}:{effect}")
for key, value in manifest.get("non_authorization", {}).items():
if value is not False:
errors.append(f"non_authorization_enabled:{host}:{key}")
if len(manifests) == len(HOSTS):
current_sets = {tuple(m["current_tool_candidates"]) for m in manifests.values()}
planned_sets = {tuple(m["planned_tool_candidates"]) for m in manifests.values()}
_expect(len(current_sets) == 1, "current_tool_parity_failed", errors)
_expect(len(planned_sets) == 1, "planned_tool_parity_failed", errors)
_check_snippets(
"docs/VISION-HOST-ADAPTER-CANDIDATES.md",
["Claude Code", "Codex CLI", "Pi-Code", "no wildcard grant"],
errors,
)
result = {
"ok": not errors,
"validator": "vision-host-adapter-candidates-v1",
"checked": checked,
"errors": errors,
"warnings": [],
}
print(json.dumps(result, indent=2, sort_keys=True))
return 0 if result["ok"] else 1
def _read_json(path: Path, errors: list[str]) -> dict | None:
if not path.exists():
errors.append(f"missing:{path.relative_to(ROOT)}")
return None
try:
return json.loads(path.read_text(encoding="utf-8"))
except json.JSONDecodeError as exc:
errors.append(f"{path.relative_to(ROOT)}:json:{exc}")
return None
def _check_snippets(rel: str, snippets: list[str], errors: list[str]) -> None:
path = ROOT / rel
if not path.exists():
errors.append(f"missing:{rel}")
return
text = path.read_text(encoding="utf-8")
for snippet in snippets:
if snippet not in text:
errors.append(f"{rel}:missing:{snippet}")
def _expect(condition: bool, error: str, errors: list[str]) -> None:
if not condition:
errors.append(error)
if __name__ == "__main__":
raise SystemExit(main())