DataTalksClub / DataTalksClub/website

Build the privacy-request aggregate and domain-adapter kernel

Open
#255 4 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

auth data-migration enhancement foundation integration needs grooming operations P0 security testing
Dominant language
Python
Stars
0
Forks
0
PR merge metrics
No merged PRs in 30d

Description

Authoritative lifecycle hold — dependency reconciliation 2026-08-30

needs grooming; engineering is not authorized. This block supersedes the stale dependency wording below. The artifact gate is: accepted/merged/green #254 source-only slice → accepted/merged/green #281 → PM rewrites #255 against #281 exact paths, schema, versions, and digests → remove needs grooming → engineering lifecycle. #254 may remain open for HUMAN/legal closure; open/closed state alone is not the gate. #283 and #285 may proceed independently after #281 and are not #255 dependencies. No production proof, HUMAN value, adapter, or activation may be inferred.

Parent epic: #64
Authority/register prerequisite: #254
Kernel input prerequisite: #281
Owner privacy direction: #23 (closed)

Product outcome

Provide one domain-neutral privacy orchestration kernel for access, portable export, correction, restriction, objection, and full erasure. The kernel records a privacy request, proof assertion, policy/manifest identity, lifecycle revision, per-domain work, retry-safe progress, and bounded evidence without knowing or storing domain PII. Owning domains join later through a versioned adapter protocol and their own independently groomed issues.

This issue is backend foundation only. It introduces no requester, Studio, admin API, command, provider, processor, artifact-download, retention, erasure-tombstone, or restore surface.

Normative authority

  • _docs/PROCESS.md: role-separated lifecycle, versioned verification, and sensitive-artifact handling.
  • _docs/specs/01-platform-architecture.md, _docs/architecture/app-boundaries.md, and _docs/architecture/shared-primitives.md: one Django deployment; commands own authorization, transaction, revision/idempotency, audit, and durable work; domains mutate only through owning services; network effects occur after commit.
  • _docs/specs/06-studio-and-admin-api.md: deny-by-default authorization and future Studio/admin parity; this issue creates neither adapter.
  • _docs/specs/07-security-privacy-operations.md: supported rights, deletion propagation, approved #23 defaults, redaction, and restored-backup fencing direction.
  • _docs/specs/10-verification-strategy.md: privacy, concurrency, idempotency, migration, audit/redaction, and failure-injection evidence.
  • _docs/specs/open-decisions.md, closed #23, and parent #64: full-erasure default, no differential minors treatment, approved provisional periods, narrow already-issued real-name certificate exception, and remaining HUMAN/legal gaps.
  • #254: the accepted authority/policy and required-domain manifest consumed by this kernel. #255 must not define that register's policy content.

Dependency and delivery disposition

Depends on accepted #254. Engineering must not start #255 while #254 is open because #254 supplies the canonical policy version/digest, required-domain manifest version/digest, allowed right/domain combinations, and approved block/exception codes. The kernel may define and test their opaque interfaces now, but it may not fabricate a production manifest or policy row.

#255 may be delivered after #254 even while later legal/product inputs remain unresolved because every unresolved capability is explicitly fail-closed:

  • no production proof verifier is registered here, so neither account nor accountless proof can be asserted through a live adapter;
  • accountless identity proof/recovery remains disabled until an authorized versioned verifier contract is accepted;
  • no default request, response, escalation, or appeal interval exists; a request whose deadline policy is unresolved cannot start domain work;
  • there is no denied/appealed lifecycle and no automatic deadline decision; unresolved cases enter/stay blocked for future authorized handling;
  • holds and exceptions can only block work. This issue cannot release a hold, apply a retention exception, choose an erasure disposition, or override full erasure without an accepted #254 code/version and a later authorized management surface.

Application ownership

Create a top-level privacy Django app and update the architecture/app registry accordingly. privacy owns the aggregate, proof-assertion evidence, domain-work orchestration, adapter/verifier protocols and registries, services, and durable handler. It may depend on core and jobs; it must not import accounts, courses, events, email_app, content, studio, api, or provider code.

Do not place privacy-domain behavior in core or jobs. core continues to own generic RevisionedModel, IdempotencyRecord, Operation, and AuditEvent; jobs continues to own dispatch, leases, retry execution, and handler registration.

Exact aggregate contract

