- tomllib fallback: try tomllib (3.11+) -> tomli -> minimal parser - Works with Python 3.8+ (Ubuntu 20.04, Debian 11, etc.) - Auto-install python3 if not found (like Gemini/Qwen scripts) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
295 lines
8.8 KiB
Python
Executable File
295 lines
8.8 KiB
Python
Executable File
"""Config validator for Codex Patcher — validates 6 config targets.
|
|
|
|
Unlike Claude/Gemini patchers (regex-based), Codex validation is state-based:
|
|
checks config.toml values and environment variables.
|
|
"""
|
|
|
|
import os
|
|
try:
|
|
import tomllib
|
|
except ModuleNotFoundError:
|
|
try:
|
|
import tomli as tomllib
|
|
except ModuleNotFoundError:
|
|
import re as _re
|
|
class _T:
|
|
@staticmethod
|
|
def load(f):
|
|
raw = f.read()
|
|
return _T._parse(raw.decode("utf-8") if isinstance(raw, bytes) else raw)
|
|
@staticmethod
|
|
def _parse(text):
|
|
result, cur = {}, None
|
|
for line in text.split("\n"):
|
|
line = line.strip()
|
|
if not line or line.startswith("#"):
|
|
continue
|
|
m = _re.match(r'^\[([^\]]+)\]$', line)
|
|
if m:
|
|
keys = [k.strip() for k in m.group(1).split(".")]
|
|
cur = result
|
|
for k in keys:
|
|
cur = cur.setdefault(k, {})
|
|
continue
|
|
m = _re.match(r'^([^=]+?)\s*=\s*(.+)$', line)
|
|
if m:
|
|
k, v = m.group(1).strip(), m.group(2).strip()
|
|
if v.startswith('"') and v.endswith('"'): v = v[1:-1]
|
|
elif v == "true": v = True
|
|
elif v == "false": v = False
|
|
elif _re.match(r'^-?\d+$', v): v = int(v)
|
|
(cur if cur is not None else result)[k] = v
|
|
return result
|
|
tomllib = _T()
|
|
from dataclasses import dataclass
|
|
from typing import Callable, Optional
|
|
|
|
|
|
@dataclass
|
|
class ConfigTarget:
|
|
name: str
|
|
description: str
|
|
check_key: str # "config_toml" | "env" | "auth"
|
|
|
|
|
|
PATCH_TARGETS = [
|
|
ConfigTarget(
|
|
name="api_endpoint",
|
|
description="Custom proxy via model_providers",
|
|
check_key="config_toml",
|
|
),
|
|
ConfigTarget(
|
|
name="authentication",
|
|
description="API key auth configured",
|
|
check_key="auth",
|
|
),
|
|
ConfigTarget(
|
|
name="analytics_disabled",
|
|
description="Analytics/telemetry disabled",
|
|
check_key="config_toml",
|
|
),
|
|
ConfigTarget(
|
|
name="approval_bypass",
|
|
description="Approval policy set to never",
|
|
check_key="config_toml",
|
|
),
|
|
ConfigTarget(
|
|
name="sandbox_bypass",
|
|
description="Sandbox set to danger-full-access",
|
|
check_key="config_toml",
|
|
),
|
|
ConfigTarget(
|
|
name="env_vars",
|
|
description="System environment variables configured",
|
|
check_key="env",
|
|
),
|
|
]
|
|
|
|
|
|
def _read_toml(path):
|
|
"""Read TOML file, return dict or empty dict."""
|
|
if not os.path.isfile(path):
|
|
return None
|
|
with open(path, "rb") as f:
|
|
return tomllib.load(f)
|
|
|
|
|
|
def _check_api_endpoint(toml_data, config):
|
|
"""Check Target 1: model_providers.custom with correct base_url."""
|
|
if toml_data is None:
|
|
return "RED", "config.toml not found"
|
|
|
|
mp = toml_data.get("model_providers", {})
|
|
custom = mp.get("custom", {}) if isinstance(mp, dict) else {}
|
|
|
|
base_url = config["base_url"].rstrip("/")
|
|
if not base_url.endswith("/v1"):
|
|
base_url += "/v1"
|
|
|
|
if not custom:
|
|
return "RED", "model_providers.custom section missing"
|
|
|
|
if custom.get("base_url") != base_url:
|
|
return "YELLOW", f"base_url mismatch: {custom.get('base_url')} != {base_url}"
|
|
|
|
if toml_data.get("model_provider") != "custom":
|
|
return "YELLOW", "model_provider != 'custom'"
|
|
|
|
return "GREEN", f"base_url={base_url}"
|
|
|
|
|
|
def _check_auth(codex_dir, config):
|
|
"""Check Target 2: API key authentication."""
|
|
# Check env var
|
|
env_key = os.environ.get("OPENAI_API_KEY", "")
|
|
if env_key == config["api_key"]:
|
|
return "GREEN", "OPENAI_API_KEY set correctly"
|
|
|
|
# Check /etc/environment
|
|
etc_env = "/etc/environment"
|
|
if os.path.isfile(etc_env):
|
|
with open(etc_env) as f:
|
|
content = f.read()
|
|
if config["api_key"] in content:
|
|
return "GREEN", "API key in /etc/environment"
|
|
|
|
if env_key:
|
|
return "YELLOW", "OPENAI_API_KEY set but different value"
|
|
|
|
return "RED", "OPENAI_API_KEY not set"
|
|
|
|
|
|
def _check_analytics(toml_data):
|
|
"""Check Target 3: analytics disabled."""
|
|
if toml_data is None:
|
|
return "RED", "config.toml not found"
|
|
|
|
analytics = toml_data.get("analytics", {})
|
|
if not isinstance(analytics, dict):
|
|
return "RED", "[analytics] section missing"
|
|
|
|
if analytics.get("enabled") is False:
|
|
return "GREEN", "analytics.enabled = false"
|
|
|
|
if "enabled" not in analytics:
|
|
return "YELLOW", "[analytics] exists but 'enabled' key missing"
|
|
|
|
return "YELLOW", f"analytics.enabled = {analytics.get('enabled')}"
|
|
|
|
|
|
def _check_approval(toml_data, config):
|
|
"""Check Target 4: approval_policy."""
|
|
if toml_data is None:
|
|
return "RED", "config.toml not found"
|
|
|
|
target_policy = config.get("approval_policy", "never")
|
|
current = toml_data.get("approval_policy")
|
|
|
|
if current == target_policy:
|
|
return "GREEN", f'approval_policy = "{target_policy}"'
|
|
|
|
if current is not None:
|
|
return "YELLOW", f'approval_policy = "{current}" (expected "{target_policy}")'
|
|
|
|
return "RED", "approval_policy not set"
|
|
|
|
|
|
def _check_sandbox(toml_data, config):
|
|
"""Check Target 5: sandbox_mode."""
|
|
if toml_data is None:
|
|
return "RED", "config.toml not found"
|
|
|
|
target_mode = config.get("sandbox_mode", "danger-full-access")
|
|
current = toml_data.get("sandbox_mode")
|
|
|
|
if current == target_mode:
|
|
return "GREEN", f'sandbox_mode = "{target_mode}"'
|
|
|
|
if current is not None:
|
|
return "YELLOW", f'sandbox_mode = "{current}" (expected "{target_mode}")'
|
|
|
|
return "RED", "sandbox_mode not set"
|
|
|
|
|
|
def _check_env_vars(config):
|
|
"""Check Target 6: system environment variables."""
|
|
base_url = config["base_url"].rstrip("/")
|
|
if not base_url.endswith("/v1"):
|
|
base_url += "/v1"
|
|
|
|
etc_env = "/etc/environment"
|
|
if not os.path.isfile(etc_env):
|
|
return "RED", "/etc/environment not found"
|
|
|
|
with open(etc_env) as f:
|
|
content = f.read()
|
|
|
|
has_base = "OPENAI_BASE_URL" in content
|
|
has_key = "OPENAI_API_KEY" in content
|
|
|
|
if has_base and has_key:
|
|
return "GREEN", "OPENAI_BASE_URL + OPENAI_API_KEY set"
|
|
|
|
missing = []
|
|
if not has_base:
|
|
missing.append("OPENAI_BASE_URL")
|
|
if not has_key:
|
|
missing.append("OPENAI_API_KEY")
|
|
|
|
return "YELLOW" if (has_base or has_key) else "RED", f"Missing: {', '.join(missing)}"
|
|
|
|
|
|
def validate_all(codex_dir, config):
|
|
"""Validate all 6 targets. Returns list of (target, status, message) tuples."""
|
|
config_path = os.path.join(codex_dir, "config.toml")
|
|
toml_data = _read_toml(config_path)
|
|
|
|
results = []
|
|
|
|
# Target 1: API endpoint
|
|
status, msg = _check_api_endpoint(toml_data, config)
|
|
results.append((PATCH_TARGETS[0], status, msg))
|
|
|
|
# Target 2: Auth
|
|
status, msg = _check_auth(codex_dir, config)
|
|
results.append((PATCH_TARGETS[1], status, msg))
|
|
|
|
# Target 3: Analytics
|
|
status, msg = _check_analytics(toml_data)
|
|
results.append((PATCH_TARGETS[2], status, msg))
|
|
|
|
# Target 4: Approval
|
|
status, msg = _check_approval(toml_data, config)
|
|
results.append((PATCH_TARGETS[3], status, msg))
|
|
|
|
# Target 5: Sandbox
|
|
status, msg = _check_sandbox(toml_data, config)
|
|
results.append((PATCH_TARGETS[4], status, msg))
|
|
|
|
# Target 6: Env vars
|
|
status, msg = _check_env_vars(config)
|
|
results.append((PATCH_TARGETS[5], status, msg))
|
|
|
|
return results
|
|
|
|
|
|
# ANSI colors
|
|
GREEN_C = "\033[92m"
|
|
YELLOW_C = "\033[93m"
|
|
RED_C = "\033[91m"
|
|
BOLD_C = "\033[1m"
|
|
RESET_C = "\033[0m"
|
|
|
|
STATUS_COLORS = {
|
|
"GREEN": GREEN_C,
|
|
"YELLOW": YELLOW_C,
|
|
"RED": RED_C,
|
|
}
|
|
|
|
|
|
def print_validation_report(results):
|
|
"""Print formatted validation report."""
|
|
print(f"\n {BOLD_C}Codex Patcher — Validation Report{RESET_C}")
|
|
print(" " + "─" * 50)
|
|
|
|
counts = {"GREEN": 0, "YELLOW": 0, "RED": 0}
|
|
for target, status, msg in results:
|
|
color = STATUS_COLORS.get(status, "")
|
|
print(f" {color}[{status:6s}]{RESET_C} {target.name}: {target.description}")
|
|
if status != "GREEN":
|
|
print(f" → {msg}")
|
|
counts[status] = counts.get(status, 0) + 1
|
|
|
|
print(" " + "─" * 50)
|
|
total = len(results)
|
|
print(f" {GREEN_C}{counts['GREEN']}{RESET_C}/{total} GREEN "
|
|
f"{YELLOW_C}{counts['YELLOW']}{RESET_C} YELLOW "
|
|
f"{RED_C}{counts['RED']}{RESET_C} RED")
|
|
|
|
if counts["GREEN"] == total:
|
|
print(f"\n {GREEN_C}All targets configured correctly!{RESET_C}")
|
|
elif counts["RED"] > 0:
|
|
print(f"\n {RED_C}Critical targets missing. Run: python3 codex_patcher.py --apply{RESET_C}")
|
|
|
|
return counts
|