# Trust, Safety, and Privacy

Trust, Safety, and Privacy is the governance spine of Oshun V1: the
deterministic policy logic that decides what content and behavior is allowed,
how fast a harm gets triaged, who may appeal, when a crisis frame is broadcast
across every surface, what a user has consented to, where their data may legally
live, and how a deletion or data-subject-access request is honored end-to-end.
It serves three distinct audiences at once — the **customers** whose safety and
privacy are at stake, the **operators** (trust-and-safety reviewers, support
agents, privacy operators) who work the queues, and the **regulators** whose
regimes the platform must satisfy. It sits among the governance pages hubbed at
[../ARCHITECTURE.md](../ARCHITECTURE.md), directly alongside its companion
[Support, Entitlements, Billing, and the Aje Entitlement Bridge](./support-billing-and-crypto.md),
and it is the policy half of the same substrate whose contemplative-tone half
[Lilith — Contemplative Policy Substrate](./substrate-lilith.md) supplies.

> **Read this page for what is _shipping_ vs. _spec_.** The governance **policy
> libraries are overwhelmingly real and verified**: full category enumerations,
> SLA budgets keyed to severity with reviewer-tier gates, an appealable /
> non-appealable decision taxonomy with two-reviewer signoff, a cross-surface
> crisis-frame cascade that publishes a real event over the Redis event bus, a
> retention table with enforcing constants, a DSAR state machine, residency
> routing with `cross-region-blocked` verdicts, and a region→regime compliance
> map. Every enum, constant, and function below is quoted verbatim from code,
> not aspiration. **The honest boundary** is that these are _pure-function
> cores_: the actual enforcement — real ML classifiers feeding `PolicyHit`s,
> immutable audit-platform persistence, the live DSAR fan-out across every
> domain — is the runtime's job, and lives partly elsewhere. Iris
> (`@oshun/memory-iris`) owns consent and memory enforcement;
> `@oshun/audit-platform` owns immutable storage. Where something is a port
> awaiting a runtime binding, this page says so.

The three governance domains are implemented as three workspace libraries:

| Domain                            | Package                                                  | Backlog                                                                     |
| --------------------------------- | -------------------------------------------------------- | --------------------------------------------------------------------------- |
| Trust and Safety                  | `@oshun/trust-safety` (`libs/oshun/trust-safety/`)       | §21                                                                         |
| Privacy, Consent, Residency, DSAR | `@oshun/privacy` (`libs/oshun/privacy/`)                 | §22                                                                         |
| Support, Entitlements, Billing    | `@oshun/billing-support` (`libs/oshun/billing-support/`) | §23 — see [Support, Entitlements, Billing](./support-billing-and-crypto.md) |