privacy.PrivacyRequest (RevisionedModel)

Use a UUID primary key and these bounded fields:

  • subject_class: exactly account or accountless;
  • subject_binding_ref: opaque UUID issued by an accepted proof verifier; never an email, username, provider identifier, credential, or raw domain key;
  • status: exactly pending_proof, ready, processing, blocked, completed, or cancelled;
  • policy_version (bounded identifier) and policy_digest (lowercase SHA-256);
  • notice_version (bounded identifier);
  • domain_manifest_version (bounded identifier) and domain_manifest_digest (lowercase SHA-256);
  • deadline_state: exactly unresolved, resolved, or overdue; deadline_policy_version, due_at, and escalation_at are nullable as one all-or-none group. resolved/overdue requires all three and escalation_at >= due_at >= received_at; unresolved requires all three empty;
  • policy_block_state: exactly unresolved, clear, or blocked; a request may process only when clear;
  • nullable one-to-one proof_assertion and required one-to-one protected core.Operation of kind privacy.request;
  • bounded safe_reason_code from the kernel allowlist only; no free-text reason;
  • received_at, created_at, updated_at, nullable completed_at, and nullable cancelled_at.

Database constraints require revision >= 1; terminal timestamps match terminal state; nonterminal states have no terminal timestamp; completed and cancelled are mutually exclusive. Application validation rejects an empty rights set, an unrecognized #254 policy/manifest identity, unsafe identifier syntax, and any attempt to store a subject value outside the opaque UUID.

privacy.PrivacyRequestRight

One row per request/right with choices exactly access, portable_export, correction, restriction, objection, and erasure; unique (request, right). Rights are immutable after leaving pending_proof. Do not encode rights in a mutable JSON list or infer one right from another.

privacy.PrivacyProofAssertion

Append-only, UUID keyed, and created only from a validated verifier result:

  • subject_binding_ref, subject_class, fixed purpose privacy_request;
  • bounded verifier_key and positive verifier_contract_version;
  • lowercase SHA-256 assertion_digest over the verifier's canonical safe assertion identity; never the assertion, token, session key, email, or credential itself;
  • verified_at, expires_at, and consumed_at, with verified_at <= consumed_at < expires_at;
  • bounded assurance_code from the verifier definition and created_at.

Exactly one assertion may be consumed by one request. The request's subject class/binding must equal the assertion. Proof is revalidated through the registered verifier before ready -> processing; missing, unknown, revoked, expired, replayed, mismatched, or unavailable proof fails without enumeration and cannot advance the request. No default verifier is registered in this issue; tests use explicitly test-only definitions.

privacy.PrivacyRequestBlock

Append-only block evidence with UUID, request, block_type (policy, legal_hold, exception, deadline, adapter, proof), bounded accepted policy_code, policy_version, SHA-256 evidence_digest, domain_key optional, active_from, nullable resolved_at, and safe resolution code. It stores no reason text or source document. An active or unknown block prevents affected work. Resolution is structurally supported by the service but has no production caller in this issue; legal-hold/exception resolution additionally rejects any code/version absent from accepted #254 authority. Exceptions are blockers awaiting an approved disposition, never implicit authorization to retain or anonymize data.

privacy.PrivacyDomainWork (RevisionedModel)

One row per required domain in the pinned manifest:

  • UUID, request FK, bounded domain_key, positive adapter_contract_version, and immutable manifest_entry_digest;
  • state exactly pending, prepared, running, retry_wait, blocked, succeeded, or cancelled;
  • positive execution_generation, attempt_count, and bounded max_attempts (1..100);
  • nullable SHA-256 input_digest, result_digest, and evidence_digest;
  • nonnegative item_count and bounded numeric-only safe_counts; no strings, domain field names, record identifiers, or arbitrary adapter metadata;
  • bounded allowlisted result_code and last_error_code, nullable next_attempt_at, timestamps, and nullable protected latest DurableJob reference.

Unique (request, domain_key). prepared/later requires input_digest; succeeded requires all result/evidence digests and no retry time; retry_wait requires a retry time; terminal request completion requires every required domain row succeeded. A missing/unknown domain, adapter/version/capability mismatch, manifest drift, unsupported right, or extra unlisted adapter blocks before any adapter execution. No required domain may be skipped or marked not-applicable by runtime inference.

privacy.PrivacyDomainWorkRight

