Known Gaps & Roadmap
This page is the honest register of what Trust Relay does not yet do, or does only under
a mock/stub. It succeeds the retired atlas/gaps-and-next-steps.md register (2026-06-11),
whose items have largely closed. Keeping this list current is itself a compliance posture:
an MLRO evaluating the platform should see the limitations plainly, not discover them in
production.
Detailed engineering findings from internal code reviews (including security-sensitive
items) are tracked in the repository under docs/research/ and in GitHub issues — not on
this public page.
Closed since the 2026-06-11 register
| Former gap | Closed by |
|---|---|
| RLS inert (app ran as DB superuser) | ADR-0050 / ADR-0051 — app runs as non-superuser trustrelay_app; RLS enforced since 2026-06-12 |
| Sanctions false-positive suppression not evidence-based | ADR-0045 — evidence-based, tenant-scoped, always-visible suppression (implemented) |
| No maker-checker on high-risk decisions | ADR-0070 — four-eyes PENDING_SECOND_APPROVAL control |
| Compliance outputs could report "clear" when no check ran | ADR-0067 — fail-closed "not assessed" contract |
audit_events immutability by convention only | ADR-0064 — DB trigger + REVOKE UPDATE/DELETE + FK RESTRICT |
| RBAC log-only (Phase 1) | ADR-0074 — Phase 2 deny-by-default 403 enforcement, active 2026-06-28 |
| A lower-recall re-screen could silently overwrite an entity's persisted risk baseline (CRITICAL→medium flip on OB Holding 1 OÜ, same entity/inputs) | ADR-0089 — deterministic scoring (temperature=0 on the 7 compliance-output agents) + reconcile_baseline_risk() one-way ratchet in entity_baseline_service.upsert_baseline (migration 080) |
Open — capability gaps (mock/stub gated)
These raise NotImplementedError when their *_MOCK_MODE flag is false, so they are
explicitly not silently faked in production:
- Identity verification — real eID Easy / itsme / eIDAS integration is stubbed
(
app/services/eid_easy_service.py).IDENTITY_VERIFICATION_MOCK_MODE=trueuses realistic fixtures; with it off the service raises, which is the honest behaviour for a capability that does not exist.
No longer a gap (ADR-0103, 2026-07-11): natural-person sanctions / PEP screening
is real. It runs against OpenSanctions (app/services/kyc_screening.py →
screen_opensanctions) and is fail-closed — an unavailable provider yields an explicit
not_screened, never a clean result. It was previously listed here as stubbed, which
was wrong in a way worth naming: the real screener existed and was wired, but was
unreachable because KYC_MOCK_MODE gated both it and identity verification, and was
forced on to stop the latter crashing. One flag now gates exactly one capability.
- Live company-status monitoring —
company_status/aeo_statusmonitoring checks are stubs that fail closed: they return an indeterminate WARNING (never a benign no-change) so a possible dissolution cannot be ruled out (ADR-0067 extension).
Open — coverage gaps
Brand-directed discovery (ADR-0094) — validated, stays OFF (2026-07-06). Live OB Holding validation confirmed the mechanism works end-to-end (gate fires on the subject's adverse-media evidence; the operating brand "OlyBet" is mined from the related entities; the NorthData brand search returns 15 hits, parsed into candidates with ADR-0078 two-lane attribution). Two findings keep it dark-launched: (1) a substring false-positive ("olybet" matched "Polybeton", a French concrete firm) — fixed with word-boundary matching; (2) NorthData's "OlyBet" search does not return the specific Lithuanian operating entity (it returns a Romanian OlyBet + noise), and the ownership graph lists only parents+auditor — so the OB Holding LT entity (€8.4m fine) is genuinely not findable via NorthData by either path. Closing that specific oracle needs a direct Lithuanian-registry query, a different data source — not a smarter search. Brand-discovery remains valuable for the other group entities it does find; enablement awaits that precision + a source that reaches the operating subsidiaries.
- Per-entity / per-director adverse-media recall (Tier B) — GitHub issue #170,
ADR-0093. The recall work is ENABLED in production (2026-07-06,
adverse_recall_enabled=True) after live OB validation + a passing Calibration Review: a network-aware pass runs after the network phase and searches each discovered related entity (+ its persons) for adverse media with ADR-0078 two-lane attribution and a hard budget; the "not assessed" marker becomes coverage-aware. Enabling it in production is Calibration-Review-gated pending a live validation run (it feeds the deterministic escalator and is real API spend). Until then the honest Tier-A marker (PR #171) stands: related entities/persons that were sanctions/PEP-screened but not individually adverse-media-searched are flagged not assessed, not cleared. - Raw live-web retrieval recall is inherently non-deterministic — the same criminal/
adverse-media finding can be retrieved on one BrightData/Tavily/Google pass and missed on
another (this is the retrieval axis of issue #170, and was the proximate trigger for
ADR-0089: an OB Holding 1 OÜ
re-run on identical inputs scored CRITICAL/90 with 4 EPPO/criminal findings on one pass and
medium/51 with 1 finding on another). ADR-0089 does not — and does not claim to —
eliminate this variance; caching or replaying the web corpus was considered and rejected
(it would defeat the point of perpetual re-screening, which is to observe genuinely new
signals). What ADR-0089 changes is the blast radius: a recall miss can no longer
silently lower an entity's persisted risk. Component B's
reconcile_baseline_risk()holds the established floor and raises aRISK_DIVERGENCEalert instead of overwriting it; Components C/D (app/services/material_findings.pyfingerprint/re-injection,material_check_incompletefail-closed gap flag) mean a dropped finding is carried forward and a data-gap can never enable a downgrade. The recall gap itself — actually finding the article on every pass — remains open and is tracked under issue #170, not closed by this ADR. - Country depth — 12-country registry architecture (ADR-0034/0042/0079) with honest per-country capability gating (ADR-0068/0080); depth varies by jurisdiction, and an unsupported country/signal is labelled not assessed for X rather than silently skipped.
- Confidence calibration cold-start dead zone — GitHub issue #239. The Historical
Calibration confidence dimension (
confidence_engine.py) is scoped per(officer, template, country)and needs 10 decided cases in that exact cell to warm. Because the compliance gates (EDD SoF/SoW, open-UBO-discrepancy, SAR-first) correctly refuse a large share of cases before a terminal decision, and activity fragments across jurisdictions, a new officer — or an experienced one in a new country — sits at the neutral12.5/25cold-start indefinitely: the memory's accumulated cross-scope evidence stays invisible to the score. The neutral default is honest (it refuses to fabricate an unearned calibration), but the proposed fix is a hierarchical shrinkage fallback that borrows a disclosed, conservative prior from broader scopes (officer-overall, tenant) and converges to the specific per-country rate as volume builds. Parked pending a design ADR; surfaced during the 2026-07-05 cross-country memory-showcase run.
AMLR-readiness rollout status (epic #528)
The AMLR uplift (Regulation (EU) 2024/1624, applicable 10 July 2027) shipped a large body
of engine and read-surface work across four waves; the programme map is on
AMLR Readiness. Several determinations are dark-launched — the code is
merged and tested, but a *_enabled config flag defaults to false, so it does not execute
in production until a human flips it after a passing
Calibration Review.
This is a deliberate honesty/rollout posture, not an incomplete feature: flag-off is byte-identical
to before the change, and a read surface can be live while the engine behind it returns
not_assessed.
Dark-launched (flag defaults to false, gated on a Calibration-Review flip)
| Capability | ADR | Config flag |
|---|---|---|
Sanctioned-ownership / control 50%-rule (Art. 20(1)(d)) — read surface live, returns not_assessed | 0127 | sanctioned_ownership_control_enabled |
| Immediate-on-designation re-screen (Art. 26(4)) | 0128 | immediate_designation_rescreen_enabled |
| Art. 62 BO data-currency monitoring (28-day / annual) — dataset shape is live | 0136 | bo_dataset_currency_monitoring_enabled |
| Fail-closed insufficient-data approval gate (log-first Phase 1) | 0125 | approval_insufficient_data_gate_enabled |
| Crypto-shred of PII in immutable stores | 0142 | crypto_shred_enabled |
| Retention-purge sweep (the no-purge guard is always-on) | 0108 / 0140 | retention_purge_enabled |
| OSINT golden-record survivorship resolver | 0123 | golden_record_resolver_enabled |
| Hash-audited risk scoring | 0124 | hash_audited_scoring_enabled |
| Connector data-contract trust hook (validator/drift mechanism is built + tested) | 0143 | connector_contract_trust_enabled |
The two Wave-1 determinations most sensitive to false positives/negatives and API spend —
sanctioned ownership (0127) and immediate re-screen (0128) — are the #529-class prod
enablement: flipping them on is gated on a passing Calibration Review and a live validation
run.
Deferred (engine built + tested; the data feed / consumer is a tracked follow-up)
- CDD-register PDF/dossier consumer — the ADR-0137 nine-measure register computes, has a read surface, and now feeds the assistant (#601) and the CDD-coverage UI panel (#600); a dedicated PDF/dossier consumer of the register is the remaining tracked follow-up.
- Connector-contract live-parse wiring — ADR-0143 lands the validator + drift alert + finding
mechanism; wiring
validate_responseinto each connector's live parse boundary (and flipping the trust flag) is #518's deferred tail. - Crypto-shred PII scope — ADR-0142 covers
audit_events.details+ evidence bundles. Two further PII sinks are explicitly out of that issue's scope: assistant-memory (Letta /signal_events/ dashboard-agent, #676) and customer-portal enrolment (uploaded-doc extracts / portal answers, #677). Historical plaintext written before the encrypt-at-write cutover is an immutable residual (see the crypto-shred reconciliation below).
Live (on in production now — recorded here for contrast)
Register-discrepancy report + Art. 24(2) derogation lifecycle (ADR-0129), SMO exhausted-means
record + tipping-off abstention (ADR-0126), Decimal ownership + Art. 54 (ADR-0130), effective-dated
threshold snapshot (ADR-0133), the retention clock + no-purge guard (ADR-0139/0140), the Annex-factor
annotation read surface (ADR-0141), the risk-appetite activation four-eyes (ADR-0092/#626), the
dashboard-assistant non-downgradable readiness (ADR-0132), and the consolidated AMLR evidence dossier
(#592) are live. The sanctions FP-suppression never-suppress hardening (ADR-0131) is live for its
fail-closed check + raw-matches bypass; its force-invalidation-on-redesignation rides the
immediate_designation_rescreen_enabled flag.
Immutable evidence vs GDPR Art. 16/17 — the documented reconciliation (ADR-0107)
Two controls in this platform deliberately make records un-alterable: the append-only
audit_events trail (ADR-0064 — DB trigger + REVOKE UPDATE/DELETE; hash-chained per
ADR-0109) and the SHA-256-hashed evidence bundles behind the regulator case pack
(ADR-0021/0069 — every item hashed, plus a bundle manifest hash). GDPR Art. 16
(rectification) and Art. 17 (erasure) require that personal data inside them can
nevertheless be corrected or removed. Neither side may be weakened: a mutable audit trail
fails AMLR / EU AI Act Art. 12; refusing rights that are due fails GDPR.
The accepted reconciliation is ADR-0107 (crypto-shredding):
- Audit trail — PII fields of
audit_events.detailsare to be encrypted under a per-subject key; erasure destroys the key, never the row. The row, its timestamp, its type and its hash-chain position all survive; the plaintext becomes unrecoverable. - Evidence bundles — hashed items are never mutated in place (that would break the item's own hash and the bundle manifest). A rectification or post-retention erasure is recorded as a rights-action (tombstone) record alongside the bundle: the original item stays hash-verifiable, the tombstone states what was corrected/erased, on whose request, and on what legal basis — so sibling hashes and the manifest remain intact while the current-truth view carries the correction.
Current state (honest): the crypto-shred mechanism (KEK/DEK key hierarchy,
encrypt-on-write, shred-on-erase, hash-over-ciphertext) is now implemented behind
crypto_shred_enabled (ADR-0142, #550) — dark-launched, default false. The full
lifecycle is proven end-to-end on a real testcontainer database
(tests/test_crypto_shred_lifecycle_e2e.py): seed plaintext → flip the flag → write
ciphertext → shred → the ADR-0109 chain still verifies while the PII no longer decrypts →
regenerate a case-pack that renders [erased — crypto-shredded] and re-seals cleanly. See
the Privacy & Data-Protection → Crypto-shred
section for the architecture. While the flag is off, and for every unit the shred cannot
reach, what ships is the disclosure ADR-0107 commits to: every Art. 17 erasure report
(DSRService.handle_erasure, surfaced by POST /api/data-subject/erase and recorded on the
DSR_ERASURE audit event) carries two retained_known_residual store entries —
audit_events and evidence_bundles — naming the PII the erasure could not reach and the
ADR-0107 basis for retaining it; with the flag on, a fully-erased case's audit_events entry
flips to crypto_shredded (carrying the shred_id) instead. A residual is non-failing for
erasure_complete (it is retention-by-design, not a failed store) but is never omitted, so
the officer serving a DSR sees exactly what remains. Historical plaintext PII already written
into audit_events.details before the encrypt-at-write cutover is a residual the shred
cannot retro-fix (immutable rows cannot be re-encrypted in place) — named in ADR-0107's
consequences and in design §6.2, not waved away.
Crypto-shred restore procedure (operations runbook)
The crypto-shred implementation (ADR-0142, #550) is dark-launched behind
crypto_shred_enabled (default false). While it is off, the disclosure above is exactly
what ships. This runbook documents the backup-resurrection defence (design §6.1) that the
shred path relies on when the flag is on — an operator must follow it around any database
restore, because it protects a completed erasure from being silently undone.
The sharp edge. A crypto-shred destroys a per-(subject, case) Data-Encryption Key by
zeroing its one wrapped-DEK row in crypto_shred_keys (the deliberately-destroyable mirror of
audit_events). A backup of that table taken before the shred still holds the live wrapped
DEK. Restoring the database to a pre-shred snapshot therefore resurrects the key and
un-does the erasure — the ciphertext becomes decryptable again.
The interim defence — a shred-replay journal. Every destroyed DEK appends one immutable row
to crypto_shred_journal (a durable, append-only trail; distinct from the destroyable
keystore). After a restore, an operator re-applies every recorded shred with
replay_shred_journal(session, tenant_id=...)
(app/services/crypto_shred_service.py): it walks the journal and re-zeroes any resurrected
crypto_shred_keys row (status='shredded'). It is idempotent — a unit already tombstoned
is a no-op — and returns {rows_replayed, deks_rezeroed, already_shredded, missing_keys}.
Honest caveat — the journal reverts with its own database. Because the journal lives in the same database as the keystore, a full restore reverts the journal alongside the keystore, so the in-DB copy cannot self-heal a shred that the snapshot predates. The interim contract is therefore an operational discipline, not an automatic guarantee:
- EXPORT
crypto_shred_journalto a store outside the database being restored (e.g.\copy crypto_shred_journal to 'shred-journal-<ts>.csv' csv header), before any restore. This exported copy is the authoritative record of erasures the restore will roll back. - RESTORE the database from the backup.
- RE-IMPORT the exported journal rows (they carry
tenant_id,subject_ref,case_id,key_id) intocrypto_shred_journal, then runreplay_shred_journal— per tenant under that tenant's RLS session, or across all tenants under an admin/cross-tenant session (tenant_id=None). Every erasure the restored snapshot predates is re-destroyed; already-shredded units are no-ops.
Production hardening (tracked). The durable answer is a journal in a separate store, or DEKs held in a KMS/HSM whose delete is authoritative and un-restorable (the DB then stores only KMS handles) — design §6.1 option (b). The in-DB journal is the interim, not the final answer.
Open — platform / governance
- Letta sleeptime cadence not configurable — the "learns rules" pathway is now reachable
(issue #242, PR #245:
sleeptime runs on the tenant agent that owns
learned_procedures; episodic payload no longer dropped), butsleeptime_agent_frequencyhas no create/update param in letta-client 1.12.1, so Letta uses its default cadence, and the fire-and-forget inline write paths still lack staged retry. Assistance-layer only — not a compliance risk (the authoritative trail is PostgreSQLsignal_events/audit_events). - SSO federation — design only (ADR-0076, Proposed), not implemented.
- SCIM provisioning, ISO 27001/42001/27701 certification, data-residency controls — named as gaps in the EU AI Act conformity record (ADR-0072); not yet in place.
- CI residual — GitHub issue #167: ~80 pre-existing backend-test failures remain after the Backend Tests job was rebuilt (PR #166); tracked for cleanup.
- Baseline-downgrade approver check bypasses
FourEyesContext(minor, non-alarming) — the ADR-0089 audited-downgrade path (Component E) enforces the second-distinct-approver rule by callingassert_distinct_approver(maker_user_id, checker_user_id)directly rather than constructing aFourEyesContextand routing throughrequires_four_eyes()as the other ADR-0070 four-eyes call sites do. Functionally equivalent — same rejection rule, same audit trail — but a consistency nit flagged in the ADR-0089 review: a future refactor should route it throughFourEyesContextfor a single call pattern across all maker-checker gates.
Doc-infrastructure items fixed 2026-07-02
- Architecture-index
undocumentedlist and thecheck-docs-sync.shstaleness gate had a path-normalization mismatch that made the gate silently skip backend files — corrected so the coverage metric and the pre-commit gate work as designed. - The published ADR registry table had drifted (stopped at ADR-0049) and 16 ADR mirror
pages were uncommitted — both reconciled; a canonical
docs/adr/README.mdindex now exists.