fix(commons,daemon,skills): Windows-compat handler import guard (green CI)

The cross-branch import guard's same-branch check used a POSIX-only path test,
so on Windows (backslash paths) it failed to recognize a branch importing its
OWN handlers -> ImportError at collection, failing all commons + 2 daemon tests
on the Windows CI runner. (Unmasked once the pathspec fix let collection proceed.)

- commons: '/commons/' substring -> 'commons' in Path(caller_file).parts
- daemon + skills: add .replace('\\','/') before the check (matches the idiom
  already used by 15 other branches' guards)
- Convert AIPASS_DEBUG_GUARD debug print() -> sys.stderr.write (cli standard;
  avoids import-time logger dependency inside the guard)

Security semantics unchanged: same-branch allowed, cross-branch still blocked
(verified cross-platform). commons+daemon audit 100%, 700 daemon+skills tests
pass, commons 449 tests pass. (skills local 99% = untracked skills_json orphans,
not in CI.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
AIOSAI
2026-06-15 19:52:31 -07:00
co-authored by Claude Opus 4.8
parent 95a2d1a254
commit 6aabc8bfe6
4 changed files with 21 additions and 20 deletions
+6 -5
View File
@@ -55,8 +55,8 @@ def _guard_branch_access():
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)
sys.stderr.write(f"[GUARD DEBUG] caller_file = {caller_file}\n")
sys.stderr.write(f"[GUARD DEBUG] import_line = {import_line}\n")
if caller_file is None:
stack = inspect.stack()
@@ -65,9 +65,10 @@ def _guard_branch_access():
return # Allow command-line Python through
return
# IMPORTANT: Commons is at src/commons/, not src/aipass/commons/
# Check if caller is from within the commons directory
if "/commons/" in caller_file:
# Check if caller is from within the commons directory.
# Use path PARTS (not a "/commons/" substring) so the same-branch check
# works on Windows too, where paths use backslash separators.
if "commons" in Path(caller_file).parts:
return # Same branch, allowed
caller_branch = _extract_branch_name(caller_file)
@@ -27,6 +27,7 @@ from aipass.commons.apps.handlers.json import json_handler
# SEAL A TIME CAPSULE
# =============================================================================
def seal_capsule(args: List[str]) -> dict:
"""
Seal a time capsule that opens after N days.
@@ -51,6 +52,7 @@ def seal_capsule(args: List[str]) -> dict:
days = max(1, min(365, days))
from aipass.commons.apps.modules.commons_identity import get_caller_branch
caller = get_caller_branch()
if not caller:
return {"success": False, "error": "Could not detect calling branch. Run from a branch directory."}
@@ -64,8 +66,7 @@ def seal_capsule(args: List[str]) -> dict:
conn = get_db()
cursor = conn.execute(
"INSERT INTO time_capsules (creator, title, content, opens_at) "
"VALUES (?, ?, ?, ?)",
"INSERT INTO time_capsules (creator, title, content, opens_at) VALUES (?, ?, ?, ?)",
(creator, title, content, opens_at),
)
capsule_id = cursor.lastrowid
@@ -91,6 +92,7 @@ def seal_capsule(args: List[str]) -> dict:
# LIST TIME CAPSULES
# =============================================================================
def list_capsules(args: List[str]) -> dict:
"""
List all time capsules with status info.
@@ -103,9 +105,7 @@ def list_capsules(args: List[str]) -> dict:
try:
conn = get_db()
rows = conn.execute(
"SELECT * FROM time_capsules ORDER BY opens_at ASC"
).fetchall()
rows = conn.execute("SELECT * FROM time_capsules ORDER BY opens_at ASC").fetchall()
close_db(conn)
@@ -145,6 +145,7 @@ def list_capsules(args: List[str]) -> dict:
# OPEN A TIME CAPSULE
# =============================================================================
def open_capsule(args: List[str]) -> dict:
"""
Open a time capsule if its opens_at date has passed.
@@ -164,6 +165,7 @@ def open_capsule(args: List[str]) -> dict:
return {"success": False, "error": "Capsule ID must be a number"}
from aipass.commons.apps.modules.commons_identity import get_caller_branch
caller = get_caller_branch()
if not caller:
return {"success": False, "error": "Could not detect calling branch. Run from a branch directory."}
@@ -173,9 +175,7 @@ def open_capsule(args: List[str]) -> dict:
try:
conn = get_db()
row = conn.execute(
"SELECT * FROM time_capsules WHERE id = ?", (capsule_id,)
).fetchone()
row = conn.execute("SELECT * FROM time_capsules WHERE id = ?", (capsule_id,)).fetchone()
if not row:
close_db(conn)
+4 -4
View File
@@ -66,8 +66,8 @@ def _guard_branch_access():
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)
sys.stderr.write(f"[GUARD DEBUG] caller_file = {caller_file}\n")
sys.stderr.write(f"[GUARD DEBUG] import_line = {import_line}\n")
if caller_file is None:
# Can't determine caller from real files
@@ -82,8 +82,8 @@ def _guard_branch_access():
# 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
if branch_path in caller_file.replace("\\", "/"):
return # Same branch, allowed (normalize Windows backslash paths)
# External caller - block access
caller_branch = _extract_branch_name(caller_file)
+3 -3
View File
@@ -66,8 +66,8 @@ def _guard_branch_access():
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)
sys.stderr.write(f"[GUARD DEBUG] caller_file = {caller_file}\n")
sys.stderr.write(f"[GUARD DEBUG] import_line = {import_line}\n")
if caller_file is None:
stack = inspect.stack()
@@ -94,7 +94,7 @@ def _guard_branch_access():
return
# Check if caller is from our branch
if f"/{MY_BRANCH}/" in caller_file:
if f"/{MY_BRANCH}/" in caller_file.replace("\\", "/"):
return
# External caller - block access