Merge pull request #260 from AIOSAI/system/devpulse-s90-fix-cross-project-email-sender-detection-calle

feat(system): S90: fix cross-project email — sender detection + caller registry target resolution
This commit is contained in:
AIPass
2026-04-12 01:26:12 -07:00
committed by GitHub
4 changed files with 100 additions and 9 deletions
@@ -33,6 +33,45 @@ _REPO_ROOT = find_repo_root()
_INBOX_LOCK = None
def _load_caller_project_branches(caller_cwd: str) -> Dict[str, str]:
"""Load branches from the caller's project registry.
Walks up from caller_cwd to find a *_REGISTRY.json file, then extracts
branch email→path mappings. Used when the target branch isn't in the
AIPass registry (e.g. @strategy in Vera Studio).
"""
current = Path(caller_cwd).resolve()
for _ in range(10):
for reg_file in current.glob("*_REGISTRY.json"):
try:
with open(reg_file, "r", encoding="utf-8") as f:
data = json.load(f)
result = {}
branches = data.get("branches", [])
if isinstance(branches, list):
for b in branches:
email = b.get("email", f"@{b.get('name', '').lower()}")
path = b.get("path", "")
if path and not Path(path).is_absolute():
path = str((reg_file.parent / path).resolve())
result[email] = path
elif isinstance(branches, dict):
for name, info in branches.items():
email = info.get("email", f"@{name}")
path = info.get("path", "")
if path and not Path(path).is_absolute():
path = str((reg_file.parent / path).resolve())
result[email] = path
return result
except Exception as exc:
logger.warning("Failed reading caller registry %s: %s", reg_file, exc)
parent = current.parent
if parent == current:
break
current = parent
return {}
def _auto_register_contact(email: str, branch_path: Path, inbox_file: Path) -> None:
"""Auto-register a recipient in the contacts address book after successful delivery.
@@ -239,10 +278,17 @@ def deliver_email_to_branch(
if not matched:
return False, f"Could not resolve path to email: {to_branch}"
# Map email address to branch path
# Map email address to branch path (AIPass registry + caller's project registry)
all_branches = get_all_branches()
branches = {b["email"]: b["path"] for b in all_branches}
if to_branch not in branches:
# Check caller's project registry for local branches (e.g. @strategy in Vera Studio)
caller_cwd = os.environ.get("AIPASS_CALLER_CWD", "")
if caller_cwd:
caller_branches = _load_caller_project_branches(caller_cwd)
branches.update(caller_branches)
if to_branch not in branches:
error_msg = f"Unknown branch email: {to_branch} (available: {len(branches)} branches)"
return False, error_msg
@@ -136,6 +136,26 @@ def detect_branch_from_pwd() -> Optional[Dict]:
branch_info = _lookup_branch_by_name(caller_branch)
if branch_info:
return branch_info
# Last resort: synthesize from env vars (external project, first contact)
caller_cwd = os.environ.get("AIPASS_CALLER_CWD", "")
if caller_cwd:
cwd_path = Path(caller_cwd)
name_key = caller_branch.lstrip("@").lower()
mailbox = cwd_path / ".ai_mail.local"
if not mailbox.exists():
# Walk up to find project-level mailbox
for parent in [cwd_path] + list(cwd_path.parents):
candidate = parent / ".ai_mail.local"
if candidate.exists():
mailbox = candidate
break
return {
"name": name_key,
"path": str(cwd_path),
"email": f"@{name_key}",
"status": "active",
"type": "external",
}
# Fallback: use caller's CWD for path-based detection (local only)
caller_cwd = os.environ.get("AIPASS_CALLER_CWD")
+10 -7
View File
@@ -240,18 +240,21 @@ class TestDetectBranchFromPwd:
result = detect_branch_from_pwd()
assert result is None
def test_invalid_branch_name_falls_through(self, clean_env, tmp_path, temp_registry):
"""Nonexistent branch in AIPASS_CALLER_BRANCH should fall through to CWD.
def test_unknown_branch_synthesizes_from_env(self, clean_env, tmp_path, temp_registry):
"""Unknown branch in AIPASS_CALLER_BRANCH synthesizes info from env vars.
When env branch name is not in registry, function falls back to
AIPASS_CALLER_CWD. If that also fails, result is None.
When env branch name is not in registry or contacts, function
synthesizes a minimal branch_info dict from the env vars so
external project callers can still send mail.
"""
registry_path, _ = temp_registry
with patch("aipass.ai_mail.apps.handlers.users.branch_detection.BRANCH_REGISTRY_PATH", registry_path):
os.environ["AIPASS_CALLER_BRANCH"] = "totally_fake_branch_xyz"
os.environ["AIPASS_CALLER_CWD"] = str(tmp_path) # Also not a branch
os.environ["AIPASS_CALLER_BRANCH"] = "VERA-STUDIO"
os.environ["AIPASS_CALLER_CWD"] = str(tmp_path)
result = detect_branch_from_pwd()
assert result is None
assert result is not None
assert result["name"].lower() == "vera-studio"
assert result["email"].lower() == "@vera-studio"
# ─── find_branch_root() tests ───────────────────────────
@@ -42,7 +42,11 @@ def find_entry_point(branch_path: str, branch_name: str) -> Path:
def detect_caller_branch_name(cwd: Path) -> str | None:
"""Walk up from cwd to find .trinity/passport.json and extract branch name."""
"""Walk up from cwd to find .trinity/passport.json and extract branch name.
Falls back to project name from registry if no passport found (external projects
calling from project root without a branch-level CWD).
"""
current = cwd.resolve()
for _ in range(10):
passport = current / ".trinity" / "passport.json"
@@ -64,6 +68,24 @@ def detect_caller_branch_name(cwd: Path) -> str | None:
if parent == current:
break
current = parent
# Fallback: detect project name from registry file (external projects at root)
current = cwd.resolve()
for _ in range(10):
for reg_file in current.glob("*_REGISTRY.json"):
try:
with open(reg_file, "r", encoding="utf-8") as f:
data = json.load(f)
meta = data.get("metadata", {})
project_name = meta.get("project_name") or meta.get("name")
if project_name:
return project_name.lower().replace(" ", "-")
except Exception as exc:
logger.info("Failed to read registry %s: %s", reg_file, exc)
parent = current.parent
if parent == current:
break
current = parent
return None