From 4e183f2bf86f2484cc903ff45758c000c56dc24a Mon Sep 17 00:00:00 2001 From: AIOSAI Date: Sun, 15 Mar 2026 20:20:52 -0700 Subject: [PATCH] =?UTF-8?q?feat(seedgo):=20standards=20enforcement=20syste?= =?UTF-8?q?m=20=E2=80=94=20checklist=20command,=20checker=20fixes,=20handl?= =?UTF-8?q?er=20guards?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FPLAN-0048: Seedgo Reach — 4-phase standards enforcement overhaul. Phase 1: Checker improvements - introspection_check.py: module-level handle_command() gate validation (catches modules that execute instead of showing introspection on no-args) - encapsulation_check.py: handler guard presence detection (flags branches with empty handlers/__init__.py) Phase 2: Checklist command - New checklist.py module: `drone @seedgo checklist ` - Runs applicable standards per-file, concise pass/fail output - 23 standards on entry points, 3 (all_files scope) on modules - Designed for auto-fix hook consumption Phase 3: Auto-fix hook integration (separate from this commit) Phase 4: Handler guard deployment - Deployed runtime import guards to 6 previously unprotected branches (backup, daemon, drone, memory, spawn, seedgo) - All 12 handler-bearing branches now have active guards - Uses inspect.stack() to block cross-branch handler imports Co-Authored-By: @seedgo --- src/aipass/backup/apps/handlers/__init__.py | 135 ++++++- src/aipass/daemon/apps/handlers/__init__.py | 135 ++++++- src/aipass/drone/apps/handlers/__init__.py | 134 ++++++ src/aipass/memory/apps/handlers/__init__.py | 134 ++++++ src/aipass/seedgo/apps/handlers/__init__.py | 134 ++++++ .../aipass_standards/encapsulation_check.py | 119 +++++- .../aipass_standards/introspection_check.py | 79 ++++ src/aipass/seedgo/apps/modules/checklist.py | 381 ++++++++++++++++++ src/aipass/seedgo/apps/seedgo.py | 8 +- src/aipass/seedgo/drone_adapter.py | 7 +- src/aipass/spawn/apps/handlers/__init__.py | 134 ++++++ 11 files changed, 1394 insertions(+), 6 deletions(-) create mode 100644 src/aipass/seedgo/apps/modules/checklist.py diff --git a/src/aipass/backup/apps/handlers/__init__.py b/src/aipass/backup/apps/handlers/__init__.py index 9e488e35..537f4655 100755 --- a/src/aipass/backup/apps/handlers/__init__.py +++ b/src/aipass/backup/apps/handlers/__init__.py @@ -1 +1,134 @@ -"""Backup system handlers package.""" +"""Backup handlers package - Security protected.""" + +import inspect +from pathlib import Path + +MY_BRANCH = "aipass.backup" + + +def _find_real_caller(): + """ + Walk the stack to find the actual file that triggered this import. + + Skips: + - This file (handlers/__init__.py) + - Python's importlib internals + - Frozen modules + + Returns tuple: (file_path, import_line) or (None, None) + """ + stack = inspect.stack() + this_file = str(Path(__file__).resolve()) + + for frame_info in stack: + filename = frame_info.filename + + # Skip this file + if this_file in str(Path(filename).resolve()): + continue + + # Skip Python internals + if filename.startswith("<") or "importlib" in filename: + continue + + # Found a real file - try to get the import line + import_line = None + if frame_info.code_context: + import_line = frame_info.code_context[0].strip() + + return str(Path(filename).resolve()), import_line + + return None, None + + +def _extract_branch_name(filepath: str) -> str: + """Extract branch name from a file path.""" + parts = Path(filepath).parts + for i, part in enumerate(parts): + if part == "aipass": + if i + 1 < len(parts): + return parts[i + 1] + return "unknown" + + +def _guard_branch_access(): + """ + Block cross-branch handler imports. + + Only code from within the 'backup' branch can import these handlers. + External branches must use aipass.backup.apps.modules instead. + """ + caller_file, import_line = _find_real_caller() + + # DEBUG: Print what we found + import os + if os.environ.get("AIPASS_DEBUG_GUARD"): + import sys + print(f"[GUARD DEBUG] caller_file = {caller_file}", file=sys.stderr) + print(f"[GUARD DEBUG] import_line = {import_line}", file=sys.stderr) + + if caller_file is None: + # Can't determine caller from real files + # Check if we're being run from command line (external) + # by looking at the raw stack for or + stack = inspect.stack() + for frame in stack: + if frame.filename in ("", ""): + # Try to get the import line from the frame + target_line = "unknown" + if frame.code_context: + target_line = frame.code_context[0].strip() + raise ImportError( + f"\n{'='*60}\n" + f"ACCESS DENIED: Cross-branch handler import blocked\n" + f"{'='*60}\n" + f" Caller: interactive/script\n" + f" Blocked: {target_line}\n" + f"\n" + f" Handlers are internal to their branch.\n" + f" Use the module API instead:\n" + f" from {MY_BRANCH}.apps.modules. import \n" + f"\n" + f" Example:\n" + f" from {MY_BRANCH}.apps.modules.logger import logger\n" + f"\n" + f" For full standards guide:\n" + f" drone @seed handlers\n" + f"{'='*60}" + ) + return # Allow if truly can't determine + + # Check if caller is from our branch + # MY_BRANCH is "aipass.backup" (dotted), but filesystem uses "/aipass/backup/" + branch_path = "/" + MY_BRANCH.replace(".", "/") + "/" + if branch_path in caller_file: + return # Same branch, allowed + + # External caller - block access + caller_branch = _extract_branch_name(caller_file) + caller_filename = Path(caller_file).name + blocked_import = import_line if import_line else "unknown" + + raise ImportError( + f"\n{'='*60}\n" + f"ACCESS DENIED: Cross-branch handler import blocked\n" + f"{'='*60}\n" + f" Caller branch: {caller_branch}\n" + f" Caller file: {caller_filename}\n" + f" Blocked: {blocked_import}\n" + f"\n" + f" Handlers are internal to their branch.\n" + f" Use the module API instead:\n" + f" from {MY_BRANCH}.apps.modules. import \n" + f"\n" + f" Example:\n" + f" from {MY_BRANCH}.apps.modules.logger import logger\n" + f"\n" + f" For full standards guide:\n" + f" drone @seed handlers\n" + f"{'='*60}" + ) + + +# Run guard at import time +_guard_branch_access() diff --git a/src/aipass/daemon/apps/handlers/__init__.py b/src/aipass/daemon/apps/handlers/__init__.py index c3557dd4..84d9adf0 100644 --- a/src/aipass/daemon/apps/handlers/__init__.py +++ b/src/aipass/daemon/apps/handlers/__init__.py @@ -1 +1,134 @@ -"""Daemon handlers package.""" +"""Daemon handlers package - Security protected.""" + +import inspect +from pathlib import Path + +MY_BRANCH = "aipass.daemon" + + +def _find_real_caller(): + """ + Walk the stack to find the actual file that triggered this import. + + Skips: + - This file (handlers/__init__.py) + - Python's importlib internals + - Frozen modules + + Returns tuple: (file_path, import_line) or (None, None) + """ + stack = inspect.stack() + this_file = str(Path(__file__).resolve()) + + for frame_info in stack: + filename = frame_info.filename + + # Skip this file + if this_file in str(Path(filename).resolve()): + continue + + # Skip Python internals + if filename.startswith("<") or "importlib" in filename: + continue + + # Found a real file - try to get the import line + import_line = None + if frame_info.code_context: + import_line = frame_info.code_context[0].strip() + + return str(Path(filename).resolve()), import_line + + return None, None + + +def _extract_branch_name(filepath: str) -> str: + """Extract branch name from a file path.""" + parts = Path(filepath).parts + for i, part in enumerate(parts): + if part == "aipass": + if i + 1 < len(parts): + return parts[i + 1] + return "unknown" + + +def _guard_branch_access(): + """ + Block cross-branch handler imports. + + Only code from within the 'daemon' branch can import these handlers. + External branches must use aipass.daemon.apps.modules instead. + """ + caller_file, import_line = _find_real_caller() + + # DEBUG: Print what we found + import os + if os.environ.get("AIPASS_DEBUG_GUARD"): + import sys + print(f"[GUARD DEBUG] caller_file = {caller_file}", file=sys.stderr) + print(f"[GUARD DEBUG] import_line = {import_line}", file=sys.stderr) + + if caller_file is None: + # Can't determine caller from real files + # Check if we're being run from command line (external) + # by looking at the raw stack for or + stack = inspect.stack() + for frame in stack: + if frame.filename in ("", ""): + # Try to get the import line from the frame + target_line = "unknown" + if frame.code_context: + target_line = frame.code_context[0].strip() + raise ImportError( + f"\n{'='*60}\n" + f"ACCESS DENIED: Cross-branch handler import blocked\n" + f"{'='*60}\n" + f" Caller: interactive/script\n" + f" Blocked: {target_line}\n" + f"\n" + f" Handlers are internal to their branch.\n" + f" Use the module API instead:\n" + f" from {MY_BRANCH}.apps.modules. import \n" + f"\n" + f" Example:\n" + f" from {MY_BRANCH}.apps.modules.logger import logger\n" + f"\n" + f" For full standards guide:\n" + f" drone @seed handlers\n" + f"{'='*60}" + ) + return # Allow if truly can't determine + + # Check if caller is from our branch + # MY_BRANCH is "aipass.daemon" (dotted), but filesystem uses "/aipass/daemon/" + branch_path = "/" + MY_BRANCH.replace(".", "/") + "/" + if branch_path in caller_file: + return # Same branch, allowed + + # External caller - block access + caller_branch = _extract_branch_name(caller_file) + caller_filename = Path(caller_file).name + blocked_import = import_line if import_line else "unknown" + + raise ImportError( + f"\n{'='*60}\n" + f"ACCESS DENIED: Cross-branch handler import blocked\n" + f"{'='*60}\n" + f" Caller branch: {caller_branch}\n" + f" Caller file: {caller_filename}\n" + f" Blocked: {blocked_import}\n" + f"\n" + f" Handlers are internal to their branch.\n" + f" Use the module API instead:\n" + f" from {MY_BRANCH}.apps.modules. import \n" + f"\n" + f" Example:\n" + f" from {MY_BRANCH}.apps.modules.logger import logger\n" + f"\n" + f" For full standards guide:\n" + f" drone @seed handlers\n" + f"{'='*60}" + ) + + +# Run guard at import time +_guard_branch_access() diff --git a/src/aipass/drone/apps/handlers/__init__.py b/src/aipass/drone/apps/handlers/__init__.py index e69de29b..3a9fc998 100644 --- a/src/aipass/drone/apps/handlers/__init__.py +++ b/src/aipass/drone/apps/handlers/__init__.py @@ -0,0 +1,134 @@ +"""Drone handlers package - Security protected.""" + +import inspect +from pathlib import Path + +MY_BRANCH = "aipass.drone" + + +def _find_real_caller(): + """ + Walk the stack to find the actual file that triggered this import. + + Skips: + - This file (handlers/__init__.py) + - Python's importlib internals + - Frozen modules + + Returns tuple: (file_path, import_line) or (None, None) + """ + stack = inspect.stack() + this_file = str(Path(__file__).resolve()) + + for frame_info in stack: + filename = frame_info.filename + + # Skip this file + if this_file in str(Path(filename).resolve()): + continue + + # Skip Python internals + if filename.startswith("<") or "importlib" in filename: + continue + + # Found a real file - try to get the import line + import_line = None + if frame_info.code_context: + import_line = frame_info.code_context[0].strip() + + return str(Path(filename).resolve()), import_line + + return None, None + + +def _extract_branch_name(filepath: str) -> str: + """Extract branch name from a file path.""" + parts = Path(filepath).parts + for i, part in enumerate(parts): + if part == "aipass": + if i + 1 < len(parts): + return parts[i + 1] + return "unknown" + + +def _guard_branch_access(): + """ + Block cross-branch handler imports. + + Only code from within the 'drone' branch can import these handlers. + External branches must use aipass.drone.apps.modules instead. + """ + caller_file, import_line = _find_real_caller() + + # DEBUG: Print what we found + import os + if os.environ.get("AIPASS_DEBUG_GUARD"): + import sys + print(f"[GUARD DEBUG] caller_file = {caller_file}", file=sys.stderr) + print(f"[GUARD DEBUG] import_line = {import_line}", file=sys.stderr) + + if caller_file is None: + # Can't determine caller from real files + # Check if we're being run from command line (external) + # by looking at the raw stack for or + stack = inspect.stack() + for frame in stack: + if frame.filename in ("", ""): + # Try to get the import line from the frame + target_line = "unknown" + if frame.code_context: + target_line = frame.code_context[0].strip() + raise ImportError( + f"\n{'='*60}\n" + f"ACCESS DENIED: Cross-branch handler import blocked\n" + f"{'='*60}\n" + f" Caller: interactive/script\n" + f" Blocked: {target_line}\n" + f"\n" + f" Handlers are internal to their branch.\n" + f" Use the module API instead:\n" + f" from {MY_BRANCH}.apps.modules. import \n" + f"\n" + f" Example:\n" + f" from {MY_BRANCH}.apps.modules.logger import logger\n" + f"\n" + f" For full standards guide:\n" + f" drone @seed handlers\n" + f"{'='*60}" + ) + return # Allow if truly can't determine + + # Check if caller is from our branch + # MY_BRANCH is "aipass.drone" (dotted), but filesystem uses "/aipass/drone/" + branch_path = "/" + MY_BRANCH.replace(".", "/") + "/" + if branch_path in caller_file: + return # Same branch, allowed + + # External caller - block access + caller_branch = _extract_branch_name(caller_file) + caller_filename = Path(caller_file).name + blocked_import = import_line if import_line else "unknown" + + raise ImportError( + f"\n{'='*60}\n" + f"ACCESS DENIED: Cross-branch handler import blocked\n" + f"{'='*60}\n" + f" Caller branch: {caller_branch}\n" + f" Caller file: {caller_filename}\n" + f" Blocked: {blocked_import}\n" + f"\n" + f" Handlers are internal to their branch.\n" + f" Use the module API instead:\n" + f" from {MY_BRANCH}.apps.modules. import \n" + f"\n" + f" Example:\n" + f" from {MY_BRANCH}.apps.modules.logger import logger\n" + f"\n" + f" For full standards guide:\n" + f" drone @seed handlers\n" + f"{'='*60}" + ) + + +# Run guard at import time +_guard_branch_access() diff --git a/src/aipass/memory/apps/handlers/__init__.py b/src/aipass/memory/apps/handlers/__init__.py index e69de29b..bb0c3638 100644 --- a/src/aipass/memory/apps/handlers/__init__.py +++ b/src/aipass/memory/apps/handlers/__init__.py @@ -0,0 +1,134 @@ +"""Memory handlers package - Security protected.""" + +import inspect +from pathlib import Path + +MY_BRANCH = "aipass.memory" + + +def _find_real_caller(): + """ + Walk the stack to find the actual file that triggered this import. + + Skips: + - This file (handlers/__init__.py) + - Python's importlib internals + - Frozen modules + + Returns tuple: (file_path, import_line) or (None, None) + """ + stack = inspect.stack() + this_file = str(Path(__file__).resolve()) + + for frame_info in stack: + filename = frame_info.filename + + # Skip this file + if this_file in str(Path(filename).resolve()): + continue + + # Skip Python internals + if filename.startswith("<") or "importlib" in filename: + continue + + # Found a real file - try to get the import line + import_line = None + if frame_info.code_context: + import_line = frame_info.code_context[0].strip() + + return str(Path(filename).resolve()), import_line + + return None, None + + +def _extract_branch_name(filepath: str) -> str: + """Extract branch name from a file path.""" + parts = Path(filepath).parts + for i, part in enumerate(parts): + if part == "aipass": + if i + 1 < len(parts): + return parts[i + 1] + return "unknown" + + +def _guard_branch_access(): + """ + Block cross-branch handler imports. + + Only code from within the 'memory' branch can import these handlers. + External branches must use aipass.memory.apps.modules instead. + """ + caller_file, import_line = _find_real_caller() + + # DEBUG: Print what we found + import os + if os.environ.get("AIPASS_DEBUG_GUARD"): + import sys + print(f"[GUARD DEBUG] caller_file = {caller_file}", file=sys.stderr) + print(f"[GUARD DEBUG] import_line = {import_line}", file=sys.stderr) + + if caller_file is None: + # Can't determine caller from real files + # Check if we're being run from command line (external) + # by looking at the raw stack for or + stack = inspect.stack() + for frame in stack: + if frame.filename in ("", ""): + # Try to get the import line from the frame + target_line = "unknown" + if frame.code_context: + target_line = frame.code_context[0].strip() + raise ImportError( + f"\n{'='*60}\n" + f"ACCESS DENIED: Cross-branch handler import blocked\n" + f"{'='*60}\n" + f" Caller: interactive/script\n" + f" Blocked: {target_line}\n" + f"\n" + f" Handlers are internal to their branch.\n" + f" Use the module API instead:\n" + f" from {MY_BRANCH}.apps.modules. import \n" + f"\n" + f" Example:\n" + f" from {MY_BRANCH}.apps.modules.logger import logger\n" + f"\n" + f" For full standards guide:\n" + f" drone @seed handlers\n" + f"{'='*60}" + ) + return # Allow if truly can't determine + + # Check if caller is from our branch + # MY_BRANCH is "aipass.memory" (dotted), but filesystem uses "/aipass/memory/" + branch_path = "/" + MY_BRANCH.replace(".", "/") + "/" + if branch_path in caller_file: + return # Same branch, allowed + + # External caller - block access + caller_branch = _extract_branch_name(caller_file) + caller_filename = Path(caller_file).name + blocked_import = import_line if import_line else "unknown" + + raise ImportError( + f"\n{'='*60}\n" + f"ACCESS DENIED: Cross-branch handler import blocked\n" + f"{'='*60}\n" + f" Caller branch: {caller_branch}\n" + f" Caller file: {caller_filename}\n" + f" Blocked: {blocked_import}\n" + f"\n" + f" Handlers are internal to their branch.\n" + f" Use the module API instead:\n" + f" from {MY_BRANCH}.apps.modules. import \n" + f"\n" + f" Example:\n" + f" from {MY_BRANCH}.apps.modules.logger import logger\n" + f"\n" + f" For full standards guide:\n" + f" drone @seed handlers\n" + f"{'='*60}" + ) + + +# Run guard at import time +_guard_branch_access() diff --git a/src/aipass/seedgo/apps/handlers/__init__.py b/src/aipass/seedgo/apps/handlers/__init__.py index e69de29b..cc93d5ce 100644 --- a/src/aipass/seedgo/apps/handlers/__init__.py +++ b/src/aipass/seedgo/apps/handlers/__init__.py @@ -0,0 +1,134 @@ +"""Seedgo handlers package - Security protected.""" + +import inspect +from pathlib import Path + +MY_BRANCH = "aipass.seedgo" + + +def _find_real_caller(): + """ + Walk the stack to find the actual file that triggered this import. + + Skips: + - This file (handlers/__init__.py) + - Python's importlib internals + - Frozen modules + + Returns tuple: (file_path, import_line) or (None, None) + """ + stack = inspect.stack() + this_file = str(Path(__file__).resolve()) + + for frame_info in stack: + filename = frame_info.filename + + # Skip this file + if this_file in str(Path(filename).resolve()): + continue + + # Skip Python internals + if filename.startswith("<") or "importlib" in filename: + continue + + # Found a real file - try to get the import line + import_line = None + if frame_info.code_context: + import_line = frame_info.code_context[0].strip() + + return str(Path(filename).resolve()), import_line + + return None, None + + +def _extract_branch_name(filepath: str) -> str: + """Extract branch name from a file path.""" + parts = Path(filepath).parts + for i, part in enumerate(parts): + if part == "aipass": + if i + 1 < len(parts): + return parts[i + 1] + return "unknown" + + +def _guard_branch_access(): + """ + Block cross-branch handler imports. + + Only code from within the 'seedgo' branch can import these handlers. + External branches must use aipass.seedgo.apps.modules instead. + """ + caller_file, import_line = _find_real_caller() + + # DEBUG: Print what we found + import os + if os.environ.get("AIPASS_DEBUG_GUARD"): + import sys + print(f"[GUARD DEBUG] caller_file = {caller_file}", file=sys.stderr) + print(f"[GUARD DEBUG] import_line = {import_line}", file=sys.stderr) + + if caller_file is None: + # Can't determine caller from real files + # Check if we're being run from command line (external) + # by looking at the raw stack for or + stack = inspect.stack() + for frame in stack: + if frame.filename in ("", ""): + # Try to get the import line from the frame + target_line = "unknown" + if frame.code_context: + target_line = frame.code_context[0].strip() + raise ImportError( + f"\n{'='*60}\n" + f"ACCESS DENIED: Cross-branch handler import blocked\n" + f"{'='*60}\n" + f" Caller: interactive/script\n" + f" Blocked: {target_line}\n" + f"\n" + f" Handlers are internal to their branch.\n" + f" Use the module API instead:\n" + f" from {MY_BRANCH}.apps.modules. import \n" + f"\n" + f" Example:\n" + f" from {MY_BRANCH}.apps.modules.logger import logger\n" + f"\n" + f" For full standards guide:\n" + f" drone @seed handlers\n" + f"{'='*60}" + ) + return # Allow if truly can't determine + + # Check if caller is from our branch + # MY_BRANCH is "aipass.seedgo" (dotted), but filesystem uses "/aipass/seedgo/" + branch_path = "/" + MY_BRANCH.replace(".", "/") + "/" + if branch_path in caller_file: + return # Same branch, allowed + + # External caller - block access + caller_branch = _extract_branch_name(caller_file) + caller_filename = Path(caller_file).name + blocked_import = import_line if import_line else "unknown" + + raise ImportError( + f"\n{'='*60}\n" + f"ACCESS DENIED: Cross-branch handler import blocked\n" + f"{'='*60}\n" + f" Caller branch: {caller_branch}\n" + f" Caller file: {caller_filename}\n" + f" Blocked: {blocked_import}\n" + f"\n" + f" Handlers are internal to their branch.\n" + f" Use the module API instead:\n" + f" from {MY_BRANCH}.apps.modules. import \n" + f"\n" + f" Example:\n" + f" from {MY_BRANCH}.apps.modules.logger import logger\n" + f"\n" + f" For full standards guide:\n" + f" drone @seed handlers\n" + f"{'='*60}" + ) + + +# Run guard at import time +_guard_branch_access() diff --git a/src/aipass/seedgo/apps/handlers/aipass_standards/encapsulation_check.py b/src/aipass/seedgo/apps/handlers/aipass_standards/encapsulation_check.py index db3009e7..84a1fb1a 100644 --- a/src/aipass/seedgo/apps/handlers/aipass_standards/encapsulation_check.py +++ b/src/aipass/seedgo/apps/handlers/aipass_standards/encapsulation_check.py @@ -1,9 +1,9 @@ # =================== AIPass ==================== # Name: encapsulation_check.py # Description: Handler Encapsulation Standards Checker -# Version: 1.0.0 +# Version: 1.1.0 # Created: 2026-03-05 -# Modified: 2026-03-05 +# Modified: 2026-03-15 # ============================================= """ @@ -13,6 +13,7 @@ Validates that handlers are properly encapsulated: - No cross-branch handler imports (Branch A importing Branch B's handlers) - No cross-package handler imports (handlers/X importing handlers/Y) - Handlers should be accessed through module entry points, not directly +- Handler security guards present in handlers/__init__.py (inspect.stack guard) """ import re @@ -149,6 +150,115 @@ def get_file_handler_package(file_path: str) -> Optional[str]: return None +# Cache handler guard results per branch path (reset each audit run) +_handler_guard_cache: Dict[str, Optional[Dict]] = {} + + +def _resolve_branch_path(file_path: str) -> Optional[Path]: + """Resolve the branch root directory for a given file path.""" + branch_info = get_branch_from_path(file_path) + if not branch_info: + return None + raw_path = branch_info.get('path', '') + branch_path = Path(raw_path) + if not branch_path.is_absolute(): + registry_path = _find_registry() + branch_path = (registry_path.parent / branch_path).resolve() + return branch_path + + +def check_handler_guard(module_path: str, bypass_rules: list | None = None) -> Optional[Dict]: + """ + Check that a branch's handlers/__init__.py contains a security guard. + + The guard uses inspect.stack() to block cross-branch handler imports + at runtime. Branches without this guard have unprotected handlers. + + Returns a check dict, or None if the check is not applicable + (e.g., no handlers/ directory in the branch). + """ + branch_path = _resolve_branch_path(module_path) + if branch_path is None: + return None + + branch_key = str(branch_path) + + # Return cached result if already checked this branch + if branch_key in _handler_guard_cache: + return _handler_guard_cache[branch_key] + + # Check if bypassed + init_path = branch_path / "apps" / "handlers" / "__init__.py" + if is_bypassed(str(init_path), 'encapsulation', bypass_rules=bypass_rules): + result = { + 'name': 'Handler security guard', + 'passed': True, + 'message': 'Handler guard check bypassed' + } + _handler_guard_cache[branch_key] = result + return result + + handlers_dir = branch_path / "apps" / "handlers" + if not handlers_dir.is_dir(): + # No handlers directory — check not applicable + _handler_guard_cache[branch_key] = None + return None + + if not init_path.exists(): + result = { + 'name': 'Handler security guard', + 'passed': False, + 'message': 'Missing handlers/__init__.py — no handler security guard' + } + _handler_guard_cache[branch_key] = result + return result + + # Read the init file and check for guard patterns + try: + content = init_path.read_text(encoding='utf-8') + except Exception: + result = { + 'name': 'Handler security guard', + 'passed': False, + 'message': 'Cannot read handlers/__init__.py' + } + _handler_guard_cache[branch_key] = result + return result + + # Count non-empty, non-comment lines + code_lines = [ + ln for ln in content.split('\n') + if ln.strip() and not ln.strip().startswith('#') + ] + + # Guard detection: look for key patterns + guard_patterns = ['_guard_branch_access', 'inspect.stack', 'ImportError'] + has_guard = any(pattern in content for pattern in guard_patterns) + + if has_guard: + result = { + 'name': 'Handler security guard', + 'passed': True, + 'message': 'Handler security guard present (inspect.stack guard active)' + } + elif len(code_lines) < 10: + result = { + 'name': 'Handler security guard', + 'passed': False, + 'message': 'Missing handler security guard — cross-branch imports unprotected' + } + else: + # File has substantial code but no recognized guard patterns + result = { + 'name': 'Handler security guard', + 'passed': False, + 'message': 'handlers/__init__.py has code but no recognized security guard pattern' + } + + _handler_guard_cache[branch_key] = result + return result + + def check_module(module_path: str, bypass_rules: list | None = None) -> Dict: """ Check if file respects handler encapsulation @@ -226,6 +336,11 @@ def check_module(module_path: str, bypass_rules: list | None = None) -> Dict: ) checks.append(direct_import_check) + # Check 4: Handler security guard presence (branch-level, cached) + guard_check = check_handler_guard(module_path, bypass_rules) + if guard_check is not None: + checks.append(guard_check) + # Calculate score if not checks: return { diff --git a/src/aipass/seedgo/apps/handlers/aipass_standards/introspection_check.py b/src/aipass/seedgo/apps/handlers/aipass_standards/introspection_check.py index 305abb0b..88988524 100644 --- a/src/aipass/seedgo/apps/handlers/aipass_standards/introspection_check.py +++ b/src/aipass/seedgo/apps/handlers/aipass_standards/introspection_check.py @@ -15,12 +15,16 @@ Checks: 1. Entry points (apps/{name}.py): print_introspection function exists 2. Entry points: Execution order — no-args check before --help check in main() 3. Modules (apps/modules/*.py): print_introspection function exists +4. Modules: handle_command() no-args gate — must gate on empty args and call print_introspection() """ import ast from pathlib import Path from typing import Dict, Optional +# Run on ALL .py files so modules (apps/modules/*.py) are checked, not just entry points +AUDIT_SCOPE = "all_files" + def is_bypassed(file_path: str, standard: str, line: int | None = None, bypass_rules: list | None = None) -> bool: """Check if a violation should be bypassed""" @@ -157,6 +161,12 @@ def check_module(module_path: str, bypass_rules: list | None = None) -> Dict: if dispatch_check: checks.append(dispatch_check) + # Check 4: handle_command() no-args gate (modules only) + if is_module: + gate_check = check_module_handle_command_gate(tree, path.name) + if gate_check: + checks.append(gate_check) + # Calculate score passed_checks = sum(1 for check in checks if check['passed']) total_checks = len(checks) @@ -296,6 +306,75 @@ def _find_main_function(tree: ast.Module) -> Optional[ast.FunctionDef]: return None +def _find_handle_command_function(tree: ast.Module) -> Optional[ast.FunctionDef]: + """Find the top-level handle_command() function definition.""" + for node in tree.body: + if isinstance(node, ast.FunctionDef) and node.name == 'handle_command': + return node + return None + + +def check_module_handle_command_gate(tree: ast.Module, filename: str) -> Optional[Dict]: + """ + For modules (apps/modules/*.py), verify that handle_command() contains a + no-args gate that calls print_introspection(). + + The standard pattern is: + def handle_command(command, args): + ... + if not args: + print_introspection() # or call a wrapper that shows introspection + return True + + Without this gate, the module will immediately execute instead of showing + introspection when called with no arguments. + + Uses AST to find handle_command(), then walks its body looking for a + no-args conditional whose body calls print_introspection (or a known + introspection wrapper). + """ + handle_cmd = _find_handle_command_function(tree) + + if handle_cmd is None: + # No handle_command — module may use a different pattern, skip + return { + 'name': 'handle_command no-args gate', + 'passed': True, + 'message': f'No handle_command() found in {filename} (skipped)' + } + + # Walk handle_command body to find a no-args conditional that calls introspection + # Known introspection-related function names (direct or wrapper) + introspection_names = { + 'print_introspection', + '_show_audit_introspection', + '_show_pack_module_introspection', + } + + for node in ast.walk(handle_cmd): + if not isinstance(node, ast.If): + continue + + if not _is_no_args_check(node): + continue + + # Found a no-args gate — check if its body calls an introspection function + calls = _get_function_calls_in_block(node.body) + if calls & introspection_names: + return { + 'name': 'handle_command no-args gate', + 'passed': True, + 'message': f'handle_command() gates on no-args at line {node.lineno} → introspection' + } + + # No no-args gate found that dispatches to introspection + return { + 'name': 'handle_command no-args gate', + 'passed': False, + 'message': f'handle_command() in {filename} has no no-args gate calling print_introspection() — module will not show introspection when called with no arguments' + } + + def _find_name_main_block(tree: ast.Module) -> Optional[ast.If]: """ Find the if __name__ == '__main__': block at module level. diff --git a/src/aipass/seedgo/apps/modules/checklist.py b/src/aipass/seedgo/apps/modules/checklist.py new file mode 100644 index 00000000..5680fb9b --- /dev/null +++ b/src/aipass/seedgo/apps/modules/checklist.py @@ -0,0 +1,381 @@ +# =================== AIPass ==================== +# Name: checklist.py +# Description: Per-File Standards Checklist Module +# Version: 1.0.0 +# Created: 2026-03-15 +# Modified: 2026-03-15 +# ============================================= + +""" +Per-File Standards Checklist Module + +Quick pass/fail standards check for a single file. +Designed for hook consumption — runs after every file edit. + +Run: drone @seedgo checklist +""" + +import sys +from pathlib import Path +from typing import Dict, List, Optional + +# ============================================================================= +# INFRASTRUCTURE SETUP +# ============================================================================= + +# IMPORTS +# ============================================================================= + +# Prax logger (system-wide, always first) +from aipass.prax import logger + +# CLI services (display/output formatting) +from aipass.cli import console +from aipass.cli.apps.modules import error + +# Checker discovery (reuse existing infrastructure) +from aipass.seedgo.apps.handlers.audit.branch_audit import discover_checkers + +# Bypass system +from aipass.seedgo.apps.handlers.bypass.bypass_handler import ( + get_branch_from_path, + load_bypass_rules, +) + + +# ============================================================================= +# CHECKER APPLICABILITY +# ============================================================================= + +def _is_entry_point(file_path: str) -> bool: + """Check if file is an entry point: apps/{name}.py (directly in apps/, not subdirectory).""" + p = Path(file_path) + if not p.name.endswith('.py'): + return False + if 'apps/' not in file_path: + return False + return p.parent.name == 'apps' + + +def _is_applicable(checker, file_path: str) -> bool: + """Determine if a checker applies to the given file. + + Rules based on AUDIT_SCOPE: + - "entry_point" (default) -> only apps/{name}.py files + - "all_files" -> any .py file + - "branch_level" -> not applicable to single-file checks + """ + scope = getattr(checker, "AUDIT_SCOPE", "entry_point") + + # Branch-level checkers need a branch path, not a single file + if scope == "branch_level": + return False + + # Only check_module() capable checkers + if not hasattr(checker, "check_module"): + return False + + if scope == "all_files": + return file_path.endswith('.py') + + # Default: entry_point scope + return _is_entry_point(file_path) + + +# ============================================================================= +# CORE LOGIC +# ============================================================================= + +def run_checklist(file_path: str, pack_name: str = "aipass") -> List[Dict]: + """Run applicable standards checkers against a single file. + + Args: + file_path: Absolute path to the file to check. + pack_name: Checker pack to use (default: "aipass"). + + Returns: + List of result dicts: [{"standard": str, "passed": bool, "detail": str|None}] + """ + resolved = str(Path(file_path).resolve()) + + if not Path(resolved).exists(): + return [{"standard": "(error)", "passed": False, "detail": f"File not found: {resolved}"}] + + if not resolved.endswith('.py'): + return [{"standard": "(skip)", "passed": True, "detail": "Not a Python file"}] + + # Discover pack path + pack_path = _resolve_pack_path(pack_name) + if pack_path is None: + return [{"standard": "(error)", "passed": False, "detail": f"Pack '{pack_name}' not found"}] + + # Load checkers + checkers = discover_checkers(pack_path) + if not checkers: + return [{"standard": "(error)", "passed": False, "detail": "No checkers discovered"}] + + # Resolve branch and bypass rules + bypass_rules = _load_bypass_for_file(resolved) + + # Run applicable checkers + results = [] + for name, checker in sorted(checkers.items()): + if not _is_applicable(checker, resolved): + continue + + try: + r = checker.check_module(resolved, bypass_rules=bypass_rules) # type: ignore[attr-defined] + except Exception as e: + results.append({"standard": name, "passed": False, "detail": f"Checker error: {e}"}) + continue + + passed = r.get("passed", True) + detail = None + + if not passed: + # Extract concise failure detail from checks + detail = _format_failure(r) + + results.append({"standard": name, "passed": passed, "detail": detail}) + + if not results: + return [{"standard": "(skip)", "passed": True, "detail": "No applicable checkers for this file"}] + + return results + + +def _resolve_pack_path(pack_name: str) -> Optional[Path]: + """Resolve pack name to its directory path.""" + handlers_dir = Path(__file__).parent.parent / "handlers" + candidate = handlers_dir / f"{pack_name}_standards" + if candidate.is_dir() and list(candidate.glob("*_check.py")): + return candidate + return None + + +def _load_bypass_for_file(file_path: str) -> list: + """Load bypass rules for the branch containing file_path.""" + branch = get_branch_from_path(file_path) + if branch is None: + return [] + branch_path = branch.get("path", "") + if not branch_path: + return [] + return load_bypass_rules(branch_path) + + +def _format_failure(result: Dict) -> str: + """Extract a concise one-line failure description from checker result.""" + checks = result.get("checks", []) + failed = [c for c in checks if not c.get("passed", False)] + + if not failed: + return "Failed (no details)" + + # Use first failure's message, trimmed + msg = failed[0].get("message", "Unknown issue") + + # If multiple failures, indicate count + if len(failed) > 1: + msg = f"{msg} (+{len(failed) - 1} more)" + + return msg + + +# ============================================================================= +# OUTPUT FORMATTING +# ============================================================================= + +def _print_results(results: List[Dict], file_path: str) -> None: + """Print concise pass/fail output for hook consumption.""" + p = Path(file_path) + console.print(f"[dim]{p.name}[/dim]") + + all_passed = True + for r in results: + std = r["standard"] + if r["passed"]: + console.print(f" [green]\u2713[/green] {std}") + else: + all_passed = False + detail = r.get("detail", "") + if detail: + console.print(f" [red]\u2717[/red] {std}: {detail}") + else: + console.print(f" [red]\u2717[/red] {std}") + + if all_passed: + console.print(f"[green]All {len(results)} standards passed[/green]") + + +# ============================================================================= +# COMMAND HANDLER +# ============================================================================= + +def handle_command(command: str, args: List[str]) -> bool: + """ + Handle 'checklist' command — per-file standards check. + + Args: + command: Command name + args: Additional arguments + [] -> print_introspection() (standard no-args gate) + ["--help"] -> print_help() + [""] -> run checklist on file + ["--pack", "", ""] -> run with specific pack + + Returns: + True if handled, False if not this module's command + """ + if command != "checklist": + return False + + # No args -> introspection + if not args: + print_introspection() + return True + + # --help + if args[0] in ["--help", "-h", "help"]: + print_help() + return True + + # Parse arguments + pack_name = "aipass" + file_path = None + + i = 0 + while i < len(args): + if args[i] in ("--pack", "-p") and i + 1 < len(args): + pack_name = args[i + 1] + i += 2 + elif not args[i].startswith("-"): + file_path = args[i] + i += 1 + else: + i += 1 + + if file_path is None: + error("No file specified", suggestion="Usage: drone @seedgo checklist ") + return True + + # Resolve path + resolved = Path(file_path) + if not resolved.is_absolute(): + resolved = Path.cwd() / resolved + resolved = resolved.resolve() + + # Run checklist + results = run_checklist(str(resolved), pack_name=pack_name) + + # Print results + _print_results(results, str(resolved)) + + return True + + +# ============================================================================= +# INTROSPECTION & HELP +# ============================================================================= + +def print_introspection() -> None: + """Display module info and connected handlers.""" + console.print() + console.print("[bold cyan]checklist Module[/bold cyan]") + console.print("Per-file standards check — quick pass/fail for hook consumption") + console.print() + + # Show discovered packs + handlers_dir = Path(__file__).parent.parent / "handlers" + packs = {} + if handlers_dir.exists(): + for d in sorted(handlers_dir.iterdir()): + if d.is_dir() and d.name.endswith("_standards"): + check_files = list(d.glob("*_check.py")) + if check_files: + packs[d.name.removesuffix("_standards")] = len(check_files) + + console.print("[yellow]Discovered Packs:[/yellow]") + for name, count in packs.items(): + console.print(f" [cyan]{name}[/cyan] ({count} checker{'s' if count != 1 else ''})") + if not packs: + console.print(" [dim]No packs found[/dim]") + console.print() + + console.print("[yellow]Connected Handlers:[/yellow]") + console.print(" [cyan]handlers/audit/[/cyan]") + console.print(" [dim]- branch_audit.py (discover_checkers — dynamic checker loading)[/dim]") + console.print() + console.print(" [cyan]handlers/bypass/[/cyan]") + console.print(" [dim]- bypass_handler.py (get_branch_from_path, load_bypass_rules)[/dim]") + console.print() + + console.print("[yellow]External Dependencies:[/yellow]") + console.print(" [dim]- aipass.prax (logger)[/dim]") + console.print(" [dim]- aipass.cli (console)[/dim]") + console.print() + + console.print("[yellow]Next:[/yellow]") + console.print(" [green]drone @seedgo checklist [/green] [dim]# Check a single file[/dim]") + console.print(" [green]drone @seedgo checklist --help[/green] [dim]# Full usage guide[/dim]") + console.print() + + +def print_help() -> None: + """Print help information.""" + console.print() + console.print("[bold cyan]Per-File Standards Checklist[/bold cyan]") + console.print("Quick pass/fail check for a single file against applicable standards") + console.print() + + console.print("[yellow]USAGE:[/yellow]") + console.print(" [green]drone @seedgo checklist [/green] [dim]# Check file (aipass pack)[/dim]") + console.print(" [green]drone @seedgo checklist --pack [/green] [dim]# Check with specific pack[/dim]") + console.print(" [green]drone @seedgo checklist --help[/green] [dim]# This help message[/dim]") + console.print() + + console.print("[yellow]OUTPUT FORMAT:[/yellow]") + console.print(" [green]\u2713[/green] standard_name [dim]# Passed[/dim]") + console.print(" [red]\u2717[/red] standard_name: failure detail [dim]# Failed with reason[/dim]") + console.print() + + console.print("[yellow]SCOPE RULES:[/yellow]") + console.print(" Checkers with [cyan]AUDIT_SCOPE = \"entry_point\"[/cyan] only run on apps/{name}.py files") + console.print(" Checkers with [cyan]AUDIT_SCOPE = \"all_files\"[/cyan] run on any .py file") + console.print(" Checkers with [cyan]AUDIT_SCOPE = \"branch_level\"[/cyan] are skipped (need full branch)") + console.print() + + console.print("[yellow]EXAMPLES:[/yellow]") + console.print(" [dim]# Check a module file (only all_files checkers apply)[/dim]") + console.print(" [green]drone @seedgo checklist src/aipass/flow/apps/modules/step_runner.py[/green]") + console.print() + console.print(" [dim]# Check an entry point (all checkers apply)[/dim]") + console.print(" [green]drone @seedgo checklist src/aipass/flow/apps/flow.py[/green]") + console.print() + + console.print("[yellow]REFERENCE:[/yellow]") + console.print(" Runs after every file edit via hook. Bypass rules from .seedgo/bypass.json are respected.") + console.print(" For full branch audit, use: [green]drone @seedgo audit aipass[/green]") + console.print() + + +# ============================================================================= +# STANDALONE EXECUTION +# ============================================================================= + +if __name__ == "__main__": + # Handle help flag + if len(sys.argv) > 1 and sys.argv[1] in ['--help', '-h', 'help']: + print_help() + sys.exit(0) + + # Confirm Prax logger connection + logger.info("Prax logger connected to checklist") + + # No args -> introspection + if len(sys.argv) < 2: + print_introspection() + sys.exit(0) + + # Run checklist + handle_command("checklist", sys.argv[1:]) diff --git a/src/aipass/seedgo/apps/seedgo.py b/src/aipass/seedgo/apps/seedgo.py index 46c6207c..0f00f498 100644 --- a/src/aipass/seedgo/apps/seedgo.py +++ b/src/aipass/seedgo/apps/seedgo.py @@ -135,6 +135,7 @@ def print_introspection() -> None: console.print("[yellow]Next:[/yellow] Explore a module") console.print(" [green]drone @seedgo standards_query[/green] [dim]# Browse standards content[/dim]") console.print(" [green]drone @seedgo audit aipass[/green] [dim]# Run compliance audit[/dim]") + console.print(" [green]drone @seedgo checklist [/green] [dim]# Per-standard checklist on a file[/dim]") console.print(" [green]drone @seedgo --help[/green] [dim]# Full usage guide[/dim]") console.print() @@ -191,11 +192,16 @@ def print_help() -> None: console.print(" [green]drone @seedgo standards_query aipass_standards cli[/green] [dim]# Show standard content[/dim]") console.print() + console.print("[yellow]Checklist:[/yellow]") + console.print(" [green]drone @seedgo checklist[/green] [dim]# Show checklist introspection[/dim]") + console.print(" [green]drone @seedgo checklist [/green] [dim]# Run per-standard checklist on file[/dim]") + console.print() + console.print("─" * 70) console.print() # Commands line for drone discovery - console.print("[dim]Commands: audit, standards_audit, standards_query, diagnostics, diagnostics_audit, readme, readme_update, --help[/dim]") + console.print("[dim]Commands: audit, standards_audit, standards_query, checklist, diagnostics, diagnostics_audit, readme, readme_update, --help[/dim]") console.print() diff --git a/src/aipass/seedgo/drone_adapter.py b/src/aipass/seedgo/drone_adapter.py index aeff5ab9..f64d6649 100644 --- a/src/aipass/seedgo/drone_adapter.py +++ b/src/aipass/seedgo/drone_adapter.py @@ -120,11 +120,16 @@ def get_help(command: str | None = None) -> str: lines.append(" [dim]drone @seedgo standards_query aipass_standards cli # Show content[/dim]") lines.append("") + lines.append("[yellow]Checklist:[/yellow]") + lines.append(" [dim]drone @seedgo checklist # Show checklist introspection[/dim]") + lines.append(" [dim]drone @seedgo checklist # Run per-standard checklist on file[/dim]") + lines.append("") + lines.append("─" * 70) lines.append("") # Commands line for drone discovery - lines.append("[dim]Commands: audit, standards_audit, standards_query, diagnostics, diagnostics_audit, readme, readme_update, --help[/dim]") + lines.append("[dim]Commands: audit, standards_audit, standards_query, checklist, diagnostics, diagnostics_audit, readme, readme_update, --help[/dim]") lines.append("") return "\n".join(lines) diff --git a/src/aipass/spawn/apps/handlers/__init__.py b/src/aipass/spawn/apps/handlers/__init__.py index e69de29b..a3925ada 100644 --- a/src/aipass/spawn/apps/handlers/__init__.py +++ b/src/aipass/spawn/apps/handlers/__init__.py @@ -0,0 +1,134 @@ +"""Spawn handlers package - Security protected.""" + +import inspect +from pathlib import Path + +MY_BRANCH = "aipass.spawn" + + +def _find_real_caller(): + """ + Walk the stack to find the actual file that triggered this import. + + Skips: + - This file (handlers/__init__.py) + - Python's importlib internals + - Frozen modules + + Returns tuple: (file_path, import_line) or (None, None) + """ + stack = inspect.stack() + this_file = str(Path(__file__).resolve()) + + for frame_info in stack: + filename = frame_info.filename + + # Skip this file + if this_file in str(Path(filename).resolve()): + continue + + # Skip Python internals + if filename.startswith("<") or "importlib" in filename: + continue + + # Found a real file - try to get the import line + import_line = None + if frame_info.code_context: + import_line = frame_info.code_context[0].strip() + + return str(Path(filename).resolve()), import_line + + return None, None + + +def _extract_branch_name(filepath: str) -> str: + """Extract branch name from a file path.""" + parts = Path(filepath).parts + for i, part in enumerate(parts): + if part == "aipass": + if i + 1 < len(parts): + return parts[i + 1] + return "unknown" + + +def _guard_branch_access(): + """ + Block cross-branch handler imports. + + Only code from within the 'spawn' branch can import these handlers. + External branches must use aipass.spawn.apps.modules instead. + """ + caller_file, import_line = _find_real_caller() + + # DEBUG: Print what we found + import os + if os.environ.get("AIPASS_DEBUG_GUARD"): + import sys + print(f"[GUARD DEBUG] caller_file = {caller_file}", file=sys.stderr) + print(f"[GUARD DEBUG] import_line = {import_line}", file=sys.stderr) + + if caller_file is None: + # Can't determine caller from real files + # Check if we're being run from command line (external) + # by looking at the raw stack for or + stack = inspect.stack() + for frame in stack: + if frame.filename in ("", ""): + # Try to get the import line from the frame + target_line = "unknown" + if frame.code_context: + target_line = frame.code_context[0].strip() + raise ImportError( + f"\n{'='*60}\n" + f"ACCESS DENIED: Cross-branch handler import blocked\n" + f"{'='*60}\n" + f" Caller: interactive/script\n" + f" Blocked: {target_line}\n" + f"\n" + f" Handlers are internal to their branch.\n" + f" Use the module API instead:\n" + f" from {MY_BRANCH}.apps.modules. import \n" + f"\n" + f" Example:\n" + f" from {MY_BRANCH}.apps.modules.logger import logger\n" + f"\n" + f" For full standards guide:\n" + f" drone @seed handlers\n" + f"{'='*60}" + ) + return # Allow if truly can't determine + + # Check if caller is from our branch + # MY_BRANCH is "aipass.spawn" (dotted), but filesystem uses "/aipass/spawn/" + branch_path = "/" + MY_BRANCH.replace(".", "/") + "/" + if branch_path in caller_file: + return # Same branch, allowed + + # External caller - block access + caller_branch = _extract_branch_name(caller_file) + caller_filename = Path(caller_file).name + blocked_import = import_line if import_line else "unknown" + + raise ImportError( + f"\n{'='*60}\n" + f"ACCESS DENIED: Cross-branch handler import blocked\n" + f"{'='*60}\n" + f" Caller branch: {caller_branch}\n" + f" Caller file: {caller_filename}\n" + f" Blocked: {blocked_import}\n" + f"\n" + f" Handlers are internal to their branch.\n" + f" Use the module API instead:\n" + f" from {MY_BRANCH}.apps.modules. import \n" + f"\n" + f" Example:\n" + f" from {MY_BRANCH}.apps.modules.logger import logger\n" + f"\n" + f" For full standards guide:\n" + f" drone @seed handlers\n" + f"{'='*60}" + ) + + +# Run guard at import time +_guard_branch_access()