fix(ai_mail): caller identity detection for Docker and local (FPLAN-0011)

Drone now detects caller branch name from passport.json and passes AIPASS_CALLER_BRANCH env var. ai_mail checks this first for path-independent identity resolution, falling back to CWD-based detection. Also fixes dict-format registry handling in delivery.py and user.py.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
AIOSAI
2026-03-08 11:07:37 -07:00
co-authored by Claude Opus 4.6
parent bd22f1a076
commit d7eab7314d
5 changed files with 376 additions and 8 deletions
@@ -90,7 +90,11 @@ def get_all_branches() -> List[Dict]:
registry_data = json.load(f)
# Parse branch entries from JSON structure
for branch in registry_data.get("branches", []):
# Handle both formats: list of dicts or dict keyed by name
raw_branches = registry_data.get("branches", [])
if isinstance(raw_branches, dict):
raw_branches = list(raw_branches.values())
for branch in raw_branches:
branch_name = branch.get("name", "")
path = branch.get("path", "")
@@ -44,6 +44,20 @@ def _find_repo_root() -> Path:
BRANCH_REGISTRY_PATH = _find_repo_root() / "AIPASS_REGISTRY.json"
def _get_branches_list(registry: dict) -> list:
"""Normalize branches from registry to a list of dicts.
Handles both formats:
- List: [{"name": "DEVPULSE", ...}, ...]
- Dict: {"devpulse": {"name": "devpulse", ...}, ...}
"""
branches = registry.get("branches", [])
if isinstance(branches, dict):
return list(branches.values())
return branches
# =============================================
# BRANCH DETECTION FUNCTIONS
# =============================================
@@ -67,7 +81,14 @@ def detect_branch_from_pwd() -> Dict | None:
None if no branch detected
"""
try:
# Use caller's CWD if passed by drone, otherwise fall back to process CWD
# Primary: use explicit branch name passed by drone (works in Docker + local)
caller_branch = os.environ.get("AIPASS_CALLER_BRANCH")
if caller_branch:
branch_info = _lookup_branch_by_name(caller_branch)
if branch_info:
return branch_info
# Fallback: use caller's CWD for path-based detection (local only)
caller_cwd = os.environ.get("AIPASS_CALLER_CWD")
cwd = Path(caller_cwd) if caller_cwd else Path.cwd()
@@ -87,6 +108,38 @@ def detect_branch_from_pwd() -> Dict | None:
return None
def _lookup_branch_by_name(branch_name: str) -> Dict | None:
"""
Look up branch in the registry by name (case-insensitive).
Handles both registry formats:
- List format: {"branches": [{"name": "DEVPULSE", ...}, ...]}
- Dict format: {"branches": {"devpulse": {"name": "devpulse", ...}, ...}}
Args:
branch_name: Branch name (e.g., "DEVPULSE", "devpulse")
Returns:
Dict with branch info from registry, or None if not found
"""
if not BRANCH_REGISTRY_PATH.exists():
return None
try:
with open(BRANCH_REGISTRY_PATH, 'r', encoding='utf-8') as f:
registry = json.load(f)
name_lower = branch_name.lower()
for branch in _get_branches_list(registry):
if branch.get("name", "").lower() == name_lower:
return branch
return None
except Exception:
return None
def find_branch_root(start_path: Path) -> Path | None:
"""
Walk up directory tree to find branch root.
@@ -138,7 +191,7 @@ def get_branch_info_from_registry(branch_path: Path) -> Dict | None:
branch_path_resolved = branch_path.resolve()
# Search registry for matching path
for branch in registry.get("branches", []):
for branch in _get_branches_list(registry):
reg_path = Path(branch["path"])
# Resolve relative paths against registry location, not CWD
if not reg_path.is_absolute():
@@ -107,10 +107,9 @@ def get_user_by_email(email: str) -> Dict | None:
Returns:
Dict containing user info, or None if not found
"""
from .branch_detection import get_branch_info_from_registry
from .branch_detection import BRANCH_REGISTRY_PATH, _get_branches_list
# Use registry lookup
from .branch_detection import BRANCH_REGISTRY_PATH
registry_path = BRANCH_REGISTRY_PATH
if not registry_path.exists():
return None
@@ -120,7 +119,7 @@ def get_user_by_email(email: str) -> Dict | None:
with open(registry_path, 'r', encoding='utf-8') as f:
registry = json.load(f)
for branch in registry.get("branches", []):
for branch in _get_branches_list(registry):
if branch.get("email") == email:
branch_path = Path(branch.get("path", ""))
return {
@@ -141,7 +140,7 @@ def get_all_users() -> Dict[str, Dict]:
Returns:
Dict mapping branch emails to user info dicts
"""
from .branch_detection import BRANCH_REGISTRY_PATH
from .branch_detection import BRANCH_REGISTRY_PATH, _get_branches_list
registry_path = BRANCH_REGISTRY_PATH
if not registry_path.exists():
return {}
@@ -152,7 +151,7 @@ def get_all_users() -> Dict[str, Dict]:
registry = json.load(f)
users = {}
for branch in registry.get("branches", []):
for branch in _get_branches_list(registry):
email = branch.get("email", "")
if email:
branch_path = Path(branch.get("path", ""))
+31
View File
@@ -5,6 +5,7 @@ Routes commands to branch entry points by resolving symbolic @branch names,
locating the branch's apps/{name}.py entry point, and executing via subprocess.
"""
import json
import logging
import sys
from pathlib import Path
@@ -27,6 +28,31 @@ def _find_entry_point(branch_path: str, branch_name: str) -> Path:
return entry_point
def _detect_caller_branch_name(cwd: Path) -> str | None:
"""Walk up from cwd to find .trinity/passport.json and extract branch name."""
current = cwd.resolve()
for _ in range(10):
passport = current / ".trinity" / "passport.json"
if passport.exists():
try:
with open(passport, "r", encoding="utf-8") as f:
data = json.load(f)
# Handle both passport formats:
# v1: branch_info.branch_name (local/full passport)
# v2: identity.name (Docker/minimal passport)
name = data.get("branch_info", {}).get("branch_name")
if not name:
name = data.get("identity", {}).get("name")
return name
except Exception:
return None
parent = current.parent
if parent == current:
break
current = parent
return None
def route_command(
target: str,
command: str,
@@ -57,6 +83,11 @@ def route_command(
# Pass caller's CWD so target branches can detect who invoked them
caller_env = {"AIPASS_CALLER_CWD": str(Path.cwd())}
# Detect caller branch name from passport.json and pass it explicitly
caller_branch = _detect_caller_branch_name(Path.cwd())
if caller_branch:
caller_env["AIPASS_CALLER_BRANCH"] = caller_branch
result = execute_command(
executable=sys.executable,
args=cmd_args,
@@ -0,0 +1,281 @@
# FPLAN-0011 - Fix ai_mail caller identity detection for Docker and local
**Created**: 2026-03-08
**Branch**: flow
**Status**: Active
**Type**: Standard Plan
---
## What Are Flow Plans?
Flow Plans (FPLANs) are for **BUILDING** - autonomous construction of systems, features, modules.
**This is NOT for:**
- Research or exploration (use agents directly)
- Quick fixes (just do it)
- Discussion or planning (that happens before creating the FPLAN)
**This IS for:**
- Building features or modules
- Single focused construction tasks
- Sub-plans within a master plan
---
## When to Use This vs Master Plan
| This (Default) | Master Plan |
|----------------|-------------|
| Single focused task | 3+ phases, complex build |
| Self-contained | Roadmap + multiple sub-plans |
| Quick build | Multi-session project |
| One phase of a master | Entire branch/system build |
**Need a master plan?** `drone @flow create "subject" master`
---
## Branch Directory Structure
Use dedicated directories - don't scatter files:
| Directory | Purpose |
|-----------|---------|
| `apps/` | Code (modules/, handlers/) |
| `tests/` | All test files |
| `tools/` | Utility scripts |
| `artifacts/` | Agent outputs |
| `docs/` | Documentation |
---
## Critical: Branch Manager Role
**You are the ORCHESTRATOR, not the builder.**
Your 200k context is precious. Burning it on file reads and code writing risks compaction during autonomous work. Agents have clean context - use them for ALL building.
| You Do (Orchestrator) | Agents Do (Builders) |
|-----------------------|----------------------|
| Create plans | Write code |
| Give instructions | Run tests |
| Review output | Read/modify files |
| Course correct | Research/exploration |
| Update memories | Heavy lifting |
| Send status emails | Single-task execution |
**Pattern:** Instruct agent → Wait for completion → Review output → Next step
---
## Seek Branch Expertise
Don't figure everything out alone. Other branches are domain experts - ask them first.
**Before building anything that touches another branch's domain:**
```bash
ai_mail send @branch "Question: [topic]" "I'm working on X and need guidance on Y. What's the best approach?"
```
**Common examples:**
- Building something with email? Ask @ai_mail how delivery works
- Need routing or @ resolution? Ask @drone
- Unsure about standards? Ask @seedgo for reference code
- Need persistent storage or search? Ask @memory_bank
- Event-driven behavior? Ask @trigger about their event system
- Dashboard integration? Ask @devpulse about update_section()
They have deep memory on their systems. A 1-email question saves you hours of guessing.
---
## Notepad
Keep `notepad.md` in your branch directory as a shared scratchpad during the build. Use it for:
- **Status updates** - Quick progress lines so the user can glance without asking
- **Questions for the user** - Non-urgent questions that can wait for the next check-in
- **Notes to self** - Decisions made, things to revisit, gotchas discovered
Update it as you work - lightweight, not formal. The user checks it when they want to, skips it when busy.
---
## Command Reference
When unsure about syntax, use `--help`:
```bash
# Flow - Plan management
drone @flow create . "subject" # Create plan (. = current dir)
drone @flow close FPLAN-XXXX # Close plan
drone @flow list # List active plans
drone @flow --help # Full help
# Seedgo - Quality gates
drone @seedgo checklist <file> # 10-point check on file
drone @seedgo audit @branch # Full branch audit
drone @seedgo --help # Full help
# AI_Mail - Status updates
drone @ai_mail send @devpulse "Subject" "Message"
drone @ai_mail --help # Full help
# Discovery
drone systems # All available modules
drone list @branch # Commands for branch
```
---
## Planning Phase
### Goal
ai_mail caller identity detection works in both Docker containers and local environments. When devpulse sends an email via `drone @ai_mail send @spawn "Subject" "Body"`, ai_mail correctly identifies devpulse as the sender — regardless of mount paths.
### Problem
Drone routes commands to ai_mail via subprocess with `cwd=ai_mail_dir`. Drone passes `AIPASS_CALLER_CWD` env var with the original caller's directory. ai_mail's `branch_detection.py` reads this CWD and walks up looking for `.trinity/passport.json`, then matches against the registry.
In Docker: registry paths use container paths (`/home/coder/workspace/AIPass/...`) which are generated fresh. The CWD-to-registry path resolution fails when paths don't match exactly.
### Approach
Two-branch fix — drone and ai_mail cooperate:
**Phase 1 — Drone side** (`src/aipass/drone/apps/modules/router.py`):
- Detect caller branch name from CWD (walk up to find `.trinity/passport.json`, read `identity.name`)
- Pass as `AIPASS_CALLER_BRANCH` env var alongside existing `AIPASS_CALLER_CWD`
- Keep CWD — other branches may need it for different purposes
**Phase 2 — ai_mail side** (`src/aipass/ai_mail/apps/handlers/users/branch_detection.py`):
- Check `AIPASS_CALLER_BRANCH` first — direct name lookup in registry (path-independent)
- Fall back to `AIPASS_CALLER_CWD` path resolution (existing behavior)
- Fall back to `Path.cwd()` (original behavior)
**Phase 3 — Test in Docker**:
- Copy fixed files into `aipass-fresh-test` container
- Run `cd src/aipass/devpulse && drone @ai_mail send @spawn "Test" "Test body"`
- Verify email lands in spawn's inbox
- Verify sender is "@devpulse"
**Phase 4 — Test locally**:
- Run same command from local devpulse
- Verify existing ai_mail functionality still works
### Reference Documents
- `src/aipass/drone/apps/modules/router.py` — lines 57-66, AIPASS_CALLER_CWD
- `src/aipass/drone/apps/handlers/executor.py` — lines 46-50, env merge
- `src/aipass/ai_mail/apps/handlers/users/branch_detection.py` — lines 51-87, detect_branch_from_pwd()
- `src/aipass/ai_mail/apps/handlers/users/user.py` — lines 37-97, get_current_user()
---
## Agent Preparation (Before Deploying)
Agents can't work blind. They need context before they build.
**Your Prep Work (as orchestrator):**
1. [ ] Know where agent will work (branch path, key directories)
2. [ ] Identify files agent needs to reference or modify
3. [ ] Gather any specs, planning docs, or examples to include
4. [ ] Prepare COMPLETE instructions (agents are stateless)
**Agent's First Task (context building):**
- Agent should explore/read relevant files BEFORE writing code
- "First, read X and Y to understand the current structure"
- "Look at Z for the pattern to follow"
- Context-first, build-second
**What Agents DON'T Have:**
- No prior conversation history
- No memory files loaded automatically
- No knowledge of other branches
- Only what you put in their instructions
**Your instructions determine success - be thorough and specific.**
---
## Agent Instructions Template
```
You are working at [BRANCH_PATH].
TASK: [Specific single task]
CONTEXT:
- [What they need to know]
- Reference: [planning docs, existing code to study]
- First, READ the relevant files to understand current structure
DELIVERABLES:
- [Specific file or output expected]
- Tests → tests/
- Reports/logs → artifacts/reports/ or artifacts/logs/
CONSTRAINTS:
- Follow Seedgo standards (3-layer architecture)
- Do NOT modify files outside your task scope
- CROSS-BRANCH: Never modify other branches' files unless explicitly authorized by the user
- 2-ATTEMPT RULE: If something fails twice, note the issue and move on
- Do NOT go down rabbit holes debugging
WHEN COMPLETE:
- Verify code runs without syntax errors
- List files created/modified
- Note any issues encountered (with what was attempted)
```
---
## Execution Log
### 2026-03-08
- [ ] Created FPLAN-0011
- [ ] Agent deployed for: [task]
- [ ] Agent completed: [outcome]
- [ ] Seedgo checklist passed: [file]
- [ ] Memories updated
**Log Pattern:** Task → Agent → Outcome → Quality check → Next
**If production stops (critical blocker):**
```bash
drone @ai_mail send @devpulse "PRODUCTION STOPPED: FPLAN-0011" "Issue: [description]. Attempted: [what was tried]. Awaiting guidance."
```
---
## Notes
[Working notes, issues encountered, decisions made]
---
## Completion Checklist
### Before Closing
- [ ] All goals achieved
- [ ] Agent output reviewed and verified
- [ ] Seedgo checklist on new code: `drone @seedgo checklist <file>`
- [ ] Branch memories updated:
- [ ] `BRANCH.local.json` - session/work log
- [ ] `BRANCH.observations.json` - patterns learned (if any)
- [ ] README.md updated (if build changed status/capabilities)
- [ ] Status email sent to @devpulse:
```bash
drone @ai_mail send @devpulse "FPLAN-0011 Complete" "Summary of what was done, any issues, outcomes"
```
**Completion Order:** Memories → README → Email (README before email - don't report complete with stale docs)
### Definition of Done
[What specifically defines complete for this plan?]
---
## Close Command
When all boxes checked:
```bash
drone @flow close FPLAN-0011
```