The product scope is
[Governance, Safety, Support, and Privacy](../features.md#governance-safety-support-and-privacy);
the backlog sections are §21–23.

---

## Trust and Safety

Backlog: §21. Package: `@oshun/trust-safety` (`libs/oshun/trust-safety/`).

Trust and Safety is a deterministic adjudication engine. A real classifier or
operator produces a **`PolicyHit`** — a typed observation that some subject
(content, message, agent output, voice or avatar asset) matches a policy
category with a confidence and an evidence excerpt. The library's pure functions
then deterministically map that hit to a **severity class**, an **enforcement
action**, and (when an operator decides) a **`SafetyDecision`** that may or may
not be appealable. The classifier itself is the runtime's job; everything from
"we have a typed hit" onward is implemented and unit-tested here.

### Policy taxonomy

`libs/oshun/trust-safety/src/policy-taxonomy/categories.ts` is the canonical
enumeration. Categories are grouped into five families, each its own `const`
array so the type system rejects an unknown category at compile time:

| Family             | Constant                        | Selected members                                                                                                                                                                                                                                                  |
| ------------------ | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Content            | `CONTENT_POLICY_CATEGORIES`     | `hate`, `harassment`, `sexual-content`, `graphic-violence`, `self-harm`, `illegal.csam`, `illegal.terrorism`, `illegal.weapons`, `illegal.drugs`, `defamation`, `doxxing`, `electoral-disinformation`, `health-disinformation`, `scams`, `financial-exploitation` |
| Assistant behavior | `ASSISTANT_BEHAVIOR_CATEGORIES` | `persona-drift`, `jailbreak`, `prompt-injection`, `deceptive-roleplay`, `off-policy-advice`, `harmful-instruction-generation`, `political-influence`, `unauthorized-memory-access`                                                                                |
| Synthetic media    | `SYNTHETIC_MEDIA_CATEGORIES`    | `deceptive-realism`, `identity-impersonation`, `nonconsented-likeness`, `deepfake`, `voice-clone-abuse`, `watermark-removal`                                                                                                                                      |
| Educational        | `EDUCATIONAL_CATEGORIES`        | `fabricated-citations`, `retracted-source-use`, `plagiarism`, `exam-integrity-violation`, `answer-key-leakage`, `standards-alignment-misrepresentation`                                                                                                           |
| Support ops        | `SUPPORT_OPS_CATEGORIES`        | `operator-harassment-of-customer`, `unauthorized-data-access`, `refund-abuse`, `social-engineering`                                                                                                                                                               |

Sexual content carries an orthogonal sub-class —
`SEXUAL_NSFW_CLASSES = ['suggestive', 'explicit-adult-consensual', 'explicit-adult-nonconsensual', 'minor-sexual']`
— because the _same_ surface category needs radically different handling
depending on the sub-class. `validatePolicyHit` enforces the coupling: a
`subClass` may only attach to `sexual-content`, and `illegal.csam` _requires_
the `minor-sexual` sub-class (`csam-requires-minor-subclass`). These are not
cosmetic checks — they are what stops a mislabeled hit from slipping past the
hard-block path below.

**Why a taxonomy of categories rather than a single "bad content" flag?**
Because enforcement is category-specific. The `mapHitToAction` function
deterministically maps a validated hit to one of five `EnforcementAction` kinds
— `block`, `downgrade-visibility`, `warn-author`, `route-for-human-review`, or
`no-action` — using fixed key sets:

- **Always hard-block** (`HARD_BLOCK_KEYS`): `content:illegal.csam`,
  `content:illegal.terrorism`, `content:illegal.weapons`, and the entire
  synthetic-media abuse cluster (`nonconsented-likeness`, `voice-clone-abuse`,
  `deepfake`, `watermark-removal`), plus `support-ops:unauthorized-data-access`
  and `support-ops:social-engineering`. A `minor-sexual` or
  `explicit-adult-nonconsensual` sub-class also short-circuits to `block`
  _before_ any confidence threshold is consulted — non-consensual and child
  imagery are never confidence-gated.
- **Jurisdiction-bound** (`content:illegal.drugs`): blocked only when a
  `JurisdictionPolicyOverlay` actually defines the category for the
  jurisdiction; otherwise `route-for-human-review`. This is the escape hatch for
  categories whose legality genuinely varies by region.
- **Confidence-gated soft blocks** (`SOFT_BLOCK_KEYS`): `hate`, `harassment`,
  `graphic-violence`, `doxxing`, `defamation`, `educational:answer-key-leakage`
  — block above `minConfidenceForAction`, otherwise route for review. A
  jurisdiction overlay that _elevates_ a category folds into this same path.
- **Visibility downgrades** (`DOWNGRADE_KEYS`): `scams`,
  `financial-exploitation`, the two disinformation categories,
  `off-policy-advice`, `political-influence`.
- **Self-harm content** takes the crisis pathway rather than a blanket block:
  `downgrade-visibility` plus human follow-up, because the author may be the
  person at risk.

### Severity classes and SLAs

`libs/oshun/trust-safety/src/severity/severity.ts` defines
`SEVERITY_CLASSES = ['P0', 'P1', 'P2', 'P3']` and, crucially, the
machine-readable `SlaBudget` shape that the architecture's prose only describes
informally. Each severity maps to a concrete budget via the `SLA_BY_SEVERITY`
table, queried through `slaFor()`:

| Severity               | Triage budget    | Action budget    | `minReviewerTier` | `requiresParallelIncident` | `requiresPostIncidentReview` | `weeklyAggregateOnly` |
| ---------------------- | ---------------- | ---------------- | ----------------- | -------------------------- | ---------------------------- | --------------------- |
| **P0** (imminent harm) | 5 min (`5*60`)   | 15 min (`15*60`) | `crisis-trained`  | `true`                     | `true`                       | `false`               |
| **P1** (high severity) | 30 min (`30*60`) | 2 h (`2*3600`)   | `senior`          | `false`                    | `true`                       | `false`               |
| **P2** (moderate)      | 8 h (`8*3600`)   | 48 h (`48*3600`) | `standard`        | `false`                    | `false`                      | `false`               |
| **P3** (informational) | 7 d (`7*86400`)  | 7 d              | `standard`        | `false`                    | `false`                      | `true`                |

The `SlaBudget` interface is itself the contract:

```ts
interface SlaBudget {
  readonly triageBudgetSeconds: number;
  readonly actionBudgetSeconds: number;
  readonly requiresParallelIncident: boolean;
  readonly requiresPostIncidentReview: boolean;
  readonly weeklyAggregateOnly: boolean;
  readonly minReviewerTier: 'standard' | 'senior' | 'crisis-trained';
}
```

Severity is _assigned_, not declared, by `classifySeverity`. The classifier
escalates aggressively: a `minor-sexual` or `explicit-adult-nonconsensual`
sub-class is always P0; a credible-threat _triple_ (named target **and**
credible plan **and** geographic specificity) auto-escalates any hit to P0; the
`P0_CATEGORIES` set pins `content:illegal.csam`, `content:self-harm`, and
`synthetic-media:nonconsented-likeness` to P0; `P1_CATEGORIES` pins hate,
harassment, identity-impersonation, voice-clone-abuse, deepfake, and
exam-integrity. A repeat offense within 30 days (`priorOccurrencesInLast30Days`)
escalates a user-targeting P2 (harassment, hate, doxxing, defamation, or a
jailbreak/prompt-injection) up to P1.

`evaluateSla` turns a `CaseSlaTimer` (enqueued / triaged / actioned timestamps)
into an `SlaStatus` with `triageBreached` / `actionBreached` flags and
`secondsToTriageBudget` countdowns — a breach is recorded only when the relevant
timestamp is still `null` _and_ the elapsed time exceeds the budget, so a case
triaged at the buzzer is not retroactively flagged. Appeals get their own slower
clock through `APPEAL_SLA_BY_SEVERITY` / `appealSlaFor` (e.g. a P0 appeal has a
30-minute triage, 4-hour second-review, and 6-hour notify budget) — appeals are
deliberately _not_ held to the original incident's 5-minute clock.

### Decisions, appeals, and two-reviewer signoff

`libs/oshun/trust-safety/src/decisions/decisions.ts` splits every enforcement
outcome into appealable and non-appealable kinds:

- **`APPEALABLE_DECISION_KINDS`** (8): `content-removal`,
  `account-suspension-under-30-days`, `persona-suspension`, `comment-hiding`,
  `recommendation-suppression`, `voice-entitlement-suspension`,
  `avatar-entitlement-suspension`, `billing-action-reversal`.
- **`NON_APPEALABLE_DECISION_KINDS`** (5): `csam-removal`,
  `credible-threat-suspension`, `legal-hold-action`, `court-ordered-action`,
  `permanent-ban` — the irreversible, legally-driven outcomes.

`isAppealable` is the single predicate the appeal pipeline gates on. A
`SafetyDecision` carries a `twoReviewerSignoff` field —
`{ secondReviewerId, atUnixSeconds } | null`. `validateDecision` _requires_ that
signoff whenever the decision is P0, is one of the `TWO_REVIEWER_DECISION_KINDS`
(`csam-removal`, `credible-threat-suspension`, `court-ordered-action`,
`permanent-ban`), or touches a large account, a public figure, a cloned
voice/avatar, or _overrides Lilith policy_. The same validator rejects a
`self-signoff` (the second reviewer is the issuer) and an empty rationale. This
is the structural guarantee that no single operator can unilaterally take an
account down for cause.

Each non-appealable kind carries an operator **runbook** in
`NON_APPEALABLE_RUNBOOK` — `csam-removal` mandates an immediate NCMEC-equivalent
report, a `trust-safety-legal` contact, evidence preserved under legal hold, and
a 7-year (`365*7`) retention; `court-ordered-action` requires a `court-liaison`,
verbatim honoring of the order, and a 10-year retention. `permanent-ban` is
issued "only after repeat-offender stage `ban` reached or P0 credible threat" —
that `ban` stage is the terminal entry of
`REPEAT_OFFENDER_STAGES = ['warn', 'restrict', 'suspend', 'ban']` in the
abuse-patterns module, so the runbook and the escalation ladder are the same
source of truth.

Appeals run their own state machine —
`APPEAL_STATES = ['received', 'triaged', 'in-review', 'decided', 'notified', 'audited']`
with verdicts `uphold` / `overturn` / `partial`. The pure transitions
(`fileAppeal`, `triageAppeal`, `decideAppeal`, `notifyAppeal`, `auditAppeal`)
enforce **reviewer rotation** (the triage and second reviewers may not be the
original issuer, and the second may not be the triager) and a **cooling-off
window** (`triageAppeal` refuses until `cooledOffUntilUnixSeconds` has elapsed)
— both standard fairness protections that keep an appeal from being
rubber-stamped by the same person who made the call.

### Crisis-frame cascade

When Lilith detects a crisis signal — direct user input, content escalation, or
an operator-flagged session — every safety-bearing substrate must enter a
matching frame: Tara stops scheduling rituals, Living Scenes locks renders into
reduced motion and disables opt-in share, Isis caps generation audacity and
blocks premium personas, the assistant switches to a crisis-aware persona that
surfaces support resources, and Iris tags active session memory so recall
doesn't re-surface charged content. The frame is reversible on the same channel;
every state change is audited.

This is implemented as a **named exit-criterion cascade** in
`libs/oshun/trust-safety/src/crisis/crisis-frame-cascade.ts`. The audit that
produced it found the original gap explicitly: per-surface projectors
(`enterPsycheCrisisFrame`, `enterCrisisFrame`) already existed, but _nothing
fanned a single activation out to all of them_. The cascade closes that gap with
three concrete artifacts:

1. **The event.**
   `LILITH_CRISIS_FRAME_ACTIVATED_EVENT = 'lilith.crisis_frame.activated'` — the
   exact topic string the architecture's sequence diagram names. Its payload,
   `LilithCrisisFrameActivatedEvent`, carries the detection identity (`frameId`,
   `detectionId`, `userId`, `tenantId`, `kind`, `source`, `region`) and two
   **non-overridable** safety directives — `haltSynthesis: true` and
   `suspendMemoryWrites: true` — so a downstream projector physically cannot
   soften the frame.
2. **The surfaces.**
   `CRISIS_FRAME_SURFACES = ['psyche', 'lilith-video', 'tara', 'iris', 'assistant']`
   — the closed set every frame projects into. The `kind` and `source` are drawn
   from the crisis module's own enums (`CRISIS_KINDS` includes
   `suicidal-ideation`, `self-harm-imminent`, `violence-to-others-credible`,
   `child-protection-signal`, `domestic-violence-signal`, `medical-emergency`;
   `CRISIS_DETECTION_SOURCES` includes `model-classifier`, `user-self-report`,
   `third-party-report`, `pattern-match-across-sessions`,
   `support-copilot-escalation`).
