Spaces:
Sleeping
Sleeping
File size: 8,628 Bytes
0a66b10 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 | #!/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())
|