Skip to main content

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 081 has 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 progressive appetite 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_checks and prohibited_jurisdictions are still read from Letta (scrutiny-additive; the ADR-0091 follow-up to also move prohibited_jurisdictions to the audited source remains open).
  • The config already stored tier_thresholds; a separate observation is that _determine_tier uses the hard-coded TIER_THRESHOLDS rather 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_data is 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.