3. **The fan-out, with a clean port boundary.** `activateLilithCrisisFrame`
   publishes through a `CrisisFramePublishPort` — a one-method interface, _not_
   a direct `@oshun/event-bus` dependency. `consumeCrisisFrameActivation` then
   fans the event out to every registered `CrisisFrameProjector` whose surface
   the event targets, attempting each **independently** so one surface failing
   to enter its frame never blocks the others; failures are recorded as
   `{ status: 'failed' }` outcomes so a recovery sweep can re-project them.

The pure cascade domain stays free of the event bus **by design** (ports only).
The Redis binding lives one file over, at
`libs/oshun/trust-safety/src/crisis/crisis-frame-worker.ts`:
`createEventBusCrisisFramePublishPort(bus)` adapts a real `IEventBus` to the
publish port, and `subscribeCrisisFrameWorker(bus, deps)` subscribes a worker
that feeds every `lilith.crisis_frame.activated` event through the consumer
fan-out. This separation is why the architectural sequence diagram below is
faithful to the code: the publish step, the parallel per-surface projections,
and the audit attestation are each their own named function.

```mermaid
sequenceDiagram
    autonumber
    participant Sig as Signal Source<br/>(user input · content · escalation)
    participant L as Lilith
    participant EB as Event Bus<br/>(lilith.crisis_frame.activated)
    participant T as Tara
    participant LS as Living Scenes
    participant I as Isis
    participant Asst as Assistant
    participant Iris as Iris
    participant A as Audit Platform

    Sig->>L: crisis indicator (severity, scope)
    L->>L: Evaluate against contemplative<br/>tone + crisis policy
    L->>EB: activateLilithCrisisFrame()<br/>{frameId, haltSynthesis:true, suspendMemoryWrites:true}

    par Tara · suppress invitations
        EB-->>T: deliver event
        T->>T: Suspend scheduled rituals<br/>+ humane recovery prompts
    and Living Scenes · block sharing
        EB-->>LS: deliver event
        LS->>LS: Switch renders to reduced-motion<br/>+ disable opt-in share
    and Isis · downgrade generation
        EB-->>I: deliver event
        I->>I: Cap audacity · enforce reduced<br/>provenance · block premium personas
    and Assistant · tone shift
        EB-->>Asst: deliver event
        Asst->>Asst: Switch to crisis-aware persona<br/>+ surface support resources
    and Iris · annotate session
        EB-->>Iris: deliver event
        Iris->>Iris: Tag active session memory<br/>(suppress recall surfacing)
    end

    L->>A: append crisis-frame attestation
    Note over L,A: Frame is reversible by Lilith on the same channel — every state change is audited.
```

