ADR-0126: AMLR Art. 22(2) — SMO fallback exhausted-means record + tipping-off abstention
Date: 2026-07-24 Status: Accepted Deciders: Adrian (Soft4U BV), Claude (Opus 4.8) Extends: ADR-0055 (SMO fallback election), ADR-0053/0054 (UBO ownership/control engine), ADR-0056/0057 (verification model + min-2-source gate), ADR-0064 (immutable audit_events), ADR-0109 (per-tenant audit hash chain), ADR-0097 (mandatory ≥50-char rationale pattern), ADR-0067 (fail-closed compliance outputs) Distinct from: ADR-0071 (SAR-first customer-contact gate / AMLR Art. 73 tipping-off) — a different control (SAR disclosure boundary), deliberately not conflated
Context
AMLR Art. 22(2): where, after exhausting all possible means, no natural person is identified as the beneficial owner of a corporate entity (or there is doubt an identified person is genuine), the obliged entity shall:
- record, as a distinct typed state, that no beneficial owner was identified;
- identify AND verify all senior managing officials (SMOs);
- where verifying the SMOs would tip off the customer to the entity's beneficial- ownership doubts, abstain from that verification and instead record, as an immutable structured record, the steps taken and the difficulties encountered.
"All possible means exhausted" is itself an evidenced, timestamped record — not an implicit side effect of an empty UBO result.
State before this ADR (verified in the tree, three parts, only one fully present):
- Part 1 — typed "no BO identified" state: PRESENT as an interpretation flag.
amlr_section_c.pyemitsno_natural_person_owner_evidenced/smo_fallback_only/ownership_corporate_wall, and the SMO-only fallback correctly earns zero §2(c)/§2(e) ownership coverage (R4). But this is a read-time interpretation recomputed on each coverage read — there was no immutable, timestamped record attesting that the means were exhausted at the moment of the determination. - Part 2 — SMO election: PRESENT but elected-not-verified.
ubo_engine.compute(ADR-0055) elects the active SMO(s) of last resort, but the election was never marked for verification — the "identify AND verify" half of Art. 22(2) was silent. - Part 3 — tipping-off abstention: ABSENT. The only tipping-off control in the codebase
(
tipping_off.py/customer_contact_gate.py) is the SAR disclosure boundary (AMLR Art. 73) — a different article and a different control. There was no Art. 22(2) SMO-verification abstention state and no steps/difficulties record.
Decision
Three additive controls, one article, all fail-closed (add scrutiny, never suppress a signal):
1. Immutable "no BO identified" exhausted-means evidence record
A new pure builder smo_fallback_record.build_no_bo_identified_record(investigation_result, ubo_results) reuses amlr_section_c._partition_ubos (the single source of truth for the
evidenced/SMO/traced partition — not re-derived) and returns a structured record only
when the ownership/control computation reached no natural-person beneficial owner. The record
carries a typed reason (smo_fallback_only vs no_natural_person_owner_evidenced), the
identification means attempted (derived from concrete evidence in the investigation —
ownership/control computation ran, registry consulted, declared-UBO data reviewed;
presence ≠ evidence, a means with no evidence is never listed), the SMO election, and
all_possible_means_exhausted: true. A thin async writer stamps a timestamp and writes it to
append-only audit_events via AuditService.log_event (event_type="no_bo_identified",
ADR-0064 immutability + ADR-0109 hash chain) — never a mutable table.
A dedicated Temporal activity record_no_bo_identified runs in the KYB pre-investigation
path after populate_knowledge_graph (so the ownership structure is populated),
recomputes the authoritative UBO determination (UBOComputationService(...).compute( persist=False) — the same call get_amlr_coverage makes), and writes the record when the
fallback triggers. Guard-and-swallow: it can never break the investigation.
2. SMO elections flagged for verification
BeneficialOwnerResult gains requires_verification: bool = False (backward-compatible).
ubo_engine.compute sets it True on every SMO election — closing the "elected but
unverified" gap so downstream verification routes the SMO (Art. 22(2) "identify AND
verify"). A genuine ownership/control UBO is verified through its own path and is not
flagged here.
3. Officer-settable tipping-off abstention (typed state + immutable record)
A new append-only, tenant-scoped endpoint POST /api/cases/{workflow_id}/smo-abstention
(router app/api/smo_abstention.py, CASE_DECIDE) records an officer's Art. 22(2)
abstention: a required steps-taken + difficulties-encountered narrative (jointly ≥ 50
chars, mirroring the ADR-0097 rationale bar; 422 otherwise), written immutably to
audit_events (event_type="smo_verification_abstained_tipoff") via AuditService.log_event.
A companion GET lists both Art. 22(2) record types for the case (retrievable audit surface).
Consequences
- Never-suppress preserved: the "no BO identified" record does not clear the
ownership-coverage gap —
amlr_section_cstill credits SMO-only fallback with zero §2(c)/ §2(e) coverage. The abstention is an officer decision that is recorded, never a way to skip a check. Both records only ADD scrutiny. - Honest typed state + presence ≠ evidence: the reason distinguishes the SMO-elected case from the no-electable-SMO case; the means list contains only evidenced steps.
- Immutable + traceable: both records live in the append-only, hash-chained
audit_eventstable (ADR-0064/0109) — tamper-evident, tenant-scoped, retrievable. - Deferred (correct first cut, tracked): The abstention endpoint is backend-only (no UI
in this change). The
no_bo_identifiedactivity may over-record on an in-workflow retry (append-only, so over-recording never under-records — consistent with the ADR-0064 audit philosophy).
Revision — 2026-07-24 (Codex review, PR #562)
Five review findings addressed, all fail-closed / never-suppress:
- Honest exhausted-means (was: always
true) —assess_means_exhaustionwithholdsall_possible_means_exhaustedwhenever a traced person carries a typednot_assessedownership/control outcome (issue #530) or a truncated traversal; the record lists theunassessed_dimensionsand states the means were not exhausted rather than overstating completeness over a data gap. requires_verificationis now WIRED (was: dead flag) — the "deferred" follow-up above is done, contained to this surface.smo_fallback_record.build_smo_verification_gatereads the flag and routes each elected SMO throughevaluate_profile_gates(ADR-0057/0058, reusingverification_gate_wiring's director-profile builder), embedding the per-SMO gate outcome in the immutableno_bo_identifiedrecord — invoked from therecord_no_bo_identifiedactivity (post-graph, where the SMO election exists). No workflow reorder required.- Abstention precondition —
POST /smo-abstentionrefuses (409) unless the case carries a recordedno_bo_identifieddetermination that elected an SMO, so no legally-false immutable record can be minted for a natural-person KYC case or a KYB case with a real evidenced owner. - Fail-closed audit reads — both
smo_abstentionendpoints raise 503 on an audit-read failure, never a silent empty list / permissive write. - Documentation —
docs/architecture/ubo-determination.mddocuments the Art. 22(2) flow, the exhausted-means record, the SMO verification gate, and the endpoints.
Alternatives considered
- Emit the record from
get_amlr_coverage(the read-time interpretation): rejected — it ispersist=Falseby contract and runs on every dashboard read, which would flood the 5-year audit table and conflate "displayed interpretation" with "recorded determination." - A new mutable
no_bo_recordstable: rejected — Art. 22(2) demands an immutable record;audit_events(ADR-0064/0109) is the tamper-evident store already used for every compliance-decision artifact. - Fold the abstention into the existing
tipping_off.py: rejected — that is the SAR disclosure boundary (Art. 73); conflating two different tipping-off controls would blur the regulatory basis of both.