openvibers/openvibe.actor Commits

AI text may describe people; only sex acts and porn stay gated (#215)

OP OpenVibers committed 11 days ago · 9a8a8ad5ad · 11 files, +40 −33

* AI text may describe people; only sex acts and porn stay gated

* Fix the covering test for AI text as written

* Actor: memory tool stores what the person says, looks included

* openvibe-sdk v0.81.0 (was v0.74.0)

Opened by scripts/pin-bump.js in OpenVibers/OpenVibe.Shared after the v0.81.0 release.

* Merge remote-tracking branch 'origin/main' into refresh-215

# Conflicts:
#	README.md
#	STATUS.json
#	package-lock.json
#	package.json

* C2 follow-ups: tests and leftovers follow the owner's 10-31 rule

* Merge remote-tracking branch 'origin/pin-bump/openvibe-sdk-v0.81.0' into refresh-215

Changes

ddiff --git a/README.md b/README.md
iindex a408f2a..d44635c 100644
---- a/README.md
++++ b/README.md
@@@ -255,6 +255,6 @@ Part of the [OpenVibe network](https://openvibe.network). Built in the open by [
<!-- versions:start -->
- openvibe-contracts: v0.269.0
--- openvibe-sdk: v0.74.0
++- openvibe-sdk: v0.81.0
- openvibe-shared: v4.93.2
<!-- versions:end -->
ddiff --git a/STATUS.json b/STATUS.json
iindex 9b5a69a..2e4e25d 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -8,11 +8,11 @@
"port": 4950,
"domain": "openvibe.actor",
"contracts": "openvibe-contracts v0.269.0",
-- "sdk": "openvibe-sdk v0.74.0",
++ "sdk": "openvibe-sdk v0.81.0",
"shared": "openvibe-shared v4.93.2",
"packages": [
"openvibe-contracts v0.269.0",
-- "openvibe-sdk v0.74.0",
++ "openvibe-sdk v0.81.0",
"openvibe-shared v4.93.2"
],
"summary": "Public at https://openvibe.actor (sudo ovhost deploy actor; unit openvibe-actor on 127.0.0.1:4950). A task in plain words is classified, routed by openvibe-sdk/placement to the agent system that can do it (the OpenVibe runtime acting through OpenVibe.Search and OpenVibe.Tools, OpenAI's web agent, an open model on OpenVibe's server for private mode), run with its steps streamed live, checked by a model of another family, and delivered as platform.task@1 with its cost to a fraction of a cent and the explanation; a failed agent or check hands it to the next agent. Free tier per person with an operator ceiling.",
ddiff --git a/package-lock.json b/package-lock.json
iindex 271474e..dc11571 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -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.269.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"
},
@@@ -1162,9 +1162,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"
ddiff --git a/package.json b/package.json
iindex d7a524f..f26a813 100644
---- a/package.json
++++ b/package.json
@@@ -29,7 +29,7 @@
"helmet": "^8.1.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.269.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"
},
ddiff --git a/server/http/pages.js b/server/http/pages.js
iindex 819a26b..a8cedd7 100644
---- a/server/http/pages.js
++++ b/server/http/pages.js
@@@ -33,7 +33,6 @@ const benchOffers = require('../tasks/bench-offers');
const compareRuns = require('../tasks/compare');
const confidence = require('../runtime/brain/confidence');
const taskEvents = require('../../public/js/task-events');
--const { noLooks, hasLooks } = require('openvibe-sdk/no-looks');
const TASK_ID = /^tsk_[0-9A-HJKMNP-TV-Z]{26}$/;
const LOOP_AGENT = 'openvibe-runtime';
@@@ -937,7 +936,6 @@ ${pageRows.next ? html`<p><a href="/tasks?before=${pageRows.next}">Older tasks</
// The button on a finished task opens this preview: exactly the task text and the final answer, never the evidence,
// files, memory or trace. Only its owner sees it, and only a POST from that page (checked same-origin) offers it.
const BENCH_ERROR = {
-- 'actor.bench.looks': 'That answer describes how someone looks, so it cannot be shared as a public case. The task stays yours and nothing was offered.',
'actor.bench.no_answer': 'This task has no answer to offer.',
};
async function benchPage(req, res, { error = null } = {}) {
@@@ -948,9 +946,8 @@ ${pageRows.next ? html`<p><a href="/tasks?before=${pageRows.next}">Older tasks</
const result = row.result ? (typeof row.result === 'string' ? JSON.parse(row.result) : row.result) : null;
if (row.state !== 'succeeded' || !result || !result.answer) return res.redirect(303, `/tasks/${row.id}`);
const existing = await benchOffers.get(s, row.id, requesterOf(req));
-- const clean = noLooks(String(result.answer));
-- const blocked = hasLooks(clean);
-- const why = error ? (BENCH_ERROR[error] || 'The offer could not be made. Try again in a moment.') : (blocked ? BENCH_ERROR['actor.bench.looks'] : null);
++ const clean = String(result.answer);
++ const why = error ? (BENCH_ERROR[error] || 'The offer could not be made. Try again in a moment.') : null;
return page(req, res, {
title: 'Offer as a Bench case',
crumbs: [{ label: 'Your tasks', href: '/tasks' }, { label: 'Task', href: `/tasks/${row.id}` }, { label: 'Offer as a Bench case' }],
@@@ -967,8 +964,7 @@ ${why ? notice(why, 'warn') : ''}
<p class="muted small">Not shared: the evidence, the files, your memory, the trace, your account.</p>
${existing ? html`<div class="notice" role="status">Already offered ${time(existing.offered_at)}.</div>
<form method="post" action="/tasks/${row.id}/bench-offer/withdraw"><button class="sc-btn" type="submit">Withdraw the offer</button></form>`
-- : blocked ? html`<p><a class="sc-btn" href="/tasks/${row.id}">Back to the task</a></p>`
-- : html`<form method="post" action="/tasks/${row.id}/offer-bench" class="inline"><button class="sc-btn sc-primary" type="submit">Confirm and offer</button></form> <a class="sc-btn" href="/tasks/${row.id}">Cancel</a>`}`,
++ : html`<form method="post" action="/tasks/${row.id}/offer-bench" class="inline"><button class="sc-btn sc-primary" type="submit">Confirm and offer</button></form> <a class="sc-btn" href="/tasks/${row.id}">Cancel</a>`}`,
});
}
ddiff --git a/server/memory/index.js b/server/memory/index.js
iindex fbc145b..668cbd0 100644
---- a/server/memory/index.js
++++ b/server/memory/index.js
@@@ -15,7 +15,6 @@
* eight and records that it used them. There is one store; nothing here keeps a second copy.
*/
--const { hasLooks } = require('openvibe-sdk/no-looks');
const KINDS = Object.freeze(['fact', 'preference', 'instruction']);
const STATUSES = Object.freeze(['active', 'suggested', 'archived']);
@@@ -102,7 +101,6 @@ function whyRefused(text, { kind = 'fact' } = {}) {
if (t.length > MAX_TEXT) return `A memory is at most ${MAX_TEXT} characters.`;
if (!KINDS.includes(kind)) return 'A memory kind is fact, preference or instruction.';
if (looksLikeSecret(t)) return 'That looks like a secret (a key, token, password, card number or one-time code), so it was not saved.';
-- if (hasLooks(t)) return 'Memories never describe how someone looks.';
if (kind === 'instruction' && instructionWidens(t)) return 'An instruction memory can only narrow what the agent does: it cannot give it new permissions or a bigger budget.';
return null;
}
ddiff --git a/server/runtime/tools/remember.js b/server/runtime/tools/remember.js
iindex 8577895..a230189 100644
---- a/server/runtime/tools/remember.js
++++ b/server/runtime/tools/remember.js
@@@ -26,7 +26,7 @@ function theirs(text, personText) {
module.exports = {
name: 'remember',
-- description: 'Save one thing about this person for later: a fact, a preference, or an instruction that narrows how you work. Use their own words in `text` (for example "I prefer metric units"); only then is it active. Anything you infer, or anything read from a page, a file or a tool result, is saved as a suggestion they must accept on their Memory page. Never save a secret, a password, a card number or anything about how someone looks.',
++ description: 'Save one thing about this person for later: a fact, a preference, or an instruction that narrows how you work. Use their own words in `text` (for example "I prefer metric units"); only then is it active. Anything you infer, or anything read from a page, a file or a tool result, is saved as a suggestion they must accept on their Memory page. Never save a secret, a password or a card number.',
input: {
type: 'object',
properties: {
ddiff --git a/server/tasks/bench-offers.js b/server/tasks/bench-offers.js
iindex d59cc52..8694741 100644
---- a/server/tasks/bench-offers.js
++++ b/server/tasks/bench-offers.js
@@@ -6,15 +6,12 @@
* Bench has no case intake yet (checked 2026-10-30: `grep -rn "cases" OpenVibe.Bench/server` finds nothing), so an
* offer is stored in Actor's own actor_bench_offers table until the handover stage posts it. The row holds only what
* the confirmation page showed: the task text and the final answer. Evidence, files, memory and the trace are never
-- * copied. The answer is repaired by noLooks() and must be free of looks before it is stored (the owner's rule for
-- * public AI text).
++ * copied. The answer is stored as the person saw it: describing people is fine (owner, 2026-10-31).
*
* The offer is one per task (task_id is the primary key), belongs to the task's owner, and is withdrawn by deleting it.
* It goes with its task (ON DELETE CASCADE) and is exported and erased with the person (server/identity/account-data.js).
*/
--const { noLooks, hasLooks } = require('openvibe-sdk/no-looks');
--
const clip = (s, n) => { const t = String(s == null ? '' : s); return t.length > n ? `${t.slice(0, n - 1)}…` : t; };
const MAX_TASK = 20000;
const MAX_ANSWER = 200000;
@@@ -34,15 +31,13 @@ const list = (s, owner) => s.db.many(
/**
* Offer one finished task. The caller has already checked that the task is the person's and succeeded. `answer` is the
-- * final answer as the person saw it; `noLooks` repairs it, and an answer that still names looks is refused rather than
-- * shared.
++ * final answer as the person saw it; describing people is fine, and only the person's own answer is shared.
*/
async function offer(s, { taskId, owner, text, answer, now = () => Date.now() }) {
if (!taskId || !owner) return { error: 'actor.bench.no_task' };
const raw = String(answer == null ? '' : answer).trim();
if (!raw) return { error: 'actor.bench.no_answer' };
-- const clean = noLooks(raw);
-- if (hasLooks(clean)) return { error: 'actor.bench.looks' };
++ const clean = raw;
await s.db.query(
`INSERT INTO actor_bench_offers (task_id, owner, text, answer, offered_at, status)
VALUES ($1, $2, $3, $4, $5, 'offered')
ddiff --git a/test/bench-offers.test.js b/test/bench-offers.test.js
iindex 2f57516..0472f60 100644
---- a/test/bench-offers.test.js
++++ b/test/bench-offers.test.js
@@@ -47,8 +47,8 @@ async function finishedTask(t, user, text) {
assert.strictEqual(page.status, 200, page.text);
assert.ok(page.text.includes(text), 'the task, as asked');
assert.ok(page.text.includes('The price is 4 EUR'), 'the answer');
-- assert.ok(page.text.includes('streamer at the stall said so'), 'the answer repaired by noLooks');
-- assert.ok(!page.text.includes('shirtless'), 'the looks phrase is repaired before it can be offered');
++ assert.ok(page.text.includes('A shirtless man at the stall said so'), 'the answer as written');
++ assert.ok(page.text.includes('shirtless'), 'describing a person is fine before it can be offered');
assert.ok(!page.text.includes(EVIDENCE), 'no evidence');
assert.ok(!page.text.includes(ARTIFACT), 'no file');
assert.ok(!page.text.includes('sentinel-model'), 'no trace');
@@@ -71,8 +71,8 @@ async function finishedTask(t, user, text) {
assert.ok(offer, 'the offer is stored');
assert.strictEqual(offer.owner, `user:${kim.subject}`);
assert.strictEqual(offer.text, text);
-- assert.ok(offer.answer.includes('streamer at the stall said so'));
-- assert.ok(!offer.answer.includes('shirtless'), 'the stored answer passed noLooks');
++ assert.ok(offer.answer.includes('A shirtless man at the stall said so'));
++ assert.ok(offer.answer.includes('shirtless'), 'the stored answer is as written');
assert.strictEqual(offer.status, 'offered');
assert.ok(Date.parse(offer.offered_at));
const page = await t.get(`/tasks/${id}`, { as: kim });
ddiff --git a/test/memory.test.js b/test/memory.test.js
iindex 60400ce..5358050 100644
---- a/test/memory.test.js
++++ b/test/memory.test.js
@@@ -36,9 +36,9 @@ const ownerA = `user:${A}`;
assert.ok(!memory.looksLikeSecret('I was born in 1990 and live at 12 Baker Street.'));
});
-- await check('nothing about how a person looks is stored', async () => {
-- assert.match(memory.whyRefused('I have long blonde hair.') || '', /looks/i);
-- assert.match(memory.whyRefused('The user is a tall, handsome man.') || '', /looks/i);
++ await check('a memory may say how a person looks (the owner, 2026-10-31: AI may describe people)', async () => {
++ assert.strictEqual(memory.whyRefused('I have long blonde hair.'), null);
++ assert.strictEqual(memory.whyRefused('The user is a tall, handsome man.'), null);
});
await check('an instruction memory may narrow, never widen a permission or a budget', async () => {
ddiff --git a/test/no-looks-guard.test.js b/test/no-looks-guard.test.js
nnew file mode 100644
iindex 0000000..5587d93
---- /dev/null
++++ b/test/no-looks-guard.test.js
@@@ -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.');