fix: validate the skill name before opening its lock; key lock files on a digest
Review finding on #112218 (major): `_skill_lock_path` opened `<skills>/.locks/<name>.lock` before the name was validated, so `skill_manage(action='create', name='a'*300)` raised OSError (File name too long) and a NUL name raised ValueError instead of the handler's JSON error, and every rejected name ('../../etc', '') left a residue lock file. - tools/skill_manager_tool.py: lock filename is sha256(basename).lock (fixed width, no filesystem limit reachable; `foo` and `category/foo` still share one lock), the redundant `_find_skill` rglob is gone, and `skill_manage` runs `_validate_name` on the name (create) / basename (other actions) before the lock is opened. - '.locks' joins the skills-dir exclusion sets (EXCLUDED_SKILL_DIRS, ledger _NON_PACKAGE_TOPS, learning-graph/skill-commands skip parts, curator backup excludes). - tests: 2 invariants in TestSkillMutationLock (rejected names -> JSON + no .locks residue; digest-keyed lock shared across name forms), red on the old head.
This commit is contained in:
@@ -32,9 +32,10 @@ DEFAULT_KEEP = 5
|
||||
# Never rolled into a snapshot: .hub/ is owned by the skills hub (rolling it back breaks lockfile invariants); .curator_backups
|
||||
# is the backup dir itself; .git is repository metadata — rolling it back breaks git tracking, and snapshots that include it grow
|
||||
# with the full history (once backups are committed back, each snapshot contains the prior ones: 38MB of skills inflated to 24GB
|
||||
# in weeks). The tar filter in ``snapshot_skills`` applies the same set to nested paths, so a nested ``.git`` is skipped too.
|
||||
# in weeks); .locks holds skill_manage's per-skill lock files — restoring them would swap a lock out from under a waiting
|
||||
# writer. The tar filter in ``snapshot_skills`` applies the same set to nested paths, so a nested ``.git`` is skipped too.
|
||||
# See #91449.
|
||||
_EXCLUDE_TOP_LEVEL = {".curator_backups", ".hub", ".git"}
|
||||
_EXCLUDE_TOP_LEVEL = {".curator_backups", ".hub", ".locks", ".git"}
|
||||
|
||||
# Snapshot id: UTC ISO with colons replaced by dashes (Windows-safe filename); optional ``-NN`` suffix for same-second snapshots.
|
||||
_ID_RE = re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z(-\d{2})?$")
|
||||
|
||||
@@ -18,7 +18,7 @@ from typing import Any, Optional
|
||||
|
||||
from hermes_constants import get_hermes_home
|
||||
|
||||
_SKIP_PARTS = {".archive", ".hub", "node_modules", ".git"}
|
||||
_SKIP_PARTS = {".archive", ".hub", ".locks", "node_modules", ".git"}
|
||||
_USAGE_TS_KEYS = ("last_activity_at", "last_used_at", "last_viewed_at", "last_patched_at", "created_at")
|
||||
|
||||
|
||||
|
||||
@@ -323,7 +323,7 @@ def _scaffold_header(
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
_SCAN_SKIP_PARTS = {'.git', '.github', '.hub', '.archive'}
|
||||
_SCAN_SKIP_PARTS = {'.git', '.github', '.hub', '.archive', '.locks'}
|
||||
|
||||
|
||||
def _scan_skill_md(skill_md: Path, disabled: set, seen_names: set, commands: Dict[str, Dict[str, Any]], resolve_command) -> None:
|
||||
|
||||
@@ -21,7 +21,7 @@ logger = logging.getLogger(__name__)
|
||||
PLATFORM_MAP = {"macos": "darwin", "linux": "linux", "windows": "win32"}
|
||||
|
||||
EXCLUDED_SKILL_DIRS = frozenset((
|
||||
".git", ".github", ".hub", ".archive", ".curator_backups",
|
||||
".git", ".github", ".hub", ".archive", ".curator_backups", ".locks",
|
||||
".venv", "venv", "node_modules", "site-packages", "__pycache__",
|
||||
".tox", ".nox", ".pytest_cache", ".mypy_cache", ".ruff_cache",
|
||||
))
|
||||
|
||||
Reference in New Issue
Block a user