openvibers/openvibe.ai Commits
AI: the uncensored policy on the house's own classifiers (#318)
Changes
@@@ -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 thecatalogue. 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.nnew file mode 100644@@@ -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.nnew file mode 100644@@@ -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,++};@@@ -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,++};ddeleted file mode 100644@@@ -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();nnew file mode 100644@@@ -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();
@@ -100,4 +100,4 @@ catalogued and its key stored, but the router does not resell it.
@@ -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
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
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.
new file mode 100644
new file mode 100644
@@ -0,0 +1,79 @@
@@ -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.
new file mode 100644
new file mode 100644
@@ -0,0 +1,85 @@
@@ -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,
+};
@@ -3,29 +3,43 @@
@@ -3,29 +3,43 @@
* The hard floor (the Exchange remake, "Uncensored"): the three lines that hold on every offer, uncensored or not.
* The hard floor (the Exchange remake, "Uncensored"): the three lines that hold on every offer, uncensored or not.
*
*
* 1. no sexual content involving minors;
* 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.
* 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
* 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
* 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.
* 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;
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;
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. */
/** 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;
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
@@ -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. */
/** 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;
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 = [
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 (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 }.
* 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.
* 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());
const list = (Array.isArray(texts) ? texts : [texts]).filter((t) => t != null && String(t).trim());
for (const t of list) {
for (const t of list) {
const text = String(t);
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 };
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. */
/** The text of an answer, whatever shape the operation returned it in. */
function textsOfAnswer(result = {}) {
function textsOfAnswer(result = {}) {
@@ -108,10 +182,13 @@ function textsOfAnswer(result = {}) {
@@ -108,10 +182,13 @@ function textsOfAnswer(result = {}) {
return out.filter(Boolean);
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]); }
function checkAnswer(text) { return check(Array.isArray(text) ? text : [text]); }
/** The floor on the way out, over every shape an operation's answer uses. */
/** The floor on the way out, over every shape an operation's answer uses. */
function checkResult(result) { return check(textsOfAnswer(result)); }
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,
+};
deleted file mode 100644
deleted file mode 100644
@@ -1,148 +0,0 @@
@@ -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();
new file mode 100644
new file mode 100644
@@ -0,0 +1,201 @@
@@ -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();