Skip to main content

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:

  1. record, as a distinct typed state, that no beneficial owner was identified;
  2. identify AND verify all senior managing officials (SMOs);
  3. 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.py emits no_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_c still 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_events table (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_identified activity 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:

  1. Honest exhausted-means (was: always true)assess_means_exhaustion withholds all_possible_means_exhausted whenever a traced person carries a typed not_assessed ownership/control outcome (issue #530) or a truncated traversal; the record lists the unassessed_dimensions and states the means were not exhausted rather than overstating completeness over a data gap.
  2. requires_verification is now WIRED (was: dead flag) — the "deferred" follow-up above is done, contained to this surface. smo_fallback_record.build_smo_verification_gate reads the flag and routes each elected SMO through evaluate_profile_gates (ADR-0057/0058, reusing verification_gate_wiring's director-profile builder), embedding the per-SMO gate outcome in the immutable no_bo_identified record — invoked from the record_no_bo_identified activity (post-graph, where the SMO election exists). No workflow reorder required.
  3. Abstention preconditionPOST /smo-abstention refuses (409) unless the case carries a recorded no_bo_identified determination 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.
  4. Fail-closed audit reads — both smo_abstention endpoints raise 503 on an audit-read failure, never a silent empty list / permissive write.
  5. Documentationdocs/architecture/ubo-determination.md documents 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 is persist=False by 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_records table: 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.