Skip to main content

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:

  1. 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.
  2. 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:

ProductConfigurationDefault
Workflow (this product)backend/app/config.pyneo4j_uribolt://localhost:7687
Atlastrustrelay-atlas/src/graph/neo4j_client.pyNEO4J_URIbolt://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_id property on entity nodes; no :Entity secondary label. Some ontology-metadata nodes carry no marker and therefore report as unattributed — verified live 2026-07-17: RelationshipTypeDefinition / DerivedQueryDefinition are unmarked.)
  • Atlas — via trustrelay-atlas/src/graph/cypher_queries.py: LegalEntity, Person (natural-person data), Address, AdverseMedia, Document, Domain, with relationships such as OWNS, DIRECTS, REGISTERED_AT, ASSOCIATED_WITH, MENTIONED_IN. (Marker: a secondary :Entity label on every node, plus Atlas's own tenant_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/schemainstance_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).

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.pyART10_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:

  1. 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);
  2. it is not a benign "no matches"/clear screening record — presence ≠ evidence: a clear is never labelled criminal-offence data;
  3. it references a named natural person — via structured details keys 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

SafeguardImplementation
Transparency in the UIEvidence 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 loggingServing 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 pseudonymizationDecision 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.
RetentionTagged finding data follows the AMLR 5-year case-evidence clock (ART10_RETENTION; ADR-0069 case pack, ADR-0108 retention purge) — never independently extended.
Attribution disciplinePerson-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_risk through 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_OFFENCE joins ENCRYPTION_REQUIRED when 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):

CategoryExamplesOfficer control
Judgmentcase approved/rejected/escalated, finding confirmed/rejected, risk level changedAlways captured — compliance records (see asymmetry below)
PreferenceAI suggestion accepted/modified, chat corrections, template customizedOfficer opt-out available
Behavioralsections viewed, time spent, evidence downloads/views, chat questionsOfficer 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/reset bulk-deletes the tenant's signal_events and 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.pyreset_memory):

  • super_admin only, belt-and-suspenders: require_role("super_admin") + CONFIG_WRITE permission (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 RESET and give a written reason (≥ 10 chars; 422 otherwise).
  • Immutable audit: every reset writes a memory_reset event to the append-only, hash-chained audit_events trail (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_partial with 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 the EncryptedText keyset, 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's key_id bound 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_erasure grants 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-enforced crypto_shred_keys keystore (both tenant_isolation and admin_bypass policies). 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)

  1. Encrypt-at-write (flag on) — audit_service.log_event / evidence_bundle_service.build_bundle encrypt PII leaves under the unit's DEK before the row is hashed.
  2. Erase — the shred runs only on the DELETE branch of handle_erasure (and the run_retention_purge sweep that drives it), which already honours the AMLR 5-year clock + RetentionHold legal holds (ADR-0140). A held or in-window case is REFUSE → the DEK is provably never destroyed.
  3. Destroy + verifyCryptoShredService.shred zeroes the wrapped DEK, sets status='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 is failed and erasure_complete flips falsenever a silent partial (design §4.3).
  4. Record — one immutable crypto_shred audit event (its own details carry 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_shredded store entry, replacing retained_known_residual), the evidence surface, the regulator case-pack, and GET /monitoring/retention — reads this one event; none re-derive the decision.
  5. 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 fresh pack_hash recomputes 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_erasure keeps emitting retained_known_residual for 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.