Skills › Scientific Agent Skills › pymatgen › artifact_manifest.py
artifact_manifest.py
172 lines · skills/pymatgen/scripts/artifact_manifest.py at 5f2bc03
1#!/usr/bin/env python3
2"""Create a bounded checksum and provenance manifest for explicit local files."""
4from __future__ import annotations
6import argparse
7import mimetypes
8import platform
9import re
10from datetime import datetime, timezone
12from _common import (
13 CliError,
14 DEFAULT_MAX_OUTPUT_BYTES,
15 MP_API_VERSION,
16 PYMATGEN_CORE_VERSION,
17 PYMATGEN_VERSION,
18 checked_input_file,
19 checked_output_file,
20 emit_json,
21 package_versions,
22 positive_int,
23 sha256_file,
24 write_json_new,
25)
28SENSITIVE_NAME = re.compile(
29 r"(^|[._-])(?:env|secret|token|credential|api[-_]?key|private[-_]?key)"
30 r"($|[._-])",
31 re.IGNORECASE,
32)
33MAX_FILES = 100
34MAX_TOTAL_BYTES = 2 * 1024 * 1024 * 1024
37def build_parser() -> argparse.ArgumentParser:
38 parser = argparse.ArgumentParser(
39 description=(
40 "Hash explicitly listed local artifacts and create a new JSON "
41 "provenance manifest. Directories, symlinks, and likely secret files "
42 "are refused."
43 )
44 )
45 parser.add_argument(
46 "--artifact",
47 action="append",
48 required=True,
49 help="Local regular file to include (repeatable)",
50 )
51 parser.add_argument("--output", required=True, help="New manifest JSON path")
52 parser.add_argument(
53 "--workflow",
54 required=True,
55 help="Short workflow label; do not include credentials or full commands",
56 )
57 parser.add_argument(
58 "--source",
59 action="append",
60 default=[],
61 help="Short source/citation URL or identifier (repeatable)",
62 )
63 parser.add_argument(
64 "--max-file-bytes",
65 type=positive_int,
66 default=512 * 1024 * 1024,
67 )
68 parser.add_argument(
69 "--max-total-bytes",
70 type=positive_int,
71 default=MAX_TOTAL_BYTES,
72 )
73 parser.add_argument(
74 "--max-output-bytes",
75 type=positive_int,
76 default=DEFAULT_MAX_OUTPUT_BYTES,
77 )
78 return parser
81def main() -> int:
82 args = build_parser().parse_args()
83 try:
84 if len(args.artifact) > MAX_FILES:
85 raise CliError(f"at most {MAX_FILES} artifacts may be listed")
86 if args.max_total_bytes > MAX_TOTAL_BYTES:
87 raise CliError("--max-total-bytes may not exceed 2 GiB")
88 if not args.workflow.strip() or len(args.workflow) > 200:
89 raise CliError("--workflow must be a non-empty label of at most 200 chars")
90 if any(len(source) > 2000 for source in args.source):
91 raise CliError("--source values may contain at most 2000 chars")
92 output = checked_output_file(args.output)
93 paths = []
94 seen = set()
95 total = 0
96 for value in args.artifact:
97 path = checked_input_file(value, max_bytes=args.max_file_bytes)
98 if path == output:
99 raise CliError("manifest output cannot also be an input artifact")
100 if SENSITIVE_NAME.search(path.name):
101 raise CliError(
102 f"refusing likely credential-bearing artifact name: {path.name!r}"
103 )
104 if path in seen:
105 raise CliError(f"duplicate artifact: {path.name!r}")
106 seen.add(path)
107 total += path.stat().st_size
108 if total > args.max_total_bytes:
109 raise CliError("artifact set exceeds --max-total-bytes")
110 paths.append(path)
111 records = []
112 for path in paths:
113 stat = path.stat()
114 records.append(
115 {
116 "name": path.name,
117 "bytes": stat.st_size,
118 "sha256": sha256_file(path),
119 "media_type": mimetypes.guess_type(path.name)[0],
120 "modified_at_utc": datetime.fromtimestamp(
121 stat.st_mtime, timezone.utc
122 ).isoformat(),
123 }
124 )
125 manifest = {
126 "schema_version": "1.0",
127 "created_at_utc": datetime.now(timezone.utc).isoformat(),
128 "workflow": args.workflow,
129 "sources": args.source,
130 "software": {
131 "expected_snapshot": {
132 "pymatgen": PYMATGEN_VERSION,
133 "pymatgen-core": PYMATGEN_CORE_VERSION,
134 "mp-api": MP_API_VERSION,
135 },
136 "python": platform.python_version(),
137 "installed": package_versions(
138 ("pymatgen", "pymatgen-core", "mp-api")
139 ),
140 },
141 "artifacts": records,
142 "artifact_count": len(records),
143 "total_bytes": total,
144 "contract": {
145 "network_accessed": False,
146 "directories_traversed": False,
147 "symlinks_followed": False,
148 "artifact_contents_emitted": False,
149 "credentials_read": False,
150 "pickle_used": False,
151 "existing_files_overwritten": False,
152 },
153 }
154 write_json_new(output, manifest, max_bytes=args.max_output_bytes)
155 emit_json(
156 {
157 "ok": True,
158 "output": output.name,
159 "artifact_count": len(records),
160 "total_bytes": total,
161 "overwrote_existing": False,
162 "network_accessed": False,
163 }
164 )
165 return 0
166 except (CliError, OSError, TypeError, ValueError) as exc:
167 emit_json({"ok": False, "error": f"{type(exc).__name__}: {exc}"[:1000]})
168 return 2
171if __name__ == "__main__":
172 raise SystemExit(main())