#!/usr/bin/env python3 from __future__ import annotations import argparse import re import subprocess import sys from pathlib import Path MAX_FILE_BYTES = 2 * 1024 * 1024 FORBIDDEN = ( (re.compile(r"\bcrank_adapter_rest\b|\bRestAdapterError\b"), "direct REST adapter dependency"), (re.compile(r"\breqwest\b"), "direct HTTP client dependency"), ( re.compile(r"\.execute_with_(?:auth|context|auth_and_context)\s*\("), "legacy runtime execution bypass", ), ( re.compile(r"\.(?:execute_request|prepare_request|invoke_unary)\s*\("), "raw execution pipeline bypass", ), ( re.compile(r"\bpub\s+use\s+[^;]*(?:RuntimeExecutor|ProtocolAdapter)\b"), "execution boundary re-export", ), (re.compile(r"\bsqlx\s*::"), "direct execution persistence dependency"), ( re.compile(r"\.(?:validate_shape|apply_mapping|apply)\s*\("), "direct execution validation or mapping bypass", ), ) SCOPED_PREFIXES = ( "apps/admin-api/src/routes/", "apps/admin-api/src/service/", "apps/admin-api/src/service.rs", ) SCOPED_MCP_FILES = { "crates/crank-community-mcp/src/app.rs", "crates/crank-community-mcp/src/approval_execution.rs", "crates/crank-community-mcp/src/tool_error.rs", "crates/crank-community-mcp/src/app/invocation_history.rs", "crates/crank-community-mcp/src/app/tool_resolution.rs", "crates/crank-community-mcp/src/tool_search.rs", } SCOPED_MCP_PREFIXES = ("crates/crank-community-mcp/src/app/",) def parse_args(argv: list[str]) -> argparse.Namespace: parser = argparse.ArgumentParser(description="Check the canonical execution boundary.") parser.add_argument("--root", type=Path, default=Path.cwd()) parser.add_argument("--files", nargs="*") return parser.parse_args(argv) def candidate_paths(root: Path, explicit: list[str] | None) -> list[Path]: if explicit is None: result = subprocess.run( ["git", "ls-files", "--cached", "--others", "--exclude-standard", "-z"], cwd=root, check=True, stdout=subprocess.PIPE, ) names = [name for name in result.stdout.decode("utf-8").split("\0") if name] else: names = explicit return [root / name for name in sorted(set(names))] def logical_path(root: Path, path: Path) -> str: if path.is_absolute(): resolved = path.resolve(strict=False) else: resolved = (root / path).resolve(strict=False) try: return resolved.relative_to(root.resolve()).as_posix() except ValueError as error: raise ValueError("path escapes repository root") from error def main(argv: list[str]) -> int: args = parse_args(argv) root = args.root.resolve() failures: list[str] = [] try: paths = candidate_paths(root, args.files) except (OSError, subprocess.SubprocessError, UnicodeError) as error: print(f"error: execution boundary input unavailable ({type(error).__name__})", file=sys.stderr) return 1 for supplied in paths: if supplied.is_symlink(): failures.append("INVALID_PATH") continue try: logical = logical_path(root, supplied) except ValueError: failures.append("INVALID_PATH") continue if ( not logical.endswith(".rs") or not ( logical.startswith(SCOPED_PREFIXES) or logical.startswith(SCOPED_MCP_PREFIXES) or logical in SCOPED_MCP_FILES ) ): continue path = root / logical if path.is_symlink() or not path.is_file(): failures.append(f"INVALID_PATH {logical[:256]}") continue if path.stat().st_size > MAX_FILE_BYTES: failures.append(f"INPUT_TOO_LARGE {logical[:256]}") continue try: source = path.read_text(encoding="utf-8") except (OSError, UnicodeError): failures.append(f"INVALID_SOURCE {logical[:256]}") continue for pattern, reason in FORBIDDEN: if pattern.search(source): failures.append(f"EXECUTION_BOUNDARY {logical[:256]} {reason}") if failures: for failure in sorted(failures)[:1000]: print(f"error: {failure}", file=sys.stderr) return 1 print("Execution boundary check passed") return 0 if __name__ == "__main__": raise SystemExit(main(sys.argv[1:]))