feat(seedgo): standards enforcement system — checklist command, checker fixes, handler guards
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 <file>` - 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 <seedgo@aipass>
This commit is contained in:
@@ -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 <string> or <stdin>
|
||||
stack = inspect.stack()
|
||||
for frame in stack:
|
||||
if frame.filename in ("<string>", "<stdin>"):
|
||||
# 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.<module> import <function>\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.<module> import <function>\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()
|
||||
|
||||
@@ -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 <string> or <stdin>
|
||||
stack = inspect.stack()
|
||||
for frame in stack:
|
||||
if frame.filename in ("<string>", "<stdin>"):
|
||||
# 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.<module> import <function>\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.<module> import <function>\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()
|
||||
|
||||
@@ -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 <string> or <stdin>
|
||||
stack = inspect.stack()
|
||||
for frame in stack:
|
||||
if frame.filename in ("<string>", "<stdin>"):
|
||||
# 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.<module> import <function>\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.<module> import <function>\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()
|
||||
|
||||
@@ -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 <string> or <stdin>
|
||||
stack = inspect.stack()
|
||||
for frame in stack:
|
||||
if frame.filename in ("<string>", "<stdin>"):
|
||||
# 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.<module> import <function>\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.<module> import <function>\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()
|
||||
|
||||
@@ -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 <string> or <stdin>
|
||||
stack = inspect.stack()
|
||||
for frame in stack:
|
||||
if frame.filename in ("<string>", "<stdin>"):
|
||||
# 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.<module> import <function>\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.<module> import <function>\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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 <file>
|
||||
"""
|
||||
|
||||
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()
|
||||
["<file>"] -> run checklist on file
|
||||
["--pack", "<pack>", "<file>"] -> 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 <file>")
|
||||
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 <file>[/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 <file>[/green] [dim]# Check file (aipass pack)[/dim]")
|
||||
console.print(" [green]drone @seedgo checklist --pack <pack> <file>[/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:])
|
||||
@@ -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 <file>[/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 <file>[/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()
|
||||
|
||||
|
||||
|
||||
@@ -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 <file> # 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)
|
||||
|
||||
@@ -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 <string> or <stdin>
|
||||
stack = inspect.stack()
|
||||
for frame in stack:
|
||||
if frame.filename in ("<string>", "<stdin>"):
|
||||
# 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.<module> import <function>\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.<module> import <function>\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()
|
||||
|
||||
Reference in New Issue
Block a user