gabriel / musehub public
musehub_mist_push_validator.py python
85 lines 2.9 KB
Raw
sha256:25d96102cb2d69a038356dff26f4633156da2f1faf98fe0d0e4438ff3f367f12 refactor: rename 0054/0055 migrations to standard convention Sonnet 4.6 minor ⚠ breaking 23 days ago
1 """Hub-side validation for mist-domain pushes.
2
3 Called by ``wire_push_unpack_mpack`` before any objects are persisted when
4 ``repo.domain_id == "mist"``.
5
6 Validation contract
7 -------------------
8 Hard errors (``errors`` non-empty → push rejected 422):
9 - Null bytes in filename
10 - Path traversal sequences (``..``)
11 - Path separators (``/``, ``\\``)
12 - Control characters (0x01–0x1F, 0x7F)
13 - ANSI escape sequences
14 - Filename exceeds 255 characters
15 - Empty filename
16
17 Warnings (``warnings`` non-empty → push accepted with advisory):
18 - Unrecognised file extension (artifact type cannot be auto-detected)
19 - No file extension at all
20 """
21
22 from dataclasses import dataclass, field
23
24 from muse.plugins.mist.plugin import detect_artifact_type, validate_mist_filename
25 from musehub.types.json_types import StrDict
26
27 # Extensions whose artifact type can be reliably detected.
28 # Anything outside this set gets a warning — the mist is still accepted.
29 _KNOWN_EXTENSIONS = {
30 ".py", ".js", ".ts", ".jsx", ".tsx", ".sol", ".rs", ".go",
31 ".java", ".c", ".cpp", ".h", ".hpp", ".cs", ".rb", ".php",
32 ".swift", ".kt", ".json", ".yaml", ".yml", ".toml", ".md",
33 ".txt", ".html", ".css", ".scss", ".svg", ".xml", ".sh",
34 ".bash", ".zsh", ".fish", ".mid", ".midi", ".abi",
35 }
36
37 @dataclass
38 class MistValidationResult:
39 """Result of validating a mist snapshot manifest.
40
41 ``valid`` is ``True`` when ``errors`` is empty (warnings are non-fatal).
42 """
43 errors: list[str] = field(default_factory=list)
44 warnings: list[str] = field(default_factory=list)
45
46 @property
47 def valid(self) -> bool:
48 return len(self.errors) == 0
49
50 def validate_mist_manifest(manifest: StrDict) -> MistValidationResult:
51 """Validate all filenames in a mist snapshot manifest.
52
53 Args:
54 manifest: Mapping of ``{filename: object_id}`` from a snapshot manifest.
55
56 Returns:
57 ``MistValidationResult`` with accumulated errors and warnings.
58 Callers should reject the push when ``result.valid is False``.
59 """
60 errors: list[str] = []
61 warnings: list[str] = []
62
63 for filename in manifest:
64 # Hard gate — empty filename is always rejected.
65 if not filename:
66 errors.append("Mist filename must not be empty")
67 continue
68
69 # Hard gate — delegate to the canonical security validator.
70 try:
71 validate_mist_filename(filename)
72 except ValueError as exc:
73 errors.append(str(exc))
74 continue
75
76 # Soft gate — warn when the artifact type cannot be detected.
77 import pathlib
78 ext = pathlib.PurePosixPath(filename).suffix.lower()
79 if not ext or ext not in _KNOWN_EXTENSIONS:
80 warnings.append(
81 f"unrecognised file extension for '{filename}' — "
82 "artifact type will be stored as 'unknown'"
83 )
84
85 return MistValidationResult(errors=errors, warnings=warnings)
File History 2 commits
sha256:25d96102cb2d69a038356dff26f4633156da2f1faf98fe0d0e4438ff3f367f12 refactor: rename 0054/0055 migrations to standard convention Sonnet 4.6 minor 23 days ago
sha256:4aed3d8601c8dd3ed37074de35f11f4a9699a0a4b99d43727048fd3f8e6fd13d chore: doc sweep, ignore wrangler build state, misc fixes Sonnet 4.6 minor 25 days ago