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.
3.5 KiB
description, paths
| description | paths | |||
|---|---|---|---|---|
| pydantic-settings nested sub-configs + get_settings() singleton, SecretStr discipline, startup refusals, .env hygiene |
|
Config & startup
Config is Settings in 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 singleSettings(). Don't constructSettings()elsewhere, and don't reach foros.environ.getfor 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-cuttersOWNER_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) andPUBLIC_BASE_URL(OAuth discovery base) live on the root, not underCASDOOR_, because they cross-cut every surface — likeDATABASE_URL. The Casdoor connection knobs (enabled/endpoint/client_id/client_secret/org_name/app_name) live underCASDOOR_.HoldSlayerSettingsusesenv_prefix_allow_empty=Truewith explicitvalidation_aliases (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 asSecretStr; 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:
_check_startup_configexits ifDATABASE_URLis unset; ifCASDOOR_ENABLEDis true but any ofCASDOOR_ENDPOINT/CLIENT_ID/CLIENT_SECRET/OWNER_NAMEis missing; or ifCASDOOR_ENABLEDis false whileHOSTis off-loopback (dev-owner mode would be open to the network). See the auth-surfaces rule._handle_db_errorturns raw asyncpg failures into human-readable guidance (wrong password, missing DB, connection refused, bad hostname) andsys.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_sipis opt-in for a reason. An unconfigured trunk withoutUSE_MOCK_SIP=truemust fail startup rather than boot a gateway that silently can't place real calls. Don't default it toTrue. -
.envhygiene:.envis gitignored and holds real secrets — never commit it, never treat the checked-out.envas 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.