Extract trust check script inventory helpers
This commit is contained in:
@@ -4,31 +4,24 @@
|
||||
"generated_at": "2026-06-16",
|
||||
"skill_dir": ".",
|
||||
"summary": {
|
||||
"python_file_count": 204,
|
||||
"script_file_count": 131,
|
||||
"python_file_count": 205,
|
||||
"script_file_count": 132,
|
||||
"test_file_count": 73,
|
||||
"internal_module_count": 48,
|
||||
"cli_script_count": 85,
|
||||
"cli_script_count": 86,
|
||||
"command_handler_count": 68,
|
||||
"entrypoint_command_handler_count": 18,
|
||||
"command_module_count": 6,
|
||||
"warn_line_threshold": 900,
|
||||
"watch_line_threshold": 720,
|
||||
"block_line_threshold": 1500,
|
||||
"largest_file_lines": 714,
|
||||
"largest_file_lines": 707,
|
||||
"watchlist_count": 0,
|
||||
"hotspot_count": 0,
|
||||
"blocker_count": 0,
|
||||
"decision": "pass"
|
||||
},
|
||||
"largest_files": [
|
||||
{
|
||||
"path": "scripts/trust_check.py",
|
||||
"lines": 714,
|
||||
"kind": "internal-module",
|
||||
"severity": "pass",
|
||||
"recommendation": "Watch this file before adding new responsibilities; extract a helper module when one concern dominates."
|
||||
},
|
||||
{
|
||||
"path": "scripts/review_studio_gates.py",
|
||||
"lines": 707,
|
||||
@@ -105,6 +98,13 @@
|
||||
"kind": "cli-script",
|
||||
"severity": "pass",
|
||||
"recommendation": "Watch this file before adding new responsibilities; extract a helper module when one concern dominates."
|
||||
},
|
||||
{
|
||||
"path": "scripts/build_skill_atlas.py",
|
||||
"lines": 637,
|
||||
"kind": "cli-script",
|
||||
"severity": "pass",
|
||||
"recommendation": "Watch this file before adding new responsibilities; extract a helper module when one concern dominates."
|
||||
}
|
||||
],
|
||||
"watchlist": [],
|
||||
|
||||
@@ -5,15 +5,15 @@ Generated at: `2026-06-16`
|
||||
## Summary
|
||||
|
||||
- decision: `pass`
|
||||
- python files: `204`
|
||||
- scripts: `131`
|
||||
- python files: `205`
|
||||
- scripts: `132`
|
||||
- tests: `73`
|
||||
- internal modules: `48`
|
||||
- CLI scripts: `85`
|
||||
- CLI scripts: `86`
|
||||
- Yao CLI command handlers: `68`
|
||||
- entrypoint command handlers: `18`
|
||||
- command modules: `6`
|
||||
- largest file lines: `714`
|
||||
- largest file lines: `707`
|
||||
- watch threshold lines: `720`
|
||||
- watchlist: `0`
|
||||
- hotspots: `0`
|
||||
@@ -33,7 +33,6 @@ No near-threshold files found.
|
||||
|
||||
| File | Lines | Kind | Severity |
|
||||
| --- | ---: | --- | --- |
|
||||
| `scripts/trust_check.py` | `714` | `internal-module` | `pass` |
|
||||
| `scripts/review_studio_gates.py` | `707` | `internal-module` | `pass` |
|
||||
| `scripts/apply_adaptation.py` | `706` | `cli-script` | `pass` |
|
||||
| `tests/verify_yao_cli.py` | `696` | `test` | `pass` |
|
||||
@@ -45,6 +44,7 @@ No near-threshold files found.
|
||||
| `scripts/render_review_studio.py` | `647` | `cli-script` | `pass` |
|
||||
| `scripts/render_reference_synthesis.py` | `644` | `cli-script` | `pass` |
|
||||
| `scripts/cross_packager.py` | `638` | `cli-script` | `pass` |
|
||||
| `scripts/build_skill_atlas.py` | `637` | `cli-script` | `pass` |
|
||||
|
||||
## Release Rule
|
||||
|
||||
|
||||
+10
-10
@@ -6,16 +6,16 @@
|
||||
"context_budget_tier": "production",
|
||||
"context_budget_limit": 1000,
|
||||
"skill_body_tokens": 797,
|
||||
"other_text_tokens": 1051797,
|
||||
"other_text_tokens": 1052134,
|
||||
"estimated_initial_load_tokens": 990,
|
||||
"estimated_total_text_tokens": 1052594,
|
||||
"deferred_resource_tokens": 487270,
|
||||
"estimated_total_text_tokens": 1052931,
|
||||
"deferred_resource_tokens": 487365,
|
||||
"deferred_resource_warn_threshold": 120000,
|
||||
"deferred_resource_dirs": [
|
||||
{
|
||||
"path": "scripts",
|
||||
"estimated_tokens": 427172,
|
||||
"file_count": 131
|
||||
"estimated_tokens": 427267,
|
||||
"file_count": 132
|
||||
},
|
||||
{
|
||||
"path": "references",
|
||||
@@ -36,8 +36,8 @@
|
||||
"large_deferred_resource_dirs": [
|
||||
{
|
||||
"path": "scripts",
|
||||
"estimated_tokens": 427172,
|
||||
"file_count": 131
|
||||
"estimated_tokens": 427267,
|
||||
"file_count": 132
|
||||
}
|
||||
],
|
||||
"deferred_resource_governance": {
|
||||
@@ -59,14 +59,14 @@
|
||||
],
|
||||
"missing": [],
|
||||
"path": "scripts",
|
||||
"estimated_tokens": 427172,
|
||||
"file_count": 131,
|
||||
"estimated_tokens": 427267,
|
||||
"file_count": 132,
|
||||
"rationale": "Script resources are deterministic deferred tools, not initial-load prompt context."
|
||||
}
|
||||
],
|
||||
"summary": "Large deferred resources are indexed and backed by evidence."
|
||||
},
|
||||
"relevant_file_count": 654,
|
||||
"relevant_file_count": 656,
|
||||
"unused_resource_dirs": [],
|
||||
"quality_signal_points": 130,
|
||||
"quality_density": 131.3
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
| Target | Path | Tier | Limit | Initial | SKILL | Deferred | Resource Governance | Large Deferred Dirs | Quality Density | Unused Dirs | Status |
|
||||
| --- | --- | --- | ---: | ---: | ---: | ---: | --- | --- | ---: | --- | --- |
|
||||
| root | `.` | `production` | 1000 | 990 | 797 | 487270 | `governed` | scripts:427172 | 131.3 | - | ok |
|
||||
| root | `.` | `production` | 1000 | 990 | 797 | 487365 | `governed` | scripts:427267 | 131.3 | - | ok |
|
||||
| complex-release-orchestrator | `examples/complex-release-orchestrator/generated-skill` | `production` | 1000 | 790 | 718 | 1657 | `not-required` | - | 164.6 | - | ok |
|
||||
| governed-incident-command | `examples/governed-incident-command/generated-skill` | `production` | 1000 | 760 | 658 | 1030 | `not-required` | - | 171.1 | - | ok |
|
||||
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
"budget_limit": 1000,
|
||||
"initial_tokens": 990,
|
||||
"skill_body_tokens": 797,
|
||||
"deferred_resource_tokens": 487270,
|
||||
"deferred_resource_tokens": 487365,
|
||||
"large_deferred_resource_dirs": [
|
||||
{
|
||||
"path": "scripts",
|
||||
"estimated_tokens": 427172,
|
||||
"file_count": 131
|
||||
"estimated_tokens": 427267,
|
||||
"file_count": 132
|
||||
}
|
||||
],
|
||||
"deferred_resource_governance": {
|
||||
@@ -35,8 +35,8 @@
|
||||
],
|
||||
"missing": [],
|
||||
"path": "scripts",
|
||||
"estimated_tokens": 427172,
|
||||
"file_count": 131,
|
||||
"estimated_tokens": 427267,
|
||||
"file_count": 132,
|
||||
"rationale": "Script resources are deterministic deferred tools, not initial-load prompt context."
|
||||
}
|
||||
],
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"root": ".",
|
||||
"summary": {
|
||||
"target_python": "3.11",
|
||||
"file_count": 207,
|
||||
"file_count": 208,
|
||||
"issue_count": 0,
|
||||
"syntax_error_count": 0,
|
||||
"fstring_311_violation_count": 0,
|
||||
@@ -706,6 +706,12 @@
|
||||
"issue_count": 0,
|
||||
"issues": []
|
||||
},
|
||||
{
|
||||
"path": "scripts/trust_check_scripts.py",
|
||||
"ok": true,
|
||||
"issue_count": 0,
|
||||
"issues": []
|
||||
},
|
||||
{
|
||||
"path": "scripts/upgrade_check.py",
|
||||
"ok": true,
|
||||
|
||||
@@ -6,7 +6,7 @@ Generated at: `2026-06-16`
|
||||
|
||||
- decision: `pass`
|
||||
- target python: `3.11`
|
||||
- files scanned: `207`
|
||||
- files scanned: `208`
|
||||
- issues: `0`
|
||||
- syntax errors: `0`
|
||||
- f-string 3.11 violations: `0`
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
"ok": true,
|
||||
"skill_dir": ".",
|
||||
"summary": {
|
||||
"scanned_files": 222,
|
||||
"script_count": 131,
|
||||
"internal_module_count": 45,
|
||||
"scanned_files": 223,
|
||||
"script_count": 132,
|
||||
"internal_module_count": 46,
|
||||
"secret_findings": 0,
|
||||
"dependency_files": [
|
||||
"requirements-ci.txt"
|
||||
@@ -22,8 +22,8 @@
|
||||
"help_smoke_failed_count": 0,
|
||||
"interactive_script_count": 0,
|
||||
"package_hash_scope": "source-contract-without-generated-reports",
|
||||
"package_hash_file_count": 222,
|
||||
"package_sha256": "a163b6e288c5342e683875b47fdfe62989d3f09241c0c8fd86ae615c6d5da7ad"
|
||||
"package_hash_file_count": 223,
|
||||
"package_sha256": "9b29a4cc28ab3fb1df2434b22e26b9e92ee04c096f35da350705c4c92307260e"
|
||||
},
|
||||
"failures": [],
|
||||
"warnings": [],
|
||||
@@ -1600,6 +1600,20 @@
|
||||
"network_urls": [],
|
||||
"network_hosts": []
|
||||
},
|
||||
{
|
||||
"path": "scripts/trust_check_scripts.py",
|
||||
"interface": "internal-module",
|
||||
"interface_declared": true,
|
||||
"interface_reason": "Static script inventory helpers imported by trust_check.py.",
|
||||
"has_argparse": true,
|
||||
"has_main_guard": true,
|
||||
"uses_input": false,
|
||||
"uses_network": false,
|
||||
"uses_file_write": false,
|
||||
"uses_subprocess": false,
|
||||
"network_urls": [],
|
||||
"network_hosts": []
|
||||
},
|
||||
{
|
||||
"path": "scripts/upgrade_check.py",
|
||||
"interface": "cli",
|
||||
@@ -1915,7 +1929,7 @@
|
||||
"checked_count": 86,
|
||||
"passed_count": 86,
|
||||
"failed_count": 0,
|
||||
"skipped_count": 45,
|
||||
"skipped_count": 46,
|
||||
"failed_scripts": [],
|
||||
"results": [
|
||||
{
|
||||
@@ -2896,6 +2910,10 @@
|
||||
"path": "scripts/skillops_opportunity.py",
|
||||
"reason": "internal module"
|
||||
},
|
||||
{
|
||||
"path": "scripts/trust_check_scripts.py",
|
||||
"reason": "internal module"
|
||||
},
|
||||
{
|
||||
"path": "scripts/world_class_evidence_contract.py",
|
||||
"reason": "internal module"
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# Security Trust Report
|
||||
|
||||
- OK: `True`
|
||||
- Scanned files: `222`
|
||||
- Scripts: `131`
|
||||
- Internal script modules: `45`
|
||||
- Scanned files: `223`
|
||||
- Scripts: `132`
|
||||
- Internal script modules: `46`
|
||||
- Secret findings: `0`
|
||||
- Network-capable scripts: `3`
|
||||
- Network policy covered scripts: `3`
|
||||
@@ -15,8 +15,8 @@
|
||||
- CLI help smoke failures: `0`
|
||||
- Interactive scripts: `0`
|
||||
- Package hash scope: `source-contract-without-generated-reports`
|
||||
- Package hash files: `222`
|
||||
- Package SHA256: `a163b6e288c5342e683875b47fdfe62989d3f09241c0c8fd86ae615c6d5da7ad`
|
||||
- Package hash files: `223`
|
||||
- Package SHA256: `9b29a4cc28ab3fb1df2434b22e26b9e92ee04c096f35da350705c4c92307260e`
|
||||
|
||||
## Failures
|
||||
|
||||
@@ -173,6 +173,7 @@
|
||||
| scripts/telemetry_native_host.py | cli | False | True | True | False | False | True | False | Default CLI classification; add SCRIPT_INTERFACE for internal modules. |
|
||||
| scripts/trigger_eval.py | cli | False | True | True | False | False | False | False | Default CLI classification; add SCRIPT_INTERFACE for internal modules. |
|
||||
| scripts/trust_check.py | cli | False | True | True | False | False | True | True | Default CLI classification; add SCRIPT_INTERFACE for internal modules. |
|
||||
| scripts/trust_check_scripts.py | internal-module | True | True | True | False | False | False | False | Static script inventory helpers imported by trust_check.py. |
|
||||
| scripts/upgrade_check.py | cli | False | True | True | False | False | True | False | Default CLI classification; add SCRIPT_INTERFACE for internal modules. |
|
||||
| scripts/validate_skill.py | cli | False | True | True | False | False | False | False | Default CLI classification; add SCRIPT_INTERFACE for internal modules. |
|
||||
| scripts/verify_package.py | cli | False | True | True | False | False | True | False | Default CLI classification; add SCRIPT_INTERFACE for internal modules. |
|
||||
|
||||
+2
-134
@@ -1,6 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import ast
|
||||
import hashlib
|
||||
import json
|
||||
import subprocess
|
||||
@@ -9,20 +8,20 @@ import re
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from urllib.parse import urlparse
|
||||
|
||||
try:
|
||||
import yaml
|
||||
except ImportError: # pragma: no cover
|
||||
yaml = None
|
||||
|
||||
from trust_check_scripts import INTERNAL_SCRIPT_INTERFACE, script_inventory
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
SCAN_DIRS = ["agents", "assets", "docs", "evals", "references", "runtime", "scripts", "security", "skill-ir", "templates"]
|
||||
ROOT_FILES = ["SKILL.md", "README.md", "manifest.json", "requirements-ci.txt", "Makefile"]
|
||||
TEXT_SUFFIXES = {".css", ".js", ".md", ".json", ".jsonl", ".yaml", ".yml", ".py", ".sh", ".txt", ".toml"}
|
||||
PACKAGE_HASH_SCOPE = "source-contract-without-generated-reports"
|
||||
INTERNAL_SCRIPT_INTERFACE = "internal-module"
|
||||
NETWORK_POLICY_REL_PATH = "security/network_policy.json"
|
||||
PERMISSION_POLICY_REL_PATH = "security/permission_policy.json"
|
||||
HELP_SMOKE_TIMEOUT_SECONDS = 5.0
|
||||
@@ -94,137 +93,6 @@ def scan_secrets(skill_dir: Path, files: list[Path]) -> list[dict[str, Any]]:
|
||||
return findings
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def dependency_status(skill_dir: Path) -> dict[str, Any]:
|
||||
candidates = ["requirements-ci.txt", "requirements.txt", "pyproject.toml", "package-lock.json", "uv.lock", "poetry.lock"]
|
||||
present = [name for name in candidates if (skill_dir / name).exists()]
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
#!/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
|
||||
@@ -101,6 +101,7 @@ def main() -> None:
|
||||
"scripts/review_studio_formatting.py",
|
||||
"scripts/review_studio_gates.py",
|
||||
"scripts/review_studio_layout.py",
|
||||
"scripts/trust_check_scripts.py",
|
||||
"scripts/evidence_consistency_core.py",
|
||||
"scripts/evidence_consistency_release.py",
|
||||
"scripts/evidence_consistency_skill_os2_review.py",
|
||||
@@ -135,6 +136,7 @@ def main() -> None:
|
||||
assert "review_studio_formatting.py" not in warning_text, payload["warnings"]
|
||||
assert "review_studio_gates.py" not in warning_text, payload["warnings"]
|
||||
assert "review_studio_layout.py" not in warning_text, payload["warnings"]
|
||||
assert "trust_check_scripts.py" not in warning_text, payload["warnings"]
|
||||
assert "evidence_consistency_core.py" not in warning_text, payload["warnings"]
|
||||
assert "evidence_consistency_release.py" not in warning_text, payload["warnings"]
|
||||
assert "evidence_consistency_skill_os2_review.py" not in warning_text, payload["warnings"]
|
||||
|
||||
Reference in New Issue
Block a user