ADR-0006: One accepted-risks list instead of prod opt-out flags¶
- Status: Accepted
- Date: 2026-10-24
- Deciders: Sandloom maintainers
- Supersedes: — (amends the configuration tiers of
sandloom.core.config.tiers; implements recommendations 4–5 ofdocs/design/effect-model-best-practices-2026-10-24.md)
Context¶
The configuration tiers (sandloom.core.config.tiers) classify every setting as
semantic (changes a guarantee; its value is chosen per profile), ops or
migration. When the tiers were introduced the semantic tier held 30
settings. The best-practice review of 2026-10-24 found two kinds of inflation:
- Seven escape hatches with one meaning.
BILLING_ALLOW_FAKE_PSP_IN_PROD,NOTIFIER_ALLOW_LOG_IN_PROD,STORAGE_ALLOW_LOCAL_IN_PROD, the event-bus instance optionallow_volatile(+ the dev-onlySANDLOOM_ALLOW_VOLATILE_CONSUMERS),SANDLOOM_ALLOW_UNVERSIONED_DDL_REPAIR,PASSWORD_HASH_ALLOW_WEAKandJWT_ALLOW_SHARED_SECRET_SPLITeach say "admit a weaker thing in production". They were spread over the settings reference, reviewed one by one, and only visible as separate overrides. - Knobs nobody needs. Single-valued semantic flags whose non-default
value is never valid in production (
SCHEMA_VALIDATION_SOFT,MODULE_REQUIRE_LISTED,BEAT_LEASE_ENABLED,CONTRACT_READ_SCOPE_MEMO,BILLING_FAKE_ALLOW_UNSIGNED_WEBHOOKS) and the three migration flags whose 0.8 removal was already promised (LEGACY_UNSCOPED_EFFECTS,DB_LAZY_BEGIN,INFRA.database.*.verify_roles). Xu et al. (FSE 2015) show such knobs mostly cause support issues, not flexibility.
The review also found one ops setting that changes a guarantee:
OUTBOX_RELAY_ENABLED=off in a production app process downgrades the outbox
from at-least-once delivery to the post-commit dispatch alone.
Decision¶
SANDLOOM_ACCEPTED_RISKSis the one place a deployment accepts a weaker guarantee — a list of stable risk ids registered insandloom.core.config.accepted_risks(id, what it weakens, which check consults it), modelled on Django'sSILENCED_SYSTEM_CHECKS. Every check that used to read a boolean callssettings.accepts_risk(id). An unknown id fails startup. Each accepted risk in effect is reported like a semantic override (kind="accepted_risk"): a WARNING at every start,sandloom_accepted_risk_active{risk}(andsandloom_config_override_active),/diagnostics/core→configuration.accepted_risks,sandloom doctor. A prod-only risk accepted outside prod is inert: it is listed, not reported as an override.- The old booleans are deprecated aliases until 0.9 (migration tier,
removed_in="0.9"): each maps onto its id and is reported as an active migration flag with the replacement.SANDLOOM_ALLOW_VOLATILE_CONSUMERSkeeps its dev-only meaning (an alias ofvolatile_event_busoutside prod only); an outright removal would break dev stacks on Redis Pub/Sub for no production benefit, and folding it costs nothing. OUTBOX_RELAY_ENABLEDis semantic.offin a production app process is refused at startup (app_factory.outbox_relay_problem) unless the riskrelay_disabledis accepted. The gunicorn config marks non-leader workersdelegated(the process group still runs one relay) instead ofoff.- Removed settings fail loudly. The removed names live in
config_tiers.REMOVED; still set to their inert value they log a warning, any other value fails startup (the behaviour asked for is gone).verify_rolesin a database instance's options fails its option schema. BILLING_DENY_ON_PAST_DUEis product policy (ops tier): both values are valid in production and the framework has no opinion.
Kept semantic, with reasons: the RPC auth settings (removed in 0.9 by the
D-14 schedule, then derived from RPC_AUTH_MODE); SCHEMA_REQUIRE_ALL_VERSIONED
(profile-chosen, a real dev convenience — removed in 0.8 by the DX plan: a
missing schema now fails in every profile); BILLING_GRANT_INTENTLESS_SUBSCRIPTIONS
(an operator workflow for PSP-created subscriptions, not a legacy shape);
INTERNAL_ENDPOINTS_OPEN_IN_DEV (dev-only, never honoured in prod);
SMTP_USE_TLS (local mail catchers need it off).
Consequences¶
- The semantic tier shrinks from 30 to 18 settings (16 baseline + the two
argued here:
SANDLOOM_ACCEPTED_RISKS,OUTBOX_RELAY_ENABLED); the ratchet test pins a ceiling of 18 that only goes down (16 since the amendment below). The migration tier held the eight aliases (allremoved_in="0.9") until the amendment below removed them with the tier itself. - A security review reads one list per deployment instead of grepping for
*_ALLOW_*; alerting needs one rule onsandloom_accepted_risk_active. - New weakenings are added as a risk id, never as a new boolean — the ratchet makes a new semantic flag need its own ADR.
- Dev workflows that relied on soft schema validation or unsigned fake-PSP
webhooks now fail loudly; tests sign webhooks with
sandloom_billing.psp.sign_stripe_payload.
Amendment (2026-10-25)¶
Sandloom carries no backward compatibility before its first release, so the deprecation period of Decision 2 is dropped:
- The boolean aliases
BILLING_ALLOW_FAKE_PSP_IN_PROD,NOTIFIER_ALLOW_LOG_IN_PROD,STORAGE_ALLOW_LOCAL_IN_PROD,SANDLOOM_ALLOW_VOLATILE_CONSUMERS(its dev-only meaning included),SANDLOOM_ALLOW_UNVERSIONED_DDL_REPAIR,PASSWORD_HASH_ALLOW_WEAKandJWT_ALLOW_SHARED_SECRET_SPLITare removed. They are entries of the removed-settings list (sandloom.core.config.tiers.REMOVED, Decision 4): startup refuses them unless set to their inert value, and the error names the risk id to accept instead. SANDLOOM_ACCEPTED_RISKS=["<id>"]is the only way to accept a production risk (ids:fake_psp_in_prod,log_notifier_in_prod,local_storage_in_prod,volatile_event_bus,unversioned_ddl_repair,weak_password_hash,shared_hs_secret_split,relay_disabled,command_dedupe_below_retry_horizon,saga_horizon_beyond_command_dedupe— the last one added by ADR-0008).- The migration tier and its "deprecated value" reporting are gone: every
setting is semantic or ops. The RPC auth settings kept semantic above
are removed too (
RPC_AUTH_MODE,RPC_RESPONSE_BINDING; ADR-0004 amendment). The semantic tier holds 16 settings; the ratchet ceiling is 16.