The reviewer surface for all of the above lives in `apps/oshun/admin/`; review
queues consume from `@oshun/queue` and persist verdicts in
`@oshun/audit-platform` — those two are the runtime substrates the pure
`@oshun/trust-safety` core hands its decisions to. See
[Lilith — Contemplative Policy Substrate](./substrate-lilith.md) for the
contemplative-tone detectors that _produce_ the crisis signal in the first
place.

---

## Privacy, Consent, Residency, DSAR

Backlog: §22. Package: `@oshun/privacy` (`libs/oshun/privacy/`).

The privacy library is the deterministic core for everything a user can see and
control about their data: what they have consented to, where it lives, how to
export or delete it, and how a regulator's data-subject-access request is
fulfilled. As with Trust and Safety, the cores are pure functions; the _runtime_
ownership of consent and memory is Iris's, and immutable audit storage is
`@oshun/audit-platform`'s.

> **Where consent really lives — a correction.** The architecture hub attributes
> consent _solely_ to Iris (`@oshun/memory-iris`), where "every consent change
> emits a `ConsentRecord` event." That is only half the picture. A full consent
> taxonomy and a `ConsentRecord` type _also_ live in
> `libs/oshun/privacy/src/consent/consent.ts` — the families, memory scopes, and
> per-family purpose enums below are defined there. The honest split: the
> privacy library owns the **consent vocabulary and validation logic**; Iris
> owns the **runtime enforcement** (re-evaluating downstream memory scopes when
> a consent changes). Both are real; neither is the whole story alone.

