ADR-0092: Risk appetite is sourced from the audited risk config
Date: 2026-07-06 Status: Accepted Supersedes: ADR-0091 Deciders: Adrian (Soft4U), Claude Opus 4.8
Context
ADR-0091 closed an acute defect — the risk
tier is _determine_tier(score, risk_appetite), risk_appetite selects the
TIER_THRESHOLDS band (progressive lowers the tier for a given score), and it was read
from the mutable, unaudited Letta org_policy block on the deterministic tier path, so
a Letta edit could reduce scrutiny with no audit trail. ADR-0091's interim fix
(appetite_from_unaudited_source) clamped the unaudited value so it could only make the
tier stricter. That closed the suppression path but had two costs it named explicitly:
a legitimate progressive appetite could not be honored, and it said "the full remediation
… will supersede this clamp when it lands."
This is that remediation. The audited, versioned risk_configurations.config_data already
carries the tier_thresholds bands (build_default_config); it simply lacked the
appetite selector. Reads from that table are cached, versioned, and retained — the
correct home for a decision-affecting policy under EU AI Act Art. 12 (traceability) and
AMLR documented-methodology + 5-year retention.
Decision
Add risk_appetite (default "moderate") to the audited config_data
(build_default_config), validate it against the defined tier_thresholds bands
(validate_config check 4b), and backfill existing rows (migration 081). The three
tier-determining read sites — initial assessment (case_crud.py), mid-investigation
reassessment (activities.py), and manual recalc (risk_config.py) — now read
risk_appetite from config_data (with a "moderate" fallback), not from Letta or
additional_data. Because the source is now audited and versioned, a legitimate
progressive appetite is honored without a clamp — it carries a retained record an
MLRO can point to. locked_checks / prohibited_jurisdictions continue to come from the
Letta org_policy (scrutiny-additive only) and are the follow-up noted in ADR-0091.
The appetite_from_unaudited_source clamp and its tests are retained as defense-in-depth
— Letta still mirrors an appetite value for the AI assistant, and the clamp remains the
correct guard for any future code that reads that unaudited value for a decision.
Decision context:
- Latency: none (an already-loaded dict lookup; removes 0–1 Letta round-trips per assessment).
- Dependency surface: no new packages; one config field, one validation rule, one data migration, three call-site re-points.
- Debuggability: appetite is now a single audited field with a versioned history; a past tier can be reconstructed from the config version in effect.
- Reversibility: migration
081has a clean downgrade; call sites revert to the ADR-0091 clamp. No destructive change. - Blast radius: additive/fail-safe — existing rows default to
moderate; a tenant that had an unaudited progressive appetite is held at moderate (stricter) until it is set in the audited config, which is the correct posture. - Alternative considered: keep the clamp and never honor progressive — rejected because it permanently removes a legitimate configuration knob; the audited source restores it with an audit trail.
Consequences
Positive
- The tier-band input is now audited, versioned, and retained — an MLRO can reconstruct which appetite was in effect for any past decision.
- A legitimate
progressiveappetite is honorable again, but only through an audited channel. - The deterministic tier no longer reads any value from the mutable Letta layer — the "Letta never feeds the risk decision" invariant now holds for the tier as well as the score.
Negative
- Tenants that expressed appetite only via the Letta org_policy are reset to
moderate(stricter) until an admin sets it in the audited config. Correct, but a migration step for them. - Appetite is not yet editable in the risk-config admin UI — it is a config field an admin sets via the config-update path; a dedicated UI control is a follow-up.
Neutral
locked_checksandprohibited_jurisdictionsare still read from Letta (scrutiny-additive; the ADR-0091 follow-up to also moveprohibited_jurisdictionsto the audited source remains open).- The config already stored
tier_thresholds; a separate observation is that_determine_tieruses the hard-codedTIER_THRESHOLDSrather than the config's copy — out of scope here, noted for a future ADR.
Alternatives Considered
Alternative 1: Keep the ADR-0091 clamp only
- Never honor progressive; clamp all unaudited appetite to moderate/conservative.
- Why rejected: permanently discards a legitimate progressive appetite. The audited source is strictly better — it restores the knob and makes it traceable.
Alternative 2: Add a dedicated risk_appetite column (not inside config_data)
- A first-class column on
risk_configurations. - Why rejected:
config_datais already the versioned, validated, cached policy blob the scorer reads; adding the selector there reuses the existing audit/version machinery with no new column, cache, or read path. A column would fragment the config across two stores.