Files
hold-slayer/.claude/rules/config-startup.md
Robert Helewka 4a3c14d4af
All checks were successful
CVE Scan & Docker Build / security-scan (push) Successful in 45s
CVE Scan & Docker Build / build-and-push (push) Successful in 1m53s
docs: add Claude AI assistant rules and configuration
Add comprehensive rule documentation for AI-assisted development covering
authentication surfaces, outbound-call safety invariants, and other project
conventions to guide Claude's understanding of critical system behaviors.
2026-07-28 19:01:38 -04:00

64 lines
3.5 KiB
Markdown

---
description: pydantic-settings nested sub-configs + get_settings() singleton, SecretStr discipline, startup refusals, .env hygiene
paths:
- "config.py"
- "main.py"
- ".env*"
---
# Config & startup
Config is `Settings` in [config.py](../../config.py): a root `BaseSettings` with
**nested sub-config models**, each carrying its own `env_prefix`. Read it through
the `get_settings()` cached singleton.
- **`get_settings()` is the only accessor.** It memoises a single `Settings()`.
Don't construct `Settings()` elsewhere, and don't reach for `os.environ.get`
for Hold Slayer config — the whole point of the sub-config layout is that every
knob has one typed home.
- **Sub-configs own their prefixes.** `SIP_TRUNK_*``SIPTrunkSettings`, `LLM_*`
`LLMSettings`, `TTS_*``TTSSettings`, `RECEPTIONIST_*`
`ReceptionistSettings`, `CASDOOR_*``CasdoorSettings`, `CLASSIFIER_*`,
`SPEACHES_*`, `GATEWAY_SIP_*`. Root vars (`DATABASE_URL`, `HOST`, `PORT`,
`MAX_CONCURRENT_CALLS`, `USE_MOCK_SIP`, `NOTIFY_SMS_NUMBER`, `DEBUG`,
`LOG_LEVEL`, and the auth cross-cutters `OWNER_NAME` + `PUBLIC_BASE_URL`) are
unprefixed on the root model. A new knob goes in the sub-config it belongs to;
a genuinely new subsystem gets its own sub-config + prefix, not flat root vars.
- `OWNER_NAME` (the owner's Casdoor username) and `PUBLIC_BASE_URL` (OAuth
discovery base) live on the root, not under `CASDOOR_`, because they cross-cut
every surface — like `DATABASE_URL`. The Casdoor *connection* knobs
(`enabled`/`endpoint`/`client_id`/`client_secret`/`org_name`/`app_name`) live
under `CASDOOR_`.
- `HoldSlayerSettings` uses `env_prefix_allow_empty=True` with explicit
`validation_alias`es (`DEFAULT_TRANSFER_DEVICE`, `MAX_HOLD_TIME`,
`HOLD_CHECK_INTERVAL`) — i.e. those three are read *unprefixed* by design.
Follow that pattern only if you deliberately want an unprefixed name.
- **Secrets are `SecretStr`.** `casdoor.client_secret`, `sip_trunk.password`,
`llm.api_key`, `tts.api_key`. Keep new secrets as `SecretStr`; call
`.get_secret_value()` only at the point of use (outbound header, SDK
construction) — never store the bare string, never log it.
- **Startup refuses bad configs loudly, then exits.** In [main.py](../../main.py):
- `_check_startup_config` exits if `DATABASE_URL` is unset; if `CASDOOR_ENABLED`
is true but any of `CASDOOR_ENDPOINT`/`CLIENT_ID`/`CLIENT_SECRET`/`OWNER_NAME`
is missing; or if `CASDOOR_ENABLED` is false while `HOST` is off-loopback
(dev-owner mode would be open to the network). See the
[auth-surfaces rule](auth-surfaces.md).
- `_handle_db_error` turns raw asyncpg failures into human-readable guidance
(wrong password, missing DB, connection refused, bad hostname) and `sys.exit(1)`.
- SIP engine build failure and mock-vs-real are surfaced, not swallowed.
**Keep the pattern: a misconfiguration stops the service with a message a human
can act on — never a silent degrade or a stack trace with no guidance.**
- **`use_mock_sip` is opt-in for a reason.** An unconfigured trunk without
`USE_MOCK_SIP=true` must fail startup rather than boot a gateway that silently
can't place real calls. Don't default it to `True`.
- **`.env` hygiene:** `.env` is gitignored and holds real secrets — never commit
it, never treat the checked-out `.env` as a template. Only `.env.example`
(placeholders) is committed, and it must stay in sync with the models here and
the README config table. Every new var lands in all three: model,
`.env.example`, README.