### Consent taxonomy and granularity

`libs/oshun/privacy/src/consent/consent.ts` defines nine consent families —
`CONSENT_FAMILIES = ['memory', 'voice', 'avatar', 'synthetic-media', 'notifications', 'privacy-surface', 'support', 'research', 'educational-context']`
— each with its own purpose enum, so a `ConsentKey` is a discriminated union
that the type system can exhaustively check:

| Family                | Granularity                                                                                                              |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `memory`              | `MEMORY_SCOPES = ['profile', 'session', 'notebook']` × `MEMORY_SENSITIVITY = ['baseline', 'sensitive']`                  |
| `voice`               | `recording`, `cloning-own-voice`, `cloned-voice-in-personas`, `voice-data-analytics`                                     |
| `avatar`              | `likeness-capture`, `generated-likeness`, `animation-rights`, `tenant-scoped-distribution`                               |
| `synthetic-media`     | `ai-generation-on-uploads`, `derivative-works`, `tenant-publication`                                                     |
| `privacy-surface`     | incl. `training-data-inclusion`, `cross-tenant-data-sharing`, `third-party-processor-inclusion`, `research-data-sharing` |
| `support`             | `agent-screen-share`, `agent-memory-access`, `session-recording`                                                         |
| `research`            | per `studyId`                                                                                                            |
| `educational-context` | `assignment-data-sharing-with-teacher`, `…-with-institution`, `standards-reporting`                                      |
| `notifications`       | per channel × domain × `severityMin`                                                                                     |

The defaults are opinionated and safety-first: `defaultState` returns `granted`
for baseline memory and coarse notifications, but `denied` for every sensitive
key — sensitive memory, all of voice/avatar/synthetic-media, support, research,
educational-context, and the sensitive privacy-surface purposes
(`training-data-inclusion`, `cross-tenant-data-sharing`, fine analytics,
third-party processors, research sharing). `validateConsentRecord` additionally
**rejects bundled sensitive consent**: if `isSensitiveKey(key)` is true and the
record's `bundledWith` array is non-empty, it returns
`bundled-sensitive-consent`. You cannot smuggle a sensitive grant inside a
bundle of innocuous ones — each sensitive consent must stand on its own.

Every state change goes through `transitionConsent`, which emits _both_ the new
`ConsentRecord` and a paired `ConsentTransitionAudit` (prior state, new state,
actor, reason code, timestamp) — the architecture's "every consent change emits
a `ConsentRecord` event" made concrete. Withdrawing consent is not a silent
flag-flip: `planWithdrawalCascade` produces a `WithdrawalJob` whose steps
(`WITHDRAWAL_CASCADE_KINDS`) include `evict-from-memory`,
`remove-cloned-voice-instance`, `remove-cloned-avatar-instance`,
`unpublish-derivative`, `remove-from-training-set`,
`remove-from-research-cohort`, and `unshare-with-teacher`. Memory eviction is
marked `effectiveImmediately`; the rest carry per-artifact ETAs so the privacy
center can show honest completion times.

### Region, residency, and routing

`libs/oshun/privacy/src/residency/residency.ts` is the data-residency core. The
sharp edge is `routeRead`, which returns a `ResidencyRoutingResult` that is one
of exactly three verdicts:

```ts
type ResidencyRoutingResult =
  | { readonly route: 'primary'; readonly planeId: string }
  | { readonly route: 'failover'; readonly planeId: string }
  | {
      readonly route: 'cross-region-blocked';
      readonly reason: 'residency-violation';
    };
```

If the caller's region is not the resource's `homeRegionId`, the read is
**blocked** with a `residency-violation` reason — _before_ any data is touched.
Failover is permitted only within the home region and only when a
`failoverPlaneId` is declared. Crossing a region boundary at all requires an
explicit, two-actor-approved exception:
`CROSS_REGION_EXCEPTION_KINDS = ['legal-hold', 'security-incident', 'operator-explicit-with-consent']`,
and `approveCrossRegionException` rejects an empty rationale, a self-approval,
and — for the `operator-explicit-with-consent` kind — a missing
`customerConsentRecordId`. This is the "cross-region access requires explicit
`ConsentRecord`" claim from the architecture, made enforceable.

