OmniGuard-Evolved-V2 / scripts /validate_openenv.py
SmartKapila's picture
Making files ready for Training
0a66b10
Raw
History Blame Contribute Delete
8.63 kB
#!/usr/bin/env python3
"""validate_openenv.py — Pre-flight compliance checker for OpenEnv.
Validates that the OmniGuard-Evolved-V2 codebase does NOT use reserved tool names
(reset, step, state, close) as MCP tool identifiers, and verifies the openenv.yaml
manifest is well-formed.
Usage:
python scripts/validate_openenv.py
"""
from __future__ import annotations
import ast
import pathlib
import sys
import yaml
# OpenEnv reserves these names for the Gym-style API surface.
# MCP tools MUST NOT shadow these.
RESERVED_TOOL_NAMES = frozenset({"reset", "step", "state", "close"})
PROJECT_ROOT = pathlib.Path(__file__).resolve().parent.parent
SERVER_DIR = PROJECT_ROOT / "server"
MANIFEST_PATH = PROJECT_ROOT / "openenv.yaml"
PASS = "\033[92m✓\033[0m"
FAIL = "\033[91m✗\033[0m"
WARN = "\033[93m⚠\033[0m"
def check_reserved_tool_names() -> list[str]:
"""Scan all Python files in server/ for string literals matching reserved names
used as MCP tool identifiers (e.g., tool_name='step')."""
violations: list[str] = []
for py_file in SERVER_DIR.rglob("*.py"):
try:
source = py_file.read_text(encoding="utf-8")
tree = ast.parse(source, filename=str(py_file))
except (SyntaxError, UnicodeDecodeError):
continue
for node in ast.walk(tree):
# Check keyword arguments like tool_name="step"
if isinstance(node, ast.keyword):
if node.arg and "tool" in node.arg.lower():
if isinstance(node.value, ast.Constant) and isinstance(node.value.value, str):
if node.value.value.lower() in RESERVED_TOOL_NAMES:
violations.append(
f" {py_file.relative_to(PROJECT_ROOT)}:{node.lineno} "
f"— reserved tool name '{node.value.value}' used in kwarg '{node.arg}'"
)
# Check dict literals with tool_name keys
if isinstance(node, ast.Dict):
for key, value in zip(node.keys, node.values):
if (
isinstance(key, ast.Constant)
and isinstance(key.value, str)
and "tool" in key.value.lower()
and isinstance(value, ast.Constant)
and isinstance(value.value, str)
and value.value.lower() in RESERVED_TOOL_NAMES
):
violations.append(
f" {py_file.relative_to(PROJECT_ROOT)}:{node.lineno} "
f"— reserved tool name '{value.value}' in dict key '{key.value}'"
)
return violations
def check_mcp_tool_definitions() -> list[str]:
"""Check MCPToolContext usages don't clash with reserved names."""
violations: list[str] = []
for py_file in SERVER_DIR.rglob("*.py"):
try:
source = py_file.read_text(encoding="utf-8")
except UnicodeDecodeError:
continue
for line_no, line in enumerate(source.splitlines(), start=1):
# Quick heuristic: look for tool_name= assignments with reserved strings
if "tool_name" in line:
for reserved in RESERVED_TOOL_NAMES:
if f'"{reserved}"' in line or f"'{reserved}'" in line:
violations.append(
f" {py_file.relative_to(PROJECT_ROOT)}:{line_no} "
f"— tool_name set to reserved '{reserved}'"
)
return violations
def check_manifest() -> list[str]:
"""Validate openenv.yaml exists and has required top-level keys."""
issues: list[str] = []
if not MANIFEST_PATH.exists():
issues.append(" openenv.yaml not found at project root")
return issues
try:
with open(MANIFEST_PATH) as f:
manifest = yaml.safe_load(f)
except Exception as e:
issues.append(f" openenv.yaml parse error: {e}")
return issues
required_keys = {"name", "description", "version", "entrypoint", "tasks"}
missing = required_keys - set(manifest.keys())
if missing:
issues.append(f" openenv.yaml missing required keys: {missing}")
# Validate tasks have names
tasks = manifest.get("tasks", [])
if not tasks:
issues.append(" openenv.yaml: no tasks defined")
else:
for i, task in enumerate(tasks):
if "name" not in task:
issues.append(f" openenv.yaml: task[{i}] missing 'name'")
return issues
def check_base_class_inheritance() -> list[str]:
"""Verify OmniGuardStateMachine inherits from BaseMCPEnvironment."""
issues: list[str] = []
env_path = SERVER_DIR / "env.py"
if not env_path.exists():
issues.append(" server/env.py not found")
return issues
source = env_path.read_text(encoding="utf-8")
if "BaseMCPEnvironment" not in source:
issues.append(" server/env.py: OmniGuardStateMachine does not reference BaseMCPEnvironment")
if "class OmniGuardStateMachine" not in source:
issues.append(" server/env.py: OmniGuardStateMachine class not found")
# Verify import of openenv_adapter
if "from server.openenv_adapter import" not in source:
issues.append(" server/env.py: missing import from server.openenv_adapter")
return issues
def main() -> int:
print("=" * 60)
print(" OmniGuard-Evolved-V2 — OpenEnv Compliance Validator")
print("=" * 60)
print()
exit_code = 0
# 1. Reserved tool names (AST scan)
print("1. Checking reserved tool names (reset, step, state, close)...")
violations = check_reserved_tool_names()
violations += check_mcp_tool_definitions()
if violations:
print(f" {FAIL} Found {len(violations)} violation(s):")
for v in violations:
print(f" {v}")
exit_code = 1
else:
print(f" {PASS} No reserved tool name collisions found.")
print()
# 2. Manifest validation
print("2. Validating openenv.yaml manifest...")
manifest_issues = check_manifest()
if manifest_issues:
print(f" {FAIL} Found {len(manifest_issues)} issue(s):")
for issue in manifest_issues:
print(f" {issue}")
exit_code = 1
else:
print(f" {PASS} openenv.yaml is valid and complete.")
print()
# 3. Base class inheritance
print("3. Checking OpenEnv base class inheritance...")
inheritance_issues = check_base_class_inheritance()
if inheritance_issues:
print(f" {FAIL} Found {len(inheritance_issues)} issue(s):")
for issue in inheritance_issues:
print(f" {issue}")
exit_code = 1
else:
print(f" {PASS} OmniGuardStateMachine correctly inherits BaseMCPEnvironment.")
print()
# 4. Client/server separation
print("4. Checking client/server separation...")
client_violations: list[str] = []
training_dir = PROJECT_ROOT / "training"
eval_dir = PROJECT_ROOT / "eval"
for scan_dir in [training_dir, eval_dir]:
if not scan_dir.exists():
continue
for py_file in scan_dir.rglob("*.py"):
try:
source = py_file.read_text(encoding="utf-8")
except UnicodeDecodeError:
continue
# Clients must NOT import server internals (except models for type hints)
bad_imports = [
"from server.env import",
"from server.graders import",
"from server.generator import",
"from server.verifier import",
"from server.vector_env import",
]
for bad in bad_imports:
if bad in source:
client_violations.append(
f" {py_file.relative_to(PROJECT_ROOT)} imports server internals: {bad}"
)
if client_violations:
print(f" {WARN} Found {len(client_violations)} potential violation(s):")
for v in client_violations:
print(f" {v}")
else:
print(f" {PASS} Client code respects server boundary.")
print()
print("=" * 60)
if exit_code == 0:
print(f" {PASS} ALL CHECKS PASSED — Ready for OpenEnv submission.")
else:
print(f" {FAIL} COMPLIANCE ISSUES FOUND — Fix before submission.")
print("=" * 60)
return exit_code
if __name__ == "__main__":
sys.exit(main())