diff --git a/tools/hardcoded_path_guard.py b/tools/hardcoded_path_guard.py new file mode 100644 index 000000000..0795370e3 --- /dev/null +++ b/tools/hardcoded_path_guard.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +""" +Hardcoded Path Guard — Poka-Yoke #921 + +Detects and blocks hardcoded home-directory paths in tool arguments. +These paths work on one machine but break on others, VPS deployments, +or when HOME changes. + +Usage: + from tools.hardcoded_path_guard import check_path, validate_tool_args + + # Check a single path + err = check_path("/Users/apayne/.hermes/config.yaml") + + # Validate all path-like args in a tool call + clean_args, warnings = validate_tool_args("read_file", {"path": "/home/user/file.txt"}) +""" + +import os +import re +import json as _json +from typing import Dict, List, Optional, Tuple, Any + +# Patterns that indicate hardcoded home directories +HARDCODED_PATTERNS = [ + (r"/Users/[\w.\-]+/", "macOS home directory (/Users/...)"), + (r"/home/[\w.\-]+/", "Linux home directory (/home/...)"), + (r"(? Optional[str]: + if _NOQA_PATTERN.search(text): + return None + for pattern, desc in _COMPILED_PATTERNS: + if pattern.search(text): + return desc + return None + + +def check_path(path_value: str) -> Optional[str]: + if not isinstance(path_value, str): + return None + match_desc = has_hardcoded_path(path_value) + if match_desc: + return ( + f"Path contains hardcoded home directory ({match_desc}): '{path_value}'. " + f"Use $HOME, relative paths, or get_hermes_home(). " + f"Add '# noqa: hardcoded-path-ok' if intentional." + ) + return None + + +def validate_tool_args(tool_name: str, args: Dict[str, Any]) -> Tuple[Dict[str, Any], List[str]]: + warnings = [] + for key, value in args.items(): + if key.lower() not in _PATH_ARG_NAMES: + continue + if isinstance(value, str): + err = check_path(value) + if err: + warnings.append(err) + elif isinstance(value, list): + for item in value: + if isinstance(item, str): + err = check_path(item) + if err: + warnings.append(err) + return args, warnings + + +def scan_source_for_violations(source_code: str, filename: str = "") -> List[Tuple[int, str, str]]: + violations = [] + lines = source_code.split("\n") + for i, line in enumerate(lines, 1): + stripped = line.strip() + if stripped.startswith("#"): + if _NOQA_PATTERN.search(line): + continue + continue + if stripped.startswith("import ") or stripped.startswith("from "): + continue + for pattern, desc in _COMPILED_PATTERNS: + match = pattern.search(line) + if match: + if _NOQA_PATTERN.search(line): + continue + violations.append((i, line.strip(), desc)) + break + return violations + + +def guard_tool_dispatch(tool_name: str, args: Dict[str, Any]) -> Optional[str]: + _, warnings = validate_tool_args(tool_name, args) + if warnings: + return _json.dumps({ + "error": "Hardcoded home directory path detected", + "details": warnings, + "suggestion": "Use $HOME, relative paths, or get_hermes_home() instead of hardcoded paths.", + "pokayoke": True, + "rule": "hardcoded-path-guard" + }) + return None