feat: multi-persona support (single Cortex, multiple users)

- Add cortex/persona.py: ContextVar-based per-request routing with
  path traversal protection and persona validation
- Migrate inara/ → personas/inara/ (git history preserved via git mv)
- config.py: add personas_root(), inara_path() delegates to personas/inara
- All 14 settings.inara_path() call sites replaced with persona_path()
- ChatRequest + OrchestrateRequest: add persona field (default: "inara")
  with validation at request entry before any processing
- memory_distiller: add optional persona param for future per-persona distill
- cron_runner/tools/cron: stamp persona on jobs, prefix APScheduler IDs
  (persona:job_id) to prevent collisions across personas
- scheduler: _load_user_crons() iterates all personas at startup

Adding a new persona: create personas/<name>/ with IDENTITY.md + SOUL.md.
Auth: handled at nginx level (inject X-Cortex-Persona header per subdomain).
Future: persona maps to Aether account_id_random for full integration.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-03-20 21:50:02 -04:00
parent 6316ffa1d4
commit 5cadb836fa
40 changed files with 634 additions and 289 deletions

View File

@@ -27,7 +27,8 @@ class Settings(BaseSettings):
agent_name: str = "Inara"
user_name: str = "Scott"
inara_dir: Path = Path("../inara")
personas_dir: Path = Path("../personas")
inara_dir: Path = Path("../personas/inara") # legacy — use personas_dir
sessions_dir: Path = Path("./data/sessions")
default_model: str = "claude-sonnet-4-6"
default_tier: int = 2
@@ -39,6 +40,11 @@ class Settings(BaseSettings):
timeout_gemini: int = 120 # frequently slow under load
timeout_local: int = 300 # local models may need to load first
# Google Chat
# JWT audience (aud) claim to verify on inbound webhook requests.
# Google Chat sets aud = the Google Cloud project number (e.g. "741112865538").
# Set to "" to disable verification (dev/testing only).
google_chat_audience: str = ""
# Google Chat must receive a response within 30s or shows an error to the user
google_chat_timeout: int = 25
# Backend forced for Google Chat — Claude is more reliable within the 25s deadline
@@ -68,11 +74,15 @@ class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore")
def personas_root(self) -> Path:
"""Resolve personas_dir relative to this file's location if not absolute."""
if self.personas_dir.is_absolute():
return self.personas_dir
return (Path(__file__).parent / self.personas_dir).resolve()
def inara_path(self) -> Path:
"""Resolve inara_dir relative to this file's location if not absolute."""
if self.inara_dir.is_absolute():
return self.inara_dir
return (Path(__file__).parent / self.inara_dir).resolve()
"""Legacy helper — returns the inara persona directory. Prefer persona_path()."""
return self.personas_root() / "inara"
def sessions_path(self) -> Path:
"""Resolve sessions_dir relative to this file's location if not absolute."""