scripts/capability_manifest.py
5.3 KB| 1 | #!/usr/bin/env python3 |
| 2 | """Emit the closed capability matrix or a redacted local-file manifest.""" |
| 3 | |
| 4 | from __future__ import annotations |
| 5 | |
| 6 | import argparse |
| 7 | from pathlib import Path |
| 8 | from typing import Any |
| 9 | |
| 10 | from _capabilities import ( |
| 11 | REFERENCE_ONLY_FORMATS, |
| 12 | automated_capability_rows, |
| 13 | capability_for_path, |
| 14 | validate_magic, |
| 15 | ) |
| 16 | from _common import ( |
| 17 | DEFAULT_MAX_FILE_BYTES, |
| 18 | CliError, |
| 19 | bounded_file_limit, |
| 20 | checked_input_file, |
| 21 | display_identifier, |
| 22 | emit_json, |
| 23 | run_cli, |
| 24 | sha256_file, |
| 25 | stable_token, |
| 26 | ) |
| 27 | |
| 28 | |
| 29 | def build_parser() -> argparse.ArgumentParser: |
| 30 | parser = argparse.ArgumentParser( |
| 31 | description=( |
| 32 | "List exact EDA capabilities or inspect a bounded local file without " |
| 33 | "content sniffing, raw previews, or path disclosure." |
| 34 | ) |
| 35 | ) |
| 36 | subparsers = parser.add_subparsers(dest="command", required=True) |
| 37 | list_parser = subparsers.add_parser( |
| 38 | "list", |
| 39 | help="Print automated and reference-only capability rows", |
| 40 | ) |
| 41 | list_parser.add_argument("--output", help="Optional local .json output path") |
| 42 | list_parser.add_argument( |
| 43 | "--root", |
| 44 | default=".", |
| 45 | help="Existing local directory that bounds the optional output", |
| 46 | ) |
| 47 | list_parser.add_argument("--force", action="store_true") |
| 48 | |
| 49 | inspect_parser = subparsers.add_parser( |
| 50 | "inspect", |
| 51 | help="Create a redacted manifest for one local regular file", |
| 52 | ) |
| 53 | inspect_parser.add_argument("input", help="Local file path inside --root") |
| 54 | inspect_parser.add_argument( |
| 55 | "--root", |
| 56 | default=".", |
| 57 | help="Existing local directory that bounds all input/output paths", |
| 58 | ) |
| 59 | inspect_parser.add_argument( |
| 60 | "--max-bytes", |
| 61 | type=int, |
| 62 | default=DEFAULT_MAX_FILE_BYTES, |
| 63 | help=f"Maximum input bytes (hard ceiling: {512 * 1024 * 1024})", |
| 64 | ) |
| 65 | inspect_parser.add_argument( |
| 66 | "--sha256", |
| 67 | action="store_true", |
| 68 | help="Explicitly include a full-file SHA-256 content fingerprint", |
| 69 | ) |
| 70 | inspect_parser.add_argument( |
| 71 | "--reveal-identifiers", |
| 72 | action="store_true", |
| 73 | help="Include only a sanitized basename; never include the full path", |
| 74 | ) |
| 75 | inspect_parser.add_argument("--output", help="Optional local .json output path") |
| 76 | inspect_parser.add_argument("--force", action="store_true") |
| 77 | return parser |
| 78 | |
| 79 | |
| 80 | def capability_matrix() -> dict[str, Any]: |
| 81 | """Return exact script and reference-only registrations.""" |
| 82 | |
| 83 | return { |
| 84 | "schema_version": "1.1", |
| 85 | "policy": { |
| 86 | "unknown_formats": "rejected", |
| 87 | "content_sniffing_fallback": False, |
| 88 | "reference_only_means_executable_support": False, |
| 89 | "all_automated_inspection_is_bounded": True, |
| 90 | }, |
| 91 | "automated": automated_capability_rows(), |
| 92 | "reference_only": [ |
| 93 | {"suffix": suffix, **REFERENCE_ONLY_FORMATS[suffix]} |
| 94 | for suffix in sorted(REFERENCE_ONLY_FORMATS) |
| 95 | ], |
| 96 | } |
| 97 | |
| 98 | |
| 99 | def inspect_manifest( |
| 100 | path: Path, |
| 101 | *, |
| 102 | max_bytes: int, |
| 103 | include_sha256: bool = False, |
| 104 | reveal_identifiers: bool = False, |
| 105 | ) -> dict[str, Any]: |
| 106 | """Build a redacted manifest from a previously checked local file.""" |
| 107 | |
| 108 | capability = capability_for_path(path) |
| 109 | signature_check_supported = capability["suffix"] not in { |
| 110 | ".csv", |
| 111 | ".tsv", |
| 112 | ".json", |
| 113 | } |
| 114 | if capability["tier"].startswith("automated") and signature_check_supported: |
| 115 | validate_magic(path, capability["suffix"]) |
| 116 | info = path.stat() |
| 117 | manifest: dict[str, Any] = { |
| 118 | "schema_version": "1.1", |
| 119 | "file": { |
| 120 | "file_id": stable_token(str(path), kind="file"), |
| 121 | "basename": display_identifier( |
| 122 | path.name, |
| 123 | kind="filename", |
| 124 | reveal_identifiers=reveal_identifiers, |
| 125 | ), |
| 126 | "full_path_emitted": False, |
| 127 | "size_bytes": info.st_size, |
| 128 | "regular_file": True, |
| 129 | "symlink": False, |
| 130 | }, |
| 131 | "capability": capability, |
| 132 | "inspection": { |
| 133 | "format_signature_checked": ( |
| 134 | capability["tier"].startswith("automated") |
| 135 | and signature_check_supported |
| 136 | ), |
| 137 | "content_values_read": False, |
| 138 | "reference_only": capability["tier"] == "reference_only", |
| 139 | }, |
| 140 | } |
| 141 | if include_sha256: |
| 142 | manifest["file"]["sha256"] = sha256_file(path, max_bytes=max_bytes) |
| 143 | manifest["file"]["sha256_explicitly_requested"] = True |
| 144 | return manifest |
| 145 | |
| 146 | |
| 147 | def _main() -> None: |
| 148 | args = build_parser().parse_args() |
| 149 | if args.command == "list": |
| 150 | emit_json( |
| 151 | capability_matrix(), |
| 152 | output=args.output, |
| 153 | root=args.root, |
| 154 | force=args.force, |
| 155 | ) |
| 156 | return |
| 157 | if args.command != "inspect": |
| 158 | raise CliError("unsupported command") |
| 159 | max_bytes = bounded_file_limit(args.max_bytes) |
| 160 | path = checked_input_file( |
| 161 | args.input, |
| 162 | root=args.root, |
| 163 | max_bytes=max_bytes, |
| 164 | ) |
| 165 | report = inspect_manifest( |
| 166 | path, |
| 167 | max_bytes=max_bytes, |
| 168 | include_sha256=args.sha256, |
| 169 | reveal_identifiers=args.reveal_identifiers, |
| 170 | ) |
| 171 | emit_json( |
| 172 | report, |
| 173 | output=args.output, |
| 174 | root=args.root, |
| 175 | force=args.force, |
| 176 | ) |
| 177 | |
| 178 | |
| 179 | def main() -> int: |
| 180 | return run_cli(_main) |
| 181 | |
| 182 | |
| 183 | if __name__ == "__main__": |
| 184 | raise SystemExit(main()) |
文件内容是平台检查时读出的文本,只供查看。平台不会运行包里的任何脚本或程序。

