返回 ppt-master
attribution_guard.py
根目录 / skills / ppt-master / scripts / attribution_guard.py
1 #!/usr/bin/env python3
2 """
3 PPT Master - Skill Attribution Guard
4
5 Fail closed when the distributed Skill attribution bundle or its execution
6 gates are missing or modified.
7
8 Usage:
9 python3 "<absolute-skill-root>/scripts/attribution_guard.py"
10
11 Examples:
12 python3 "/opt/agent-skills/ppt-master/scripts/attribution_guard.py"
13
14 Dependencies:
15 None (only uses standard library)
16 """
17
18 from __future__ import annotations
19
20 import argparse
21 import ast
22 import hashlib
23 import re
24 import sys
25 from pathlib import Path
26
27
28 _ERROR_MESSAGE = (
29 "PPT Master skill integrity check failed. Please use the complete official "
30 "distribution without removing its attribution files."
31 )
32 _SKILL_DIR = Path(__file__).resolve().parent.parent
33 _EXACT_METADATA_VALUES = {
34 "copyright": '"Copyright (c) 2025-2026 Hugo He"',
35 "license": '"MIT"',
36 "official_repository": '"https://github.com/hugohe3/ppt-master"',
37 }
38 _REQUIRED_METADATA_FIELDS = ("sponsors",)
39 _REQUIRED_ATTRIBUTION_FILES = ("LICENSE", "SPONSORS.md", "SPONSORS_CN.md")
40 _LICENSE_DIGEST = "80cefc234c1ec12a8cece4344f16300c634fa03df7891686fcf979e3828f0921"
41 _REQUIRED_GATE_FILES = (
42 "scripts/console_encoding.py",
43 "scripts/project_manager.py",
44 "scripts/project_management/cli.py",
45 "scripts/svg_quality_checker.py",
46 "scripts/svg_quality/cli.py",
47 "scripts/svg_to_pptx.py",
48 "scripts/svg_to_pptx/pptx_package/cli.py",
49 "scripts/template_fill_pptx.py",
50 "scripts/template_fill_pptx/cli.py",
51 "scripts/native_enhance_pptx.py",
52 "scripts/native_enhance_pptx_core.py",
53 "scripts/register_template.py",
54 "scripts/template_preview_pptx.py",
55 )
56 _SKILL_GATE_MARKER = 'python3 "${SKILL_DIR}/scripts/attribution_guard.py"'
57 _SECONDARY_GATE_FILE = "scripts/console_encoding.py"
58 _SECONDARY_GATE_NAME = "_require_official_distribution_identity"
59
60
61 def _normalized_bytes(path: Path) -> bytes:
62 """Return UTF-8 text bytes with platform line endings normalized."""
63 data = path.read_bytes()
64 if data.startswith(b"\xef\xbb\xbf"):
65 raise ValueError("UTF-8 BOM is not allowed")
66 text = data.decode("utf-8")
67 return text.replace("\r\n", "\n").replace("\r", "\n").encode("utf-8")
68
69
70 def _frontmatter(skill_text: str) -> str:
71 """Return the opening YAML frontmatter or reject malformed input."""
72 if not skill_text.startswith("---\n"):
73 raise ValueError("missing frontmatter")
74 end = skill_text.find("\n---\n", 4)
75 if end < 0:
76 raise ValueError("unterminated frontmatter")
77 return skill_text[4:end]
78
79
80 def _metadata_is_valid() -> bool:
81 """Require fixed identity values and each structural attribution field."""
82 skill_text = _normalized_bytes(_SKILL_DIR / "SKILL.md").decode("utf-8")
83 metadata = _frontmatter(skill_text)
84 return (
85 all(
86 len(re.findall(
87 rf"(?m)^ {re.escape(field)}\s*:\s*{re.escape(value)}\s*$",
88 metadata,
89 )) == 1
90 for field, value in _EXACT_METADATA_VALUES.items()
91 )
92 and all(
93 len(re.findall(rf"(?m)^ {re.escape(field)}\s*:", metadata)) == 1
94 for field in _REQUIRED_METADATA_FIELDS
95 )
96 and skill_text.count(_SKILL_GATE_MARKER) == 1
97 )
98
99
100 def _protected_files_are_valid() -> bool:
101 """Require all attribution paths and the exact MIT license text."""
102 for relative_path in _REQUIRED_ATTRIBUTION_FILES:
103 path = _SKILL_DIR / relative_path
104 if not path.is_file():
105 return False
106 license_digest = hashlib.sha256(_normalized_bytes(_SKILL_DIR / "LICENSE")).hexdigest()
107 return license_digest == _LICENSE_DIGEST
108
109
110 def _is_zero_arg_call(node: ast.stmt, function_name: str) -> bool:
111 """Return whether one statement directly invokes the named function."""
112 return (
113 isinstance(node, ast.Expr)
114 and isinstance(node.value, ast.Call)
115 and isinstance(node.value.func, ast.Name)
116 and node.value.func.id == function_name
117 and not node.value.args
118 and not node.value.keywords
119 )
120
121
122 def _is_gate_call(node: ast.stmt) -> bool:
123 """Return whether one statement directly invokes the primary integrity gate."""
124 return _is_zero_arg_call(node, "require_skill_integrity")
125
126
127 def _execution_gate_is_valid(path: Path) -> bool:
128 """Require one live import and one live entry-point call."""
129 tree = ast.parse(_normalized_bytes(path).decode("utf-8"), filename=str(path))
130 has_import = any(
131 isinstance(node, ast.ImportFrom)
132 and node.module == "attribution_guard"
133 and any(alias.name == "require_skill_integrity" for alias in node.names)
134 for node in tree.body
135 )
136 has_entry_call = False
137 for node in tree.body:
138 if (
139 isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
140 and node.name in {"configure_utf8_stdio", "main"}
141 ):
142 body = node.body[1:] if (
143 node.body
144 and isinstance(node.body[0], ast.Expr)
145 and isinstance(node.body[0].value, ast.Constant)
146 and isinstance(node.body[0].value.value, str)
147 ) else node.body
148 has_entry_call = bool(body and _is_gate_call(body[0]))
149 elif isinstance(node, ast.If) and any(_is_gate_call(statement) for statement in node.body):
150 has_entry_call = True
151 return has_import and has_entry_call
152
153
154 def _execution_gates_are_valid() -> bool:
155 """Require every supported route to retain executable guard calls."""
156 for relative_path in _REQUIRED_GATE_FILES:
157 if not _execution_gate_is_valid(_SKILL_DIR / relative_path):
158 return False
159 return _secondary_execution_gate_is_valid(_SKILL_DIR / _SECONDARY_GATE_FILE)
160
161
162 def _secondary_execution_gate_is_valid(path: Path) -> bool:
163 """Require the independent identity guard and its live bootstrap call."""
164 tree = ast.parse(_normalized_bytes(path).decode("utf-8"), filename=str(path))
165 has_guard = any(
166 isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
167 and node.name == _SECONDARY_GATE_NAME
168 for node in tree.body
169 )
170 for node in tree.body:
171 if not (
172 isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
173 and node.name == "configure_utf8_stdio"
174 ):
175 continue
176 body = node.body[1:] if (
177 node.body
178 and isinstance(node.body[0], ast.Expr)
179 and isinstance(node.body[0].value, ast.Constant)
180 and isinstance(node.body[0].value.value, str)
181 ) else node.body
182 return (
183 has_guard
184 and len(body) >= 2
185 and _is_gate_call(body[0])
186 and _is_zero_arg_call(body[1], _SECONDARY_GATE_NAME)
187 )
188 return False
189
190
191 def _integrity_is_valid() -> bool:
192 """Validate every local attribution and execution invariant."""
193 return (
194 _metadata_is_valid()
195 and _protected_files_are_valid()
196 and _execution_gates_are_valid()
197 )
198
199
200 def require_skill_integrity() -> None:
201 """Stop the active command with one generic message on any expected failure."""
202 try:
203 valid = _integrity_is_valid()
204 except (OSError, SyntaxError, UnicodeError, ValueError):
205 valid = False
206 if valid:
207 return
208 print(_ERROR_MESSAGE, file=sys.stderr)
209 raise SystemExit(78)
210
211
212 def build_parser() -> argparse.ArgumentParser:
213 """Build the zero-argument integrity-check CLI parser."""
214 return argparse.ArgumentParser(
215 description="Validate the PPT Master Skill attribution and execution gates.",
216 )
217
218
219 def main(argv: list[str] | None = None) -> int:
220 """Run the fail-closed Skill integrity gate."""
221 build_parser().parse_args(argv)
222 require_skill_integrity()
223 return 0
224
225
226 if __name__ == "__main__":
227 raise SystemExit(main())
228
228 lines PYTHON