Residency is woven through the rest of the data plane too:
`residencyAwareCacheKey` prefixes every cache key with the home region
(`{homeRegionId}::{baseKey}`) so a cached entry from one region can never be
served to a caller from another, and `logSegmentForRegion(regionId, logName)`
returns a per-region log path (`logs/{region}/{name}`) so logs are physically
segregated. When an approved exception _does_ move data, a `PropagationTracker`
requires every replica destination to reach a terminal state (`completed` or
`failed`) before `evaluatePropagationCompletion` reports `allTerminal`. Only
then does `applyTrackerToExceptionRequest` flip the request's
`completionTracked` flag, which is the signal the §22.7 compliance dashboard
alerts on. The shared enforcer `libs/shared/data-residency/src/enforcer.ts` is
the cross-cutting binding referenced elsewhere in the architecture; the routing
_policy_ above is where the verdicts originate.

### Export, deletion, retention, and legal hold

`libs/oshun/privacy/src/export-deletion/deletion.ts` makes the architecture's
"propagates via tombstones" claim concrete and enforces the retention numbers
that elsewhere appear only as prose. Retention is a real table,
`RETENTION_DAYS`, queried through `retentionDaysFor`:

| Data class           | Retention             |
| -------------------- | --------------------- |
| `raw-chat`           | 30 days               |
| `summarized-profile` | `durable` (kept)      |
| `billing`            | `365 * 7` (7 years)   |
| `audit`              | `365 * 7` (7 years)   |
| `generated-artifact` | `per-artifact-policy` |

The soft-delete window is governed by three constants —
`SOFT_DELETE_DEFAULT_SECONDS = 30 * 86400`,
`SOFT_DELETE_MIN_SECONDS = 24 * 3600`, `SOFT_DELETE_MAX_SECONDS = 90 * 86400` —
and `validateSoftDeleteWindow` enforces the bounds: tenant policy may override
the 30-day default but never below 24 h (a UX safeguard against an accidental
click) nor above 90 d (a regulatory cap on how long "soft-deleted" data may
dangle while still controlled). A deletion moves through a state machine —
`pending → soft-deleted → hard-deleted`, with `cancelled` and `blocked-by-hold`
branches — where `advanceToSoftDelete` sets `tombstoneIssued: true`, so a
tombstone is the _visible_ record that a deletion happened and prevents silent
re-creation downstream.

The hard-delete step is deliberately honest about cryptographic shredding.
`advanceToHardDelete` does not _claim_ a shred — it is **told** the outcome by
the real eraser via a `CryptographicShredOutcome` (`applied: boolean` with a
`reason` of `shredded` / `deletion-by-row-removal` / `no-encryption-keys` /
`not-supported` / `skipped`) and records `cryptographicShredApplied` verbatim.
Row-level deletion without key destruction is recorded as `applied: false` — the
state machine can never fabricate a shred that did not happen. Legal holds gate
the whole machine: `holdsApplicable` matches a `LegalHold` to a request
(including the `full-account` scope catching everything), and an applicable hold
blocks both the initial enqueue and the hard-delete transition.

The user-facing entry point is `apps/oshun/web/src/app/profile/data/` (a single
data page providing export and deletion controls).

### DSAR and operator access

`libs/oshun/privacy/src/dsar/dsar.ts` implements the data-subject-access-request
state machine the architecture's sequence diagram describes. The kinds are
`DSAR_KINDS = ['access', 'portability', 'rectification', 'erasure', 'restriction', 'objection']`
and the states are the exact lifecycle:

```
DSAR_STATES = ['received', 'identity-verified', 'scope-determined',
               'in-execution', 'completed', 'rejected', 'restored']
```

A `DsarRequest` records _who_ is asking via
`requestingActorKind ∈ {subject, authorized-representative, operator}` — a
representative or operator is treated differently from the subject themselves.
`checkEligibility` gates on identity verification and on **per-jurisdiction kind
support**: the US set is narrower (`access`, `portability`, `erasure`) than the
EU/UK sets (all six kinds), reflecting the actual scope of CCPA/CPRA vs. GDPR
rights — an `erasure` request in an unsupported jurisdiction is rejected, not
silently dropped. Fulfillment is tracked per scope: `recordClassCompletion`
marks each `ExportScope` as `completed` or `unavailable`, and the request only
advances to `completed` when _every_ scope is terminal, with an
`integrityManifest` (`bundleId`, `digest`) proving what was delivered. This is
the "Iris audits completeness across the full scope before the receipt goes
back" guarantee, made into a per-class checklist.

The DSAR/deletion fan-out — Iris walking the user's `MemoryScope` set and
writing tombstones to every domain — is the runtime half:

```mermaid
sequenceDiagram
    autonumber
    actor U as User
    participant Adm as Admin Web<br/>(DSAR Operator)
    participant Iris as Iris
    participant Doms as Domain Services
    participant DB as Persistence
    participant A as Audit Platform

    U->>Adm: Submit DSAR / deletion request
    Adm->>Iris: Verify identity + initiate
    Iris->>Iris: Enumerate MemoryScopes<br/>profile · session · notebook · operator-copilot · tenant
    Iris->>Doms: Fan-out tombstone writes
    par Tara · Arete
        Doms->>DB: Write tombstones
    and Veritas · Nyx
        Doms->>DB: Write tombstones
    and Nisaba · Metis
        Doms->>DB: Write tombstones
    end
    Doms->>A: Append attestation per tombstone (immutable)
    A-->>Iris: Confirm scope complete
    Iris->>Adm: Fulfillment bundle<br/>(export receipts + deletion receipts)
    Adm-->>U: DSAR receipt
    Note over A,Iris: Tombstones propagate — no silent re-creation.
```

