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-blockedverdicts, 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 feedingPolicyHits, 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-platformowns 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), plussupport-ops:unauthorized-data-accessandsupport-ops:social-engineering. Aminor-sexualorexplicit-adult-nonconsensualsub-class also short-circuits toblockbefore any confidence threshold is consulted — non-consensual and child imagery are never confidence-gated. - Jurisdiction-bound (
content:illegal.drugs): blocked only when aJurisdictionPolicyOverlayactually defines the category for the jurisdiction; otherwiseroute-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 aboveminConfidenceForAction, 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-visibilityplus 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:
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:
- 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: trueandsuspendMemoryWrites: true— so a downstream projector physically cannot soften the frame. - The surfaces.
CRISIS_FRAME_SURFACES = ['psyche', 'lilith-video', 'tara', 'iris', 'assistant']— the closed set every frame projects into. Thekindandsourceare drawn from the crisis module's own enums (CRISIS_KINDSincludessuicidal-ideation,self-harm-imminent,violence-to-others-credible,child-protection-signal,domestic-violence-signal,medical-emergency;CRISIS_DETECTION_SOURCESincludesmodel-classifier,user-self-report,third-party-report,pattern-match-across-sessions,support-copilot-escalation). - The fan-out, with a clean port boundary.
activateLilithCrisisFramepublishes through aCrisisFramePublishPort— a one-method interface, not a direct@oshun/event-busdependency.consumeCrisisFrameActivationthen fans the event out to every registeredCrisisFrameProjectorwhose 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.
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.
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 aConsentRecordevent." That is only half the picture. A full consent taxonomy and aConsentRecordtype also live inlibs/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:
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:
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'], withFEATURE_KEYSincluding the safety-relevantvoice.cloning,avatar.cloning,generated.video, andinstitutional.gradebook. Premium personas, voices, avatars, and generated media are gated here and through the Isis release-gate model — and a trust-and-safetyvoice-entitlement-suspensionoravatar-entitlement-suspensiondecision (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.tscollapses the sixEntitlementClassvalues onto the three canonicalOshunEntitlementTiervalues viaTIER_BY_CLASS(free→free;starter/plus→pro;pro/scholar/institutional→premium), so the product reads one tier vocabulary instead of two.applyPaymentSettlementToSubscriptionadvances the subscription state machine from a settled Aje payment — aconfirmedpayment moves trial/past-due/grace/paused/restored toactive(or canceled/lapsed torestored), afailedrenewal movesactivetopast-due, arefundedpayment 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) metersMETERED_DIMENSIONS = ['agentic-cost-units', 'voice-seconds', 'avatar-seconds', 'storage-bytes-hours', 'gpu-minutes']— note both thegpu-minutesdimension and theagentic-cost-unitsunit name, neither of which the architecture lists. - Dunning (
dunning/dunning.ts) runsDUNNING_STAGES = ['first-warning', 'second-warning', 'final-warning', 'grace', 'lapse']. - Support cases (
support-cases/support-cases.ts) — not a TODO contract and not monitored by thelibs/shared/queue/src/sla-monitor.tspatterns the architecture cites — implement a concrete state machine here:CASE_ROUTING_QUEUES = ['general', 'billing', 'technical', 'safety', 'privacy', 'institutional', 'crisis']withCASE_STATES(received,triaged,in-progress,awaiting-customer,resolved,reopened,escalated).routeCasesends any crisis-signal case to thecrisisqueue (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 aghana-data-protection-compliance-engine, aregulatory-filing-automation-engine, and anesg-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 threelibs/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.
Related#
- Support, Entitlements, Billing, and the Aje Entitlement Bridge — the §23 companion: entitlements, dunning, metered billing, support cases, and the Aje bridge in full
- Lilith — Contemplative Policy Substrate — the contemplative-tone detectors and crisis pre-screen that produce the safety signals
- Iris — Assistant Memory Substrate — the runtime owner of consent enforcement and the DSAR memory-scope walk
- Aje — Non-Custodial Payment Substrate — the crypto settlement the billing bridge consumes
- Governance, Safety, Support, and Privacy — the product-scope companion for this governance area
- ../ARCHITECTURE.md — the architecture hub