#!/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())