openvibers/openvibe.exchange Commits
AI text may describe people; only sex acts and porn stay gated (#22)
Changes
@@@ -80,7 +80,7 @@ Environment variable names are in [.env.example](.env.example); no value is comm- **PostgreSQL** — `DATABASE_URL` (through PgBouncer) and `DATABASE_DIRECT_URL` (migrations); embedded PGlite in`EXCHANGE_PGLITE_DIR` for development.- **Valkey** — optional `VALKEY_URL`, `VALKEY_PREFIX` (readiness lists it; the limits in v1 count in-process).--- **Packages** — `openvibe-contracts` v0.229.0, `openvibe-sdk` v0.74.0, `openvibe-shared` v4.93.2, the same tags++- **Packages** — `openvibe-contracts` v0.229.0, `openvibe-sdk` v0.81.0, `openvibe-shared` v4.93.2, the same tagsOpenVibe.Meme pins.## API@@@ -207,6 +207,6 @@ AGPL-3.0-only. See [LICENSE](LICENSE).<!-- versions:start -->- openvibe-contracts: v0.235.0--- openvibe-sdk: v0.74.0++- openvibe-sdk: v0.81.0- openvibe-shared: v4.93.2<!-- versions:end -->@@@ -9,11 +9,11 @@"port": "5150","domain": "openvibe.exchange","contracts": "openvibe-contracts v0.235.0",-- "sdk": "openvibe-sdk v0.74.0",++ "sdk": "openvibe-sdk v0.81.0","shared": "openvibe-shared v4.93.2","packages": ["openvibe-contracts v0.235.0",-- "openvibe-sdk v0.74.0",++ "openvibe-sdk v0.81.0","openvibe-shared v4.93.2"],"summary": "OpenVibe.Exchange is the OpenVibe marketplace for what people make: agents, MCP tools, templates, design themes and kits, game assets and creator services. v1 is free and swap only — there is no price, no Billing and no money — and every listing carries a permission passport (reads, writes, spends, contacts) taken from its public manifest. Submitting a listing runs four conformance tests for its version and a draft is only publishable when they pass. A sandbox trial is a manifest/passport dry run (v1 does not execute a listing's code); a passed trial is consumed by the install, which keeps the exact passport snapshot. A revoke is one tap and idempotent. A free listing takes claims and a swap listing takes offers with what the other person gives; accepting closes it as given or swapped. Every public listing is one Search Fabric document queued in the service's outbox, with a tombstone on close or hide.",@@@ -16,7 +16,7 @@"helmet": "^8.1.0","iovalkey": "^0.4.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.235.0",-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.74.0",++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.81.0","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.93.2","pg": "^8.23.0"},@@@ -1166,9 +1166,9 @@}},"node_modules/openvibe-sdk": {-- "version": "0.74.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.74.0",-- "integrity": "sha512-DO9KVPnUriY/I4lX6ZEJtXaUvRDbJyqnqJxvlpl+zkNGFILpmuBXVJogNOCfK071Qci1gg7C4Iw1Xrseklcajg==",++ "version": "0.81.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.81.0",++ "integrity": "sha512-GWD+qkkKiOyer1SATAaVs8qdWR1Z4WIRvtcYQU0NijzoL9BDRmkaXzLHjwwkRezdkHmdOgtvNIU8GAFaUaS8Ow==","license": "MIT","bin": {"openvibe-download": "bin/openvibe-download.js"@@@ -25,7 +25,7 @@"helmet": "^8.1.0","iovalkey": "^0.4.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.235.0",-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.74.0",++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.81.0","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.93.2","pg": "^8.23.0"},ddeleted file mode 100644@@@ -1,22 +0,0 @@--'use strict';----/**-- * No looks in public AI text (the network rule, openvibe-sdk/no-looks, shared with Live, Community, MediaHub and-- * Meme): an AI-written summary never describes a person's body, clothing, hair, face or guessed gender. Exchange-- * v1 has no AI-written copy — a listing's words are the maker's own — but noLooks is here for the day an AI-- * summary, sandbox description or catalogue line is generated. A summary that names looks is replaced by the-- * listing's title, exactly as Live and Meme do; a maker's own verbatim text is never rewritten by this module.-- */--const { noLooks, hasLooks } = require('openvibe-sdk/no-looks');----/** The summary to store for an AI-written listing summary. */--function cleanSummary(title, summary) {-- const t = String(title || '').trim() || 'Untitled listing';-- const s = String(summary == null ? '' : summary).trim();-- if (!s) return t;-- if (hasLooks(s)) return t;-- const cleaned = String(noLooks(s) || '').trim();-- return cleaned || t;--}----module.exports = { cleanSummary, noLooks, hasLooks };nnew file mode 100644@@@ -0,0 +1,18 @@++'use strict';++// Owner, 2026-10-31: AI may describe people; only sex acts and porn stay gated. No server file may call the old++// looks filter again.++const assert = require('assert');++const fs = require('fs');++const path = require('path');++++const root = path.join(__dirname, '..');++const offenders = [];++(function walk(dir) {++ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {++ const p = path.join(dir, e.name);++ if (e.isDirectory()) walk(p);++ else if (e.name.endsWith('.js') && /\b(noLooks|noLooksTags|hasLooks)\s*\(|openvibe-sdk\/no-looks/.test(fs.readFileSync(p, 'utf8'))) offenders.push(path.relative(root, p));++ }++})(path.join(root, 'server'));++assert.deepStrictEqual(offenders, [], 'no looks filter in server/');++console.log('No looks filter: AI text describes people as written.');ddeleted file mode 100644@@@ -1,44 +0,0 @@--'use strict';--/**-- * No looks in public AI text: a summary that names looks is replaced by the listing's title; an ordinary summary-- * survives. Exchange v1 has no AI-written listing copy — this is the helper used the day a generated summary or-- * sandbox line arrives, shared with Live, Community, MediaHub and Meme.-- */--const assert = require('assert');--const { cleanSummary, hasLooks } = require('../server/text/no-looks');--const { boot, check, done } = require('./helpers/boot');----(async () => {-- await check('a summary that names looks becomes the title', async () => {-- assert.strictEqual(cleanSummary('Starter site template', 'A shirtless man in a kitchen laughs.'), 'Starter site template');-- assert.strictEqual(cleanSummary('Starter site template', 'A pretty girl shows the theme.'), 'Starter site template');-- assert.ok(hasLooks('A shirtless man in a kitchen laughs.'));-- });---- await check('an ordinary summary survives', async () => {-- const s = cleanSummary('Starter site template', 'A static starter site with one page and no tracking.');-- assert.strictEqual(s, 'A static starter site with one page and no tracking.');-- });---- const t = await boot();-- try {-- await check('a creator\'s own listing words are not rewritten by noLooks', async () => {-- const user = t.network.addUser('maker');-- const r = await t.get('/api/v1/listings', {-- as: user,-- json: {-- title: 'A portrait lighting theme', kind: 'theme', mode: 'free',-- summary: 'A warm portrait theme; the maker describes the lighting, not a person.',-- license: 'MIT', version: '1.0.0',-- manifest: { name: 'Portrait lighting', entry: 'theme.json', runtime: 'css' },-- passport: { reads: [], writes: [], spends: [], contacts: [] },-- },-- });-- assert.strictEqual(r.status, 201, r.text);-- assert.strictEqual(r.json().summary, 'A warm portrait theme; the maker describes the lighting, not a person.');-- });-- } finally {-- await t.close();-- }-- done();--})();
@@ -80,7 +80,7 @@ Environment variable names are in [.env.example](.env.example); no value is comm
@@ -80,7 +80,7 @@ Environment variable names are in [.env.example](.env.example); no value is comm
- **PostgreSQL** — `DATABASE_URL` (through PgBouncer) and `DATABASE_DIRECT_URL` (migrations); embedded PGlite in
- **PostgreSQL** — `DATABASE_URL` (through PgBouncer) and `DATABASE_DIRECT_URL` (migrations); embedded PGlite in
`EXCHANGE_PGLITE_DIR` for development.
`EXCHANGE_PGLITE_DIR` for development.
- **Valkey** — optional `VALKEY_URL`, `VALKEY_PREFIX` (readiness lists it; the limits in v1 count in-process).
- **Valkey** — optional `VALKEY_URL`, `VALKEY_PREFIX` (readiness lists it; the limits in v1 count in-process).
-- **Packages** — `openvibe-contracts` v0.229.0, `openvibe-sdk` v0.74.0, `openvibe-shared` v4.93.2, the same tags
+- **Packages** — `openvibe-contracts` v0.229.0, `openvibe-sdk` v0.81.0, `openvibe-shared` v4.93.2, the same tags
OpenVibe.Meme pins.
OpenVibe.Meme pins.
## API
## API
@@ -207,6 +207,6 @@ AGPL-3.0-only. See [LICENSE](LICENSE).
@@ -207,6 +207,6 @@ AGPL-3.0-only. See [LICENSE](LICENSE).
<!-- versions:start -->
<!-- versions:start -->
- openvibe-contracts: v0.235.0
- openvibe-contracts: v0.235.0
-- openvibe-sdk: v0.74.0
+- openvibe-sdk: v0.81.0
- openvibe-shared: v4.93.2
- openvibe-shared: v4.93.2
<!-- versions:end -->
<!-- versions:end -->
@@ -9,11 +9,11 @@
@@ -9,11 +9,11 @@
"port": "5150",
"port": "5150",
"domain": "openvibe.exchange",
"domain": "openvibe.exchange",
"contracts": "openvibe-contracts v0.235.0",
"contracts": "openvibe-contracts v0.235.0",
- "sdk": "openvibe-sdk v0.74.0",
+ "sdk": "openvibe-sdk v0.81.0",
"shared": "openvibe-shared v4.93.2",
"shared": "openvibe-shared v4.93.2",
"packages": [
"packages": [
"openvibe-contracts v0.235.0",
"openvibe-contracts v0.235.0",
- "openvibe-sdk v0.74.0",
+ "openvibe-sdk v0.81.0",
"openvibe-shared v4.93.2"
"openvibe-shared v4.93.2"
],
],
"summary": "OpenVibe.Exchange is the OpenVibe marketplace for what people make: agents, MCP tools, templates, design themes and kits, game assets and creator services. v1 is free and swap only — there is no price, no Billing and no money — and every listing carries a permission passport (reads, writes, spends, contacts) taken from its public manifest. Submitting a listing runs four conformance tests for its version and a draft is only publishable when they pass. A sandbox trial is a manifest/passport dry run (v1 does not execute a listing's code); a passed trial is consumed by the install, which keeps the exact passport snapshot. A revoke is one tap and idempotent. A free listing takes claims and a swap listing takes offers with what the other person gives; accepting closes it as given or swapped. Every public listing is one Search Fabric document queued in the service's outbox, with a tombstone on close or hide.",
"summary": "OpenVibe.Exchange is the OpenVibe marketplace for what people make: agents, MCP tools, templates, design themes and kits, game assets and creator services. v1 is free and swap only — there is no price, no Billing and no money — and every listing carries a permission passport (reads, writes, spends, contacts) taken from its public manifest. Submitting a listing runs four conformance tests for its version and a draft is only publishable when they pass. A sandbox trial is a manifest/passport dry run (v1 does not execute a listing's code); a passed trial is consumed by the install, which keeps the exact passport snapshot. A revoke is one tap and idempotent. A free listing takes claims and a swap listing takes offers with what the other person gives; accepting closes it as given or swapped. Every public listing is one Search Fabric document queued in the service's outbox, with a tombstone on close or hide.",
@@ -16,7 +16,7 @@
@@ -16,7 +16,7 @@
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.235.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.235.0",
- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.74.0",
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.81.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.93.2",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.93.2",
"pg": "^8.23.0"
"pg": "^8.23.0"
},
},
@@ -1166,9 +1166,9 @@
@@ -1166,9 +1166,9 @@
}
}
},
},
"node_modules/openvibe-sdk": {
"node_modules/openvibe-sdk": {
- "version": "0.74.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.74.0",
- "integrity": "sha512-DO9KVPnUriY/I4lX6ZEJtXaUvRDbJyqnqJxvlpl+zkNGFILpmuBXVJogNOCfK071Qci1gg7C4Iw1Xrseklcajg==",
+ "version": "0.81.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.81.0",
+ "integrity": "sha512-GWD+qkkKiOyer1SATAaVs8qdWR1Z4WIRvtcYQU0NijzoL9BDRmkaXzLHjwwkRezdkHmdOgtvNIU8GAFaUaS8Ow==",
"license": "MIT",
"license": "MIT",
"bin": {
"bin": {
"openvibe-download": "bin/openvibe-download.js"
"openvibe-download": "bin/openvibe-download.js"
@@ -25,7 +25,7 @@
@@ -25,7 +25,7 @@
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.235.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.235.0",
- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.74.0",
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.81.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.93.2",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.93.2",
"pg": "^8.23.0"
"pg": "^8.23.0"
},
},
deleted file mode 100644
deleted file mode 100644
@@ -1,22 +0,0 @@
@@ -1,22 +0,0 @@
-'use strict';
-
-/**
- * No looks in public AI text (the network rule, openvibe-sdk/no-looks, shared with Live, Community, MediaHub and
- * Meme): an AI-written summary never describes a person's body, clothing, hair, face or guessed gender. Exchange
- * v1 has no AI-written copy — a listing's words are the maker's own — but noLooks is here for the day an AI
- * summary, sandbox description or catalogue line is generated. A summary that names looks is replaced by the
- * listing's title, exactly as Live and Meme do; a maker's own verbatim text is never rewritten by this module.
- */
-const { noLooks, hasLooks } = require('openvibe-sdk/no-looks');
-
-/** The summary to store for an AI-written listing summary. */
-function cleanSummary(title, summary) {
- const t = String(title || '').trim() || 'Untitled listing';
- const s = String(summary == null ? '' : summary).trim();
- if (!s) return t;
- if (hasLooks(s)) return t;
- const cleaned = String(noLooks(s) || '').trim();
- return cleaned || t;
-}
-
-module.exports = { cleanSummary, noLooks, hasLooks };
new file mode 100644
new file mode 100644
@@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
+'use strict';
+// Owner, 2026-10-31: AI may describe people; only sex acts and porn stay gated. No server file may call the old
+// looks filter again.
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+
+const root = path.join(__dirname, '..');
+const offenders = [];
+(function walk(dir) {
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
+ const p = path.join(dir, e.name);
+ if (e.isDirectory()) walk(p);
+ else if (e.name.endsWith('.js') && /\b(noLooks|noLooksTags|hasLooks)\s*\(|openvibe-sdk\/no-looks/.test(fs.readFileSync(p, 'utf8'))) offenders.push(path.relative(root, p));
+ }
+})(path.join(root, 'server'));
+assert.deepStrictEqual(offenders, [], 'no looks filter in server/');
+console.log('No looks filter: AI text describes people as written.');
deleted file mode 100644
deleted file mode 100644
@@ -1,44 +0,0 @@
@@ -1,44 +0,0 @@
-'use strict';
-/**
- * No looks in public AI text: a summary that names looks is replaced by the listing's title; an ordinary summary
- * survives. Exchange v1 has no AI-written listing copy — this is the helper used the day a generated summary or
- * sandbox line arrives, shared with Live, Community, MediaHub and Meme.
- */
-const assert = require('assert');
-const { cleanSummary, hasLooks } = require('../server/text/no-looks');
-const { boot, check, done } = require('./helpers/boot');
-
-(async () => {
- await check('a summary that names looks becomes the title', async () => {
- assert.strictEqual(cleanSummary('Starter site template', 'A shirtless man in a kitchen laughs.'), 'Starter site template');
- assert.strictEqual(cleanSummary('Starter site template', 'A pretty girl shows the theme.'), 'Starter site template');
- assert.ok(hasLooks('A shirtless man in a kitchen laughs.'));
- });
-
- await check('an ordinary summary survives', async () => {
- const s = cleanSummary('Starter site template', 'A static starter site with one page and no tracking.');
- assert.strictEqual(s, 'A static starter site with one page and no tracking.');
- });
-
- const t = await boot();
- try {
- await check('a creator\'s own listing words are not rewritten by noLooks', async () => {
- const user = t.network.addUser('maker');
- const r = await t.get('/api/v1/listings', {
- as: user,
- json: {
- title: 'A portrait lighting theme', kind: 'theme', mode: 'free',
- summary: 'A warm portrait theme; the maker describes the lighting, not a person.',
- license: 'MIT', version: '1.0.0',
- manifest: { name: 'Portrait lighting', entry: 'theme.json', runtime: 'css' },
- passport: { reads: [], writes: [], spends: [], contacts: [] },
- },
- });
- assert.strictEqual(r.status, 201, r.text);
- assert.strictEqual(r.json().summary, 'A warm portrait theme; the maker describes the lighting, not a person.');
- });
- } finally {
- await t.close();
- }
- done();
-})();