diff --git a/cortex/routers/files.py b/cortex/routers/files.py index 9bc3d71..4c24eb5 100644 --- a/cortex/routers/files.py +++ b/cortex/routers/files.py @@ -6,6 +6,7 @@ import re from fastapi import APIRouter, HTTPException, Query from pydantic import BaseModel from persona import persona_path, set_context, validate as validate_persona +from config import settings as _settings router = APIRouter() @@ -22,6 +23,9 @@ ALLOWED = { "HELP.md", } +# Files served from home/{user}/ instead of persona path +USER_FILES = {"email_allowlist.json"} + def _resolve(user: str, persona: str) -> None: """Validate and set context from query params. Raises HTTPException on bad input.""" @@ -32,7 +36,11 @@ def _resolve(user: str, persona: str) -> None: raise HTTPException(status_code=404, detail=str(e)) -def _path(filename: str): +def _path(filename: str, user: str = ""): + if filename in USER_FILES: + if not user: + raise HTTPException(status_code=400, detail="user param required for this file") + return _settings.home_root() / user / filename if filename not in ALLOWED: raise HTTPException(status_code=404, detail=f"File not found: {filename}") return persona_path() / filename @@ -55,6 +63,16 @@ async def list_files( "size": st.st_size if st else 0, "modified": st.st_mtime if st else None, }) + for name in sorted(USER_FILES): + p = _settings.home_root() / user / name + st = p.stat() if p.exists() else None + files.append({ + "name": name, + "exists": p.exists(), + "size": st.st_size if st else 0, + "modified": st.st_mtime if st else None, + "scope": "user", + }) return {"files": files} @@ -65,7 +83,7 @@ async def get_file( persona: str = Query("inara"), ) -> dict: _resolve(user, persona) - p = _path(filename) + p = _path(filename, user=user) if not p.exists(): raise HTTPException(status_code=404, detail=f"{filename} does not exist") return {"name": filename, "content": p.read_text()} @@ -83,7 +101,7 @@ async def save_file( persona: str = Query("inara"), ) -> dict: _resolve(user, persona) - p = _path(filename) + p = _path(filename, user=user) p.write_text(req.content) return {"ok": True, "name": filename, "size": len(req.content)} diff --git a/cortex/routers/settings.py b/cortex/routers/settings.py index 1b58a1d..a01424c 100644 --- a/cortex/routers/settings.py +++ b/cortex/routers/settings.py @@ -8,6 +8,8 @@ Routes: POST /settings/persona/rename → rename a persona directory """ +import html as _html +import json import logging import re from pathlib import Path @@ -63,6 +65,14 @@ def _settings_page(username: str, personas: list[str], back_persona: str = "", s role = auth_data.get("role", "user") html = html.replace("{{ user_role }}", role) + al_path = app_settings.home_root() / username / "email_allowlist.json" + try: + patterns = json.loads(al_path.read_text()) + allowlist_text = _html.escape("\n".join(str(p) for p in patterns if str(p).strip())) + except Exception: + allowlist_text = "" + html = html.replace("{{ email_allowlist }}", allowlist_text) + persona_items = "\n".join( f'''
  • {p} @@ -228,3 +238,21 @@ async def rename_persona( old_dir.rename(new_dir) logger.info("persona renamed: %s/%s → %s", username, old_name, new_name) return RedirectResponse("/settings", status_code=302) + + +@router.post("/settings/email-allowlist", include_in_schema=False) +async def save_email_allowlist( + request: Request, + patterns: str = Form(""), +): + username = _get_session_user(request) + if not username: + return RedirectResponse("/login", status_code=302) + + personas = list_user_personas(username) + back_persona = _preferred_persona(request, username) + lines = [ln.strip() for ln in patterns.splitlines() if ln.strip()] + path = app_settings.home_root() / username / "email_allowlist.json" + path.write_text(json.dumps(lines, indent=2)) + logger.info("email allowlist updated for %s (%d patterns)", username, len(lines)) + return HTMLResponse(_settings_page(username, personas, back_persona, success=f"Email allowlist saved ({len(lines)} pattern{'s' if len(lines) != 1 else ''}).")) diff --git a/cortex/static/app.js b/cortex/static/app.js index 5fedb18..f3843e7 100644 --- a/cortex/static/app.js +++ b/cortex/static/app.js @@ -1285,6 +1285,7 @@ { label: 'Identity', files: ['IDENTITY.md', 'SOUL.md', 'PROTOCOLS.md', 'CONTEXT_TIERS.md'] }, { label: 'Memory', files: ['MEMORY_LONG.md', 'MEMORY_MID.md', 'MEMORY_SHORT.md'] }, { label: 'Profile', files: ['USER.md', 'HELP.md'] }, + { label: 'Settings', files: ['email_allowlist.json'] }, ]; function fmtSize(bytes) { diff --git a/cortex/static/settings.html b/cortex/static/settings.html index 4e177b1..354a46c 100644 --- a/cortex/static/settings.html +++ b/cortex/static/settings.html @@ -240,6 +240,22 @@ color: var(--pg-muted); border: 1px solid var(--pg-border); } + + textarea { + width: 100%; + padding: 0.65rem 0.85rem; + background: var(--pg-bg); + border: 1px solid var(--pg-border); + border-radius: 6px; + color: var(--pg-text); + font-size: 0.88rem; + font-family: 'SF Mono', 'Fira Mono', 'Menlo', monospace; + line-height: 1.55; + resize: vertical; + outline: none; + transition: border-color 0.15s; + } + textarea:focus { border-color: #7c3aed; } @@ -309,6 +325,25 @@

    + +
    +

    Email Allowlist

    +

    + One regex pattern per line. The email_send + tool will only send to addresses that match at least one pattern. + Leave blank to block all outbound email. +

    +
    +
    + + +
    + +
    +
    +

    Browser Cache