One immutable row per domain work/right; unique (domain_work, right). It is populated only from the exact accepted #254 manifest entry and must be a subset of both request rights and the registered adapter's declared capabilities.

Lifecycle and transition table

Only the shared privacy application service may transition rows. Every mutation requires current authorization, expected request/work revision as applicable, and a required idempotency key.

From To Allowed condition
create pending_proof accepted policy/manifest identity is pinned; rights valid; core.Operation and create audit are atomic
pending_proof ready one unexpired, unconsumed, purpose-bound assertion matches subject class/binding; verifier is registered and currently validates it
pending_proof cancelled no domain work exists
ready processing proof revalidates; deadline is resolved and not overdue; policy block is clear; exact manifest and every adapter/version/right match; all domain-work rows and durable intents are persisted atomically
ready cancelled no work has been prepared or executed
ready/processing blocked proof/policy/deadline/manifest/adapter invariant fails, an active hold/exception exists, or a domain reaches a non-retryable/exhausted safe failure
blocked processing explicit idempotent resume; every blocking invariant is rechecked and no unresolved/active block remains; a new execution generation is created only for affected work
processing completed every manifest-required domain work is succeeded; operation succeeds in the same transaction
processing cancelled only before any work reaches prepared, running, or succeeded; otherwise cancellation fails closed

All other transitions are invalid. completed and cancelled are terminal. There is no denied, appealed, automatic approval, automatic exception, automatic cancellation, or terminal “partial success” state. Deadline expiry changes a nonterminal request to blocked/overdue; it does not decide the right or emit an appeal. An adapter failure never causes another domain to be silently treated as complete.

Verifier and domain-adapter protocols

Definitions are immutable, code-registered dataclasses. Duplicate keys with different definitions, malformed keys, unknown versions, and import-order disagreement fail startup/system checks. Registry enumeration is deterministic.

PrivacyProofVerifier declares key, positive contract_version, supported subject classes, bounded assurance codes, and two side-effect-free methods: consume/validate a canonical proof assertion into the safe PrivacyProofAssertion DTO, and revalidate that stored DTO at processing time. It returns only opaque UUIDs, versions, timestamps, codes, and digests. No production verifier implementation belongs to #255.

PrivacyDomainAdapter declares domain_key, positive contract_version, and supported rights. It exposes:

  1. prepare(context) -> DomainWorkPlan: side-effect-free; returns the exact immutable input digest, nonnegative item count, and numeric safe counts for the pinned request/policy/manifest/subject-binding identity.
  2. execute(context, plan) -> DomainWorkResult: calls only the owning domain's future application service, using the stable domain-work UUID plus execution generation as its domain idempotency identity; returns only allowlisted result code, numeric safe counts, and result/evidence digests.
  3. reconcile(context, plan) -> DomainWorkResult: used after an ambiguous crash boundary to obtain the same committed result or a safe retry/block decision; it never blindly repeats a non-idempotent mutation.

Context contains only request/work/operation UUIDs, subject-binding UUID, rights, pinned policy/manifest identities, execution generation, correlation/job IDs, and lease token. It contains no actor credentials, PII, domain record IDs, export content, provider payload, or free-form reason. Future export/correction artifact content and domain locators remain owned by separate issues and must not be returned through the kernel's JSON evidence.

#255 registers only the kernel durable handler and explicitly test-only fake verifier/adapters in tests. It registers no accounts/courses/events/email/content/search/audit/provider production adapter and does not absorb #251.

