Zero behavior change; tool schemas byte-identical; golden corpus (376 guard commands, 93 heredoc forms, sanitizer/unrename cases) identical old vs new. registry.py (1263 -> 924): _memo_check per-pass check_fn memo (2 sites), _grouped/_toolset_entries toolset grouping (6 sites), _unique_env replaces _extend_unique, _attr accessor for get_schema/get_toolset_for_tool/get_emoji/ get_max_result_size, fold cache-prune loops, flatten _callable_module walk, merge guard try-blocks, docstring/comment compaction (all WHY kept). schema_sanitizer.py (511 -> 392): _rewrite bottom-up tree map shared by _strip_ref_siblings/strip_nullable_unions/collapse_const_unions, _dict_nodes generator replaces nested _walk closure, collapsed _const_branch_type guards. self_repo_guard.py (678 -> 517): _scope_keys state machine as one if/elif ladder, heredoc opener parsed by one regex (_HEREDOC_OPENER_RE), _masked_line inlined, _operator_before via rstrip, folded tail expressions. plugin_guard.py (235 -> 163): positional Finding ctor, packed tables, docs. project_tools.py (181 -> 156): _activated shared by create/switch, _ACTIONS dict dispatch replaces the if-chain in _handle_project. path_security.py (24 -> 18): unused logger/logging import dropped.
19 lines
663 B
Python
19 lines
663 B
Python
"""Shared path validation helpers for tool implementations (skills, cron, credential files)."""
|
|
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
|
|
def validate_within_dir(path: Path, root: Path) -> Optional[str]:
|
|
"""Error message if *path* does not resolve inside *root* (symlinks and ``..`` followed)."""
|
|
try:
|
|
path.resolve().relative_to(root.resolve())
|
|
except (ValueError, OSError) as exc:
|
|
return f"Path escapes allowed directory: {exc}"
|
|
return None
|
|
|
|
|
|
def has_traversal_component(path_str: str) -> bool:
|
|
"""Cheap pre-check for a literal ``..`` component before full resolution."""
|
|
return ".." in Path(path_str).parts
|