Separately, the **operator-access** workflow handles non-DSAR access for
support/safety/privacy/research. `OPERATOR_ACCESS_REASONS` are
`support-investigation`, `safety-investigation`, `privacy-review`,
`research-cohort`, `legal-discovery`; the sensitive subset
(`SENSITIVE_OPERATOR_ACCESS = {legal-discovery, research-cohort}`) requires
`authorizeOperatorAccess` to enforce **two-operator authorization** with no
self-approval, a valid time-bound window, and a deadline by which the subject
must be notified. Operators do not get unlogged, untimed access to user data.

### Compliance regimes and mandatory disclosures

`libs/oshun/privacy/src/compliance/compliance.ts` flattens the architecture's
prose regulatory list into a real lookup. `REGULATORY_REGIMES` includes `GDPR`,
`CCPA`, `CPRA`, `LGPD`, `PIPEDA`, `US-State-Privacy`, `FERPA`, `COPPA`, and
`UK-DPA-2018`, and `regimesFor(region)` maps a region to the regimes that apply:

| Region | Regimes                                              |
| ------ | ---------------------------------------------------- |
| EU     | `GDPR`                                               |
| UK     | `GDPR`, `UK-DPA-2018`                                |
| US     | `CCPA`, `CPRA`, `US-State-Privacy`, `FERPA`, `COPPA` |
| CA     | `PIPEDA`                                             |
| BR     | `LGPD`                                               |

Note that **FERPA and COPPA** — the US education-records and children's-privacy
regimes — are first-class here even though the prose compliance bullet omits
them; Oshun's educational surfaces make them load-bearing. Mandatory disclosures
(`MANDATORY_DISCLOSURE_KINDS`) include `synthetic-content-disclosure`,
`ai-use-disclosure`, `third-party-processor-list`, `breach-notice`, and
`retention-policy`; `requireDisclosureCoverage` is a release gate that returns
the missing `(region, kind)` pairs and the compliance dashboard blocks a release
while that list is non-empty. Breach response is a seven-stage runbook
(`BREACH_RESPONSE_STAGES`: detect → contain → assess → notify → remediate →
postmortem → disclosure) whose `regulatoryDeadlineSecondsFromNow` derives the
notification deadline from the _strictest_ applicable regime (72 h for the
GDPR-class regimes, 168 h for the US-state/FERPA/COPPA regimes).

### The customer-facing privacy surface

`libs/oshun/privacy/src/privacy-surface/privacy-surface.ts` assembles the
Privacy Center the user actually sees. `buildPrivacyCenter` returns a
`PrivacyCenterEntry` per section, where
`section ∈ {consents, memory, exports, deletions, residency, operator-access-audit, subprocessors, disclosures}`
— note that **operator-access events are surfaced to the user**, not hidden,
closing the loop on the two-operator workflow above. Disclosure copy is held to
a jurisdiction-required reading level (`checkRevealReadingGrade` caps at grade 8
for US/CA/UK/EU/BR), and contextual consent prompts are governed by
`decideContextualPrompt`, which skips a prompt that is already granted, recently
dismissed, or not required for the feature — so consent is asked for at the
moment of first relevant use, not buried in a settings wall.

---

## Support, Entitlements, and Billing — pointer

Backlog: §23. Package: `@oshun/billing-support` (`libs/oshun/billing-support/`).

The third governance domain — support cases, entitlements, metered billing,
dunning, and the bridge that wires settled crypto payments into subscription
state — has its own deep-dive at
[Support, Entitlements, Billing, and the Aje Entitlement Bridge](./support-billing-and-crypto.md).
A few facts matter here because they interlock with Trust and Safety and
Privacy, and because neither the architecture hub nor the features doc names
`@oshun/billing-support` — leaving the reader unable to find the actual code:

- **Entitlement classes and feature gates** live in
  `libs/oshun/billing-support/src/entitlements/entitlements.ts`:
  `ENTITLEMENT_CLASSES = ['free', 'starter', 'plus', 'pro', 'scholar', 'institutional']`,
  with `FEATURE_KEYS` including the safety-relevant `voice.cloning`,
  `avatar.cloning`, `generated.video`, and `institutional.gradebook`. Premium
  personas, voices, avatars, and generated media are gated here and through the
  Isis release-gate model — and a trust-and-safety
  `voice-entitlement-suspension` or `avatar-entitlement-suspension` decision
  (both appealable) acts directly on these gates.
