Files
yao-meta-skill/scripts/trust_check_scripts.py
T
YAO 31ce04c655 Split meta skill CLI and review gates
Merge the beta-ready Yao Meta Skill architecture, report, evidence gate, and release-boundary updates.\n\nRelease boundary: beta/public testing is allowed; formal world-class, fully reviewed, or superiority claims remain blocked until the pending evidence gates are accepted.
2026-06-17 18:43:02 +08:00

147 lines
5.8 KiB
Python

#!/usr/bin/env python3
import ast
import re
from pathlib import Path
from typing import Any
from urllib.parse import urlparse
SCRIPT_INTERFACE = "internal-module"
SCRIPT_INTERFACE_REASON = "Static script inventory helpers imported by trust_check.py."
INTERNAL_SCRIPT_INTERFACE = "internal-module"
def relpath(skill_dir: Path, path: Path) -> str:
return str(path.relative_to(skill_dir))
def script_inventory(skill_dir: Path) -> list[dict[str, Any]]:
scripts_dir = skill_dir / "scripts"
if not scripts_dir.exists():
return []
inventory = []
for path in sorted(scripts_dir.glob("*.py")):
text = path.read_text(encoding="utf-8", errors="replace")
flags = script_flags(text)
interface = script_interface(text)
urls = extract_url_literals(text)
inventory.append(
{
"path": relpath(skill_dir, path),
"interface": interface["name"],
"interface_declared": interface["declared"],
"interface_reason": interface["reason"],
"has_argparse": "argparse" in text,
"has_main_guard": 'if __name__ == "__main__"' in text,
"uses_input": flags["uses_input"],
"uses_network": flags["uses_network"],
"uses_file_write": flags["uses_file_write"],
"uses_subprocess": flags["uses_subprocess"],
"network_urls": urls,
"network_hosts": sorted({urlparse(url).hostname or "" for url in urls if urlparse(url).hostname}),
}
)
return inventory
def string_assignment(tree: ast.Module, variable_name: str) -> str:
for node in tree.body:
value_node = None
if isinstance(node, ast.Assign):
if any(isinstance(target, ast.Name) and target.id == variable_name for target in node.targets):
value_node = node.value
elif isinstance(node, ast.AnnAssign):
target = node.target
if isinstance(target, ast.Name) and target.id == variable_name:
value_node = node.value
if isinstance(value_node, ast.Constant) and isinstance(value_node.value, str):
return value_node.value
return ""
def script_interface(text: str) -> dict[str, Any]:
try:
tree = ast.parse(text)
except SyntaxError:
match = re.search(r"SCRIPT_INTERFACE\s*=\s*['\"]([^'\"]+)['\"]", text)
reason_match = re.search(r"SCRIPT_INTERFACE_REASON\s*=\s*['\"]([^'\"]+)['\"]", text)
name = match.group(1) if match else "cli"
reason = reason_match.group(1) if reason_match else ""
return {"name": name, "declared": bool(match), "reason": reason}
name = string_assignment(tree, "SCRIPT_INTERFACE")
reason = string_assignment(tree, "SCRIPT_INTERFACE_REASON")
if name:
return {"name": name, "declared": True, "reason": reason}
return {"name": "cli", "declared": False, "reason": "Default CLI classification; add SCRIPT_INTERFACE for internal modules."}
def extract_url_literals(text: str) -> list[str]:
values: list[str] = []
try:
tree = ast.parse(text)
for node in ast.walk(tree):
if isinstance(node, ast.Constant) and isinstance(node.value, str):
values.append(node.value)
except SyntaxError:
values = re.findall(r"['\"]([^'\"]+)['\"]", text)
urls = []
seen = set()
for value in values:
for match in re.finditer(r"https?://[^\s'\"<>]+", value):
url = match.group(0).rstrip(").,]")
if url not in seen:
seen.add(url)
urls.append(url)
return urls
def call_name(node: ast.AST) -> str:
if isinstance(node, ast.Name):
return node.id
if isinstance(node, ast.Attribute):
base = call_name(node.value)
return f"{base}.{node.attr}" if base else node.attr
return ""
def script_flags(text: str) -> dict[str, bool]:
try:
tree = ast.parse(text)
except SyntaxError:
return {
"uses_input": bool(re.search(r"\b(?:input|getpass)\s*\(", text)),
"uses_network": bool(re.search(r"\b(?:urlopen|Request)\s*\(|\brequests\.", text)),
"uses_file_write": bool(
re.search(r"\.(?:write_text|write_bytes|mkdir|unlink)\s*\(", text)
or re.search(r"\b(?:open)\s*\([^)]*['\"][wa+x]", text)
or "shutil.rmtree" in text
),
"uses_subprocess": "subprocess." in text,
}
call_nodes = [node for node in ast.walk(tree) if isinstance(node, ast.Call)]
calls = [call_name(node.func) for node in call_nodes]
return {
"uses_input": any(name in {"input", "getpass.getpass"} or name.endswith(".getpass") for name in calls),
"uses_network": any(name in {"urlopen", "Request"} or name.startswith("requests.") for name in calls),
"uses_file_write": any(is_file_write_call(node, call_name(node.func)) for node in call_nodes),
"uses_subprocess": any(name.startswith("subprocess.") for name in calls),
}
def is_file_write_call(node: ast.Call, name: str) -> bool:
if name.endswith((".write_text", ".write_bytes", ".mkdir", ".unlink", ".rmdir")):
return True
if name in {"shutil.copy", "shutil.copy2", "shutil.copytree", "shutil.move", "shutil.rmtree"}:
return True
if name == "zipfile.ZipFile":
return True
if name in {"open", "Path.open"} or name.endswith(".open"):
args = list(node.args)
keywords = {keyword.arg: keyword.value for keyword in node.keywords if keyword.arg}
mode_node = args[1] if len(args) > 1 else keywords.get("mode")
if isinstance(mode_node, ast.Constant) and isinstance(mode_node.value, str):
return any(flag in mode_node.value for flag in ("w", "a", "x", "+"))
return False