Transactions, idempotency, durable retry, and concurrency

  • Reuse core.IdempotencyRecord; do not create a second idempotency table. Scopes are stable per privacy command and authorized actor/subject binding. Store only the fenced key hash and canonical request hash. Exact replay returns the original bounded safe DTO; the same key with changed action/request/revision/arguments conflicts; in-progress ownership and fence loss fail safely.
  • Request creation atomically creates request, rights, core.Operation, idempotency ownership/result, and append-only audit. Start atomically validates the frozen manifest/registry, creates all domain-work/right rows, advances request/operation, persists one jobs.DurableJob intent per work, and completes idempotency before after-commit wakeups.
  • Durable handler name is versioned (for example privacy.domain_work.v1). Payload contains only request/work/operation UUID and execution generation. Deduplication identity is stable for (work UUID, generation); payload or version drift conflicts.
  • Preparation is side-effect-free. The worker persists the prepared digest with optimistic revision control, then invokes the adapter. A domain mutation must be idempotent on (work UUID, generation, input digest). If a crash occurs after a domain commit but before the kernel result commit, retry calls reconcile with the same identity before any execute retry.
  • Adapter execution never performs network I/O in the kernel/domain transaction. Any future network effect must be a domain-owned durable intent created in that domain service transaction and use its stable idempotency identity after commit.
  • Worker completion re-reads proof, request, active blocks, manifest, adapter definition/version, input digest, lease, and work revision. Drift or lease/revision loss cannot commit success. Kernel work result, safe progress, request/operation state, and audit are committed atomically.
  • Retry uses DurableJob's fenced lease and bounded attempts. Only explicit transient safe codes enter retry_wait; permanent, unknown, ambiguous-after-reconcile, or exhausted failures block the work/request. Manual resume requires an idempotent command, expected revision, all invariant checks, and a new generation. It never edits or reuses an old job intent.
  • SQLite correctness relies on portable declarative constraints and optimistic conditional updates, not select_for_update; PostgreSQL may lock for efficiency but must preserve the same outcomes.

Authorization, audit, and security

There are no HTTP/Studio/API/command adapters in this issue. Services require an injected registered authorization policy; absence is deny-by-default. Test policies prove subject-scoped and privileged-system behavior. Actor references are attribution snapshots only and never authorization input. Worker/system authorization is limited to one request/work and rechecked from persisted state.

Every create, proof success/failure, transition, block/resolution attempt, prepare, execute/reconcile result, retry, cancellation, stale/conflict/denied attempt, and terminal outcome creates core.AuditEvent through the shared writer. Use stable action names under privacy.request.* and privacy.domain_work.*, opaque target IDs, standard request/correlation/job/idempotency hashes, safe before/after state/revision, domain key, adapter/policy versions, counts, digests, and allowlisted reason/result codes only.

Never persist or emit names, emails, usernames, IPs, accountless contact values, tokens, credentials, session/cookie values, raw proof, domain record IDs, field values, export content, provider payloads, exception/hold prose, or reversible PII. Run shared redaction before bounded canonical serialization; reject (do not merely redact and accept) any protocol result with unknown keys/types, strings outside code allowlists, sensitive-key names, URLs/emails/credential shapes, excessive depth/items/bytes, NaN, or noncanonical data. Exceptions and logs expose stable safe error codes only. Issue comments/test artifacts use synthetic opaque IDs and canaries.

Scope

  • Add the privacy app, exact models/constraints/migrations above, application services, DTOs, verifier/adapter registries and system checks, authorization seam, and versioned durable handler.
  • Reuse core.Operation, IdempotencyRecord, AuditEvent, revision conflicts, redaction, execution context, and jobs.DurableJob rather than duplicating them.
  • Add architecture/app-boundary and internal developer documentation for the kernel contract only.
  • Add model/service/protocol/worker/security/migration tests with synthetic non-PII fixtures.

Explicit non-goals

  • No production proof verifier or account/accountless intake/recovery; no email link, OTP, session reauthentication, identity lookup, or rate-limit product policy.
  • No requester/self, Studio, admin API, public API, management command, OpenAPI, URL, template, navigation, static asset, email, notification, or browser behavior.
  • No domain PII queries/mutations or production adapters; no #251 absorption; no accounts-to-courses import.
  • No export artifact/content, correction implementation, retention runner, erasure/anonymization implementation, tombstone/restore fence, cache/search invalidation, processor/Relay/provider call, receipt, or production data.
  • No legal rule, deadline interval, escalation/appeal outcome, hold release, exception disposition, lawful basis, notice wording, retention exception, or processor policy invention.
  • No commit, push, merge, deployment, protected-data access, production execution, or external side effect outside the normal issue lifecycle.

