refactor(drone): .trinity/-marker walk-ups replace src/aipass hardcodes — portable resolution + access checks (TDPLAN-0010, FPLAN-0296)

This commit is contained in:
AIOSAI
2026-07-01 08:54:05 -07:00
parent 13463c0ce0
commit f914ab616e
9 changed files with 102 additions and 51 deletions
+15
View File
@@ -81,6 +81,21 @@ PyPI version — not the changelog header.
degrades gracefully when `@memory` is unavailable (empty meta-tabs, no crash).
297 tests pass. (built by @spawn, FPLAN-0294, TDPLAN-0010)
- **Drone resolution + access checks made project-portable (TDPLAN-0010
foundation)** — five `src/aipass`/fixed-depth self-location hardcodes are
replaced with `.trinity/`-marker walk-ups: `rm_handler` sibling protection,
`commit_handler` test-gate branch detection, `broker/daemon` allowed-bases,
the `handlers/__init__` import-guard access check (now `is_relative_to()`
instead of scanning path parts for the literal `aipass`), and
`registry_handler`'s `parents[4]` last-resort (now a
`.git`/`pyproject.toml`/`setup.py`/`setup.cfg` marker walk). `@name`→path
resolution now works for an agent in any project layout via a CWD-first
registry walk (AIPASS_HOME only as a last resort when the CWD ancestry has no
registry at all). The `_validate_branch_path` containment invariant is
untouched — per-project isolation preserved. (Drone uses its own resolver, not
the shared `registry_discovery.py`.) 838 tests pass. (built by @drone,
FPLAN-0296, TDPLAN-0010)
- **ai_mail routing made project-portable (TDPLAN-0010 foundation)** — the
fixed-depth `_REPO_ROOT = parents[2].parents[2]` self-location in
`email.py` / `email_send.py` / `dispatch.py` (4 sites) is replaced with the
+13 -18
View File
@@ -41,13 +41,15 @@ def _find_real_caller():
return None, None
_BRANCH_ROOT = str(Path(__file__).resolve().parents[2])
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]
"""Extract branch name from a file path by walking up to .trinity/."""
path = Path(filepath)
for parent in [path, *path.parents]:
if (parent / ".trinity").is_dir():
return parent.name
return "unknown"
@@ -60,14 +62,13 @@ def _guard_branch_access():
"""
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)
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
@@ -79,10 +80,7 @@ def _guard_branch_access():
return # Allow command-line Python through
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.replace("\\", "/"):
if Path(caller_file).is_relative_to(_BRANCH_ROOT):
return # Same branch, allowed
# External caller - block access
@@ -90,6 +88,7 @@ def _guard_branch_access():
caller_filename = Path(caller_file).name
blocked_import = import_line if import_line else "unknown"
module_api = f"{MY_BRANCH}.apps.modules"
raise ImportError(
f"\n{'=' * 60}\n"
f"ACCESS DENIED: Cross-branch handler import blocked\n"
@@ -99,11 +98,7 @@ def _guard_branch_access():
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" Use the module API instead: {module_api}.<module>\n"
f"\n"
f" For full standards guide:\n"
f" drone @seedgo handlers\n"
@@ -27,6 +27,7 @@ from __future__ import annotations
import hashlib
import hmac as hmac_mod
import json
import os
import secrets
import socket
import threading
@@ -50,8 +51,6 @@ _TMP_BASES = (Path("/tmp"), Path("/var/tmp"))
def _find_project_root() -> Path | None:
"""Walk up from CWD to find *_REGISTRY.json; return its parent as project root."""
import os
cwd = Path.cwd()
for parent in [cwd, *cwd.parents]:
if list(parent.glob("*_REGISTRY.json")):
@@ -166,11 +165,24 @@ class BrokerDaemon:
if identity == "devpulse":
bases.append(self._repo_root)
return bases
branch_dir = self._repo_root / "src" / "aipass" / identity
if branch_dir.is_dir():
branch_dir = self._resolve_branch_dir(identity)
if branch_dir and branch_dir.is_dir():
bases.append(branch_dir)
return bases
def _resolve_branch_dir(self, identity: str) -> Path | None:
"""Find a branch directory by name via .trinity/ marker walk."""
if self._repo_root is None:
return None
for root, dirs, _files in os.walk(self._repo_root):
depth = len(Path(root).relative_to(self._repo_root).parts)
if depth > 3:
dirs.clear()
continue
if Path(root).name == identity and (Path(root) / ".trinity").is_dir():
return Path(root).resolve()
return None
def _handle_identify(self, req: BrokerRequest) -> tuple[BrokerResponse, str | None]:
"""Verify HMAC and bind identity to the connection."""
audit_entry: dict = {
@@ -18,6 +18,18 @@ from aipass.drone.apps.handlers.json import json_handler
from aipass.drone.apps.handlers.git.lock_handler import find_repo_root
def _find_branch_for_path(filepath: str, repo_root: Path) -> tuple[str, Path] | None:
"""Find which branch a changed file belongs to by walking up to .trinity/."""
abs_path = (repo_root / filepath).resolve()
root = repo_root.resolve()
for parent in [abs_path.parent, *abs_path.parent.parents]:
if not parent.is_relative_to(root):
break
if (parent / ".trinity").is_dir():
return parent.name, parent
return None
def _run_test_gate(repo_root: Path) -> dict | None:
"""Run pytest for changed branches. Returns error dict if tests fail, None if all pass."""
status_result = subprocess.run(
@@ -26,21 +38,22 @@ def _run_test_gate(repo_root: Path) -> dict | None:
text=True,
cwd=str(repo_root),
)
changed_branches: set[str] = set()
changed_branches: dict[str, Path] = {}
for line in status_result.stdout.splitlines():
if len(line) < 4:
continue
filepath = line[3:].split(" -> ")[-1]
parts = Path(filepath).parts
if len(parts) >= 3 and parts[0] == "src" and parts[1] == "aipass":
changed_branches.add(parts[2])
result = _find_branch_for_path(filepath, repo_root)
if result:
name, branch_path = result
changed_branches[name] = branch_path
venv_python = repo_root / ".venv" / "bin" / "python"
python_bin = str(venv_python) if venv_python.exists() else "python3"
failed_branches: list[tuple[str, str]] = []
for branch_name in sorted(changed_branches):
test_dir = repo_root / "src" / "aipass" / branch_name / "tests"
test_dir = changed_branches[branch_name] / "tests"
if not test_dir.is_dir():
continue
try:
@@ -144,13 +144,16 @@ def find_registry() -> Path:
if hit is not None:
return hit
# Fallback — use package-relative path; glob there too
fallback_dir = Path(__file__).resolve().parents[4]
# Fallback — walk up from this file to the project root (marker-based)
_markers = (".git", "pyproject.toml", "setup.py", "setup.cfg")
fallback_dir = Path(__file__).resolve().parent
for parent in [fallback_dir, *fallback_dir.parents]:
if any((parent / m).exists() for m in _markers):
fallback_dir = parent
break
hit = _first_registry_in(fallback_dir)
if hit is not None:
return hit
# Ultimate fallback: return a conventional name so the caller
# gets a clear "not found" path in the error message.
return fallback_dir / "AIPASS_REGISTRY.json"
+18 -14
View File
@@ -67,16 +67,23 @@ def get_allowed_roots() -> list[Path]:
return roots
def _find_branch_root(path: Path, project_root: Path) -> Path | None:
"""Walk up from *path* looking for .trinity/; return branch dir or None."""
root = project_root.resolve()
for parent in [path, *path.parents]:
if not parent.is_relative_to(root):
break
if (parent / ".trinity").is_dir():
return parent
return None
def _detect_current_branch(project_root: Path | None) -> str | None:
"""Return the branch name the CWD lives in, or None."""
if project_root is None:
return None
cwd = Path.cwd().resolve()
aipass_src = project_root / "src" / "aipass"
if not cwd.is_relative_to(aipass_src):
return None
rel = cwd.relative_to(aipass_src)
return rel.parts[0] if rel.parts else None
branch_root = _find_branch_root(Path.cwd().resolve(), project_root)
return branch_root.name if branch_root else None
def _resolve_git_dir(path: Path) -> Path | None:
@@ -109,14 +116,11 @@ def check_carveouts(resolved: Path, project_root: Path | None) -> tuple[bool, st
return True, "Protected: path resolves inside .git worktree gitdir"
if project_root is not None:
aipass_src = project_root / "src" / "aipass"
if resolved.is_relative_to(aipass_src):
rel = resolved.relative_to(aipass_src)
if rel.parts:
target_branch = rel.parts[0]
current_branch = _detect_current_branch(project_root)
if current_branch is None or target_branch != current_branch:
return True, (f"Protected: path is inside sibling branch src/aipass/{target_branch}/")
target_branch_root = _find_branch_root(resolved, project_root)
if target_branch_root is not None:
current_branch = _detect_current_branch(project_root)
if current_branch is None or target_branch_root.name != current_branch:
return True, f"Protected: path is inside sibling branch {target_branch_root.name}/"
return False, ""
+3 -1
View File
@@ -90,11 +90,12 @@ def _send_identified(broker: BrokerDaemon, branch: str, req: BrokerRequest) -> B
@pytest.fixture()
def repo_root(tmp_path: Path) -> Path:
"""Set up a mock repo root with branch directories."""
"""Set up a mock repo root with branch directories marked by .trinity/."""
root = tmp_path / "repo"
root.mkdir()
branch = root / "src" / "aipass" / "testbranch"
branch.mkdir(parents=True)
(branch / ".trinity").mkdir()
(branch / "deleteme.txt").write_text("delete me", encoding="utf-8")
(branch / "subdir").mkdir()
(branch / "subdir" / "nested.txt").write_text("nested", encoding="utf-8")
@@ -102,6 +103,7 @@ def repo_root(tmp_path: Path) -> Path:
(branch / ".git" / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
sibling = root / "src" / "aipass" / "sibling"
sibling.mkdir(parents=True)
(sibling / ".trinity").mkdir()
(sibling / "important.txt").write_text("don't delete", encoding="utf-8")
return root
+10 -4
View File
@@ -387,8 +387,11 @@ class TestCommitChanges:
stderr="",
)
test_dir = repo_dir / "src" / "aipass" / "drone" / "tests"
test_dir.mkdir(parents=True)
drone_dir = repo_dir / "src" / "aipass" / "drone"
drone_dir.mkdir(parents=True)
(drone_dir / ".trinity").mkdir()
test_dir = drone_dir / "tests"
test_dir.mkdir()
with (
patch("shutil.which", return_value="/usr/bin/ruff"),
@@ -417,8 +420,11 @@ class TestCommitChanges:
mock_diff = MagicMock(returncode=1, stdout="", stderr="")
mock_commit = MagicMock(returncode=0, stdout="[main abc999] green commit", stderr="")
test_dir = repo_dir / "src" / "aipass" / "drone" / "tests"
test_dir.mkdir(parents=True)
drone_dir = repo_dir / "src" / "aipass" / "drone"
drone_dir.mkdir(parents=True)
(drone_dir / ".trinity").mkdir()
test_dir = drone_dir / "tests"
test_dir.mkdir()
with (
patch("shutil.which", return_value="/usr/bin/ruff"),
+2 -1
View File
@@ -36,10 +36,11 @@ def project_dir(tmp_path):
@pytest.fixture()
def project_with_branches(project_dir):
"""Project root with src/aipass/<branch> layout for sibling tests."""
"""Project root with branch dirs containing .trinity/ markers."""
for branch in ("drone", "api", "flow"):
d = project_dir / "src" / "aipass" / branch
d.mkdir(parents=True)
(d / ".trinity").mkdir()
(d / "README.md").write_text(f"# {branch}")
return project_dir