aider_run multi-provider credentials (tools/aider.py):
- _resolve_credentials() — general credential resolver; replaces the previous
OpenRouter-only injection; resolution priority: Anthropic model hint → explicit
host_label → model prefix (openrouter/*, groq/*, deepseek/*, …) → OpenRouter
default → Anthropic API key → any keyed cloud host → local/generic host
- _host_flags() — generates --api-key slug=key for known cloud providers (OpenRouter,
OpenAI, Groq, Together, Fireworks, X.ai, DeepSeek, Mistral); generates
--openai-api-base + --openai-api-key for generic/local hosts (Open WebUI, Ollama);
appends /api suffix for openwebui host_type; auto-prefixes model with 'openai/'
for generic endpoints when model has no / prefix
- Anthropic API keys from providers.anthropic.credentials (not a host entry)
- host_label param added to aider_run and FunctionDeclaration — pick a configured
host by partial label match (e.g. 'OpenRouter', 'Local', 'scott-lt-i7-rtx')
- 16 unit tests for _resolve_credentials covering all resolution paths
main.py: move @app.get("/health") before app.include_router(ui.router) — the
/{username} catch-all in ui.router was swallowing the /health path
Test suite: 37 pre-existing failures → 182/182 passing
- test_tools.py: _task_list() missing priority arg (6 callsites); cron ID regex
c_\w+ → c_[\w-]+ (token_urlsafe includes '-', causing intermittent truncation)
- test_webhooks.py: rewritten for per-user channel config architecture —
patch routers.nextcloud_talk/google_chat.get_user_channels instead of removed
settings fields; corrected endpoints /webhook/nextcloud/scott and
/channels/google-chat/scott; non-empty cfg dicts so falsy-guard passes
- test_health.py: test_unknown_route_404 now uses 3-segment path (/{u}/{p}/x)
since single-segment paths hit the /{username} UI catch-all
- test_api_files.py: removed '../config.py' from not-in-allowed test (ASGI
normalizes it to /config.py which hits /{username} catch-all, not files router)
- test_security.py: same webhook patch target fix; per-user endpoint URLs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tool schema optimization (PLAN__Tool_Schema_Optimization.md Phases 1-3):
- model_registry.py: ROLE_DEFAULT_TOOLS — distill gets [], research/coder get
narrow tool lists by default; applied in get_role_config() when user hasn't
configured a custom list
- openai_orchestrator.py: keyword routing via narrow_tools_by_keywords() — scans
user message + last assistant turn; narrows active schemas to matched categories
only (e.g. "weather" → 3 web tools instead of 69); zero tools sent for pure chat
- openai_orchestrator.py: _get_cached_tools() — module-level schema cache keyed by
(role, sorted_tool_list, risk_params); eliminates redundant schema rebuilds
- openai_orchestrator.py: _TOOL_SCHEMA_OVERHEAD 3000 → 500 tokens (schemas now
excluded from the per-call fixed estimate since they're cached separately)
- tools/__init__.py: CATEGORY_TOOL_MAP + _KEYWORD_CATEGORY_MAP + classify_tool_categories()
+ narrow_tools_by_keywords() — the classifier logic lives here so both orchestrators
can share it
aider_run tool (cortex/tools/aider.py):
- Invokes Aider as a subprocess with --message --yes-always --no-pretty --no-stream
- Project aliases: cortex / aether_api / aether_frontend / aether_container
- Auto-injects OpenRouter API key from Cortex model registry (no ~/.env needed)
- background=True fires async + registers in agent_manager; notify=True sends push
notification on completion
- admin-only, confirm-required, TOOL_RISK=high
- .gitignore: added .aider.chat.history.md / .aider.input.history / .aider.llm.history
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Copy button appears on all messages (user + assistant), not just assistant
- Delete requires a confirm step — clarified in description
- Model tag position is below-left for assistant messages, not bottom-right
- Tool calls render as expandable per-call cards, not a single ⚡ N summary note
- help.html: default-open sections updated to Getting Started / Chat / Sessions / Model Registry
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Model Registry:
- Add per-user custom roles (add/remove via UI); required roles chat/orchestrator/distill
are always present and cannot be removed
- Auto-migrate legacy .env-defined roles to custom_roles on first access
- Role config panel (gear): Remove role button moved inside panel; required badge below name
- Role select: Primary + Backup slots only (was three)
Settings pages — Tailwind CSS migration (CDN, preflight: false):
- local_llm.html, settings.html, help.html, notifications.html, tools_settings.html,
crons.html, integrations.html all migrated; pg-* color tokens; dark/light via data-theme
pg.css fixes:
- input[type=checkbox/radio]: width: auto — prevents pg.css width:100% from stretching checkboxes
- btn-submit: responsive sizing via Tailwind w-full md:w-96 on each button (no longer full-width on desktop)
Documentation:
- MASTER.md, TODO__Agents.md: remove "/ Inara" from titles; description updated to "per-user AI personas"
- HELP.md: persona-agnostic language throughout (NC Talk, Google Chat, push, schedules, HA sections);
roles section restructured to show required vs. custom roles with examples
- notifications.html: subtitle and HA description use "your persona" not "Inara"
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds `anthropic_api` model type so users can authenticate with a direct
Anthropic API key instead of (or alongside) the CLI OAuth session.
- model_registry.py: `anthropic_api` type; `save/get/remove_anthropic_api_key()`
mirroring the Google account pattern; `save_cloud_model()` now picks type
based on credential type (cli → claude_cli, api_key → anthropic_api);
`_resolve_model()` merges api_key from the credential entry
- llm_client.py: `_anthropic_api()` backend (AsyncAnthropic SDK); dispatch
and fallback wiring; usage tracking
- routers/local_llm.py: Anthropic API key management routes
(POST /settings/local/anthropic-key, /anthropic-key/{id}/remove);
`anthropic_api` badge and edit-form credential selector
- static/local_llm.html: Anthropic Cloud Provider block now shows API key
management (add/remove); Add Model → Anthropic tab has credential selector
(CLI vs API key)
- requirements.txt: enable anthropic>=0.40.0
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Schedules web UI (/settings/crons): list, add, edit, pause/resume, delete jobs
- cron task type: full orchestrator tool loop on a schedule, result → notification channel
- parse_schedule: monthly/yearly formats (monthly:DD:HH:MM, yearly:MM:DD:HH:MM)
- HA inbound webhook tools toggle: orchestrator loop vs. direct LLM, configurable in UI
- ae_db_query/describe/show_view: SELECT-only Aether MariaDB access (admin, per-user creds)
- /settings/integrations: admin-only page for Aether DB credentials
- Schedules nav link added to all settings pages
- pymysql added to requirements
- Docs updated: HELP.md, MASTER.md, CLAUDE.md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Text files (.md, .py, .js, .json, etc.): read client-side and injected
into the message body as a fenced code block — works with all backends
with zero model capability requirements.
Images (PNG/JPG/WebP/GIF, max 5 MB): encoded as base64 data URL on the
client and sent as a separate attachment field. Backend formats them as
OpenAI multimodal content (text + image_url) for local_openai backends.
Claude CLI and Gemini CLI see the text message with a "📎 filename.png"
note; image data is never written to session history.
- index.html: 📎 button + hidden file input in mode-select row;
attachment-row preview area with thumbnail (images) or filename chip
- app.js: _resolveAttachment(), file reader, clearAttachment();
sendMessage/sendOrchestrate updated to allow no-text sends when a
file is pending; attachment spread into chat payload for images
- chat.py: Attachment model; attachment field on ChatRequest;
llm_attachment extracted in _stream_chat and passed to complete()
- llm_client.py: attachment param through complete()/_dispatch()/_local();
_local() builds multimodal content array for vision calls
- style.css: #attach-btn, #attachment-row, #attachment-preview, thumb
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the role-cycle toggle with a slot model toggle in the Context &
Memory panel. The active model label is shown on the button; clicking cycles
through Primary → Backup 1 → Backup 2 slots configured for the Chat role.
- app.js: remove activeRole()/availableRoles role-cycling; add
activeChatModel()/chatModels slot cycling; update send/orchestrate
payloads to send slot + chat_role:"chat"; fix updateSendBtnTitle and
startRunTimer to use activeChatModel()
- chat.py: add slot field to ChatRequest; pass slot= to complete();
resolve backend_label from slot config; add _chat_slot_models() helper;
include chat_models in GET /backend response
- HELP.md: update Model toggle description, tool count (62/16),
Backends section, API chat payload example
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Clicking del now shows 'confirm delete / cancel' inline in the action
bar. Cancel rebuilds the original buttons; confirm proceeds as before.
Matches the session delete pattern added in the prior commit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- task_list: add priority param ('low'/'normal'/'high') alongside existing status filter
- Session delete: inline confirm row (Delete / Cancel) instead of immediate delete
- spawn_agent: allow_tools and deny_tools per-call params; role config remains ceiling;
deny_tools falls back to confirm_deny gate when no explicit tool_list is set
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reflects file_diff and git_status/log/diff additions, pg.css refactor,
and reasoning level controls added this session.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Read-only wrappers around git commands, project-scoped. Covers working
tree status, commit history browsing (with optional path filter), and
diffs between refs or the working tree — cleaner than shell_exec for
code review and change verification.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Runs diff -u on two project-scoped files. Low risk, no admin required.
Covers code review, config comparison, and before/after verification.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove Tool Permissions form from settings.html; replace with a
"Tool Settings →" link that redirects to /settings/tools
- Add Confirmation Gate section to tools_settings.html (allow/deny
textareas) inside the same form as risk policy — one save covers all
- tools_settings.py save handler now writes allow/deny alongside
max_risk/whitelist/blacklist into tool_policy.json
- Remove /settings/tool-policy POST route from settings.py (no longer needed)
- Remove get_tool_policy, save_tool_policy, CONFIRM_REQUIRED imports
from settings.py (now owned by tools_settings.py)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New /settings/tools page: max_risk selector (low/medium/high) + per-tool
override dropdowns (Default / Force include / Force exclude) for all 58 tools
grouped by category with color-coded risk badges; JS updates Auto status live
- get_tools_for_role() + get_openai_tools_for_role() now accept max_risk,
whitelist, blacklist; _apply_risk_policy() handles the filtering logic
- get_risk_policy() helper in auth_utils reads from tool_policy.json
- Risk policy wired through orchestrator.py, openai_orchestrator.py,
orchestrator_engine.py, nextcloud_talk.py, homeassistant.py
- Tools nav link added to settings.html and notifications.html
- CLAUDE.md and ARCH__SYSTEM.md updated: tool count 50→58, risk system docs,
tool access control three-layer model documented
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add five project-scoped tools (user-level, no admin required):
project_file_read — read with 1-based offset for paging large files
project_file_list — list with sizes + timestamps
file_stat — size, modified time, line count / entry count
file_grep — regex search with context lines, up to 50 matches
file_syntax_check — py_compile (.py) or json.loads (.json)
Also add offset support to existing file_read (system scope).
Rename "Files" tool category to "System Files"; add "Project Files" category.
Project scope restricted to Cortex_and_Inara_dev/ project root.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Register three HA orchestrator tools so Inara can read device states and
control devices via the HA REST API. ha_call_service requires admin role
and user confirmation. Also includes accumulated UI fixes (setProcessing
helper, wasNewSession flag cleanup).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
notifications.html: add Home Assistant section with two collapsible
blocks — Connection (HA URL + Long-Lived Access Token) and Inbound
webhook (webhook ID with endpoint URL hint showing the username).
Token field uses keep-existing pattern (blank = no change).
settings.py: wire ha_url, ha_token, ha_webhook_id through
_notifications_page() template substitution and save_notifications()
POST handler. Preserves existing HA config fields (persona, tier,
role, tools) on save.
TODO__Agents.md: add Home Assistant integration planning section
(event design, richer payload template, HA API tools).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
nextcloud_talk.py:
- Fix missing import hmac / import hashlib (NameError bug in _verify_signature)
- Add orchestrator routing when channels.json "tools": true — sends
"⏳ Working on it…" immediately, then runs the full tool loop and
replies with the result; checkpoint case gets a web UI confirmation note
- Read tier and role from channel config (defaults: default_tier / "chat")
- Pass cfg through to _process_message
homeassistant.py (new):
- POST /webhook/ha/{username}/{webhook_id}
- Auth: webhook_id path segment matched against channels.json
- Accepts JSON or form-encoded body from HA automations
- Builds natural-language task from payload (uses "message" key if present,
otherwise serialises full body as context)
- Same orchestrator/direct dispatch as NCT
- Delivers response via notify() — NC Talk, web push, or configured channel
- Session key: ha_{username} for continuity across HA events
- Registered in main.py; /webhook/ prefix already public in auth_middleware
channels.json schema addition:
"homeassistant": {
"webhook_id": "your-secret-id",
"persona": "inara",
"tier": 2,
"role": "chat",
"tools": false
}
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace free-form reasoning_budget_tokens number input with a 5-level
select in both the edit form (local_llm.py) and add-model form
(local_llm.html). Values: 0 / 1024 / 4096 / 8192 / 32768 tokens.
Edit form pre-selects the stored value.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- reasoning_budget_tokens: optional int field on local_openai models;
when set, injects {"reasoning": {"budget_tokens": N}} via extra_body
into every OpenRouter API call (both tool-loop and confirmation-gate
rounds). Field exposed in the model edit form in Settings.
- session name moved from standalone full-row div between #messages
and #input-area into the persona-switcher block in the header, as a
third dim line under "Cortex · Local". Collapses when empty via
:empty CSS. No JS changes required.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- inject_mode: per-role toggle (parallel to inject_datetime) gates the
"Current mode: Off The Record" line in the system prompt; wired through
model_registry, context_loader, chat router, orchestrator router, and
local_llm settings UI
- OTR orchestrator fix: OrchestrateRequest now carries off_record;
_finalize_job stores it per message and gates log_turn on it; JS
orchestrate payload sends off_record correctly
- Per-message hover metadata: removed always-visible .model-tag; replaced
with .msg-meta strip in the action bar (hover-only); shows model label,
host, fallback indicator, and OTR badge; stored in session JSON
- Send/stop button tooltip: shows role + model and (when tools on)
separate orchestrator model + engine label; live elapsed timer on stop
button via startRunTimer/stopRunTimer
- OrchestratorResult.backend_label: new field; openai_orchestrator fills
it; finalize_job propagates it to job dict and session messages
- GET /backend: exposes orchestrator_model label so the frontend tooltip
can show both models separately
- TODO: session delete confirmation added
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
context_loader.load_context() now accepts a mode param ("chat"|"otr").
In OTR mode, the --- System --- block gains a second line:
Current mode: Off The Record — this conversation is private
and will not be logged or included in memory distillation
routers/chat.py passes mode="otr" when req.off_record is True.
Normal chat and all orchestrator calls stay at mode="chat" (no change
to the System block). The System block consolidates date/time and mode
in one place, matching the existing timestamp pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Notifications page:
- NC Talk section expanded: url, bot_secret, notification_room,
nc_username, nc_app_password — all fields from channels.json now editable
- Per-channel sections use <details>/<summary> collapsibles; auto-open
when values are present
- Secrets use type=password with "leave blank to keep" semantics
- Google Chat outbound webhook in its own collapsible section
Account settings:
- HTTP POST Allowlist section added (same textarea pattern as email allowlist)
- POST /settings/http-allowlist route saves home/{user}/http_allowlist.json
- Example placeholder shows ha.dgrzone.com and n8n patterns
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Inara's exploration of a living-wiki knowledge compilation architecture
as an alternative to RAG — three-layer model, ingest/query/lint ops,
and a mapping to existing Cortex concepts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Both tools now accept max_chars up to 131072 to accommodate long
documentation pages and large API responses.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
web_read(url, max_chars=16000) — fetches a URL and extracts clean article
text via trafilatura, stripping ads/nav/boilerplate. Returns markdown.
session_read(date) — reads a full session log by YYYY-MM-DD date; lists
available dates if the requested one is not found.
http_fetch gains a max_chars param (default 8192, max 32768) so the cap
is configurable instead of hardcoded.
Tool count: 45 → 47.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extract orchestrator inner loop into _doOrchestrate() so the retry button
can re-run without re-adding the user message to DOM or history — same
pattern as the existing chat retry.
Also set AsyncOpenAI(timeout=settings.timeout_local) so slow remote models
(OpenRouter/DeepSeek) get the same 300s budget as local chat calls instead
of the SDK default which varies by connection.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- CLAUDE.md: date → 2026-05-08, add Proactive notifications row to channel table
- HELP.md: update Notifications settings entry, expand Push Notifications section
with channel config link, add test API endpoints to reference table
- TODO__Agents.md: mark notifications dedicated page and pywebpush fix as done
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
pywebpush 2.x routes string keys through Vapid.from_string() which only
handles raw/DER base64 — not PEM. Pre-build the Vapid object so the key
deserializes correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds GET /settings/notifications (dedicated page with channel form + two
test buttons) and updates POST /settings/notifications to render that page.
Settings page now shows a compact link card instead of the full form.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- POST /api/push/test: sends "Test notification from Cortex" via the
user's configured notification channel (web_push / NCT / email / etc.)
- POST /api/push/reminders/check: runs the daily reminder check immediately
for the current user, returns reminders_found count
Both require an active session cookie. Useful for verifying channel setup
without waiting for the 09:00 scheduler job.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Routes web_push through notification.py alongside NCT/email/Google Chat,
and fires daily reminder summaries via the scheduler.
- notification.py: _notify_web_push() + "web_push" case in notify();
all four channels (web_push/email/nextcloud/google_chat) now routable
- scheduler.py: _run_reminder_check() daily at 09:00 — reads due reminders
per persona via set_context(), formats up to 3 entries, calls notify()
- routers/settings.py: "web_push" added to valid notification_channel values
- static/settings.html: "Browser Push Notification" option in channel selector
- TODO__Agents.md: proactive notifications section marked complete
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mirrors the pattern already in openai_orchestrator.py. The Gemini engine
was still hardcoded to the global orchestrator_max_rounds setting.
- orchestrator_engine.py: max_rounds param on run() and _run_from_contents();
effective_limit = min(per_model_limit, global_limit); stored in checkpoint
so resume() respects it across confirmation gates
- routers/orchestrator.py: passes orch_model.get("max_rounds") to run()
- tools/agents.py: passes model_cfg.get("max_rounds") for gemini_api spawns
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>