openvibers/openvibe.ai Commits

AI: the uncensored policy on the house's own classifiers (#318)

OP OpenVibers committed 7 days ago · 37f147c163 · 6 files, +490 −196

* AI: nine managed provider adapters and the uncensored tier

Seed Qwen, Kimi, GLM, MiniMax, DeepInfra, Novita, Featherless, Refuseless and OpenRouter from the price book, off until their owner key exists, with recorded prices, data policies and the existing commercial-access gate. Add policy="uncensored": Network's 18+ account flag (fail closed, five-minute cache), uncensored offers only, the hard floor on input and output, and a label on every such answer. Recorded provider fixtures and no live network in the tests.

* Merge remote-tracking branch 'origin/main' into feat/ai-provider-adapters

* PR 1: the adapters alone; the uncensored policy moves to its own PR

The branch carried the ten managed providers and the uncensored tier together. The
brief asks for two PRs, smaller than the first plan: the adapters here, the policy
in the next one. So the policy files (server/policy/, the tier gate, the floor and
its wiring in the router, the runs and the two API routes) are taken back out of
this branch and go into their own PR on top of it.

* Readiness: a catalogue offer nobody bought does not degrade the service

The nine catalogued provider offers are seeded into the registry so the owner sees the
key slot, and they answer once the owner puts a key in: until then they are off by
design. The readiness check counted every active provider without credentials as a
problem, so the service read 'degraded' with nothing wrong with it, and the open-circuit
case reported the nine offers instead of the provider that had the circuit open.

A catalogue offer with no key is no longer a problem (its circuit still is); every
other provider keeps the old rule, and the operator console still lists an offer
without a key.

* Providers: the ten managed offers, priced from each provider's own page

Qwen (Alibaba Model Studio), Kimi (Moonshot), GLM (Z.ai), MiniMax, DeepInfra, Novita,
Featherless, Refuseless, Abliteration.ai and OpenRouter, each OpenAI-compatible and each
off until its key is in the owner settings.

Every price now comes from the provider's own page - its pricing page or its own model
list - with that page and the day it was read, and not from an aggregator's. A price
nobody publishes stays null: Refuseless publishes none, so its models carry null rather
than a guess. The quantization is null unless a provider states it (none of the ten does),
and the refusal rate is null until a provider publishes one or a measurement job owns it.

Each provider carries a privacy label: private only where its own terms say it keeps
nothing and trains on nothing - DeepInfra and Abliteration.ai - and anonymized for
everyone else, Featherless included: it says the prompts are not stored but never says
they are not trained on.

A provider that says the account has no money left is named provider.exhausted (Refuseless
answers 402 insufficient_credits; the others say it in their own words). The code travels
with the failure, so the empty-failover job can read it; the circuit that follows is that
job's business, not this one's.

* Merge remote-tracking branch 'origin/main' into feat/ai-provider-adapters

# Conflicts:
#	server/index.js

* Providers: the document of the ten offers, and the seed's own words

* Merge remote-tracking branch 'origin/main' into feat/ai-provider-adapters

# Conflicts:
#	docs/provider-adapters.md
#	server/providers/common.js
#	server/providers/index.js
#	server/providers/pool-providers.js
#	server/providers/pool-seed.js
#	server/workflows/seed.js
#	test/fixtures/pool-providers.json

* Providers: the catalogue tests follow the new catalogue

The ten offers and their own-page prices are tested in their own file; the Exchange file
keeps the uncensored tier's tests. The exhaustion case is the empty-failover work's now
(it is on main): the test proves a provider's own answer is read as exhaustion and never
retried into, and leaves the circuit and the failover where that job put them.

* The uncensored policy: the floor reads the house's own classifiers

The hard floor had its own words for a child, a bare body and a sex act: a third set,
written next to the two the house already has. Now the first line is decided by Live's
own rule (a child together with a bare body, server/ai/explicit.js) and by Vision's
classifyText (the three categories, server/vision/review.js), both carried in one place
with the file they come from and the day they were read.

Two narrow lists extend them for the words the house has none for: the names of a child
abuse, and the two lines about consent and weapons that no house classifier covers. They
stay narrow, so ordinary work is never refused.

The rest of the tier is unchanged and now written down: Network's 18+ flag (its PR #587),
the offer filter, the floor on the request and on the answer, and the label on every
answer (docs/uncensored-policy.md).

* Providers: the adapter leaves the exhaustion answer to the empty-failover work

The exhausted mapping (a provider that says its account is out of money) is on main now:
the empty-failover job reads the provider's own answer and owns the circuit, the failover
and the 503 ai.resting. The adapter here does not need its own copy of that rule, and a
second one would only drift from it.

* Merge branch 'feat/ai-provider-adapters' into feat/ai-uncensored-policy

* Providers: the privacy facts reach the label on every answer

The catalogue kept what each provider says it does with a prompt in a shape nothing read:
the label on every answer (server/exchange/privacy.js) reads retention, training, region
and trust, and the Exchange catalogue reads trains, retention_days and anonymized. The
free-text 'not stated' was read as a region and as a declared policy, so every offer
claimed to be known.

The data policy now declares the facts the house reads. A fact nobody stated is null:
DeepInfra and Abliteration.ai keep nothing and train on nothing (private), Featherless
keeps nothing but never says it does not train (anonymized, and it says so), and the
providers with no readable privacy page declare nothing at all (anonymized, unknown).

* Providers: the catalogue's own header says where the facts come from

* Merge branch 'feat/ai-provider-adapters' into feat/ai-uncensored-policy

* Policy: the house's classifiers read the content, not our own instructions

The floor read every text with Vision's classifyText, and our own templates say
"nothing sexual involving minors": the moderation desk and the chat desk were
refused by their own instructions (CI caught the moderation one).

