31ce04c655
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.
147 lines
5.8 KiB
Python
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
|