CLI display.py now routes error() and warning() to stderr via err_console. Added fatal() for unrecoverable errors. Drone imports err_console from CLI instead of creating its own. RegistryMismatchError added to drone exception hierarchy for credential verification failures. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
9.0 KiB
DPLAN-003: Registry Credential Model
The Idea
Registries get a unique token. Passports carry that token. Access is identity-based, not filesystem-based. No walk-up needed — your credential proves which registry is yours.
Why
Current system finds registries by walking up directories. Works for one project, breaks with multiple. If two AIPass projects exist on one machine, a citizen launched from the wrong directory finds the wrong registry. Credentials solve this — your passport carries proof of membership.
Prior Art (Research)
| System | Pattern | Fit |
|---|---|---|
| Macaroons (Google Research) | Token IS the credential. Delegatable with caveats. Offline verification. DeepMind validated for AI agent delegation (2026). | Highest |
| Vault Namespaces | Project = namespace. Token scoped to namespace. Mini-registry per project. | High |
| AWS STS / Token Vending | Agent presents project ID, gets scoped credential. Credential itself is the boundary. | High |
| K8s Namespace + ServiceAccount | Token carries project scope as claim. RBAC composable. | High |
| SPIFFE/SPIRE | Process-level attestation without static secrets. | Medium |
| direnv | Auto-set env vars on directory entry. Zero-friction UX. | UX pattern |
Full research: agent output from session 25.
Design: Two Stages
Stage 1: UUID Match (Manual, Now)
Simple. Prove the concept works before adding crypto.
Registry gets an ID:
{
"metadata": {
"id": "a1b2c3d4-...",
"name": "AIPASS",
"version": "1.0.0",
"last_updated": "2026-03-13",
"total_branches": 15
},
"branches": [...]
}
Passports get the matching ID:
{
"citizenship": {
"registered": true,
"registry_id": "a1b2c3d4-...",
"registry_name": "AIPASS",
"citizen_number": 7
}
}
Lookup flow:
- Walk up from CWD, find
*_REGISTRY.json - Read its
metadata.id - Check citizen's
citizenship.registry_idmatches - If mismatch → error ("citizen belongs to registry X, found registry Y")
- If match → proceed
Spawn changes:
aipass init(or manual setup) generates the UUID for the registry- Spawn reads registry UUID and injects into new passports via
{{REGISTRY_ID}}placeholder - Existing 15 branches get the UUID added to their passports (one-time migration)
Stage 2: Macaroon Tokens (Future, When Cross-Project Needed)
Upgrade path when we need delegation and cross-project access.
Root token: Created at aipass init. HMAC-based. Stored at ~/.secrets/aipass/projects/<uuid>.token
Citizen token: Attenuated copy in passport. Can prove membership but can't mint new citizens.
Agent token: Further attenuated. Carries:
- Registry ID (which project)
- Scope (full access — agents do the real work in AIPass)
- Expiry (session-scoped, dies when agent dies)
- Issuer (which citizen spawned this agent)
Agents are NOT read-only. They're the builders — they write code, run tests, modify files. The token proves they belong to a project, it doesn't restrict what they do within it. Scope restrictions would be role-based (e.g., "can't modify other branches' files") not capability-based.
Verification: Local HMAC check. No daemon needed for basic validation. Daemon is optional enhancement for audit logging and revocation.
direnv integration: Entering a project directory auto-sets AIPASS_PROJECT_TOKEN in env. Agents inherit it.
What Changes (Stage 1)
| Component | Change |
|---|---|
AIPASS_REGISTRY.json |
Add metadata.id (UUID) |
| Passport template | Add citizenship.registry_id placeholder |
Spawn build_replacements_dict() |
Read registry UUID, add {{REGISTRY_ID}} |
Spawn add_to_registry() |
No change (branch entries stay the same) |
Drone find_registry() |
Optional: verify passport.registry_id matches found registry |
| All 15 passports | One-time: add registry_id field |
What Does NOT Change
- Registry filename stays
*_REGISTRY.json - Walk-up discovery still works (credential is verification layer on top, not replacement)
- Branch structure unchanged
- No daemon needed
- No new dependencies
Resolved Questions
- Registry ID location →
metadata.id— it's the project's identity, not the citizen's. - UUID4 vs hash → UUID4 (random). Simple, guaranteed unique, no inputs needed.
- Verification mode → Hard error on mismatch. Fail loudly — that's the AIPass way.
- Where does init live? → Temporary:
src/aipass/devpulse/apps/init_project.py. Future: CLI branch.
Bugs, Quirks, and Findings
Discovered during testing. Reference for future work.
init_project.py (10 edge cases tested)
- Spaces in dir name → registry filename gets spaces (
MY COOL PROJECT_REGISTRY.json). Fixed:_sanitize_name()replaces non-alphanumeric with_. - Root path
/→Path("/").nameis empty string, creates_REGISTRY.json. Fixed: validation rejects empty name. - Permission errors → raw traceback instead of clean message. Fixed:
main()catchesOSError. - Passport overwrite on re-init → if registry deleted but
.trinity/survives, re-init would silently overwrite passport with new UUID. Fixed: passport guarded withexists()check. - Double init → correctly blocked by
FileExistsErroron registry file. - Deep nested paths →
mkdir(parents=True)handles correctly. - UUID uniqueness → 5 runs, 5 unique UUIDs. No collisions.
- JSON validity → all generated files parse clean.
Drone isolation (6 scenarios tested)
- Sibling projects → PASS. Two projects in same parent dir, fully isolated.
- Nested project → PASS. Inner registry wins over outer. No bleed-through.
- Deep subdir walk-up → PASS. Finds nearest ancestor registry correctly.
- Cross-project contamination → PASS. Branches are registry-scoped; modules are global.
- Empty directory (no registry) → CONCERN. Drone silently falls back to AIPass source registry via
__file__walk-up. Any dir on this machine without a registry sees production branches. This is by design infind_registry()but will be dangerous with multi-project. Credential verification would catch this — citizen's registry_id won't match the fallback registry. drone @ai_mailfrom mock project → correctly returns "Branch not found in registry" (empty project has no branches).
Registry/passport gitignore
AIPASS_REGISTRY.jsonand all.trinity/passport.jsonfiles are gitignored. UUID migration is local-only. This is correct for now — credentials are machine-specific, not repo state. But meansaipass initmust run on every clone/install. Future: consider whether UUID should be in-repo or machine-local.
AI mail after migration
- Send/receive works fine after UUID migration. AI mail's 4 internal
find_registry()copies still hardcodeAIPASS_REGISTRY.json— functional for now since that filename exists, but won't find*_REGISTRY.jsonin other projects.
Drone built-in modules vs branches
@droneand@seedgoare hardcoded as "modules" inmodule_registry.py, always visible everywhere. Other branches (@ai_mail,@spawn, etc.) are registry-scoped. This distinction matters: modules are global services, branches are project citizens.
Decision Log
- 2026-03-14: Patrick proposed credential-based registry access. Agents do the real work — tokens prove membership, not restrict capability. Manual first,
aipass initlater. - 2026-03-14: Research confirmed macaroons as best-fit pattern (Google Research + DeepMind 2026 validation). Stage 1 = UUID match, Stage 2 = macaroon upgrade.
- 2026-03-14: Scope limited to
src/aipass/branches only. Commons and skills excluded. - 2026-03-14: All 4 open questions resolved.
init_project.pybuilt and tested — creates registry with UUID, passport with matching registry_id, .trinity/, .aipass/, AIPASS.md. Tested with temp directory — drone isolation confirmed (only built-in modules visible, not AIPass branches). - 2026-03-14: UUID migration executed (FPLAN-0030). Registry + 13 passports updated. Registry and passports are gitignored — UUID is machine-local, not repo state.
- 2026-03-14: Drone verification added (
_verify_registry_credentialin load_registry). Drone dispatched and fixed error propagation — newRegistryMismatchErrorclass separates "not found" (fallback OK) from "mismatch" (hard error). Spawn templates updated with{{REGISTRY_ID}}placeholder. - 2026-03-14: Stage 1 complete. Credential model is end-to-end: init creates credentialed registries, drone verifies on load, spawn injects into new passports, mismatch = hard error. Remaining: drone CLI stderr surfacing (DPLAN-0031),
aipass initCLI command (future). - 2026-03-14: FPLAN-0032 executed — CLI stderr standardization Phase 1+2 done. CLI owns err_console, error()/warning() go to stderr. Drone imports from CLI instead of creating its own Console(stderr=True). Credential mismatch errors now properly surface to terminal via stderr. The stderr issue that blocked error visibility during DPLAN-003 testing is resolved.