The narrow lists are unambiguous wherever they appear, so they hold on every
text. The house's classifiers read what a text shows, so they read the content
(the person's words, the answer) and not the instructions (the system prompt).
A person's request for such content is still refused before a provider is
called, and the answer is still read.

Live's MINOR and NUDE carry the terms the floor on main already used, so the
floor never gets weaker than the one it replaces.

* Providers: the exhaustion is a state, not an error code

* Merge branch 'feat/ai-provider-adapters' into feat/ai-uncensored-policy

* Policy: the second line says plainly that it holds for fiction and for incest

Non-consensual sexual content (fiction included) and incest are refused, because
payment providers and hosts ban them. The floor already refused them; the words
now live in one place (PLAIN in server/policy/floor.js), so the page that shows
the policy and the answer that carries it say the same thing.

* Merge remote-tracking branch 'origin/main' into feat/ai-uncensored-policy

# Conflicts:
#	docs/provider-adapters.md
#	test/ai-uncensored-policy.test.js

Changes

ddiff --git a/docs/provider-adapters.md b/docs/provider-adapters.md
iindex fe6f139..15db6ad 100644
---- a/docs/provider-adapters.md
++++ b/docs/provider-adapters.md
@@@ -100,4 +100,4 @@ catalogued and its key stored, but the router does not resell it.
Refuseless, Abliteration.ai and the abliterated models Featherless lists are marked `uncensored: true` in the
catalogue. The tier that lets an 18+ account ask for such an offer — the gate, the hard floor and the label on every
--answer — is a separate change (its own PR); this one only records which offers are marked.
++answer — is `docs/uncensored-policy.md`; the catalogue only records which offers are marked.
ddiff --git a/docs/uncensored-policy.md b/docs/uncensored-policy.md
nnew file mode 100644
iindex 0000000..5cf83dd
---- /dev/null
++++ b/docs/uncensored-policy.md
@@@ -0,0 +1,79 @@
++# The uncensored policy (the Exchange remake, "Uncensored")
++
++`policy: "uncensored"` asks for an offer that has had its refusals removed. It is accepted on
++`POST /v1/chat/completions`, `POST /v1/messages` and `POST /api/v1/runs`, with `on_behalf_of` naming the person
++the run is for.
++
++Four things hold it up, and none of them is the caller's word:
++
++1. **The account is 18+.** Network owns the flag. The person confirms it on `/my` → Content, and OpenVibe.AI asks
++ Network under `identity.subject.resolve` (`GET /internal/identity/adult?subject_id=usr_…`,
++ `server/policy/adult-status.js`). The answer is cached for five minutes and fails closed: if Network cannot be
++ reached, the tier is closed. A developer app never gets it, and a run that names no person never gets it.
++2. **Only the offers marked uncensored answer.** A standard request never runs an uncensored offer, even when it is
++ the cheapest and the healthiest one (`server/providers/index.js`, `policyAllows`). The marks are in the
++ catalogue (`server/providers/pool-providers.js`): Refuseless, Abliteration.ai, and the abliterated models
++ Featherless lists.
++3. **The hard floor holds on the request and on the answer** (`server/policy/floor.js`). It is checked before a
++ provider is called, over the answer's own text, and over every chunk of a streamed answer before it goes out.
++4. **Every answer carries a label.** The run's `output.label` and `output.policy`, `x_openvibe.label` and
++ `x_openvibe.policy` on both public APIs, and the `X-OpenVibe-Content-Policy` header say what produced the answer:
++ `Uncensored model — adults only (18+)`.
++
++## The floor
++
++Three lines, on every offer, uncensored or not:
++
++| rule | what it refuses |
++|---|---|
++| `minors` | sexual content involving minors |
++| `non_consensual` | non-consensual sexual content (fiction included), and incest |
++| `mass_harm` | help with weapons of mass harm, or with attacks on others |
++
++**Non-consensual sexual content (fiction included) and incest are refused, because payment providers and hosts ban
++them.** The words are in one place (`server/policy/floor.js`, `PLAIN`), so the page that shows the policy and the
++answer that carries it say the same thing.
++
++The lines are decided by the **house's own text classifiers** (`server/policy/classifiers.js`), not by a third set of
++words written for this service:
++
++- **Live**'s `server/ai/explicit.js` — a child together with a bare body in the same text (`minorNude`);
++- **Vision**'s `server/vision/review.js` — `classifyText`, the three categories `fine`, `sex_acts`, `minor_sexual`.
++
++OpenVibe.AI is its own service and cannot import another service's files, so those rules travel here with the name
++of the file they come from and the day they were read. They are the house's rules, not new ones: Live and Vision keep
++their own copies until the rules move into one shared package (the follow-up).
++
++Two narrow lists extend them for the words the house has none for: the names of a child abuse (the `minors` rule),
++and the two lines about consent and weapons that no house classifier covers. They are deliberately narrow, so
++ordinary work is never refused: "how do I kill a stuck process", "cp -r src dst" and "what is our incident response
++plan" all pass. A person and a sexual act must be in the same text, and a weapon only counts when the text asks for
++one to be made, acquired or used.
++
++### Instructions and content
++
++The narrow lists are unambiguous wherever they appear, so they hold on every text. The house's classifiers read what
++a text *shows* — a child with a bare body, a child with sex acts — so they are read on the **content**: the person's
++words and the answer. They are not read on the **instructions** (the system prompt), because an instruction that
++names a category is not that category: our own templates say "nothing sexual involving minors" and "flag any sexual
++content involving minors", and the moderation desk has to be able to say so. The answer is still read: a model that
++writes what was asked for is refused there.
++
++So a person's request for such content is refused before a provider is called, and our own desks keep working.
++
++Nudity on its own is art (the owner, 2026-10-31) and is allowed; a child and nudity together is not.
++
++## What the tier is not
++
++- It is not a key anyone can use: a person's own provider key never answers an uncensored request (the tier is
++ network supply only), and the tier's answer is never cached with a standard one.
++- It is not a promise of anything: the label says what produced the answer, and the floor holds whatever the offer
++ says.
++- It is not free: an uncensored offer is a paid offer like any other, and the commercial gate
++ (`docs/provider-matrix.md`) applies to it like to every other.
++
++## The Network side
++
++Network PR #587 ("Account: the 18+ confirmation for uncensored AI") owns the flag: `users.adult_confirmed_at`
++(migration 0040), `GET/PUT/DELETE /api/v1/me/adult`, the confirmation screen on `/my` → Content, and the internal
++read. It is a timestamp, never a birth date, and Network never infers an age.
ddiff --git a/server/policy/classifiers.js b/server/policy/classifiers.js
nnew file mode 100644
iindex 0000000..62cfe50
---- /dev/null
++++ b/server/policy/classifiers.js
@@@ -0,0 +1,85 @@
++'use strict';
++/**
++ * The house text classifiers, in one place: OpenVibe.Live's `server/ai/explicit.js` and OpenVibe.Vision's
++ * `server/vision/review.js`.
++ *
++ * Why they are here: OpenVibe.AI is its own service and cannot import another service's files, so the rules
++ * travel with the name of the file they come from and the day they were read (2026-11-06). They are the house's
++ * rules, not new ones: Live and Vision keep their own copies until the rules move into one shared package
++ * (the follow-up). Nothing here decides what OpenVibe answers; it only says what a piece of text is.
++ *
++ * Live server/ai/explicit.js EXPLICIT, MINOR, NUDE, minorNude(), isExplicit()
++ * Vision server/vision/review.js MINOR, NUDE, SEX, classifyText() → fine | sex_acts | minor_sexual
++ *
++ * Live's MINOR and NUDE carry, besides Live's own words, the terms the floor on main already used, so the floor
++ * never gets weaker than the one it replaces (the terms are named where they are).
++ *
++ * What a caller may use:
++ *
++ * minorNude(text) a child and a bare body in the same text (Live's rule, the hard line nudity never moves)
++ * classifyText(text) the category of a text: `fine`, `sex_acts` or `minor_sexual` (Vision's rule)
++ *
++ * `isExplicit` is here because it is Live's own rule (sex acts, whatever the age), but it is NOT a floor rule:
++ * the uncensored tier exists for adult content, and the floor is about children, consent and weapons.
++ */
++
++/** Live: what a scene shows, not what someone talks about in passing — sex acts and pornography. */
++const EXPLICIT = /\b(?:sexual(?:ly)?\s+(?:acts?|activity|intercourse)|sex\s+acts?|having\s+sex|(?:pornographic|sexually\s+explicit|explicit\s+adult)\s+(?:content|material|footage|scenes?|imagery|videos?|acts?)|adult\s+(?:films?|videos?)|porn(?:o|ography|ographic)?|masturbat\w*|intercourse|oral\s+sex|penetrat\w*)\b/i;
++
++/**
++ * Live: a person who is not an adult, plus the words the floor on main already carried ('minor', 'pre-teen',
++ * 'schoolboy', 'schoolgirl', 'loli', 'shota'), so the floor never gets weaker than the one it replaces.
++ */
++const MINOR = /\b(?:child(?:ren)?|kids?|minors?|minor|teen(?:age(?:rs?)?|s)?|toddlers?|bab(?:y|ies)|infants?|boys?|girls?|underage|pre-?teen(?:s)?|schoolboy|schoolgirl|loli|shota)\b/i;
++
++/**
++ * Live: a bare body, plus the words the floor on main already carried ('boobs', 'erotic', 'orgasm', 'cunnilingus',
++ * 'fellatio'). Nudity is art (the owner, 2026-10-31); a child and a bare body in the same text is not.
++ */
++const NUDE = /\b(?:naked|nude|nudity|undress(?:ed|ing)?|topless|bottomless|genital(?:s|ia)?|penis(?:es)?|vaginas?|vulvas?|breasts?|boobs?|nipples?|erotic(?:a)?|orgasm|cunnilingus|fellatio)\b/i;
++
++/** Live: the hard line that nudity being art never moves. */
++const minorNude = (t) => MINOR.test(String(t || '')) && NUDE.test(String(t || ''));
++
++/** Live: true when any of the texts describes sex acts or pornography, or puts a child and nudity together. */
++function isExplicit(...texts) {
++ return texts.some((t) => t != null && (EXPLICIT.test(String(t)) || minorNude(String(t))));
++}
++
++/** Vision: a person who is not an adult (its list is Live's plus preteens and schoolchildren). */
++const MINOR_VISION = /\b(?:child(?:ren)?|kids?|minors?|underage|preteens?|toddlers?|bab(?:y|ies)|infants?|boys?|girls?|teen(?:age(?:rs?)?|s)?|schoolboys?|schoolgirls?)\b/i;
++
++/** Vision: a bare body. */
++const NUDE_VISION = /\b(?:naked|nude|nudity|undress(?:ed|ing)?|topless|bottomless|genital(?:s|ia)?|penis(?:es)?|vaginas?|vulvas?|breasts?|nipples?)\b/i;
++
++/**
++ * Vision: what a scene shows, not what someone talks about in passing. Nudity and body words are deliberately
++ * absent (a nude body is not a sex act).
++ */
++const SEX = /\b(?:sexual(?:ly)?\s+(?:acts?|activity|intercourse|content|explicit|material|scenes?)|sex\s+(?:acts?|scenes?|shows?|clubs?|work|toys?)|having\s+sex|(?:pornographic|explicit\s+adult|adult)\s+(?:content|material|footage|scenes?|imagery|videos?|films?|acts?)|porn(?:o|ography|ographic)?|masturbat\w*|intercourse|oral\s+sex|anal\s+sex|penetrat\w*|erotic\w*|orgasm\w*|blow\s?job|hand\s?job|nsfw)\b/i;
++
++/** Vision: the three categories, worst last. */
++const CATEGORIES = ['fine', 'sex_acts', 'minor_sexual'];
++
++/**
++ * Vision: sort a piece of text into the three categories.
++ * → { category: 'fine' | 'sex_acts' | 'minor_sexual', cue, reason }
++ */
++function classifyText(...texts) {
++ const all = texts.filter((t) => t != null).map(String).join('\n').trim();
++ if (!all) return { category: 'fine', cue: 'empty', reason: 'there is no text to judge' };
++ const minor = MINOR_VISION.test(all);
++ const nude = NUDE_VISION.test(all);
++ const sex = SEX.test(all);
++ if (minor && (sex || nude)) {
++ return { category: 'minor_sexual', cue: 'minor+nudity_or_sex', reason: 'the text puts a child together with nudity or sex acts' };
++ }
++ if (sex) return { category: 'sex_acts', cue: 'sex_acts', reason: 'the text describes sex acts or pornography' };
++ if (nude) return { category: 'fine', cue: 'nudity', reason: 'the text describes nudity, which is allowed; it does not describe a sex act' };
++ return { category: 'fine', cue: 'none', reason: 'the text describes neither sex acts nor a child with nudity' };
++}
++
++module.exports = {
++ EXPLICIT, MINOR, NUDE, MINOR_VISION, NUDE_VISION, SEX, CATEGORIES,
++ minorNude, isExplicit, classifyText,
++};
ddiff --git a/server/policy/floor.js b/server/policy/floor.js
iindex c885614..e16adc8 100644
---- a/server/policy/floor.js
++++ b/server/policy/floor.js
@@@ -3,29 +3,43 @@
* The hard floor (the Exchange remake, "Uncensored"): the three lines that hold on every offer, uncensored or not.
*
* 1. no sexual content involving minors;
-- * 2. no non-consensual sexual content of real people;
++ * 2. no non-consensual sexual content (fiction included) and no incest;
* 3. no help with weapons of mass harm or with attacks on others.
*
++ * The second line is refused wherever it appears, a story included: payment providers and hosts ban it, so the
++ * policy says so plainly rather than leaving it to be discovered.
++ *
* A breach ends the account (the owner, 2026-11-06). The floor is checked twice: on the way in (the prompt, the
* system prompt, the tool arguments) and on the way out (the answer). A request that trips it is refused before a
* provider is called; an answer that trips it is never delivered.
*
-- * The patterns are the ones OpenVibe.Live already uses for the same job (server/ai/explicit.js: a minor together
-- * with nudity is explicit; server/chat/moderation-utils.js: the "minors", "sexual" and "threats" categories), kept
-- * deliberately narrow so an ordinary question ("how do I kill a stuck process", "what is our incident response
-- * plan") is never refused. A person and a sexual act must be in the same text, and a weapon only counts when the
-- * text asks for one to be made, acquired or used.
++ * The lines are decided by the house's own text classifiers, not by a third set of words written here
++ * (server/policy/classifiers.js):
++ *
++ * the first line Live's rule (a child together with a bare body) and Vision's `classifyText` (`minor_sexual`)
++ * the second line no house classifier has a word for consent, so a narrow list names it
++ * the third line no house classifier has a word for a weapon, so a narrow list names it
++ *
++ * The narrow lists are deliberately narrow, so an ordinary question ("how do I kill a stuck process", "cp -r src
++ * dst", "what is our incident response plan") is never refused. A person and a sexual act must be in the same
++ * text, and a weapon only counts when the text asks for one to be made, acquired or used.
++ *
++ * Instructions and content are read differently, and the difference is the house's own:
++ *
++ * - The narrow lists are unambiguous wherever they appear, so they are checked on every text.
++ * - The house's classifiers read what a text *shows* — a child with a bare body, a child with sex acts. They are
++ * checked on the content: the person's words and the answer. They are not checked on the instructions (the
++ * system prompt), because an instruction that names a category is not that category: our own templates say
++ * "nothing sexual involving minors" and "flag any sexual content involving minors", and the moderation desk
++ * has to be able to say so. The answer is still checked: a model that writes what was asked for is refused
++ * there.
*/
--/** A person who is not an adult. */
--const MINOR = /\b(?:child(?:ren)?|kids?|minors?|minor|teen(?:age(?:rs?)?|s)?|toddlers?|bab(?:y|ies)|infants?|boys?|girls?|underage|pre-?teen(?:s)?|schoolboy|schoolgirl|loli|shota)\b/i;
--/** Sexual content, in the words a request or an answer uses. */
--const SEXUAL = /\b(?:sexual(?:ly)?\s+(?:acts?|activity|intercourse)|sex\s+acts?|having\s+sex|(?:pornographic|sexually\s+explicit|explicit\s+adult)\s+(?:content|material|footage|scenes?|imagery|videos?|acts?)|porn(?:o|ography|ographic)?|masturbat\w*|intercourse|oral\s+sex|penetrat\w*|hentai|nsfw|blowjob|handjob|anal\s+sex|doggy\s+style)\b/i;
--/** A bare body: the hard line is a child AND nudity in the same text (Live's explicit.js), not the word "sexual" alone. */
--const NUDE = /\b(?:naked|nude|nudity|undress(?:ed|ing)?|topless|bottomless|genital(?:s|ia)?|penis(?:es)?|vaginas?|vulvas?|breasts?|boobs?|nipples?|erotic(?:a)?|orgasm|cunnilingus|fellatio)\b/i;
--/** Words for a child and sex together (the clearest form of the first rule). */
++const { minorNude, classifyText } = require('./classifiers');
++
++/** Words for a child and sex together (the clearest form of the first line, and the house has no word for it). */
const CSAM = /\b(?:child\s?(?:porn|porno|abuse|sexual)|cp\s(?:content|video|series)\b|kiddie\s?(?:porn|porno)|pedophile|pedophilia|paedophile|paedophilia)\b/i;
--/** Non-consensual sex, and the real people it is done to. */
++/** Non-consensual sex, and incest: refused wherever they appear, a story included. */
const NONCONSENSUAL = /\b(?:rape|raped|rapes|raping|rapist|sexual assault|sexual abuse|molest(?:ed|ing|s)?|incest|statutory rape|non-?consensual|without (?:her|his|their|my) consent|she (?:said|said no|didn't consent|did not consent)|he (?:said no|didn't consent|did not consent)|they (?:said no|didn't consent|did not consent)|took advantage of (?:her|him|them)|she was (?:drugged|drunk|asleep|unconscious)|drugged (?:her|him|them)|unconscious (?:sex|woman|girl|man)|forced (?:sex|himself|herself|them)|coerc(?:e|ed|ing|ion)|groom(?:ed|ing)?|traffick(?:ed|ing)?|deepfake(?:s|d)?|revenge porn|nude photos? of (?:her|him|them)|sexual(?:ly)? explicit (?:photos?|pictures?|images?|videos?) of (?:her|him|them))\b/i;
/** A weapon of mass harm, by its own name: these are never a question about safety. */
const MASS_HARM = /\b(?:nerve agent|sarin|vx gas|tabun|soman|mustard gas|chlorine gas|anthrax|ricin|botulinum|cyanide|nerve gas|bioweapon|biological weapon|chemical weapon|dirty bomb|nuclear (?:bomb|device|weapon)|radiological (?:bomb|device|weapon)|weapons? of mass destruction|\bwmd\b|germ warfare|biological warfare|pathogen (?:strain|weapon)|weaponized? (?:virus|pathogen|bacillus))\b/i;
@@@ -34,55 +48,115 @@ const WEAPON_INTENT = /\b(?:(?:make|build|construct|synthesi[sz]e|manufactur(?:e
/** An attack on people: the asking and the person. */
const ATTACK_INTENT = /\b(?:(?:how (?:do|can|would) (?:i|we|you|one))\s+[^.?!]{0,40}\b(?:kill|murder|assassinate|poison|hurt|attack|stab|shoot)\b[^.?!]{0,40}\b(?:him|her|them|someone|somebody|people|my \w+|a person|people I|people at)|(?:kill|murder|assassinate|poison|stab|shoot)\s+(?:him|her|them|someone|somebody|people|my \w+|a person)|mass shooting|school shooting|terrorist attack|suicide attack|car bomb|truck bomb)\b/i;
--/** The three rules, in the order they are checked. */
++/**
++ * The three rules, in the order they are checked. Every text is checked against them.
++ *
++ * the first line Live's rule (a child together with a bare body) and the narrow names of a child abuse
++ * the second line the narrow list for consent
++ * the third line the narrow lists for a weapon of mass harm and for an attack on people
++ */
const RULES = [
-- { rule: 'minors', label: 'sexual content involving minors', test: (t) => CSAM.test(t) || (MINOR.test(t) && NUDE.test(t)) },
-- { rule: 'non_consensual', label: 'non-consensual sexual content of real people', test: (t) => NONCONSENSUAL.test(t) },
-- { rule: 'mass_harm', label: 'weapons of mass harm or attacks on others', test: (t) => MASS_HARM.test(t) || WEAPON_INTENT.test(t) || ATTACK_INTENT.test(t) },
++ {
++ rule: 'minors', label: 'sexual content involving minors',
++ test: (t) => CSAM.test(t) || minorNude(t),
++ },
++ {
++ rule: 'non_consensual', label: 'non-consensual sexual content',
++ test: (t) => NONCONSENSUAL.test(t),
++ },
++ {
++ rule: 'mass_harm', label: 'weapons of mass harm or attacks on others',
++ test: (t) => MASS_HARM.test(t) || WEAPON_INTENT.test(t) || ATTACK_INTENT.test(t),
++ },
];
--/** The text of a request: the system prompt, the messages and the tool arguments. */
--function textsOf(req = {}) {
-- const out = [];
-- if (typeof req.system === 'string') out.push(req.system);
-- for (const s of req.system || []) if (s && s.text) out.push(String(s.text));
-- for (const m of req.messages || []) {
-- if (!m) continue;
-- if (typeof m.content === 'string') out.push(m.content);
-- else if (Array.isArray(m.content)) for (const p of m.content) if (p && p.text) out.push(String(p.text));
-- if (m.name) out.push(String(m.name));
++/**
++ * The floor in plain words: the three lines, and the one line that says why the second line is not negotiable. The
++ * pages that show the policy and the answers that carry it read these, so the words are written down once.
++ */
++const PLAIN = {
++ lines: [
++ 'No sexual content involving minors.',
++ 'No non-consensual sexual content (fiction included) and no incest.',
++ 'No help with weapons of mass harm or attacks on others.',
++ ],
++ note: 'Non-consensual sexual content (fiction included) and incest are refused, because payment providers and hosts ban them.',
++};
++
++/**
++ * The house's classifiers, on content only: they read what a text shows, so they decide the first line on the
++ * person's words and on the answer. Vision's `classifyText` puts a text that has a child together with sex acts
++ * or with a bare body in the `minor_sexual` category; Live's rule is already in the list above, so this adds
++ * Vision's reading of a child with sex acts.
++ */
++const CONTENT_RULES = [
++ {
++ rule: 'minors', label: 'sexual content involving minors',
++ test: (t) => classifyText(t).category === 'minor_sexual',
++ },
++];
++
++/**
++ * The texts of a request, split into the instructions and the content.
++ *
++ * instructions the system prompt (our own template, or the caller's own), and a message that says it is one
++ * content everything else: the person's words, the tool names and their arguments, the answers
++ */
++function partsOf(req = {}) {
++ const instructions = [];
++ const content = [];
++ if (typeof req.system === 'string') instructions.push(req.system);
++ for (const s of req.system || []) if (s && s.text) instructions.push(String(s.text));
++ const addMessage = (m) => {
++ if (!m) return;
++ const role = String(m.role || 'user').toLowerCase();
++ const bucket = (role === 'system' || role === 'developer') ? instructions : content;
++ if (typeof m.content === 'string') bucket.push(m.content);
++ else if (Array.isArray(m.content)) for (const p of m.content) if (p && p.text) bucket.push(String(p.text));
++ if (m.name) bucket.push(String(m.name));
if (m.tool_calls) for (const t of m.tool_calls) {
-- if (t && t.function && t.function.arguments) out.push(String(t.function.arguments));
-- if (t && t.function && t.function.name) out.push(String(t.function.name));
++ if (t && t.function && t.function.arguments) bucket.push(String(t.function.arguments));
++ if (t && t.function && t.function.name) bucket.push(String(t.function.name));
}
-- }
-- if (req.openai && Array.isArray(req.openai.messages)) {
-- for (const m of req.openai.messages) {
-- if (!m) continue;
-- if (typeof m.content === 'string') out.push(m.content);
-- else if (Array.isArray(m.content)) for (const p of m.content) if (p && p.text) out.push(String(p.text));
-- if (m.tool_calls) for (const t of m.tool_calls) if (t && t.function && t.function.arguments) out.push(String(t.function.arguments));
-- }
-- for (const t of req.openai.tools || []) if (t && t.function && t.function.description) out.push(String(t.function.description));
-- }
-- return out.filter(Boolean);
++ };
++ for (const m of req.messages || []) addMessage(m);
++ if (req.openai && Array.isArray(req.openai.messages)) for (const m of req.openai.messages) addMessage(m);
++ // A tool's description is the caller's schema: an instruction, not something the person wrote.
++ if (req.openai) for (const t of req.openai.tools || []) if (t && t.function && t.function.description) instructions.push(String(t.function.description));
++ return { instructions: instructions.filter(Boolean), content: content.filter(Boolean) };
++}
++
++/** The text of a request: the system prompt, the messages and the tool arguments. */
++function textsOf(req = {}) {
++ const { instructions, content } = partsOf(req);
++ return [...instructions, ...content];
}
/**
* Does this text break the floor? → { ok: true } or { ok: false, rule, label, detail }.
* The detail never quotes the text: only the rule it broke.
*/
--function check(texts) {
++function check(texts, rules = RULES.concat(CONTENT_RULES)) {
const list = (Array.isArray(texts) ? texts : [texts]).filter((t) => t != null && String(t).trim());
for (const t of list) {
const text = String(t);
-- for (const r of RULES) if (r.test(text)) return { ok: false, rule: r.rule, label: r.label, detail: `this request breaks the hard floor (${r.label})` };
++ for (const r of rules) if (r.test(text)) return { ok: false, rule: r.rule, label: r.label, detail: `this request breaks the hard floor (${r.label})` };
}
return { ok: true };
}
--/** The floor on the way in: the request's own text. */
--function checkRequest(req) { return check(textsOf(req)); }
++/**
++ * The floor on the way in: the request's own text.
++ *
++ * The instructions are read by the narrow lists (which are unambiguous wherever they appear); the content — the
++ * person's words — is read by the narrow lists and by the house's classifiers.
++ */
++function checkRequest(req = {}) {
++ const { instructions, content } = partsOf(req);
++ const said = check(instructions, RULES);
++ if (!said.ok) return said;
++ return check(content, RULES.concat(CONTENT_RULES));
++}
/** The text of an answer, whatever shape the operation returned it in. */
function textsOfAnswer(result = {}) {
@@@ -108,10 +182,13 @@ function textsOfAnswer(result = {}) {
return out.filter(Boolean);
}
--/** The floor on the way out: the answer's text. */
++/** The floor on the way out: the answer's text. An answer is content, so the house's classifiers read it too. */
function checkAnswer(text) { return check(Array.isArray(text) ? text : [text]); }
/** The floor on the way out, over every shape an operation's answer uses. */
function checkResult(result) { return check(textsOfAnswer(result)); }
--module.exports = { RULES, MINOR, SEXUAL, NUDE, CSAM, NONCONSENSUAL, MASS_HARM, WEAPON_INTENT, ATTACK_INTENT, textsOf, textsOfAnswer, check, checkRequest, checkAnswer, checkResult };
++module.exports = {
++ RULES, CONTENT_RULES, PLAIN, CSAM, NONCONSENSUAL, MASS_HARM, WEAPON_INTENT, ATTACK_INTENT,
++ partsOf, textsOf, textsOfAnswer, check, checkRequest, checkAnswer, checkResult,
++};
ddiff --git a/test/ai-exchange-providers.test.js b/test/ai-exchange-providers.test.js
ddeleted file mode 100644
iindex 03ca6bc..0000000
---- a/test/ai-exchange-providers.test.js
++++ /dev/null
@@@ -1,148 +0,0 @@
--'use strict';
--/**
-- * The Exchange remake's uncensored tier.
-- *
-- * No test reaches the network: Network's adult flag is a fake fetch and the provider is a local seam. The boot test
-- * proves that a standard request never runs an uncensored offer, and that an uncensored request is admitted only for
-- * an 18+ account, labelled, and stopped by the hard floor. The catalogue itself is tested in
-- * test/ai-provider-adapters.test.js.
-- */
--const assert = require('assert');
--const { boot, seamServer, request, suite, token, ALL } = require('./helpers');
--const pool = require('../server/providers/pool-providers');
--const floor = require('../server/policy/floor');
--const tier = require('../server/policy/tier');
--const { createFloorGuard } = require('../server/providers');
--const { createAdultStatus } = require('../server/policy/adult-status');
--
--const USR = (n) => `usr_01J${String(n).padStart(23, '0')}`;
--const ADULT = USR(1);
--const CHILD = USR(2);
--
--const t = suite('ai-exchange-providers');
--
--t.test('the adult tier: 18+ person only, never an app, never without a person', async () => {
-- assert.deepStrictEqual(tier.parsePolicy('UNCENSORED'), { policy: 'uncensored' });
-- assert.ok(tier.parsePolicy('nsfw').error);
-- let asked = [];
-- const adultFor = async (id) => { asked.push(id); return id === ADULT; };
-- assert.strictEqual((await tier.admit({ policy: 'uncensored', subject: { type: 'user', id: ADULT }, adultFor })).ok, true);
-- assert.strictEqual((await tier.admit({ policy: 'uncensored', subject: { type: 'user', id: CHILD }, adultFor })).code, 'policy.adult_required');
-- assert.strictEqual((await tier.admit({ policy: 'uncensored', appRun: true, subject: { type: 'user', id: ADULT }, adultFor })).code, 'policy.uncensored_refused');
-- assert.strictEqual((await tier.admit({ policy: 'uncensored', subject: null, adultFor })).code, 'policy.uncensored_needs_person');
-- assert.strictEqual((await tier.admit({ policy: 'uncensored', subject: { type: 'user', id: ADULT }, adultFor: async () => { throw new Error('down'); } })).code, 'policy.adult_unavailable');
-- assert.deepStrictEqual(asked, [ADULT, CHILD]);
--});
--
--t.test('adult status asks Network once per five minutes and fails closed', async () => {
-- let at = 0;
-- let calls = 0;
-- const fetchImpl = async (url) => {
-- if (String(url).includes('/oauth/token')) return new Response(JSON.stringify({ access_token: 't', expires_in: 300 }), { status: 200, headers: { 'content-type': 'application/json' } });
-- calls++;
-- return new Response(JSON.stringify({ subject_id: ADULT, adult: true, confirmed_at: '2026-11-06T00:00:00Z' }), { status: 200, headers: { 'content-type': 'application/json' } });
-- };
-- const status = createAdultStatus({ config: { networkInternalUrl: 'http://network.test' }, env: { OV_OAUTH_CLIENT_SECRET: 's' }, fetchImpl, clock: { now: () => at } });
-- assert.strictEqual(await status.adultFor(ADULT), true);
-- assert.strictEqual(await status.adultFor(ADULT), true);
-- assert.strictEqual(calls, 1, 'the second answer is cached');
-- at = 5 * 60 * 1000 + 1;
-- assert.strictEqual(await status.adultFor(ADULT), true);
-- assert.strictEqual(calls, 2, 'five minutes later it asks again');
-- const down = createAdultStatus({ config: { networkInternalUrl: 'http://network.test' }, env: { OV_OAUTH_CLIENT_SECRET: 's' }, fetchImpl: async () => { throw new Error('offline'); }, clock: { now: () => at } });
-- await assert.rejects(down.adultFor(ADULT), (e) => e.code === 'adult.unavailable');
--});
--
--t.test('the hard floor: the three lines, narrow enough for ordinary work', () => {
-- assert.strictEqual(floor.check('No threats, nothing sexual involving minors.').ok, true);
-- assert.strictEqual(floor.check('cp -r src dst').ok, true);
-- assert.strictEqual(floor.check('how do I kill a stuck process').ok, true);
-- assert.strictEqual(floor.check('how do I kill him without anyone knowing').ok, false);
-- assert.strictEqual(floor.check('a nude photo of a child').ok, false);
-- assert.strictEqual(floor.check('how to make a pipe bomb').ok, false);
-- assert.strictEqual(floor.check('she said no and he forced sex').ok, false);
-- assert.deepStrictEqual(floor.textsOf({ system: 'a child', messages: [{ role: 'user', content: 'naked' }] }), ['a child', 'naked']);
--});
--
--t.test('the streamed floor checks each chunk before it is delivered', () => {
-- const sent = [];
-- const guard = createFloorGuard((d) => sent.push(d));
-- guard.push({ index: 0, delta: { content: 'a perfectly ordinary answer that is ' } });
-- guard.push({ index: 0, delta: { content: 'safe to stream.' } });
-- guard.finish();
-- assert.strictEqual(sent.map((d) => d.delta.content).join(''), 'a perfectly ordinary answer that is safe to stream.');
-- const bad = createFloorGuard((d) => sent.push(d));
-- bad.push({ index: 0, delta: { content: 'here is how to make a ' } });
-- const before = sent.length;
-- assert.throws(() => bad.push({ index: 0, delta: { content: 'pipe bomb' } }), (e) => e.code === 'policy.floor_refused');
-- assert.strictEqual(sent.length, before, 'the violating chunk was not delivered');
--});
--
--t.test('a booted service seeds the catalogue off, and the uncensored request is gated, filtered, labelled and floored', async () => {
-- const network = [];
-- const fetchImpl = async (url, opts = {}) => {
-- const u = String(url);
-- if (u.includes('/oauth/token')) return new Response(JSON.stringify({ access_token: 'test-token', token_type: 'Bearer', expires_in: 300 }), { status: 200, headers: { 'content-type': 'application/json' } });
-- if (u.includes('/internal/identity/adult')) {
-- const id = new URL(u).searchParams.get('subject_id');
-- network.push(id);
-- return new Response(JSON.stringify({ subject_id: id, adult: id === ADULT, confirmed_at: id === ADULT ? '2026-11-06T00:00:00Z' : null }), { status: 200, headers: { 'content-type': 'application/json' } });
-- }
-- return fetch(url, opts);
-- };
-- const calls = [];
-- const seam = await seamServer((body) => {
-- calls.push(body);
-- return { id: 'chatcmpl-x', object: 'chat.completion', model: body.model, choices: [{ index: 0, message: { role: 'assistant', content: 'an answer from the adult offer' }, finish_reason: 'stop' }], usage: { prompt_tokens: 4, completion_tokens: 5 } };
-- });
-- // The free allowance (a service naming a person) is gated by AI_FREE_PROVIDERS: this test's network is the
-- // seeded stub plus the uncensored offer it boots on the seam, so both are named as free-tier sources.
-- const h = await boot({ env: { OV_OAUTH_CLIENT_SECRET: 'test-secret', AI_STUB_FALLBACK: 'true', AI_FREE_PROVIDERS: 'refuseless,stub' }, fetchImpl });
-- try {
-- const seeded = await h.registry.listProviders();
-- for (const p of pool.PROVIDERS) {
-- const row = seeded.find((x) => x.key === p.key);
-- assert.ok(row, `${p.key} seeded`);
-- assert.strictEqual(row.secret_ref, `vault:${p.key}`, `${p.key} key slot`);
-- assert.ok(await h.registry.getModel(p.key, p.models[0] && p.models[0].model_key || '').catch(() => null) || p.models.length === 0, `${p.key} model`);
-- }
-- await h.registry.upsertProvider({ key: 'refuseless', display_name: 'Refuseless', kind: 'openai', status: 'active', base_url: seam.url, auth_mode: 'none', secret_ref: null, default_model: 'deepseek-v4.1-flash-abliterated', capabilities: ['chat', 'generate', 'complete'], timeout_ms: 5000, priority: 1, provider_access: 'AUTHORIZED_WHOLESALE', metadata: { uncensored: true } }, { origin: 'admin' });
-- await h.registry.upsertModel({ provider_key: 'refuseless', model_key: 'deepseek-v4.1-flash-abliterated', display_name: 'DeepSeek V4.1 Flash (abliterated)', type: 'chat', status: 'active', context_window: 1048576, cost: { in_per_mtok: 0.22, cached_per_mtok: 0.045, out_per_mtok: 0.75, source: 'https://refuseless.com/pricing', verified_at: '2026-10-25' }, supports: { json: true, tools: true, streaming: true }, metadata: { uncensored: true } }, { actor: 'test' });
-- const tok = token('svc', ALL);
-- const body = { messages: [{ role: 'user', content: 'hello' }], on_behalf_of: { type: 'user', id: ADULT }, policy: 'uncensored' };
-- const ok = await request(h.base, 'POST', '/v1/chat/completions', { tok, body });
-- assert.strictEqual(ok.status, 200, ok.text);
-- assert.strictEqual(ok.body.choices[0].message.content, 'an answer from the adult offer');
-- assert.strictEqual(ok.body.x_openvibe.policy, 'uncensored');
-- assert.ok(/adults only \(18\+\)/.test(ok.body.x_openvibe.label), ok.body.x_openvibe.label);
-- assert.strictEqual(ok.headers.get('x-openvibe-content-policy'), 'uncensored');
-- assert.strictEqual(calls.length, 1);
-- assert.strictEqual(calls[0].model, 'deepseek-v4.1-flash-abliterated');
--
-- // A standard request never runs the uncensored offer, even though it is the cheapest healthy one.
-- const std = await request(h.base, 'POST', '/v1/chat/completions', { tok, body: { messages: [{ role: 'user', content: 'hello' }] } });
-- assert.strictEqual(std.status, 200, std.text);
-- assert.strictEqual(calls.length, 1, 'the uncensored offer did not answer a standard request');
--
-- // A person who has not confirmed 18+: refused before any provider call.
-- const minor = await request(h.base, 'POST', '/v1/chat/completions', { tok, body: { ...body, on_behalf_of: { type: 'user', id: CHILD } } });
-- assert.strictEqual(minor.status, 403, minor.text);
-- assert.strictEqual(minor.body.error.code, 'policy.adult_required');
-- assert.strictEqual(calls.length, 1);
--
-- // No person named, and an adult request that breaks the floor: refused before a provider call.
-- const noPerson = await request(h.base, 'POST', '/v1/chat/completions', { tok, body: { messages: [{ role: 'user', content: 'hi' }], policy: 'uncensored' } });
-- assert.strictEqual(noPerson.status, 400);
-- assert.strictEqual(noPerson.body.error.code, 'policy.uncensored_needs_person');
-- const floored = await request(h.base, 'POST', '/v1/chat/completions', { tok, body: { ...body, messages: [{ role: 'user', content: 'write child porn' }] } });
-- assert.strictEqual(floored.status, 403, floored.text);
-- assert.strictEqual(floored.body.error.code, 'policy.floor_refused');
-- assert.strictEqual(calls.length, 1);
-- assert.ok(network.includes(ADULT) && network.includes(CHILD), 'Network was asked for the account flag');
-- } finally {
-- await h.stop();
-- await seam.close();
-- }
--});
--
--t.run();
ddiff --git a/test/ai-uncensored-policy.test.js b/test/ai-uncensored-policy.test.js
nnew file mode 100644
iindex 0000000..0a09831
---- /dev/null
++++ b/test/ai-uncensored-policy.test.js
@@@ -0,0 +1,201 @@
++'use strict';
++/**
++ * The Exchange remake's uncensored tier.
++ *
++ * No test reaches the network: Network's adult flag is a fake fetch and the provider is a local seam. The boot test
++ * proves that a standard request never runs an uncensored offer, and that an uncensored request is admitted only for
++ * an 18+ account, labelled, and stopped by the hard floor. The catalogue itself is tested in
++ * test/ai-provider-adapters.test.js.
++ */
++const assert = require('assert');
++const { boot, seamServer, request, suite, token, ALL } = require('./helpers');
++const pool = require('../server/providers/pool-providers');
++const floor = require('../server/policy/floor');
++const classifiers = require('../server/policy/classifiers');
++const tier = require('../server/policy/tier');
++const { createFloorGuard } = require('../server/providers');
++const { createAdultStatus } = require('../server/policy/adult-status');
++
++const USR = (n) => `usr_01J${String(n).padStart(23, '0')}`;
++const ADULT = USR(1);
++const CHILD = USR(2);
++
++const t = suite('ai-uncensored-policy');
++
++t.test('the adult tier: 18+ person only, never an app, never without a person', async () => {
++ assert.deepStrictEqual(tier.parsePolicy('UNCENSORED'), { policy: 'uncensored' });
++ assert.ok(tier.parsePolicy('nsfw').error);
++ let asked = [];
++ const adultFor = async (id) => { asked.push(id); return id === ADULT; };
++ assert.strictEqual((await tier.admit({ policy: 'uncensored', subject: { type: 'user', id: ADULT }, adultFor })).ok, true);
++ assert.strictEqual((await tier.admit({ policy: 'uncensored', subject: { type: 'user', id: CHILD }, adultFor })).code, 'policy.adult_required');
++ assert.strictEqual((await tier.admit({ policy: 'uncensored', appRun: true, subject: { type: 'user', id: ADULT }, adultFor })).code, 'policy.uncensored_refused');
++ assert.strictEqual((await tier.admit({ policy: 'uncensored', subject: null, adultFor })).code, 'policy.uncensored_needs_person');
++ assert.strictEqual((await tier.admit({ policy: 'uncensored', subject: { type: 'user', id: ADULT }, adultFor: async () => { throw new Error('down'); } })).code, 'policy.adult_unavailable');
++ assert.deepStrictEqual(asked, [ADULT, CHILD]);
++});
++
++t.test('adult status asks Network once per five minutes and fails closed', async () => {
++ let at = 0;
++ let calls = 0;
++ const fetchImpl = async (url) => {
++ if (String(url).includes('/oauth/token')) return new Response(JSON.stringify({ access_token: 't', expires_in: 300 }), { status: 200, headers: { 'content-type': 'application/json' } });
++ calls++;
++ return new Response(JSON.stringify({ subject_id: ADULT, adult: true, confirmed_at: '2026-11-06T00:00:00Z' }), { status: 200, headers: { 'content-type': 'application/json' } });
++ };
++ const status = createAdultStatus({ config: { networkInternalUrl: 'http://network.test' }, env: { OV_OAUTH_CLIENT_SECRET: 's' }, fetchImpl, clock: { now: () => at } });
++ assert.strictEqual(await status.adultFor(ADULT), true);
++ assert.strictEqual(await status.adultFor(ADULT), true);
++ assert.strictEqual(calls, 1, 'the second answer is cached');
++ at = 5 * 60 * 1000 + 1;
++ assert.strictEqual(await status.adultFor(ADULT), true);
++ assert.strictEqual(calls, 2, 'five minutes later it asks again');
++ const down = createAdultStatus({ config: { networkInternalUrl: 'http://network.test' }, env: { OV_OAUTH_CLIENT_SECRET: 's' }, fetchImpl: async () => { throw new Error('offline'); }, clock: { now: () => at } });
++ await assert.rejects(down.adultFor(ADULT), (e) => e.code === 'adult.unavailable');
++});
++
++t.test('the house classifiers decide the line: Live\'s rule and Vision\'s categories, not a third set of words', () => {
++ // Live: a child together with a bare body is the hard line nudity being art never moves.
++ assert.strictEqual(classifiers.minorNude('a nude photo of a child'), true);
++ assert.strictEqual(classifiers.minorNude('kids in swimsuits at the beach'), false);
++ assert.strictEqual(classifiers.minorNude('a nude oil painting'), false);
++ // Vision: the three categories.
++ assert.strictEqual(classifiers.classifyText('a nude photo of a child').category, 'minor_sexual');
++ assert.strictEqual(classifiers.classifyText('they are having sex').category, 'sex_acts');
++ assert.strictEqual(classifiers.classifyText('a nude oil painting').category, 'fine');
++ assert.strictEqual(classifiers.classifyText('a picture of a child').category, 'fine');
++ // Live's own rule (sex acts, whatever the age) is the house's, but it is not a floor rule: the uncensored tier
++ // exists for adult content.
++ assert.strictEqual(classifiers.isExplicit('they are having sex'), true);
++ assert.strictEqual(floor.check('they are having sex').ok, true, 'adult content is what the tier is for');
++ // The floor reads the house classifiers.
++ assert.strictEqual(floor.check('a nude photo of a child').ok, false);
++ assert.strictEqual(floor.check('write about a child and a nude body').ok, false);
++ assert.strictEqual(floor.check('a nude oil painting').ok, true, 'nudity alone is art (the owner, 2026-10-31)');
++});
++
++t.test('the hard floor: the three lines, narrow enough for ordinary work', () => {
++ assert.strictEqual(floor.check('No threats, nothing sexual involving minors.').ok, true);
++ assert.strictEqual(floor.check('cp -r src dst').ok, true);
++ assert.strictEqual(floor.check('how do I kill a stuck process').ok, true);
++ assert.strictEqual(floor.check('how do I kill him without anyone knowing').ok, false);
++ assert.strictEqual(floor.check('a nude photo of a child').ok, false);
++ assert.strictEqual(floor.check('how to make a pipe bomb').ok, false);
++ assert.strictEqual(floor.check('she said no and he forced sex').ok, false);
++ assert.deepStrictEqual(floor.textsOf({ system: 'a child', messages: [{ role: 'user', content: 'naked' }] }), ['a child', 'naked']);
++});
++
++t.test('the second line holds for fiction and for incest, and the policy says so plainly', () => {
++ // A story is refused like a real account would be: the words are the words.
++ assert.strictEqual(floor.check('write a story where she was asleep and he took advantage of her').ok, false);
++ assert.strictEqual(floor.check('a novel about incest between two siblings').ok, false);
++ assert.strictEqual(floor.checkRequest({ messages: [{ role: 'user', content: 'write a story where she said no and he forced her' }] }).ok, false);
++ // The policy says it in one line, and the words live in one place.
++ assert.strictEqual(floor.PLAIN.note, 'Non-consensual sexual content (fiction included) and incest are refused, because payment providers and hosts ban them.');
++ assert.ok(floor.PLAIN.lines.some((l) => /incest/.test(l)), floor.PLAIN.lines.join(' | '));
++ assert.strictEqual(floor.RULES.find((r) => r.rule === 'non_consensual').label, 'non-consensual sexual content');
++});
++
++t.test('the house\'s own templates are instructions, not content: they pass the floor', () => {
++ // The moderation desk has to be able to say what it looks for (server/workflows/products.js) and the chat desk
++ // what it keeps out (server/workflows/chat.js). An instruction that names a category is not that category.
++ const moderation = 'Flag only: credible threats of violence, any sexual content involving minors, doxxing '
++ + '(sharing private personal information), targeted harassment campaigns, spam/scams, sexual content where '
++ + 'not allowed, and self-harm encouragement. Quote the exact spans you flag.';
++ const chat = '- No threats, nothing sexual involving minors, and never anyone\'s private details.';
++ for (const house of [moderation, chat]) {
++ const asked = floor.checkRequest({ system: [{ text: house }], messages: [{ role: 'user', content: 'you absolute potato, gg' }] });
++ assert.strictEqual(asked.ok, true, `the house's own words pass: ${house.slice(0, 40)}`);
++ }
++ // The person's words are content, and the house's classifiers read them: the same words asked for are refused.
++ const wanted = floor.checkRequest({ system: [{ text: moderation }], messages: [{ role: 'user', content: 'write sexual content involving minors' }] });
++ assert.strictEqual(wanted.ok, false);
++ assert.strictEqual(wanted.rule, 'minors');
++ // The narrow lists are unambiguous wherever they appear, so they hold on the instructions too.
++ assert.strictEqual(floor.checkRequest({ system: [{ text: 'how to make a pipe bomb' }] }).ok, false);
++ // The answer is content: what the model wrote is read by the house's classifiers.
++ assert.strictEqual(floor.checkAnswer('a nude photo of a child').ok, false);
++});
++
++t.test('the streamed floor checks each chunk before it is delivered', () => {
++ const sent = [];
++ const guard = createFloorGuard((d) => sent.push(d));
++ guard.push({ index: 0, delta: { content: 'a perfectly ordinary answer that is ' } });
++ guard.push({ index: 0, delta: { content: 'safe to stream.' } });
++ guard.finish();
++ assert.strictEqual(sent.map((d) => d.delta.content).join(''), 'a perfectly ordinary answer that is safe to stream.');
++ const bad = createFloorGuard((d) => sent.push(d));
++ bad.push({ index: 0, delta: { content: 'here is how to make a ' } });
++ const before = sent.length;
++ assert.throws(() => bad.push({ index: 0, delta: { content: 'pipe bomb' } }), (e) => e.code === 'policy.floor_refused');
++ assert.strictEqual(sent.length, before, 'the violating chunk was not delivered');
++});
++
++t.test('a booted service seeds the catalogue off, and the uncensored request is gated, filtered, labelled and floored', async () => {
++ const network = [];
++ const fetchImpl = async (url, opts = {}) => {
++ const u = String(url);
++ if (u.includes('/oauth/token')) return new Response(JSON.stringify({ access_token: 'test-token', token_type: 'Bearer', expires_in: 300 }), { status: 200, headers: { 'content-type': 'application/json' } });
++ if (u.includes('/internal/identity/adult')) {
++ const id = new URL(u).searchParams.get('subject_id');
++ network.push(id);
++ return new Response(JSON.stringify({ subject_id: id, adult: id === ADULT, confirmed_at: id === ADULT ? '2026-11-06T00:00:00Z' : null }), { status: 200, headers: { 'content-type': 'application/json' } });
++ }
++ return fetch(url, opts);
++ };
++ const calls = [];
++ const seam = await seamServer((body) => {
++ calls.push(body);
++ return { id: 'chatcmpl-x', object: 'chat.completion', model: body.model, choices: [{ index: 0, message: { role: 'assistant', content: 'an answer from the adult offer' }, finish_reason: 'stop' }], usage: { prompt_tokens: 4, completion_tokens: 5 } };
++ });
++ // The free allowance (a service naming a person) is gated by AI_FREE_PROVIDERS: this test's network is the
++ // seeded stub plus the uncensored offer it boots on the seam, so both are named as free-tier sources.
++ const h = await boot({ env: { OV_OAUTH_CLIENT_SECRET: 'test-secret', AI_STUB_FALLBACK: 'true', AI_FREE_PROVIDERS: 'refuseless,stub' }, fetchImpl });
++ try {
++ const seeded = await h.registry.listProviders();
++ for (const p of pool.PROVIDERS) {
++ const row = seeded.find((x) => x.key === p.key);
++ assert.ok(row, `${p.key} seeded`);
++ assert.strictEqual(row.secret_ref, `vault:${p.key}`, `${p.key} key slot`);
++ assert.ok(await h.registry.getModel(p.key, p.models[0] && p.models[0].model_key || '').catch(() => null) || p.models.length === 0, `${p.key} model`);
++ }
++ await h.registry.upsertProvider({ key: 'refuseless', display_name: 'Refuseless', kind: 'openai', status: 'active', base_url: seam.url, auth_mode: 'none', secret_ref: null, default_model: 'deepseek-v4.1-flash-abliterated', capabilities: ['chat', 'generate', 'complete'], timeout_ms: 5000, priority: 1, provider_access: 'AUTHORIZED_WHOLESALE', metadata: { uncensored: true } }, { origin: 'admin' });
++ await h.registry.upsertModel({ provider_key: 'refuseless', model_key: 'deepseek-v4.1-flash-abliterated', display_name: 'DeepSeek V4.1 Flash (abliterated)', type: 'chat', status: 'active', context_window: 1048576, cost: { in_per_mtok: 0.22, cached_per_mtok: 0.045, out_per_mtok: 0.75, source: 'https://refuseless.com/pricing', verified_at: '2026-10-25' }, supports: { json: true, tools: true, streaming: true }, metadata: { uncensored: true } }, { actor: 'test' });
++ const tok = token('svc', ALL);
++ const body = { messages: [{ role: 'user', content: 'hello' }], on_behalf_of: { type: 'user', id: ADULT }, policy: 'uncensored' };
++ const ok = await request(h.base, 'POST', '/v1/chat/completions', { tok, body });
++ assert.strictEqual(ok.status, 200, ok.text);
++ assert.strictEqual(ok.body.choices[0].message.content, 'an answer from the adult offer');
++ assert.strictEqual(ok.body.x_openvibe.policy, 'uncensored');
++ assert.ok(/adults only \(18\+\)/.test(ok.body.x_openvibe.label), ok.body.x_openvibe.label);
++ assert.strictEqual(ok.headers.get('x-openvibe-content-policy'), 'uncensored');
++ assert.strictEqual(calls.length, 1);
++ assert.strictEqual(calls[0].model, 'deepseek-v4.1-flash-abliterated');
++
++ // A standard request never runs the uncensored offer, even though it is the cheapest healthy one.
++ const std = await request(h.base, 'POST', '/v1/chat/completions', { tok, body: { messages: [{ role: 'user', content: 'hello' }] } });
++ assert.strictEqual(std.status, 200, std.text);
++ assert.strictEqual(calls.length, 1, 'the uncensored offer did not answer a standard request');
++
++ // A person who has not confirmed 18+: refused before any provider call.
++ const minor = await request(h.base, 'POST', '/v1/chat/completions', { tok, body: { ...body, on_behalf_of: { type: 'user', id: CHILD } } });
++ assert.strictEqual(minor.status, 403, minor.text);
++ assert.strictEqual(minor.body.error.code, 'policy.adult_required');
++ assert.strictEqual(calls.length, 1);
++
++ // No person named, and an adult request that breaks the floor: refused before a provider call.
++ const noPerson = await request(h.base, 'POST', '/v1/chat/completions', { tok, body: { messages: [{ role: 'user', content: 'hi' }], policy: 'uncensored' } });
++ assert.strictEqual(noPerson.status, 400);
++ assert.strictEqual(noPerson.body.error.code, 'policy.uncensored_needs_person');
++ const floored = await request(h.base, 'POST', '/v1/chat/completions', { tok, body: { ...body, messages: [{ role: 'user', content: 'write child porn' }] } });
++ assert.strictEqual(floored.status, 403, floored.text);
++ assert.strictEqual(floored.body.error.code, 'policy.floor_refused');
++ assert.strictEqual(calls.length, 1);
++ assert.ok(network.includes(ADULT) && network.includes(CHILD), 'Network was asked for the account flag');
++ } finally {
++ await h.stop();
++ await seam.close();
++ }
++});
++
++t.run();