Merge pull request #375 from AIOSAI/citizen/seedgo-track-e-single-path

feat(hooks+seedgo): DPLAN-0139 Track E — single-path enforcement
This commit is contained in:
AIPass
2026-04-21 08:20:59 -07:00
committed by GitHub
8 changed files with 813 additions and 25 deletions
+128
View File
@@ -0,0 +1,128 @@
#!/usr/bin/env python3
"""
PreToolUse Gate — Blocks unsafe edits at the hook layer.
Rules (checked in order):
1. Inbox lock — any write targeting *.ai_mail.local/inbox.json is BLOCKED.
Use `drone @ai_mail email` instead.
2. Cross-branch — writes to src/aipass/X/** from a CWD inside src/aipass/Y/**
are BLOCKED unless the calling branch is in TRUSTED_CROSS_WRITERS.
3. State-file — edits to OTHER .py files while the current branch has unresolved
type errors are BLOCKED. (original v1.2.0 logic)
Track E additions: rules 1 + 2 (DPLAN-0139).
Version: 1.3.0
"""
import json
import os
import sys
from pathlib import Path
STATE_FILE = Path(__file__).parent / ".diagnostics_state.json"
EDIT_TOOLS = {"Edit", "Write", "MultiEdit", "NotebookEdit"}
# Single source of truth lives in permissions.py — inline here as fallback
# so the hook works even when aipass package is not on sys.path.
TRUSTED_CROSS_WRITERS: tuple[str, ...] = ("devpulse", "seedgo", "spawn")
def _get_branch(file_path: str) -> str:
"""Extract AIPass branch name from a file path (src/aipass/{branch}/ pattern)."""
parts = Path(file_path).parts
for i, part in enumerate(parts):
if part == "aipass" and i > 0 and parts[i - 1] == "src" and i + 1 < len(parts):
return parts[i + 1]
return ""
def _block(reason: str) -> None:
print(json.dumps({"decision": "block", "reason": reason}))
sys.exit(2)
def main():
try:
input_data = json.load(sys.stdin)
tool_name = input_data.get("tool_name", "")
tool_input = input_data.get("tool_input", {})
file_path = tool_input.get("file_path", "")
if tool_name not in EDIT_TOOLS:
return
if not file_path:
return
# ------------------------------------------------------------------
# Rule 1: Inbox lock — block all writes to *.ai_mail.local/inbox.json
# ------------------------------------------------------------------
fp = Path(file_path)
if fp.name == "inbox.json" and ".ai_mail.local" in fp.parts:
_block(
"Direct writes to inbox.json are blocked.\n"
"Use: drone @ai_mail email @<branch> \"Subject\" \"Body\""
)
# ------------------------------------------------------------------
# Rule 2: Cross-branch write enforcement
# ------------------------------------------------------------------
cwd = input_data.get("cwd", "") or os.getcwd()
cwd_branch = _get_branch(cwd)
target_branch = _get_branch(str(fp.resolve()) if not fp.is_absolute() else str(fp))
if cwd_branch and target_branch and cwd_branch != target_branch:
if cwd_branch not in TRUSTED_CROSS_WRITERS:
_block(
f"Cross-branch write blocked: '{cwd_branch}' cannot write to '{target_branch}'.\n"
f"Trusted cross-writers: {', '.join(TRUSTED_CROSS_WRITERS)}"
)
# ------------------------------------------------------------------
# Rule 3: State-file (original v1.2.0) — .py files only
# ------------------------------------------------------------------
if not file_path.endswith(".py"):
return
if not STATE_FILE.exists():
return
try:
state = json.loads(STATE_FILE.read_text(encoding="utf-8"))
except (json.JSONDecodeError, IOError):
return
errored_file = state.get("file", "")
errors = state.get("errors", [])
if not errors:
return
try:
current = str(Path(file_path).resolve())
errored = str(Path(errored_file).resolve())
except (OSError, ValueError):
return
if current == errored:
return
current_branch = _get_branch(current)
errored_branch = _get_branch(errored)
if not errored_branch:
return
if current_branch and errored_branch and current_branch != errored_branch:
return
error_summary = "\n".join(f" L{e['line']}: {e['message']}" for e in errors[:5])
_block(
f"Fix {len(errors)} error(s) in {Path(errored_file).name} before editing other files:\n"
f"{error_summary}"
)
except Exception:
pass # Silent fail → allow
if __name__ == "__main__":
main()
@@ -378,6 +378,69 @@ def deliver_email_to_branch(
return True, ""
def deliver_to_inbox_file(inbox_file: Path, email_data: Dict) -> Tuple[bool, str, str]:
"""Write *email_data* to an inbox.json file and fire a desktop notification.
Single canonical path for direct-path delivery (used by cross-project
reply.py to replace the raw-write backdoor). Always fires notify-send.
Args:
inbox_file: Absolute path to the target inbox.json.
email_data: Dict with at minimum ``from``, ``to``, ``subject``,
``message``, ``timestamp``. An ``id`` key is assigned
internally if absent.
Returns:
``(success, error_msg, reply_id)`` — ``reply_id`` is the 8-char hex
string assigned to the message (empty string on failure).
"""
if not inbox_file.exists():
return False, f"inbox not found: {inbox_file}", ""
try:
with _get_inbox_lock()(inbox_file):
try:
with open(inbox_file, "r", encoding="utf-8") as fh:
inbox_data = json.load(fh)
except Exception as exc:
logger.warning("[delivery] deliver_to_inbox_file read failed %s: %s", inbox_file, exc)
return False, f"Failed to read inbox: {exc}", ""
inbox_data = _migrate_inbox_format(inbox_data, inbox_file)
reply_id = str(uuid.uuid4())[:8]
email_data = dict(email_data)
email_data.setdefault("id", reply_id)
reply_id = email_data["id"]
inbox_data.setdefault("messages", []).insert(0, email_data)
inbox_data["total_messages"] = len(inbox_data["messages"])
inbox_data["unread_count"] = sum(
1
for m in inbox_data["messages"]
if m.get("status") == "new" or (m.get("status") is None and not m.get("read", False))
)
try:
with open(inbox_file, "w", encoding="utf-8") as fh:
json.dump(inbox_data, fh, indent=2, ensure_ascii=False)
except Exception as exc:
logger.warning("[delivery] deliver_to_inbox_file write failed %s: %s", inbox_file, exc)
return False, f"Failed to write inbox: {exc}", ""
except OSError as exc:
logger.warning("[delivery] deliver_to_inbox_file lock failed %s: %s", inbox_file, exc)
return False, f"Failed to acquire inbox lock: {exc}", ""
_send_desktop_notification(
email_data.get("from", "@unknown"),
email_data.get("to", str(inbox_file)),
email_data.get("subject", ""),
email_data.get("message", ""),
)
return True, "", reply_id
_NOTIFICATION_TIMESTAMPS: Dict[str, List[float]] = {}
# Rate limit: max notifications per recipient within time window
@@ -20,6 +20,7 @@ from datetime import datetime
from aipass.prax.apps.modules.logger import system_logger as logger
from aipass.ai_mail.apps.handlers.json import json_handler
from aipass.ai_mail.apps.handlers.email.delivery import deliver_to_inbox_file
# Services imported in __main__ only (handlers should not display)
@@ -185,31 +186,12 @@ def _deliver_via_reply_path(
Tuple of (success, message, reply_id or None)
"""
inbox_file = Path(reply_path)
if not inbox_file.exists():
return False, f"reply_path inbox not found: {reply_path}", None
success, error_msg, reply_id = deliver_to_inbox_file(inbox_file, reply_email_data)
if not success:
logger.warning("[reply] _deliver_via_reply_path failed for %s: %s", reply_path, error_msg)
return False, f"Failed to deliver to reply_path: {error_msg}", None
try:
with open(inbox_file, "r", encoding="utf-8") as f:
inbox_data = json.load(f)
except Exception as e:
logger.warning("[reply] _deliver_via_reply_path read failed %s: %s", reply_path, e)
return False, f"Failed to read target inbox: {e}", None
reply_id = str(uuid.uuid4())[:8]
reply_email_data["id"] = reply_id
inbox_data.setdefault("messages", []).insert(0, reply_email_data)
inbox_data["total_messages"] = len(inbox_data["messages"])
new_count = sum(1 for m in inbox_data["messages"] if m.get("status") == "new" or not m.get("read", False))
inbox_data["unread_count"] = new_count
try:
with open(inbox_file, "w", encoding="utf-8") as f:
json.dump(inbox_data, f, indent=2, ensure_ascii=False)
except Exception as e:
logger.warning("[reply] _deliver_via_reply_path write failed %s: %s", reply_path, e)
return False, f"Failed to write to target inbox: {e}", None
logger.info("[reply] Cross-project reply delivered to %s", reply_path)
# Save to sender's sent folder
@@ -20,8 +20,9 @@ from pathlib import Path
from aipass.prax import logger
from aipass.drone.apps.handlers.json import json_handler
from aipass.seedgo.apps.modules.permissions import TRUSTED_CROSS_WRITERS
ALLOWED_CALLERS: list[str] = ["devpulse"]
ALLOWED_CALLERS: list[str] = list(TRUSTED_CROSS_WRITERS)
def verify_caller() -> str:
@@ -52,7 +53,7 @@ def verify_caller() -> str:
logger.error(msg)
raise PermissionError(msg)
if name not in ALLOWED_CALLERS:
msg = f"Branch '{name}' is not authorized for system-pr. Allowed callers: {ALLOWED_CALLERS}"
msg = f"Branch '{name}' is not authorized for system-pr. Trusted cross-writers: {ALLOWED_CALLERS}"
logger.error(msg)
raise PermissionError(msg)
json_handler.log_operation(
+15
View File
@@ -219,6 +219,21 @@
"file": "templates/",
"standard": "unused_function",
"reason": "Template files are reference implementations for other branches to copy. They contain function definitions that are not called within seedgo itself."
},
{
"file": "tests/test_hooks_track_e.py",
"standard": "architecture",
"reason": "Test file lives in tests/ by convention — outside the 3-layer apps/ structure by design."
},
{
"file": "tests/test_hooks_track_e.py",
"standard": "encapsulation",
"reason": "Unit tests must import handlers directly to test them in isolation. Same pattern as test_checkers_batch5.py."
},
{
"file": "apps/modules/permissions.py",
"standard": "unused_function",
"reason": "is_trusted_caller() and identify_caller() are public API consumed by pre_edit_gate.py (hook layer) and drone auth.py. Checker cannot trace cross-file dynamic dispatch to the hook scripts."
}
],
"notes": {
@@ -0,0 +1,120 @@
# =================== AIPass ====================
# Name: inbox_audit.py
# Description: Inbox ID validator — scans all inbox.json files for non-8-hex ids
# Version: 1.0.0
# Created: 2026-04-21
# Modified: 2026-04-21
# =============================================
"""Inbox ID validator for the drone @seedgo audit inbox-ids command.
Walks all .ai_mail.local/inbox.json files in the AIPass repo and flags any
message ids that are not 8-character lowercase hex strings. Alerts devpulse
when violations are found.
"""
from __future__ import annotations
import json
import re
from pathlib import Path
from typing import List
from aipass.prax import logger
from aipass.cli import console, header
from aipass.seedgo.apps.handlers.json import json_handler
_HEX8_RE = re.compile(r"^[0-9a-f]{8}$")
def _find_repo_root() -> Path:
current = Path(__file__).resolve().parent
for parent in (current, *current.parents):
if (parent / ".git").exists():
return parent
return current
def _scan_inbox(inbox_path: Path) -> List[dict]:
"""Return a list of violation dicts for messages with bad ids in *inbox_path*."""
violations: List[dict] = []
try:
data = json.loads(inbox_path.read_text(encoding="utf-8"))
except Exception as exc:
logger.warning("[inbox_audit] could not read %s: %s", inbox_path, exc)
return violations
for msg in data.get("messages", []):
msg_id = msg.get("id", "")
if not _HEX8_RE.match(str(msg_id)):
violations.append(
{
"inbox": str(inbox_path),
"id": msg_id,
"subject": msg.get("subject", ""),
"from": msg.get("from", ""),
}
)
return violations
def _run_inbox_id_scan() -> int:
"""Scan all inbox.json files; return number of violations found."""
json_handler.log_operation("inbox_audit_scan", {})
repo_root = _find_repo_root()
inbox_files = list(repo_root.rglob(".ai_mail.local/inbox.json"))
console.print()
header("SEEDGO — Inbox ID Validator")
console.print(f"[dim]Scanning {len(inbox_files)} inbox file(s) for non-8-hex message ids...[/dim]")
console.print()
all_violations: List[dict] = []
for inbox_path in sorted(inbox_files):
violations = _scan_inbox(inbox_path)
all_violations.extend(violations)
if not all_violations:
console.print("[green]✓[/green] All message ids are valid 8-char hex strings.")
console.print()
return 0
console.print(f"[red]✗[/red] Found [bold]{len(all_violations)}[/bold] id violation(s):\n")
for v in all_violations:
rel = Path(v["inbox"]).relative_to(repo_root) if Path(v["inbox"]).is_absolute() else v["inbox"]
console.print(
f" [red]•[/red] [bold]{rel}[/bold] id=[yellow]{v['id']!r}[/yellow] from={v['from']} subject={v['subject']!r}"
)
console.print()
console.print("[yellow]Action:[/yellow] Alert devpulse — run:")
console.print(
f' [green]drone @ai_mail email @devpulse "inbox-id violations" '
f'"Found {len(all_violations)} bad message id(s) — run drone @seedgo audit inbox-ids for details"[/green]'
)
console.print()
return len(all_violations)
def print_introspection() -> None:
"""Show inbox_audit module structure."""
console.print("[bold cyan]inbox_audit[/bold cyan] — Inbox ID validator")
console.print(" Connected Handlers: none (uses stdlib + pathlib only)")
console.print(" Command: drone @seedgo audit inbox-ids")
def handle_command(command: str, args: List[str]) -> bool:
"""Handle `audit inbox-ids` — return True only for that exact subcommand."""
if command not in ("audit", "standards_audit"):
return False
if not args:
print_introspection()
return True
if args[0] in ("--help", "-h", "help"):
console.print("Usage: drone @seedgo audit inbox-ids")
console.print(" Scans all .ai_mail.local/inbox.json files for non-8-hex message ids.")
return True
if args[0] != "inbox-ids":
return False
_run_inbox_id_scan()
return True
@@ -0,0 +1,77 @@
# =================== AIPass ====================
# Name: permissions.py
# Description: Shared trust list for hook layer and drone authorization
# Version: 1.0.0
# Created: 2026-04-21
# Modified: 2026-04-21
# =============================================
"""Shared permission definitions for cross-branch write authorization.
Single source of truth for which branches may write outside their own
directory. Consumed by pre_edit_gate.py (hook layer) and
drone/apps/plugins/devpulse_ops/auth.py (drone layer).
"""
from __future__ import annotations
import json
from pathlib import Path
from aipass.prax import logger
from aipass.seedgo.apps.handlers.json import json_handler
TRUSTED_CROSS_WRITERS: tuple[str, ...] = ("devpulse", "seedgo", "spawn")
def is_trusted_caller(name: str) -> bool:
"""Return True if *name* is in TRUSTED_CROSS_WRITERS."""
json_handler.log_operation("is_trusted_caller", {"name": name})
return name.lower() in TRUSTED_CROSS_WRITERS
def identify_caller(cwd: str | None = None) -> str:
"""Walk up from *cwd* (default: CWD) to find passport.json, return branch_name.
Returns the branch name string, or empty string if no passport is found
or the file cannot be parsed.
"""
start = Path(cwd).resolve() if cwd else Path.cwd().resolve()
current = start
for _ in range(10):
passport = current / ".trinity" / "passport.json"
if passport.exists():
try:
data = json.loads(passport.read_text(encoding="utf-8"))
name = data.get("branch_info", {}).get("branch_name")
if not name:
name = data.get("identity", {}).get("name")
return name or ""
except Exception as exc:
logger.warning("[permissions] identify_caller: failed to parse passport at %s: %s", passport, exc)
return ""
parent = current.parent
if parent == current:
break
current = parent
return ""
def print_introspection() -> None:
"""Display permissions module info."""
from aipass.cli import console
console.print("[bold cyan]permissions[/bold cyan] — shared trust list for hook + drone layers")
console.print(f" TRUSTED_CROSS_WRITERS: {TRUSTED_CROSS_WRITERS}")
console.print(" Functions: is_trusted_caller(name), identify_caller(cwd)")
def handle_command(command: str, args: list) -> bool:
"""Library module — not a command handler. Returns False for all commands."""
if not args:
print_introspection()
return False
if args[0] in ("--help", "-h", "help"):
print_introspection()
return False
return False
@@ -0,0 +1,402 @@
# =================== AIPass ====================
# Name: test_hooks_track_e.py
# Description: DPLAN-0139 Track E — single-path enforcement tests
# Version: 1.0.0
# Created: 2026-04-21
# Modified: 2026-04-21
# =============================================
"""Tests for DPLAN-0139 Track E — single-path enforcement.
Covers:
- permissions.py: TRUSTED_CROSS_WRITERS, is_trusted_caller(), identify_caller()
- pre_edit_gate.py v1.3.0: inbox lock rule + cross-branch write rule
- drone auth.py: ALLOWED_CALLERS derived from TRUSTED_CROSS_WRITERS
- inbox_audit.py: handle_command routing + _scan_inbox validation
- delivery.py: deliver_to_inbox_file single-path helper
"""
import importlib.util
import io
import json
from pathlib import Path
from unittest.mock import patch
import pytest
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _find_repo_root() -> Path:
"""Walk up from this file to find the git repo root."""
current = Path(__file__).resolve().parent
for parent in (current, *current.parents):
if (parent / ".git").exists():
return parent
return Path(__file__).resolve().parents[4]
REPO_ROOT = _find_repo_root()
HOOKS_DIR = REPO_ROOT / ".claude" / "hooks"
def _load_hook(name: str):
"""Import a hook script by filename via importlib (outside package)."""
path = HOOKS_DIR / name
if not path.exists():
pytest.skip(f"Hook script not found: {path}")
spec = importlib.util.spec_from_file_location(name.replace(".py", ""), path)
assert spec is not None and spec.loader is not None
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod) # type: ignore[union-attr]
return mod
# ---------------------------------------------------------------------------
# permissions.py
# ---------------------------------------------------------------------------
def test_trusted_cross_writers_contains_expected_members():
"""TRUSTED_CROSS_WRITERS must include devpulse, seedgo, and spawn."""
from aipass.seedgo.apps.modules.permissions import TRUSTED_CROSS_WRITERS
assert "devpulse" in TRUSTED_CROSS_WRITERS
assert "seedgo" in TRUSTED_CROSS_WRITERS
assert "spawn" in TRUSTED_CROSS_WRITERS
def test_is_trusted_caller_returns_true_for_devpulse():
"""devpulse is a trusted cross-writer."""
from aipass.seedgo.apps.modules.permissions import is_trusted_caller
assert is_trusted_caller("devpulse") is True
def test_is_trusted_caller_returns_true_for_seedgo():
"""seedgo is a trusted cross-writer."""
from aipass.seedgo.apps.modules.permissions import is_trusted_caller
assert is_trusted_caller("seedgo") is True
def test_is_trusted_caller_returns_true_for_spawn():
"""spawn is a trusted cross-writer."""
from aipass.seedgo.apps.modules.permissions import is_trusted_caller
assert is_trusted_caller("spawn") is True
def test_is_trusted_caller_returns_false_for_unknown():
"""Regular branches are not trusted cross-writers."""
from aipass.seedgo.apps.modules.permissions import is_trusted_caller
assert is_trusted_caller("flow") is False
assert is_trusted_caller("memory") is False
assert is_trusted_caller("random_branch") is False
def test_identify_caller_returns_empty_when_no_passport(tmp_path):
"""identify_caller returns empty string when no passport.json is found."""
from aipass.seedgo.apps.modules.permissions import identify_caller
result = identify_caller(str(tmp_path))
assert result == ""
def test_identify_caller_reads_branch_name_from_passport(tmp_path):
"""identify_caller reads branch_name from branch_info section."""
from aipass.seedgo.apps.modules.permissions import identify_caller
trinity = tmp_path / ".trinity"
trinity.mkdir()
passport = trinity / "passport.json"
passport.write_text(json.dumps({"branch_info": {"branch_name": "testbranch"}}), encoding="utf-8")
result = identify_caller(str(tmp_path))
assert result == "testbranch"
def test_identify_caller_falls_back_to_identity_name(tmp_path):
"""identify_caller falls back to identity.name when branch_info absent."""
from aipass.seedgo.apps.modules.permissions import identify_caller
trinity = tmp_path / ".trinity"
trinity.mkdir()
passport = trinity / "passport.json"
passport.write_text(json.dumps({"identity": {"name": "fallback_branch"}}), encoding="utf-8")
result = identify_caller(str(tmp_path))
assert result == "fallback_branch"
# ---------------------------------------------------------------------------
# pre_edit_gate.py v1.3.0 — Track E rules
# ---------------------------------------------------------------------------
def test_gate_allows_non_edit_tool():
"""Non-edit tools (Read) must pass through without blocking."""
mod = _load_hook("pre_edit_gate.py")
payload = json.dumps({"tool_name": "Read", "tool_input": {"file_path": "/tmp/foo.py"}})
with patch("sys.stdin", io.StringIO(payload)):
mod.main()
def test_gate_blocks_inbox_json_write(capsys):
"""Any write targeting .ai_mail.local/inbox.json must be blocked."""
mod = _load_hook("pre_edit_gate.py")
inbox_path = "/home/user/Projects/AIPass/src/aipass/flow/.ai_mail.local/inbox.json"
payload = json.dumps({"tool_name": "Write", "tool_input": {"file_path": inbox_path}})
with patch("sys.stdin", io.StringIO(payload)):
with pytest.raises(SystemExit) as exc_info:
mod.main()
assert exc_info.value.code == 2
captured = capsys.readouterr()
result = json.loads(captured.out)
assert result["decision"] == "block"
assert "inbox.json" in result["reason"].lower() or "drone" in result["reason"].lower()
def test_gate_blocks_cross_branch_write_from_untrusted(capsys):
"""Untrusted branch writing to a different branch must be blocked."""
mod = _load_hook("pre_edit_gate.py")
payload = json.dumps(
{
"tool_name": "Edit",
"tool_input": {"file_path": "/repo/src/aipass/flow/apps/modules/foo.py"},
"cwd": "/repo/src/aipass/memory",
}
)
with patch("sys.stdin", io.StringIO(payload)):
with pytest.raises(SystemExit) as exc_info:
mod.main()
assert exc_info.value.code == 2
captured = capsys.readouterr()
result = json.loads(captured.out)
assert result["decision"] == "block"
def test_gate_allows_cross_branch_write_from_devpulse(capsys):
"""devpulse may write to any branch without being blocked."""
mod = _load_hook("pre_edit_gate.py")
payload = json.dumps(
{
"tool_name": "Edit",
"tool_input": {"file_path": "/repo/src/aipass/flow/apps/modules/foo.py"},
"cwd": "/repo/src/aipass/devpulse",
}
)
with patch("sys.stdin", io.StringIO(payload)):
mod.main()
captured = capsys.readouterr()
assert captured.out == ""
def test_gate_allows_cross_branch_write_from_seedgo(capsys):
"""seedgo may write to any branch without being blocked."""
mod = _load_hook("pre_edit_gate.py")
payload = json.dumps(
{
"tool_name": "Edit",
"tool_input": {"file_path": "/repo/src/aipass/flow/apps/modules/foo.py"},
"cwd": "/repo/src/aipass/seedgo",
}
)
with patch("sys.stdin", io.StringIO(payload)):
mod.main()
captured = capsys.readouterr()
assert captured.out == ""
def test_gate_allows_cross_branch_write_from_spawn(capsys):
"""spawn may write to any branch without being blocked."""
mod = _load_hook("pre_edit_gate.py")
payload = json.dumps(
{
"tool_name": "Write",
"tool_input": {"file_path": "/repo/src/aipass/prax/apps/modules/bar.py"},
"cwd": "/repo/src/aipass/spawn",
}
)
with patch("sys.stdin", io.StringIO(payload)):
mod.main()
captured = capsys.readouterr()
assert captured.out == ""
def test_gate_allows_same_branch_write(capsys):
"""Writes within the same branch must not be blocked by the cross-branch rule."""
mod = _load_hook("pre_edit_gate.py")
payload = json.dumps(
{
"tool_name": "Edit",
"tool_input": {"file_path": "/repo/src/aipass/flow/apps/modules/foo.py"},
"cwd": "/repo/src/aipass/flow",
}
)
with patch("sys.stdin", io.StringIO(payload)):
mod.main()
captured = capsys.readouterr()
assert captured.out == ""
# ---------------------------------------------------------------------------
# drone auth.py — ALLOWED_CALLERS derived from permissions
# ---------------------------------------------------------------------------
def test_drone_auth_allowed_callers_matches_permissions():
"""Hook and drone must reach the same decision for the same caller."""
from aipass.drone.apps.plugins.devpulse_ops.auth import ALLOWED_CALLERS
from aipass.seedgo.apps.modules.permissions import TRUSTED_CROSS_WRITERS
for branch in TRUSTED_CROSS_WRITERS:
assert branch in ALLOWED_CALLERS, f"'{branch}' in TRUSTED_CROSS_WRITERS but missing from drone ALLOWED_CALLERS"
def test_drone_auth_allowed_callers_includes_devpulse():
"""devpulse must remain in drone ALLOWED_CALLERS."""
from aipass.drone.apps.plugins.devpulse_ops.auth import ALLOWED_CALLERS
assert "devpulse" in ALLOWED_CALLERS
def test_drone_auth_allowed_callers_includes_seedgo():
"""seedgo must be in drone ALLOWED_CALLERS."""
from aipass.drone.apps.plugins.devpulse_ops.auth import ALLOWED_CALLERS
assert "seedgo" in ALLOWED_CALLERS
def test_drone_auth_allowed_callers_includes_spawn():
"""spawn must be in drone ALLOWED_CALLERS."""
from aipass.drone.apps.plugins.devpulse_ops.auth import ALLOWED_CALLERS
assert "spawn" in ALLOWED_CALLERS
# ---------------------------------------------------------------------------
# inbox_audit.py — handle_command routing + _scan_inbox
# ---------------------------------------------------------------------------
def test_inbox_audit_ignores_non_audit_command():
"""handle_command returns False for non-audit command names."""
from aipass.seedgo.apps.modules.inbox_audit import handle_command
assert handle_command("standards_query", ["inbox-ids"]) is False
assert handle_command("checklist", ["inbox-ids"]) is False
def test_inbox_audit_handles_inbox_ids_subcommand():
"""handle_command returns True and runs scan for `audit inbox-ids`."""
from aipass.seedgo.apps.modules.inbox_audit import handle_command
with patch("aipass.seedgo.apps.modules.inbox_audit._run_inbox_id_scan", return_value=0):
result = handle_command("audit", ["inbox-ids"])
assert result is True
def test_inbox_audit_ignores_other_audit_subcommands():
"""handle_command returns False for audit subcommands other than inbox-ids."""
from aipass.seedgo.apps.modules.inbox_audit import handle_command
assert handle_command("audit", ["aipass"]) is False
assert handle_command("audit", ["flow"]) is False
def test_inbox_audit_scan_detects_bad_id(tmp_path):
"""_scan_inbox flags message ids that are not 8-char lowercase hex."""
from aipass.seedgo.apps.modules.inbox_audit import _scan_inbox
inbox = tmp_path / "inbox.json"
inbox.write_text(
json.dumps(
{
"messages": [
{"id": "not-hex!", "subject": "bad", "from": "@test", "status": "new"},
{"id": "a1b2c3d4", "subject": "ok", "from": "@test", "status": "new"},
]
}
),
encoding="utf-8",
)
violations = _scan_inbox(inbox)
assert len(violations) == 1
assert violations[0]["id"] == "not-hex!"
def test_inbox_audit_scan_passes_valid_ids(tmp_path):
"""_scan_inbox returns empty list when all message ids are valid 8-hex."""
from aipass.seedgo.apps.modules.inbox_audit import _scan_inbox
inbox = tmp_path / "inbox.json"
inbox.write_text(
json.dumps(
{
"messages": [
{"id": "a1b2c3d4", "subject": "ok1", "from": "@x", "status": "new"},
{"id": "deadbeef", "subject": "ok2", "from": "@y", "status": "new"},
]
}
),
encoding="utf-8",
)
violations = _scan_inbox(inbox)
assert violations == []
# ---------------------------------------------------------------------------
# delivery.py — deliver_to_inbox_file single-path helper
# Load by file path to avoid cross-branch package import restriction.
# ---------------------------------------------------------------------------
def _load_delivery():
"""Load ai_mail delivery.py by file path (bypasses cross-branch import check)."""
delivery_path = REPO_ROOT / "src" / "aipass" / "ai_mail" / "apps" / "handlers" / "email" / "delivery.py"
if not delivery_path.exists():
pytest.skip(f"delivery.py not found: {delivery_path}")
spec = importlib.util.spec_from_file_location("delivery", delivery_path)
assert spec is not None and spec.loader is not None
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod) # type: ignore[union-attr]
return mod
def test_deliver_to_inbox_file_returns_false_for_missing_inbox(tmp_path):
"""deliver_to_inbox_file returns (False, error, '') when inbox does not exist."""
delivery = _load_delivery()
missing = tmp_path / "inbox.json"
success, error_msg, reply_id = delivery.deliver_to_inbox_file(
missing,
{"from": "@x", "to": "@y", "subject": "s", "message": "m", "timestamp": "t"},
)
assert success is False
assert reply_id == ""
def test_deliver_to_inbox_file_writes_message_and_returns_id(tmp_path):
"""deliver_to_inbox_file writes to inbox and returns the assigned 8-char id."""
delivery = _load_delivery()
inbox = tmp_path / "inbox.json"
inbox.write_text(
json.dumps({"mailbox": "inbox", "total_messages": 0, "unread_count": 0, "messages": []}),
encoding="utf-8",
)
email_data = {
"from": "@sender",
"to": "@recv",
"subject": "Hello",
"message": "body",
"timestamp": "2026-04-21 00:00:00",
}
with patch.object(delivery, "_send_desktop_notification"):
success, error_msg, reply_id = delivery.deliver_to_inbox_file(inbox, email_data)
assert success is True
assert len(reply_id) == 8
data = json.loads(inbox.read_text())
assert len(data["messages"]) == 1
assert data["messages"][0]["id"] == reply_id