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.
64 lines
3.5 KiB
Markdown
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.
|