Privacy & Data-Protection Posture
This page is the single home for Trust Relay's user-facing data-protection disclosures. Every statement here follows two rules:
- Facts only, stated by engineering. Each subsection describes what the system actually does — which stores hold what, which products read and write them, and what the UI discloses — with references to the code and ADRs that implement it.
- Legal wording is never generated. Controller/processor designations, lawful-basis texts, and works-council wording are marked as explicit operator sign-off placeholders. Engineering states the data flow; the operator and legal counsel make and sign the legal determination.
Subsections are added per privacy-remediation issue. This page currently covers the cross-product data-sharing statement (issue #394), the GDPR Art. 10 criminal-offence data safeguards (issue #381), the officer-monitoring transparency notice (issue #383), the memory-reset governance record (issue #393), and the crypto-shred erasure of PII inside immutable stores (issue #550).
Cross-product data sharing — Trust Relay Atlas
Issue #394. Entities investigated in Trust Relay Atlas surface inside the Workflow product's knowledge-graph views (and vice versa) because both products share one graph store. This section is the data-sharing statement those views link to. The design decision is disclosure, not concealment: the shared data is shown and labelled, never silently filtered away.
The facts
Shared store. Both products target the same Neo4j instance in the default single-host deployment:
| Product | Configuration | Default |
|---|---|---|
| Workflow (this product) | backend/app/config.py → neo4j_uri | bolt://localhost:7687 |
| Atlas | trustrelay-atlas/src/graph/neo4j_client.py → NEO4J_URI | bolt://neo4j:7687 |
One Neo4j container binds ports 7474/7687 on the host (verified against the running deployment 2026-07-17), so both defaults resolve to the same database. This is by design, not accident: the versioned ontology is deliberately cross-product (see Ontology Layer — one canonical schema across the Trust Relay products).
What each product writes (write-side markers in parentheses — these drive the provenance split below):
- Workflow — via
app/services/graph_etl.py/graph_service.py:Investigation,Company,Person(directors and UBOs — natural-person data),Finding,ComplianceCheck, evidence/provenance nodes, plus shared ontology metadata (OntologyVersion,NodeTypeDefinition,RelationshipTypeDefinition,DerivedQueryDefinition,RegulatoryArticle,SegmentConfig). (Marker:tenant_idproperty on entity nodes; no:Entitysecondary label. Some ontology-metadata nodes carry no marker and therefore report as unattributed — verified live 2026-07-17:RelationshipTypeDefinition/DerivedQueryDefinitionare unmarked.) - Atlas — via
trustrelay-atlas/src/graph/cypher_queries.py:LegalEntity,Person(natural-person data),Address,AdverseMedia,Document,Domain, with relationships such asOWNS,DIRECTS,REGISTERED_AT,ASSOCIATED_WITH,MENTIONED_IN. (Marker: a secondary:Entitylabel on every node, plus Atlas's owntenant_id.)
Personal data involved. Person nodes from both products (names, roles,
directorships/UBO links) and AdverseMedia mentions naming natural persons.
Direction of flow. Bidirectional at the store level: both products read and write the same database. Concretely visible in this product: the Modules → Ontology page's instance counts include Atlas-written entities.
Purposes per product (engineering description — the formal purpose-limitation statement is part of the sign-off block below):
- Workflow: KYB/KYC case investigation, cross-referencing, risk assessment, and regulatory reporting/audit under AMLR/AMLD obligations.
- Atlas: OSINT knowledge-graph construction and portfolio intelligence.
Disclosure surfaces
- The Modules → Ontology page shows a persistent "Shared knowledge graph" notice linking to this statement.
- The same page breaks the node instance counts into a provenance split:
This product / Atlas / Unattributed, derived from the write-side
markers listed above (
GET /api/graph/ontology/schema→instance_provenance/provenance_totals). - The unattributed bucket is shown honestly, never dropped: it holds nodes carrying no provenance marker (ontology metadata, pre-multi-tenancy remnants). A failed or empty provenance query renders as no split, never as "0 shared".
Controller / processor designation — ⚠️ OPERATOR SIGN-OFF REQUIRED
:::caution Placeholder — no legal designation has been made
The legal characterisation of this sharing — joint controllership (GDPR Art. 26) vs controller–processor (GDPR Art. 28) vs two independent controllers — and the corresponding lawful-basis and purpose-limitation wording for each product have not been determined by engineering and must not be inferred from this page. This block is to be completed and signed by the deployment operator with legal counsel before production use:
- Controller designation per product: ______________________
- Lawful basis for the cross-product sharing: ______________________
- Purpose-limitation statement: ______________________
- Signed / date: ______________________
:::
Tracked follow-ups
- Write-side product-of-origin stamping in both products (an explicit origin property on every node), replacing the current marker-derived attribution — engineering follow-up to issue #394.
- Store separation option (dedicated Neo4j instances per product) as an operator deployment choice; the disclosure above stands either way.
GDPR Art. 10 — criminal-offence data on named persons
Issue #381. OSINT screening necessarily processes personal data relating to criminal convictions and offences (GDPR Art. 10): sanctions designations, enforcement actions, and criminal adverse media about named directors and UBOs. Art. 10 permits this only under Union or Member-State law with appropriate safeguards. This section states the legal-basis facts and the implemented safeguards. The design decision mirrors #394: transparency, not visibility reduction — Art. 10 findings are never role-gated away from the deciding officer (that would suppress a risk signal from the decision-maker, violating the never-suppress doctrine).
Legal basis (facts; wording gated on sign-off below)
Screening for criminal-offence signals about a customer's directors and
beneficial owners is a mandatory component of customer due diligence under
the EU AML framework (AMLR/AMLD CDD, screening and ongoing-monitoring
obligations — the same obligations already cited per finding via
regulatory_basis, e.g. "AMLD6 Art. 18", "EU Reg 269/2014 Art. 2"). The
machine-readable basis token stamped on every tagged finding is
art10_union_law_aml_obligation (app/pii/categories.py →
ART10_LEGAL_BASIS); the PII taxonomy carries a dedicated
PIICategory.CRIMINAL_OFFENCE tier.
The deterministic Art. 10 tagger
app/services/art10_tagger.py stamps findings as they enter investigation
results (OSINTService.run_investigation — mock and real pipeline alike) and
on evidence-bundle copies at read time (GET /api/cases/{id}/evidence). A
finding is tagged only when all three hold:
- its category is in the Art. 10 families (adverse media / sanctions / criminal-enforcement) and is not a coverage-state "not assessed" marker (ADR-0093/0094 taxonomy-conflation lesson);
- it is not a benign "no matches"/clear screening record — presence ≠ evidence: a clear is never labelled criminal-offence data;
- it references a named natural person — via structured
detailskeys or the case's known director/UBO roster matched in the finding text (a closed name list; deterministic, no NER, no LLM).
Tagged findings carry details.art10 = true, the legal-basis token, the
matched person names, and a source-authority tier derived from the
ADR-0077 per-provider provenance: official_register / regulator /
court_enforcement / press / leak_database / self_published. Unknown
provenance deliberately falls back to the low-authority end — the UI never
lends official weight to an unattributed claim.
Safeguards implemented
| Safeguard | Implementation |
|---|---|
| Transparency in the UI | Evidence tab renders an "Art. 10 — processed under AMLR CDD/screening obligation" chip + source-authority badges on tagged entries (EvidencePanel.tsx / EvidenceSheet.tsx), linking here. Display-only — nothing is filtered. |
| Access logging | Serving Art. 10 items over GET /api/cases/{id}/evidence captures an evidence_viewed behavioral signal (officer, case, finding count) through the existing signal infrastructure — deliberately not the immutable audit_events table (read volume; ADR-0064 stays decision-scoped). |
| Assistance-memory pseudonymization | Decision narratives archived to Letta memory have Art. 10 person names replaced with stable pseudonyms (Person-N) before write (format_signal_for_archival). Audit events and decision records keep verbatim text (ADR-0064); Letta never feeds the audited risk tier (ADR-0092), so the scrub suppresses nothing. |
| Retention | Tagged finding data follows the AMLR 5-year case-evidence clock (ART10_RETENTION; ADR-0069 case pack, ADR-0108 retention purge) — never independently extended. |
| Attribution discipline | Person-level hits already require identifier-level corroboration before binding to the subject (ADR-0078 person-identifier guard; ADR-0073 R9 name-collision guard). |
What is deliberately NOT done
- No role-gating of finding content. The deciding officer (and MLRO) see the full finding — Art. 10 safeguards here are transparency + logging + assistance-layer scrubbing. Hiding criminal-offence findings from the decision-maker would suppress the very signal AML law requires them to act on.
- No deletion. Findings are case evidence under the AMLR retention obligation; erasure follows the case lifecycle (ADR-0107/0108, DSR flow).
Art. 10 processing statement — ⚠️ OPERATOR SIGN-OFF REQUIRED
:::caution Placeholder — no legal wording has been authored
The customer/data-subject-facing Art. 10 processing statement (the precise Union/Member-State law citation per deployment jurisdiction, the safeguards enumeration in legal language, and the data-subject rights notice) must be authored and signed by the deployment operator with legal counsel. The engineering facts above are the input; the chip and this page must not be read as that legal statement.
- Union/Member-State law citation(s): ______________________
- Safeguards statement (Art. 10 "appropriate safeguards"): ______________________
- Data-subject notice text: ______________________
- Signed / date: ______________________
:::
Tracked follow-ups
- The ADR-0075 post-document adverse-content scan findings feed
reassess_riskthrough a separate path and are not yet Art. 10-tagged — engineering follow-up to issue #381. - JSONB at-rest encryption for finding
details(would cover Art. 10 data) is the tracked ADR-0106 follow-up (#310);CRIMINAL_OFFENCEjoinsENCRYPTION_REQUIREDwhen a dedicated encrypted column exists (claim-vs-check: no encryption is claimed that no writer provides).
Officer activity capture — transparency & control
Issue #383. The compliance-memory system continuously records officer action signals and derives an Officer Profile dossier (cases reviewed, decision tallies, country expertise) from them. That is workplace monitoring of identified employees and requires transparency, a stated purpose, and a control. This section is the documented policy behind the in-app "What is captured and why" notice on the Memory Admin page.
What is captured (facts)
Signal classification is fully deterministic
(app/services/signal_capture_service.py — no LLM involved):
| Category | Examples | Officer control |
|---|---|---|
| Judgment | case approved/rejected/escalated, finding confirmed/rejected, risk level changed | Always captured — compliance records (see asymmetry below) |
| Preference | AI suggestion accepted/modified, chat corrections, template customized | Officer opt-out available |
| Behavioral | sections viewed, time spent, evidence downloads/views, chat questions | Officer opt-out available |
Signals land in the signal_events PostgreSQL table; a Letta assistance layer
may additionally archive derived passages. The Officer Profile shown on the
Memory Admin page is derived at read time from signal_events
(app/api/memory.py → _enrich_officer_profile).
Purpose and guarantees (facts, with the governing ADRs)
- Sole purpose: AI-assistance personalization and historical calibration of confidence scores (ADR-0112). Captured signals are not used for performance appraisal.
- Never feeds the audited risk decision: the deterministic risk tier reads only the versioned, audited risk configuration — never the memory layer (ADR-0092; the Letta layer is assistance-only).
- Self-service access: officers see their own captured signals, profile, and passages on the Memory Admin page; export/erasure requests route through the DSR console (issue #382 surface).
The opt-out and its deliberate asymmetry
Each officer can switch off behavioral + preference capture for themselves
(users.behavioral_capture_opt_out, set via
PUT /api/memory/capture-preferences — self-service only; admins can view
but not change another officer's preference). When opted out, the profile
panel states "behavioural capture disabled" instead of showing undercounting
activity metrics.
Judgment signals are NOT covered by the opt-out. They are compliance records of decisions on regulated cases, captured under a legal-obligation / traceability basis (EU AI Act Art. 12 record-keeping; ADR-0112's calibration math depends on the decision counts). Removing them would silently degrade the audited calibration evidence — the never-suppress doctrine applies. This asymmetry is stated verbatim on the in-app notice.
Retention (honest state)
signal_events rows and Letta passages currently persist until covered by the
platform retention schedule; a per-category retention pin for the memory
stores is tracked on the retention-schedule surface (issue #384 coverage map,
ADR-0108). No shorter retention is claimed than exists (claim-vs-check).
Works-council / employment-law wording — ⚠️ OPERATOR SIGN-OFF REQUIRED
:::caution Placeholder — no employment-law wording has been authored
Employee-monitoring notices are subject to national employment law and, in several Member States, works-council consultation (e.g. BE CAO 81-adjacent obligations, DE BetrVG §87). The in-app notice states technical facts and cites ADRs only. The formal employee-facing wording must be authored and signed by the deployment operator with legal counsel / works council:
- Employee notice text (per jurisdiction): ______________________
- Works-council consultation outcome / date: ______________________
- Lawful basis for behavioral/preference capture (consent vs legitimate interest): ______________________
- Signed / date: ______________________
:::
Memory reset — destructive-action governance
Issue #393.
POST /api/memory/resetbulk-deletes the tenant'ssignal_eventsand clears the Letta agent cache — a destructive act over the very records the officer-monitoring section above describes. It is governed accordingly.
The facts (all enforced in code, app/api/memory.py → reset_memory):
- super_admin only, belt-and-suspenders:
require_role("super_admin")+CONFIG_WRITEpermission (ADR-0074 Phase 2 → 403 otherwise). The frontend renders the control only for super_admin — officers never see a destructive button they cannot use. - Typed confirmation + mandatory reason: the operator must type
RESETand give a written reason (≥ 10 chars; 422 otherwise). - Immutable audit: every reset writes a
memory_resetevent to the append-only, hash-chainedaudit_eventstrail (ADR-0064/0109) carrying the cleared counts, actor, tenant, and reason. The audit write is deliberately outside the endpoint's guard-and-swallow blocks — a reset that cannot be audited fails loudly. - Honest partial outcomes: if one store clears and another fails, the
response and the audit record say
reset_partialwith per-store errors — never "complete". - Tenant-scoped: the delete carries an explicit tenant predicate (never RLS reliance alone — a prior cross-tenant wipe is documented in the code).
- The reset does not touch
audit_events(immutable, ADR-0064) or any case evidence — it clears the assistance-memory layer only.
Crypto-shred — right to erasure of PII inside immutable stores
Issue #550 (ADR-0142, refining ADR-0107). GDPR Art. 17 (erasure) meets AMLR Art. 77 (5-year retention) over the same PII, which lives inside stores built to make deletion impossible: the append-only, hash-chained
audit_events.details(ADR-0064/0109) and the SHA-256-hashed evidence bundles (ADR-0021/0069). A row cannot be deleted (that breaks immutability + tamper-evidence) and erasure cannot be refused (that breaches Art. 17). Crypto-shred resolves it: the PII is encrypted under a destroyable key, and "erase" means destroying that key — the row, its timestamp, its type and its hash-chain position all survive; the plaintext becomes unrecoverable by anyone, us included.
Dark-launched. The mechanism ships behind crypto_shred_enabled (default
false); flag-off is byte-identical to the prior disclosure state (the
retained_known_residual entries below). The facts here describe the behaviour
when the flag is on.
The key architecture — a KEK/DEK hierarchy (facts)
Two tiers, so the destroyable unit is one small row while the master secret can move to a KMS/HSM later without touching millions of ciphertexts:
- per-tenant KEK (master; a distinct secret from the platform PII key —
CRYPTO_SHRED_KEK, never registered in theEncryptedTextkeyset, so a KEK compromise without the wrapped-DEK rows yields nothing). The KEK never decrypts a PII value directly — it only wraps/unwraps DEKs (AES-256-GCM key-wrap, the DEK'skey_idbound as associated data). - per-
(subject, case)DEK (32-byte AES-256, the shreddable unit). The granularity is the tuple because erasure is already decided per(subject, case)—DSRService.handle_erasuregrants or refuses per case, so the shred inherits that gate for free. A DEK is wrapped by the KEK and stored, wrapped, in the RLS-enforcedcrypto_shred_keyskeystore (bothtenant_isolationandadmin_bypasspolicies). This is the one table where a DELETE is not only permitted but required — its whole job is to be destroyable.
The subject id in the keystore and in every erasure record is the non-reversible
person_hash (HMAC-SHA256), never a plaintext name. A multi-subject audit blob
whose leaves cannot each be attributed to one subject is encrypted under a
case-scoped DEK (subject_ref="__case__"), shreddable only once every
subject in the case is erased (§6.7, below).
Hash-over-ciphertext — the load-bearing correction
ADR-0107 (2026-07-12) assumed the audit hash chain already covered ciphertext.
The ADR-0109 chain that actually shipped (#286, later) hashes over the
plaintext details. ADR-0142 corrects this: when the flag is on, the PII
sub-values of details (audit) / data (evidence) are replaced with DEK
ciphertext before compute_entry_hash / hash_data runs, so every hash is
computed over the ciphertext. A shred destroys the DEK but never touches the
row: the stored details bytes, the entry_hash, and the prev_hash links are
byte-for-byte identical, so verify_tenant_chain passes before and after the
shred — only the PII decrypt fails. A field-level PII classifier
(app/pii/details_crypto.py) decides which leaves are encrypted; it is
fail-closed (introspected from the PIIRegistry column annotations, and an
unknown free-text key is treated as PII-bearing — over-encryption is the safe
direction). Non-PII metadata (ids, enums, counts, timestamps, hashes, country
codes) stays plaintext and queryable.
The shred lifecycle (facts)
- Encrypt-at-write (flag on) —
audit_service.log_event/evidence_bundle_service.build_bundleencrypt PII leaves under the unit's DEK before the row is hashed. - Erase — the shred runs only on the DELETE branch of
handle_erasure(and therun_retention_purgesweep that drives it), which already honours the AMLR 5-year clock +RetentionHoldlegal holds (ADR-0140). A held or in-window case is REFUSE → the DEK is provably never destroyed. - Destroy + verify —
CryptoShredService.shredzeroes the wrapped DEK, setsstatus='shredded', then verifies the state: a ciphertext that decrypted before must fail to decrypt after. If it still decrypts (a no-op destroy, a cached/replicated key) the shred isfailedanderasure_completeflipsfalse— never a silent partial (design §4.3). - Record — one immutable
crypto_shredaudit event (its owndetailscarry only non-reversible ids + a controlled basis string, so the proof of erasure needs no encryption and is retained for the full AMLR horizon). Every consumer — the DSR console (crypto_shreddedstore entry, replacingretained_known_residual), the evidence surface, the regulator case-pack, andGET /monitoring/retention— reads this one event; none re-derive the decision. - Regenerate a case-pack — a pack built after a shred renders the shredded
PII as the stable
[erased — crypto-shredded]mask (read from the decrypt-failure, never re-derived), and its freshpack_hashrecomputes cleanly. An already-exported pack (in the regulator's hands) is lawful for what it disclosed at export time and is never retro-altered.
Restore contract — the shred-replay journal (§6.1 option (a))
A crypto_shred_keys backup taken before a shred still holds the live wrapped
DEK, so restoring the database to a pre-shred snapshot would resurrect the
key and silently undo the erasure. The interim defence is a shred-replay
journal (crypto_shred_journal): every destroyed DEK appends one row, and
after a restore an operator runs replay_shred_journal to re-destroy any
resurrected key. The full operational runbook — including the honest caveat
that the in-DB journal reverts with its own database, so it must be EXPORTED
before a restore and RE-IMPORTED after — is documented once in
Known Gaps → Crypto-shred restore procedure.
The durable answer (a journal in a separate store, or KMS-held DEKs whose delete
is authoritative) is tracked as production hardening (design §6.1 option (b)).
Honest residuals (not waved away)
- Historical plaintext (§6.2). Audit rows written before the
encrypt-at-write cutover hash over plaintext and cannot be retro-encrypted
(immutable rows cannot be updated). Their PII survives a shred;
handle_erasurekeeps emittingretained_known_residualfor exactly those rows (detectable — no DEK exists for the unit), so a report never claims a completeness it lacks. These age out only with the 5-year retention horizon. - Case-scoped multi-subject (§6.7). A leaf naming several persons is
encrypted under the
__case__DEK and is shreddable only when every subject in the case is erased — one subject's request is never over-shredded into another's still-protected data. - In-DB journal caveat. As above — the interim journal is not an automatic guarantee across a restore; it depends on the export/re-import discipline until the separate-store / KMS hardening lands.