Acceptance criteria

  • A dedicated privacy app owns the kernel and the architecture dependency diagram remains one-way; import-boundary tests prove privacy does not import any domain, presentation, worker-task, or provider module.
  • The exact aggregate, right, proof, block, domain-work, and domain-work-right models/constraints above exist; unsafe combinations fail on SQLite and production-like PostgreSQL.
  • The transition table is exhaustive: every allowed edge has success/replay/stale-revision/concurrent tests and every other edge fails without mutation; terminal states cannot reopen.
  • Account and accountless subject classes are structurally supported through opaque bindings, but the absence/unknown/expired/replayed/mismatched/unavailable verifier denies advancement non-enumeratingly; no live verifier is registered.
  • Unresolved deadline, overdue deadline, unresolved policy, active hold/exception, unknown #254 code/version, missing/extra/unknown adapter, version/capability/manifest mismatch, and unsupported rights all block before domain execution.
  • Exact idempotent command replay returns the original safe result; changed payload/revision/action conflicts; concurrent create/start/resume/cancel yields one authoritative outcome, one work set/generation, and no duplicate durable intent.
  • Empty, one, and many required domains; zero/one/many rights; partial success; transient retry; retry exhaustion; permanent failure; timeout; crash before domain commit; crash after domain commit/before kernel commit; reconciliation; worker restart; stale lease; and resume prove no skipped domain or duplicate mutation.
  • Each adapter invocation is pinned to subject-binding/request/work/operation IDs, policy/manifest/adapter versions, rights, generation, and input digest; changed input or registry state fails closed and cannot reuse evidence.
  • Operation progress/result and request/domain state remain consistent, bounded, revisioned, and atomically audited across success, block, retry, cancel, and failure injection.
  • Audit, logs, exceptions, job payloads, operation summaries, idempotency results, fixtures, migration output, and verification artifacts pass explicit PII/credential/token/URL/provider-payload canaries and contain only the allowed opaque IDs/codes/counts/digests.
  • New migrations pass forward, reverse, fresh install, and migrate-back/migrate-forward (“apply twice”) checks on SQLite and production-like PostgreSQL; migration drift is clean.
  • Focused privacy/core/jobs tests, import/system checks, formatting/lint/type checks, container tests, and the graph-selected versioned verification plan pass. Property/state-machine and real concurrent transaction tests cover transitions/idempotency on both supported databases.
  • Render contract is exactly no templates, URLs, navigation, static assets, context processors, OpenAPI, or response changes. The independent tester records Playwright/screenshots as not_applicable only when the final change graph proves no render path; otherwise the graph-selected browser tier and desktop/mobile screenshots are mandatory.

Required verification scenarios

  1. Accepted versus unknown policy/manifest versions and digests; duplicate/missing/extra domain rows; adapter registry order; conflicting registration; startup/system-check failure.
  2. Account/accountless proof missing, valid test assertion, expired, revoked/unavailable, replayed, wrong purpose/class/binding, and proof expiring between ready and start; all denial output is indistinguishable and safe.
  3. Every lifecycle edge, invalid edge, expected-revision race, duplicate idempotency key with same/different request, two simultaneous starts, two workers, stale lease, cancellation before/after preparation, and terminal replay.
  4. Empty/one/many domain manifests; every right; mixed adapter capabilities; partial completion; transient/permanent/unknown error; retry timing/attempt bound; reconcile-after-ambiguous-crash; resume generation and old-job fencing.
  5. Active/unresolved/resolved test-only policy block, hold, exception, and deadline records; unknown authority code; due/escalation ordering; overdue behavior; prove none selects a legal disposition or appeal.
  6. Canary strings resembling email, URL, bearer/JWT/GitHub/AWS credentials, cookie/session/token, domain values, nested provider payload, cycles, excessive JSON, Unicode/control characters, NaN, and free-form error text at every protocol/audit/job/result boundary.
  7. Migration constraint introspection plus forward/reverse/fresh/apply-twice on SQLite and PostgreSQL; crash/rollback injection proves aggregate/work/audit/job/idempotency atomicity.

Browser and screenshot disposition

Expected: not_applicable. #255 must have no rendered route or API response. The engineer records pending_independent_tester until the independent tester recomputes the final graph. Only that tester may mark Playwright/screenshots not_applicable; any actual render impact restores the normal browser and inspected desktop/mobile screenshot gate.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Do not begin implementation: the issue is explicitly blocked for grooming and depends on accepted #254 and #281. First read _docs/PROCESS.md, the listed architecture and verification documents, then review those prerequisite issues. The work is done only after PM rewrites #255 with exact paths, schemas, versions, and digests and removes the grooming block.

Written by the indexing model from the issue text.

Assessment

Tech stack
django, python
Domain
backend, backend-api-design, security
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
15/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.