gabriel / muse public
paths.py python
197 lines 7.4 KB
Raw
sha256:c0eba5ad689cec79f4a3fcdc4f5da78556cb4b8cb7b330f944634356c10379ed chore: pivot to nightly channel — bump version to 0.2.0.dev… Sonnet 5 patch 14 days ago
1 """Harmony path helpers and path-level validation.
2
3 Single responsibility: compute filesystem paths for harmony storage and
4 validate ID strings before they touch the filesystem. No I/O beyond
5 path construction.
6 """
7
8 from __future__ import annotations
9
10 import pathlib
11 import re
12
13 from muse.core.paths import harmony_dir as _harmony_dir
14 from muse.core.types import split_id
15
16 # ---------------------------------------------------------------------------
17 # String constants — harmony storage layout
18 # ---------------------------------------------------------------------------
19
20 _HARMONY = "harmony"
21 _PATTERNS = "patterns"
22 _POLICIES = "policies"
23 _AUDIT = "audit"
24 _ESCALATIONS = "escalations"
25 _RESOLUTIONS = "resolutions"
26 _PATTERN_FILE = "pattern.json"
27
28 # ---------------------------------------------------------------------------
29 # Size and format constants used by validation
30 # ---------------------------------------------------------------------------
31
32 #: Maximum bytes for a semantic fingerprint string.
33 #: Fingerprints may be rich token-bag strings (code plugin) rather than hex64.
34 _MAX_FINGERPRINT_BYTES: int = 4_096 # 4 KiB
35
36 #: Regex matching a canonical content-addressed ID: ``sha256:`` + 64 lowercase hex chars.
37 _SHA256_ID_RE: re.Pattern[str] = re.compile(r"^sha256:[0-9a-f]{64}$")
38
39 #: Regex matching a bare 64-char lowercase hex string (used for filesystem dir names).
40 _BARE_HEX64_RE: re.Pattern[str] = re.compile(r"^[0-9a-f]{64}$")
41
42 #: Regex matching a valid policy ID: alphanumeric + hyphen + underscore, 1–128 chars.
43 _POLICY_ID_RE: re.Pattern[str] = re.compile(r"^[A-Za-z0-9_-]{1,128}$")
44
45 # _id_hex is retired — use split_id() from muse.core.types directly.
46
47 # ---------------------------------------------------------------------------
48 # Directory helpers
49 # ---------------------------------------------------------------------------
50
51 def patterns_dir(root: pathlib.Path) -> pathlib.Path:
52 """Return ``.muse/harmony/patterns/`` (may not yet exist)."""
53 return _harmony_dir(root) / _PATTERNS
54
55 def policies_dir(root: pathlib.Path) -> pathlib.Path:
56 """Return ``.muse/harmony/policies/`` (may not yet exist)."""
57 return _harmony_dir(root) / _POLICIES
58
59 def audit_dir(root: pathlib.Path) -> pathlib.Path:
60 """Return ``.muse/harmony/audit/`` (may not yet exist)."""
61 return _harmony_dir(root) / _AUDIT
62
63 def escalations_dir(root: pathlib.Path) -> pathlib.Path:
64 """Return ``.muse/harmony/escalations/`` (may not yet exist)."""
65 return _harmony_dir(root) / _ESCALATIONS
66
67 def escalation_path(root: pathlib.Path, escalation_id: str) -> pathlib.Path:
68 """Return the JSON file path for an escalation record.
69
70 Path shape: ``.muse/harmony/escalations/<algo>/<hex>.json``
71
72 Args:
73 root: Repository root.
74 escalation_id: A ``<algo>:<hex>`` escalation ID, or bare 64-char hex.
75 """
76 algo, hex_id = split_id(escalation_id)
77 return escalations_dir(root) / algo / f"{hex_id}.json"
78
79 def pattern_dir(root: pathlib.Path, pattern_id: str) -> pathlib.Path:
80 """Return the entry directory for *pattern_id*.
81
82 Path shape: ``.muse/harmony/patterns/<algo>/<hex>/``
83
84 The algorithm is derived from *pattern_id* itself via :func:`split_id`,
85 so the directory reflects the actual hash algorithm used — no colon ever
86 appears in a path segment.
87
88 Args:
89 root: Repository root.
90 pattern_id: A ``<algo>:<hex>`` pattern ID, or bare 64-char hex.
91
92 Raises:
93 ValueError: If *pattern_id* is malformed or contains path-unsafe chars.
94 """
95 algo, hex_id = split_id(pattern_id)
96 return patterns_dir(root) / algo / hex_id
97
98 # Keep old name as an alias so internal callers migrate gradually.
99 _pattern_entry_dir = pattern_dir
100
101 def _resolutions_dir(root: pathlib.Path, pattern_id: str) -> pathlib.Path:
102 """Return the resolutions subdirectory for *pattern_id*."""
103 return pattern_dir(root, pattern_id) / _RESOLUTIONS
104
105 def resolution_path(root: pathlib.Path, pattern_id: str, resolution_id: str) -> pathlib.Path:
106 """Return the JSON file path for a resolution record.
107
108 Path shape: ``.muse/harmony/patterns/<p-algo>/<p-hex>/resolutions/<r-algo>/<r-hex>.json``
109
110 Both the pattern and the resolution carry independent algorithm segments —
111 they may differ if patterns and resolutions are created at different points
112 in the system's lifecycle. No colon ever appears in a path segment.
113
114 Args:
115 root: Repository root.
116 pattern_id: A ``<algo>:<hex>`` pattern ID, or bare 64-char hex.
117 resolution_id: A ``<algo>:<hex>`` resolution ID, or bare 64-char hex.
118
119 Raises:
120 ValueError: If either ID is malformed or contains path-unsafe chars.
121 """
122 r_algo, r_hex = split_id(resolution_id)
123 return _resolutions_dir(root, pattern_id) / r_algo / f"{r_hex}.json"
124
125 # Keep old name as an alias.
126 _resolution_path = resolution_path
127
128 # ---------------------------------------------------------------------------
129 # Validation
130 # ---------------------------------------------------------------------------
131
132 def _validate_id(value: str, label: str = "id") -> None:
133 """Raise :exc:`ValueError` if *value* is not a valid content-addressed ID.
134
135 Prevents path-traversal attacks: a crafted value like ``"../traversal"`` cannot
136 match ``sha256:[0-9a-f]{64}``, so it is rejected before any filesystem path
137 is constructed from it.
138
139 Args:
140 value: The string to validate — must be ``sha256:<64-hex>``.
141 label: Field name used in the error message.
142
143 Raises:
144 ValueError: When *value* does not match ``sha256:[0-9a-f]{64}``.
145 """
146 if not _SHA256_ID_RE.match(value):
147 raise ValueError(
148 f"Invalid {label} {value!r} — expected sha256:<64-hex> (length 71)."
149 )
150
151 def _validate_fingerprint(value: str, label: str = "semantic_fingerprint") -> None:
152 """Raise :exc:`ValueError` if *value* is not a valid semantic fingerprint.
153
154 Unlike :func:`_validate_id`, this does not require hex64 format — domain
155 plugins may use richer representations such as normalized token-bag
156 strings produced by ``code_fingerprint()``.
157
158 Constraints:
159 - Must be non-empty.
160 - Must not contain null bytes (``\\x00``).
161 - Must not exceed :data:`_MAX_FINGERPRINT_BYTES` bytes (UTF-8 encoded).
162
163 Args:
164 value: The fingerprint string to validate.
165 label: Field name used in the error message.
166
167 Raises:
168 ValueError: When *value* is empty, contains a null byte, or is too large.
169 """
170 if not value:
171 raise ValueError(f"Invalid {label}: must not be empty.")
172 if "\x00" in value:
173 raise ValueError(f"Invalid {label}: must not contain null bytes.")
174 if len(value.encode()) > _MAX_FINGERPRINT_BYTES:
175 raise ValueError(
176 f"Invalid {label}: exceeds {_MAX_FINGERPRINT_BYTES}-byte limit "
177 f"({len(value.encode())} bytes)."
178 )
179
180 def _validate_policy_id(policy_id: str) -> None:
181 """Raise :exc:`ValueError` if *policy_id* is not URL-safe.
182
183 Policy IDs are used directly as filenames. This rejects slashes, spaces,
184 dots, and control characters that could cause path traversal or injection.
185
186 Args:
187 policy_id: The policy identifier to validate.
188
189 Raises:
190 ValueError: When *policy_id* does not match ``[A-Za-z0-9_-]{1,128}``.
191 """
192 if not _POLICY_ID_RE.match(policy_id):
193 raise ValueError(
194 f"Invalid policy_id {policy_id!r} — "
195 "only alphanumeric characters, hyphens, and underscores are allowed "
196 "(1–128 characters)."
197 )
File History 8 commits
sha256:c287f599c5429903a139eadf3c5db5d930520e57cb0c3c575d9570e953c3b2d6 chore: bump version to 0.2.0.dev2 — nightly.2 Sonnet 4.6 patch 13 days ago
sha256:8de4334a98c945aace420969d389ad678aa926d4ab4e886b2ac4c4241cb3bf2b revert: keep pyproject.toml in canonical PEP 440 form Sonnet 4.6 patch 16 days ago
sha256:50bb615c573aa4b928fca75b60f82ba2a659910066507cec6a95c412ae22ccb9 docs: add issue docs for push have-negotiation bug (#55) an… Sonnet 4.6 19 days ago
sha256:d90d175cded68aae1d4ffcf4858917854195d0cd8ce1fe73cee4dbc02541cb74 chore: trigger push to surface null-OID paths Sonnet 4.6 25 days ago
sha256:e452ad9a6ace6ccc6d875a35e06caf9da5576a970c1c36133b69a891ce5fefa8 chore: prebuild timing test Sonnet 4.6 35 days ago
sha256:0008ab6695e3e064b3e236b24fd19e538fef6a588eb0d211622f4466d919c0b1 merge: pull staging/dev — advance to 0.2.0rc12 Sonnet 4.6 patch 37 days ago
sha256:9c33d61749fff814c5226d5386aa2af7064c2c02788594a25fdd709358132eea fix: _PROPOSAL_PREFIX_RESOLVE_LIMIT 200 → 100 to match hub … Sonnet 4.6 48 days ago
sha256:36c3cb3e76619d4c30a6d9bf81b5ec4ff148e30dcfed913e3114ca7b43b81c7e fix: rename objects→blobs in push client and all stale test… Sonnet 4.6 patch 51 days ago