Skip to main content

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 gapClosed 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-basedADR-0045 — evidence-based, tenant-scoped, always-visible suppression (implemented)
No maker-checker on high-risk decisionsADR-0070 — four-eyes PENDING_SECOND_APPROVAL control
Compliance outputs could report "clear" when no check ranADR-0067 — fail-closed "not assessed" contract
audit_events immutability by convention onlyADR-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=true uses 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.pyscreen_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 monitoringcompany_status / aeo_status monitoring 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 a RISK_DIVERGENCE alert instead of overwriting it; Components C/D (app/services/material_findings.py fingerprint/re-injection, material_check_incomplete fail-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 neutral 12.5/25 cold-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)

CapabilityADRConfig flag
Sanctioned-ownership / control 50%-rule (Art. 20(1)(d)) — read surface live, returns not_assessed0127sanctioned_ownership_control_enabled
Immediate-on-designation re-screen (Art. 26(4))0128immediate_designation_rescreen_enabled
Art. 62 BO data-currency monitoring (28-day / annual) — dataset shape is live0136bo_dataset_currency_monitoring_enabled
Fail-closed insufficient-data approval gate (log-first Phase 1)0125approval_insufficient_data_gate_enabled
Crypto-shred of PII in immutable stores0142crypto_shred_enabled
Retention-purge sweep (the no-purge guard is always-on)0108 / 0140retention_purge_enabled
OSINT golden-record survivorship resolver0123golden_record_resolver_enabled
Hash-audited risk scoring0124hash_audited_scoring_enabled
Connector data-contract trust hook (validator/drift mechanism is built + tested)0143connector_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_response into 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.details are 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:

  1. EXPORT crypto_shred_journal to 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.
  2. RESTORE the database from the backup.
  3. RE-IMPORT the exported journal rows (they carry tenant_id, subject_ref, case_id, key_id) into crypto_shred_journal, then run replay_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), but sleeptime_agent_frequency has 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 PostgreSQL signal_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 calling assert_distinct_approver(maker_user_id, checker_user_id) directly rather than constructing a FourEyesContext and routing through requires_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 through FourEyesContext for a single call pattern across all maker-checker gates.

Doc-infrastructure items fixed 2026-07-02

  • Architecture-index undocumented list and the check-docs-sync.sh staleness 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.md index now exists.