openvibers/openvibe.actor Commits
AI text may describe people; only sex acts and porn stay gated (#215)
Changes
@@@ -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 -->@@@ -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.",@@@ -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"@@@ -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"},@@@ -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>`}`,});}@@@ -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;}@@@ -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: {@@@ -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')@@@ -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 });@@@ -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 () => {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.');
@@ -255,6 +255,6 @@ Part of the [OpenVibe network](https://openvibe.network). Built in the open by [
@@ -255,6 +255,6 @@ Part of the [OpenVibe network](https://openvibe.network). Built in the open by [
<!-- versions:start -->
<!-- versions:start -->
- openvibe-contracts: v0.269.0
- openvibe-contracts: v0.269.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 -->
@@ -8,11 +8,11 @@
@@ -8,11 +8,11 @@
"port": 4950,
"port": 4950,
"domain": "openvibe.actor",
"domain": "openvibe.actor",
"contracts": "openvibe-contracts v0.269.0",
"contracts": "openvibe-contracts v0.269.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.269.0",
"openvibe-contracts v0.269.0",
- "openvibe-sdk v0.74.0",
+ "openvibe-sdk v0.81.0",
"openvibe-shared v4.93.2"
"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.",
"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.",
@@ -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.269.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",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.93.2",
"pg": "^8.23.0"
"pg": "^8.23.0"
},
},
@@ -1162,9 +1162,9 @@
@@ -1162,9 +1162,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"
@@ -29,7 +29,7 @@
@@ -29,7 +29,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.269.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",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.93.2",
"pg": "^8.23.0"
"pg": "^8.23.0"
},
},
@@ -33,7 +33,6 @@ const benchOffers = require('../tasks/bench-offers');
@@ -33,7 +33,6 @@ const benchOffers = require('../tasks/bench-offers');
const compareRuns = require('../tasks/compare');
const compareRuns = require('../tasks/compare');
const confidence = require('../runtime/brain/confidence');
const confidence = require('../runtime/brain/confidence');
const taskEvents = require('../../public/js/task-events');
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 TASK_ID = /^tsk_[0-9A-HJKMNP-TV-Z]{26}$/;
const LOOP_AGENT = 'openvibe-runtime';
const LOOP_AGENT = 'openvibe-runtime';
@@ -937,7 +936,6 @@ ${pageRows.next ? html`<p><a href="/tasks?before=${pageRows.next}">Older tasks</
@@ -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,
// 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.
// files, memory or trace. Only its owner sees it, and only a POST from that page (checked same-origin) offers it.
const BENCH_ERROR = {
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.',
'actor.bench.no_answer': 'This task has no answer to offer.',
};
};
async function benchPage(req, res, { error = null } = {}) {
async function benchPage(req, res, { error = null } = {}) {
@@ -948,9 +946,8 @@ ${pageRows.next ? html`<p><a href="/tasks?before=${pageRows.next}">Older tasks</
@@ -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;
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}`);
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 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, {
return page(req, res, {
title: 'Offer as a Bench case',
title: 'Offer as a Bench case',
crumbs: [{ label: 'Your tasks', href: '/tasks' }, { label: 'Task', href: `/tasks/${row.id}` }, { label: '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') : ''}
@@ -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>
<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>
${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>`
<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>`}`,
});
});
}
}
@@ -15,7 +15,6 @@
@@ -15,7 +15,6 @@
* eight and records that it used them. There is one store; nothing here keeps a second copy.
* 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 KINDS = Object.freeze(['fact', 'preference', 'instruction']);
const STATUSES = Object.freeze(['active', 'suggested', 'archived']);
const STATUSES = Object.freeze(['active', 'suggested', 'archived']);
@@ -102,7 +101,6 @@ function whyRefused(text, { kind = 'fact' } = {}) {
@@ -102,7 +101,6 @@ function whyRefused(text, { kind = 'fact' } = {}) {
if (t.length > MAX_TEXT) return `A memory is at most ${MAX_TEXT} characters.`;
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 (!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 (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.';
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;
return null;
}
}
@@ -26,7 +26,7 @@ function theirs(text, personText) {
@@ -26,7 +26,7 @@ function theirs(text, personText) {
module.exports = {
module.exports = {
name: 'remember',
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: {
input: {
type: 'object',
type: 'object',
properties: {
properties: {
@@ -6,15 +6,12 @@
@@ -6,15 +6,12 @@
* Bench has no case intake yet (checked 2026-10-30: `grep -rn "cases" OpenVibe.Bench/server` finds nothing), so an
* 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
* 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
* 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.
* 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).
* 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 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_TASK = 20000;
const MAX_ANSWER = 200000;
const MAX_ANSWER = 200000;
@@ -34,15 +31,13 @@ const list = (s, owner) => s.db.many(
@@ -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
* 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() }) {
async function offer(s, { taskId, owner, text, answer, now = () => Date.now() }) {
if (!taskId || !owner) return { error: 'actor.bench.no_task' };
if (!taskId || !owner) return { error: 'actor.bench.no_task' };
const raw = String(answer == null ? '' : answer).trim();
const raw = String(answer == null ? '' : answer).trim();
if (!raw) return { error: 'actor.bench.no_answer' };
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(
await s.db.query(
`INSERT INTO actor_bench_offers (task_id, owner, text, answer, offered_at, status)
`INSERT INTO actor_bench_offers (task_id, owner, text, answer, offered_at, status)
VALUES ($1, $2, $3, $4, $5, 'offered')
VALUES ($1, $2, $3, $4, $5, 'offered')
@@ -47,8 +47,8 @@ async function finishedTask(t, user, text) {
@@ -47,8 +47,8 @@ async function finishedTask(t, user, text) {
assert.strictEqual(page.status, 200, page.text);
assert.strictEqual(page.status, 200, page.text);
assert.ok(page.text.includes(text), 'the task, as asked');
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('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(EVIDENCE), 'no evidence');
assert.ok(!page.text.includes(ARTIFACT), 'no file');
assert.ok(!page.text.includes(ARTIFACT), 'no file');
assert.ok(!page.text.includes('sentinel-model'), 'no trace');
assert.ok(!page.text.includes('sentinel-model'), 'no trace');
@@ -71,8 +71,8 @@ async function finishedTask(t, user, text) {
@@ -71,8 +71,8 @@ async function finishedTask(t, user, text) {
assert.ok(offer, 'the offer is stored');
assert.ok(offer, 'the offer is stored');
assert.strictEqual(offer.owner, `user:${kim.subject}`);
assert.strictEqual(offer.owner, `user:${kim.subject}`);
assert.strictEqual(offer.text, text);
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.strictEqual(offer.status, 'offered');
assert.ok(Date.parse(offer.offered_at));
assert.ok(Date.parse(offer.offered_at));
const page = await t.get(`/tasks/${id}`, { as: kim });
const page = await t.get(`/tasks/${id}`, { as: kim });
@@ -36,9 +36,9 @@ const ownerA = `user:${A}`;
@@ -36,9 +36,9 @@ const ownerA = `user:${A}`;
assert.ok(!memory.looksLikeSecret('I was born in 1990 and live at 12 Baker Street.'));
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 () => {
await check('an instruction memory may narrow, never widen a permission or a budget', async () => {
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.');