Oshun Platform · Architecture

Trust, Safety, and Privacy

A focused page within the Oshun Platform Architecture documentation. The full map and every sibling page live in the Architecture hub.

5sections21 minread2diagrams6tables

On this page

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, directly alongside its companion Support, Entitlements, Billing, and the Aje Entitlement Bridge, and it is the policy half of the same substrate whose contemplative-tone half Lilith — Contemplative Policy Substrate 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 PolicyHits, 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

The product scope is 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_RUNBOOKcsam-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.

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 for the contemplative-tone detectors that produce the crisis signal in the first place.


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.

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.

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:

text
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:

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. 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 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 productCOMPLIANCE_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 PolicyHits 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.