- **The Aje bridge is a real single-source-of-truth fix.**
  `libs/oshun/billing-support/src/billing-aje-bridge.ts` collapses the six
  `EntitlementClass` values onto the three canonical `OshunEntitlementTier`
  values via `TIER_BY_CLASS` (`free→free`; `starter`/`plus→pro`;
  `pro`/`scholar`/`institutional→premium`), so the product reads _one_ tier
  vocabulary instead of two. `applyPaymentSettlementToSubscription` advances the
  subscription state machine from a settled Aje payment — a `confirmed` payment
  moves trial/past-due/grace/paused/restored to `active` (or canceled/lapsed to
  `restored`), a `failed` renewal moves `active` to `past-due`, a `refunded`
  payment cancels — and a settlement with no legal transition is an honest no-op
  (`changed: false`). This wires the
  [Aje — Non-Custodial Payment Substrate](./substrate-aje.md) into entitlement
  state, and it is the implementation the prose omits.
- **Metered billing** (`metered/metered.ts`) meters
  `METERED_DIMENSIONS = ['agentic-cost-units', 'voice-seconds', 'avatar-seconds', 'storage-bytes-hours', 'gpu-minutes']`
  — note both the `gpu-minutes` dimension and the `agentic-cost-units` unit
  name, neither of which the architecture lists.
- **Dunning** (`dunning/dunning.ts`) runs
  `DUNNING_STAGES = ['first-warning', 'second-warning', 'final-warning', 'grace', 'lapse']`.
- **Support cases** (`support-cases/support-cases.ts`) — _not_ a TODO contract
  and _not_ monitored by the `libs/shared/queue/src/sla-monitor.ts` patterns the
  architecture cites — implement a concrete state machine here:
  `CASE_ROUTING_QUEUES = ['general', 'billing', 'technical', 'safety', 'privacy', 'institutional', 'crisis']`
  with `CASE_STATES` (`received`, `triaged`, `in-progress`, `awaiting-customer`,
  `resolved`, `reopened`, `escalated`). `routeCase` sends any crisis-signal case
  to the `crisis` queue (5-minute first-response SLA) and any
  safety/privacy-tagged case to its dedicated queue — the direct seam between
  support intake and the Trust-and-Safety pipeline above.

> **A note on `libs/maat`.** Do not confuse Oshun governance with `@maat/*`
> (`libs/maat/`). That is a _separate Ghana B2B intelligence product_ —
> `COMPLIANCE_LIBRARY = '@maat/compliance'` with a
> `ghana-data-protection-compliance-engine`, a
> `regulatory-filing-automation-engine`, and an `esg-reporting-engine`. It is
> **not** the implementation of Oshun's review, trust-and-safety, privacy, or
> billing logic; the real Oshun governance code is the three
> `libs/oshun/{trust-safety,privacy,billing-support}` packages documented above.

---

## Honest boundary, in one place

Every enum, constant, state machine, and validator on this page is real,
deterministic, and unit-tested — quoted verbatim from `@oshun/trust-safety`,
`@oshun/privacy`, and `@oshun/billing-support`. What these libraries
deliberately do _not_ do is fabricate a result they did not compute:

- The ML **classifiers** that produce `PolicyHit`s are the runtime's job; the
  library scores, routes, and decides on a hit it is given.
- **Immutable audit persistence** is `@oshun/audit-platform`'s; the cores emit
  the attestation records, the platform stores them.
- **Consent and memory enforcement** at runtime is Iris's
  (`@oshun/memory-iris`); the privacy library owns the vocabulary and
  validation.
- The crisis cascade's **publish/subscribe** is bound to the real Redis bus only
  in `crisis-frame-worker.ts`; the cascade domain itself is event-bus-free by
  design.
- The **cryptographic shred** flag is recorded from a real eraser's reported
  outcome, never inferred from a capability.

This is the same candor the rest of the V1 architecture docs hold to: a real,
verifiable policy core with clearly-marked ports where the runtime takes over.

---

## Related

- [Support, Entitlements, Billing, and the Aje Entitlement Bridge](./support-billing-and-crypto.md)
  — the §23 companion: entitlements, dunning, metered billing, support cases,
  and the Aje bridge in full
- [Lilith — Contemplative Policy Substrate](./substrate-lilith.md) — the
  contemplative-tone detectors and crisis pre-screen that _produce_ the safety
  signals
- [Iris — Assistant Memory Substrate](./substrate-iris.md) — the runtime owner
  of consent enforcement and the DSAR memory-scope walk
- [Aje — Non-Custodial Payment Substrate](./substrate-aje.md) — the crypto
  settlement the billing bridge consumes
- [Governance, Safety, Support, and Privacy](../features.md#governance-safety-support-and-privacy)
  — the product-scope companion for this governance area
- [../ARCHITECTURE.md](../ARCHITECTURE.md) — the architecture hub
