Data retention, GDPR right-to-erasure, data-residency, and the live compliance
posture. Import root: rag_compliance.
| Concern | Entry point |
|---|---|
| Retention purge + GDPR erasure | RetentionEnforcer |
| Live control posture | compliance_posture(...) → CompliancePosture |
| Data-residency check | residency_ok(tenant_region, deployment_region) |
Core types (rag_core.types): DataClass, RetentionPolicy, ErasureResult,
CompliancePosture. Errors: ComplianceError (400), ResidencyViolationError
(403). SPI: purge_before / purge_tenant on FeedbackStore + ProvenanceStore.
| Method / path | Purpose |
|---|---|
GET /v1/status/compliance |
The live posture (controls + region + retention) — the SOC 2 / GDPR mapping backing |
POST /v1/compliance/erase |
GDPR right-to-erasure of the calling tenant's data |
curl -s $GATEWAY/v1/status/compliance | jq
# erase: dry-run preview by default; delete needs dry_run=false AND confirm=true
curl -X POST $GATEWAY/v1/compliance/erase \
-H "X-Tenant-Id: acme" -d '{"dry_run": false, "confirm": true}'The residency guard runs at POST /v1/ingest/document: when cfg.compliance.enabled
and a tenant's data_region differs from cfg.compliance.region, ingest is refused
with HTTP 403 (residency_violation).
from rag_compliance import RetentionEnforcer, compliance_posture, residency_ok
from rag_core.types import RetentionPolicy
enforcer = RetentionEnforcer(feedback_store=fs, provenance_store=ps)
preview = await enforcer.purge(ctx, RetentionPolicy(feedback_days=30), dry_run=True) # count
applied = await enforcer.purge(ctx, RetentionPolicy(feedback_days=30)) # delete
erased = await enforcer.erase_tenant(ctx) # GDPR erasure
posture = compliance_posture(region="eu-west-1", controls={"audit": True}, retention=...)
assert residency_ok("eu-west-1", "eu-west-1")RetentionEnforcer.purge(ctx, policy, *, dry_run=False)— purge each data class past its window; returnsErasureResult(per-class counts). Emitscompliance.data_purgedwhen ≥1 removed.RetentionEnforcer.erase_tenant(ctx, *, dry_run=False)— erase all of the tenant's data; emitscompliance.tenant_erased.dry_runcounts without deleting (the SPIpurge_*methods honour it).
compliance:
enabled: false # off by default (residency can refuse ingest; erasure deletes)
region: "" # this deployment's data region (GDPR residency); empty = none
retention: # per-class windows in days; omit/null = retain indefinitely
chunks_days: null
feedback_days: 30
provenance_days: 90
traces_days: 30
audit_days: 365 # advisory — audit retention is the WORM export, not in-place purge
tenants:
- id: acme
data_region: eu-west-1 # must match compliance.region or ingest is refused
retention_days: 60 # per-tenant cap across all classes (tightens the global windows)ragctl compliance report -f rag.yaml # the posture: controls on/off, region, retention windows
ragctl compliance demo --tenant acme # in-process retention purge + erasure demo| Event | When |
|---|---|
compliance.data_purged |
A retention purge removed ≥1 record |
compliance.tenant_erased |
A GDPR right-to-erasure run |
compliance.residency_violation |
An ingest refused on a region mismatch |
All carry only operation / counts / region labels — never subject data.
- Durable retention — implement
purge_before/purge_tenanton a realFeedbackStore/ProvenanceStore(delete-by-age / by-tenant in the backend); the contract suite istests/contract/test_retention_purge.py. - More data classes — wire additional stores into the
RetentionEnforcer(chunk/document erasure needs an index-side delete-by-document path — deferred). - The posture —
compliance_posture(...)takes the control booleans, so a custom deployment can report extra controls.
See architecture/compliance.md, compliance/soc2-control-mapping.md, and ADR-0042.