0066. The Anthropic key is a dashboard setting, kept out of backups¶
- Status: accepted
- Amended by: ADR-0068
- Boundary moved: the storage and trust model for secrets. The Anthropic API key moves
out of the process environment into
kenny.sqlite, where a superuser can set it from the dashboard, and it becomes the first stored value that is deliberately excluded from every backup copy. Every AI feature also gets its own live switch behind one access point. - Amends: ADR-0032, ADR-0054
- Date: 2026-09-23
Context and Problem Statement¶
kenny runs six AI features: Ask kenny, the section recommendation, the forecast prose,
reliability-event classification, the ticket assistant (dashboard and Discord) and triage.
They all depended on ANTHROPIC_API_KEY in the environment. Only triage could be switched
off, and each feature resolved the key its own way: a per-call os.environ read, a client
built once at startup, a process-lifetime lru_cache. Rotating the key or pausing a
feature meant shell access and a restart. The operator of a family-scale deployment runs it
from the dashboard (ADR-0032).
ADR-0032 and ADR-0054 kept this key env_only on purpose. It is billed per use, and every
database backup travels to remote targets that may be plain FTP or SCP (ADR-0039). So the
question is not just "make it editable". It is where a GUI-set key may live without ending
up on a backup server.
Considered Options¶
- Keep it env-only. Leaves the problem as it is.
- Store it in
kenny.sqlitein cleartext, like the alert channels (ADR-0054). Simple, but the key would then sit in every local and remote backup. - Store it in a separate secrets file on the data volume, outside the database. It stays out of backups, but adds a second persistence mechanism beside SQLite, with its own permissions and its own restore story.
- Store it in the settings table and scrub it from every backup copy. Chosen.
Decision Outcome¶
ANTHROPIC_API_KEY is a live, sensitive setting in the AI group. A value saved in the
dashboard wins over the environment (ADR-0032's DB > env > default). Like every
sensitive setting it is never serialised back out.
A catalog flag, backup_excluded, marks settings that must not leave the live database.
BackupManager.create deletes those rows from the staged VACUUM INTO copy and vacuums
the copy again before it is hashed, checked or pushed to any target. The live database
keeps the key. After a restore the key is unset, so the environment value applies if there
is one; the dashboard says so next to the key.
kenny_server.ai is the single access point:
- Key: it resolves the key through
Settings. - Client: it builds the Anthropic client for the key currently in force and rebuilds it when the key changes.
- Feature switches: it answers
enabled(feature)from one live switch per feature. The switches areKENNY_AI_ASK_ENABLED,_RECOMMEND_,_FORECAST_,_CLASSIFY_,_TICKET_ASSISTANT_ENABLED, andKENNY_TRIAGE_ENABLEDfor triage. - Rule: a feature runs only when a key is set and its switch is on.
- Binding: the composition root binds one
AiAccessfor the whole process (one process, oneSettings). Module-level callers such as the classifier and the MCPagent_healthpath read it throughai.current().
GET /api/ai/status tells the dashboard what to offer, so a switched-off feature shows no
control that would answer 503. POST /api/ai/test checks the key with the cheapest API
call.
The dashboard's settings list now contains only what it can write. The read-only view onto
env_only values that ADR-0032 described is gone; those stay in the environment and in
docs/setup.md.
Consequences¶
- Good, because a superuser can set, rotate or clear the key and switch any AI feature off from the dashboard, and the next call honours it. There is no restart.
- Good, because no backup copy, local or remote, ever holds the key. ADR-0054's channel secrets still do, as that record accepted.
- Bad, because the key sits in cleartext in the live
kenny.sqlite. Anyone with read access to that file can read the key. That is the same access that already reveals the server's Ed25519 seed and the TOTP secrets, so the key adds no new exposure class. - Bad, because a restore silently loses a dashboard-set key. It is announced next to the key and falls back to the environment, but an operator who never set one there has AI off until they set it again.
- Bad, because
ai.current()is process-global state. Tests unbind it after each test (tests/conftest.py), and anything built outsidebuild_appreads the environment.
More Information¶
ADR-0009 (the copilot), ADR-0026 (classification), ADR-0056 (triage), ADR-0039 (what a backup carries).