openvibers/openvibe.chat Commits
Gate Vibe task delegation behind owner switch (#462)
Changes
@@@ -236,6 +236,8 @@ module.exports = {// Vibe, OpenVibe's assistant (server/rooms/vibe.js): on unless CHAT_VIBE=0 (it also needs AI members on); a person// gets this many answers a UTC day across all their Vibe chats.vibe: process.env.CHAT_VIBE !== '0',++ // C3's explicit "do it for me" task modes (Actor-backed Research, Agent and Code); site owner opt-in.++ vibeDoIt: bool(process.env.CHAT_VIBE_DO_IT_ENABLED, false),// C3 web search in Vibe's composer (the existing Search Fabric tool and cited results); owner enables it explicitly.vibeWebSearch: bool(process.env.CHAT_VIBE_WEB_SEARCH_ENABLED, false),vibeRepliesPerDay: Math.max(1, int(process.env.CHAT_VIBE_REPLIES_PER_DAY, 100)),@@@ -139,6 +139,7 @@ const ON_IT = {};async function work(room, user, text, mode) {++ if (!config.ai.vibeDoIt) fail(503, 'vibe.work_disabled', 'Do it for me is not enabled on this Chat server');const kind = MODES[String(mode || '')];if (!kind) fail(422, 'vibe.mode', 'Pick Chat, Research, Agent or Code');if (!isVibeRoom(room) || !user || Number(room.owner_id) !== Number(user.id)) fail(404, 'rooms.not_found', 'No such chat');@@@ -206,7 +206,8 @@ function composer({ slug, signedIn, enabled, prefill = '', engine = null, models? `<button class="ovt-btn vb-attach" type="button" data-vb-attach aria-label="${label}" title="${label}">${ICON.attach}</button><dialog class="ovk-sheet vb-upload" data-vb-upload-dialog aria-labelledby="vb-upload-h"><div class="ovk-sheet-head" data-ov-sheet-head><h2 id="vb-upload-h">${label}</h2><button type="button" class="ovk-icon-btn" data-vb-upload-close aria-label="Close">${thread.icon('close')}</button></div><div class="ovk-sheet-body"><div data-vb-upload data-accept=""></div></div></dialog>`: '';-- const modes = `<fieldset class="vb-modes" aria-label="What Vibe does with it">${MODES.map(([value, label, hint, placeholder], i) => `<label title="${esc(hint)}"><input type="radio" name="mode" value="${value}" data-placeholder="${esc(placeholder)}"${i ? '' : ' checked'}><span>${label}</span></label>`).join('')}</fieldset>`;++ const offeredModes = require('../config').ai.vibeDoIt ? MODES : MODES.slice(0, 1);++ const modes = `<fieldset class="vb-modes" aria-label="What Vibe does with it">${offeredModes.map(([value, label, hint, placeholder], i) => `<label title="${esc(hint)}"><input type="radio" name="mode" value="${value}" data-placeholder="${esc(placeholder)}"${i ? '' : ' checked'}><span>${label}</span></label>`).join('')}</fieldset>`;const fine = !enabled ? 'Vibe is resting right now. Try again later.' : engine && engine.own ? `Vibe thinks with ${esc(engine.you)} (<a href="/settings#oc-ai-key">change</a>). It can get things wrong: check what matters.` : 'Vibe can get things wrong. Check what matters.';let html = thread.composer({action, method: signedIn ? 'post' : 'get', name: 'message', id: 'vb-composer', attrs: signedIn ? {} : { 'data-signin': '1' },@@@ -17,6 +17,7 @@ const pageJson = (html) => JSON.parse((/window\.__OV_PAGE = (\{.*?\});\n/.exec(ht('boot: a Vibe chat whose newest task is Actor\'s', async () => {h = await boot();++ require('../server/config').ai.vibeDoIt = true; // This fixture explicitly enables Actor-backed tasks.workspace = require('../server/rooms/workspace');workspace._setTokens({ getToken: async () => 'chat-actor-token' });ann = h.addUser('mira', { subject: ids.newId('user') });@@@ -12,7 +12,7 @@ const { boot, suite } = require('./helpers');const t = suite('vibe modes');const SITE = 'https://openvibe.chat';--let h, ann, bob, workspace;++let h, ann, bob, workspace, config;const turns = [];const actorCalls = [];let actorTask = { id: 'tsk_r1', state: 'queued' };@@@ -22,6 +22,8 @@ const realFetch = globalThis.fetch;t('boot', async () => {h = await boot({ env: { CHAT_WEB_URL: SITE } });++ config = require('../server/config');++ config.ai.vibeDoIt = false;workspace = require('../server/rooms/workspace');workspace._setTokens({ getToken: async () => 'chat-actor-token' });require('../server/ai/client')._setClient({@@@ -40,6 +42,9 @@ t('boot', async () => {});t('Research in a new chat: a task on Actor as the person, Vibe says it is on it, no instant answer', async () => {++ const disabled = await vibeApi('POST', '', ann, { message: 'Research this first', mode: 'research' });++ assert.strictEqual(disabled.status, 503, 'the owner switch is off by default');++ config.ai.vibeDoIt = true;const r = await vibeApi('POST', '', ann, { message: 'Compare the three cheapest cloud browsers for agents', mode: 'research' });assert.strictEqual(r.status, 201, r.text);const slug = r.body.room.slug;@@@ -151,6 +156,7 @@ t('refusals: an unknown mode, someone else\'s chat, no sign-in', async () => {});t('the composer shows the four modes, Chat chosen, each saying what it does', async () => {++ config.ai.vibeDoIt = true;const page = await h.http('GET', '/vibe', { headers: { cookie: `ov_token=${ann.token}` } });assert.match(page.text, /<fieldset class="vb-modes" aria-label="What Vibe does with it">/);assert.match(page.text, /value="chat" data-placeholder="Message Vibe" checked><span>Chat<\/span>/);
@@ -236,6 +236,8 @@ module.exports = {
@@ -236,6 +236,8 @@ module.exports = {
// Vibe, OpenVibe's assistant (server/rooms/vibe.js): on unless CHAT_VIBE=0 (it also needs AI members on); a person
// Vibe, OpenVibe's assistant (server/rooms/vibe.js): on unless CHAT_VIBE=0 (it also needs AI members on); a person
// gets this many answers a UTC day across all their Vibe chats.
// gets this many answers a UTC day across all their Vibe chats.
vibe: process.env.CHAT_VIBE !== '0',
vibe: process.env.CHAT_VIBE !== '0',
+ // C3's explicit "do it for me" task modes (Actor-backed Research, Agent and Code); site owner opt-in.
+ vibeDoIt: bool(process.env.CHAT_VIBE_DO_IT_ENABLED, false),
// C3 web search in Vibe's composer (the existing Search Fabric tool and cited results); owner enables it explicitly.
// C3 web search in Vibe's composer (the existing Search Fabric tool and cited results); owner enables it explicitly.
vibeWebSearch: bool(process.env.CHAT_VIBE_WEB_SEARCH_ENABLED, false),
vibeWebSearch: bool(process.env.CHAT_VIBE_WEB_SEARCH_ENABLED, false),
vibeRepliesPerDay: Math.max(1, int(process.env.CHAT_VIBE_REPLIES_PER_DAY, 100)),
vibeRepliesPerDay: Math.max(1, int(process.env.CHAT_VIBE_REPLIES_PER_DAY, 100)),
@@ -139,6 +139,7 @@ const ON_IT = {
@@ -139,6 +139,7 @@ const ON_IT = {
};
};
async function work(room, user, text, mode) {
async function work(room, user, text, mode) {
+ if (!config.ai.vibeDoIt) fail(503, 'vibe.work_disabled', 'Do it for me is not enabled on this Chat server');
const kind = MODES[String(mode || '')];
const kind = MODES[String(mode || '')];
if (!kind) fail(422, 'vibe.mode', 'Pick Chat, Research, Agent or Code');
if (!kind) fail(422, 'vibe.mode', 'Pick Chat, Research, Agent or Code');
if (!isVibeRoom(room) || !user || Number(room.owner_id) !== Number(user.id)) fail(404, 'rooms.not_found', 'No such chat');
if (!isVibeRoom(room) || !user || Number(room.owner_id) !== Number(user.id)) fail(404, 'rooms.not_found', 'No such chat');
@@ -206,7 +206,8 @@ function composer({ slug, signedIn, enabled, prefill = '', engine = null, models
@@ -206,7 +206,8 @@ function composer({ slug, signedIn, enabled, prefill = '', engine = null, models
? `<button class="ovt-btn vb-attach" type="button" data-vb-attach aria-label="${label}" title="${label}">${ICON.attach}</button>
? `<button class="ovt-btn vb-attach" type="button" data-vb-attach aria-label="${label}" title="${label}">${ICON.attach}</button>
<dialog class="ovk-sheet vb-upload" data-vb-upload-dialog aria-labelledby="vb-upload-h"><div class="ovk-sheet-head" data-ov-sheet-head><h2 id="vb-upload-h">${label}</h2><button type="button" class="ovk-icon-btn" data-vb-upload-close aria-label="Close">${thread.icon('close')}</button></div><div class="ovk-sheet-body"><div data-vb-upload data-accept=""></div></div></dialog>`
<dialog class="ovk-sheet vb-upload" data-vb-upload-dialog aria-labelledby="vb-upload-h"><div class="ovk-sheet-head" data-ov-sheet-head><h2 id="vb-upload-h">${label}</h2><button type="button" class="ovk-icon-btn" data-vb-upload-close aria-label="Close">${thread.icon('close')}</button></div><div class="ovk-sheet-body"><div data-vb-upload data-accept=""></div></div></dialog>`
: '';
: '';
- const modes = `<fieldset class="vb-modes" aria-label="What Vibe does with it">${MODES.map(([value, label, hint, placeholder], i) => `<label title="${esc(hint)}"><input type="radio" name="mode" value="${value}" data-placeholder="${esc(placeholder)}"${i ? '' : ' checked'}><span>${label}</span></label>`).join('')}</fieldset>`;
+ const offeredModes = require('../config').ai.vibeDoIt ? MODES : MODES.slice(0, 1);
+ const modes = `<fieldset class="vb-modes" aria-label="What Vibe does with it">${offeredModes.map(([value, label, hint, placeholder], i) => `<label title="${esc(hint)}"><input type="radio" name="mode" value="${value}" data-placeholder="${esc(placeholder)}"${i ? '' : ' checked'}><span>${label}</span></label>`).join('')}</fieldset>`;
const fine = !enabled ? 'Vibe is resting right now. Try again later.' : engine && engine.own ? `Vibe thinks with ${esc(engine.you)} (<a href="/settings#oc-ai-key">change</a>). It can get things wrong: check what matters.` : 'Vibe can get things wrong. Check what matters.';
const fine = !enabled ? 'Vibe is resting right now. Try again later.' : engine && engine.own ? `Vibe thinks with ${esc(engine.you)} (<a href="/settings#oc-ai-key">change</a>). It can get things wrong: check what matters.` : 'Vibe can get things wrong. Check what matters.';
let html = thread.composer({
let html = thread.composer({
action, method: signedIn ? 'post' : 'get', name: 'message', id: 'vb-composer', attrs: signedIn ? {} : { 'data-signin': '1' },
action, method: signedIn ? 'post' : 'get', name: 'message', id: 'vb-composer', attrs: signedIn ? {} : { 'data-signin': '1' },
@@ -17,6 +17,7 @@ const pageJson = (html) => JSON.parse((/window\.__OV_PAGE = (\{.*?\});\n/.exec(h
@@ -17,6 +17,7 @@ const pageJson = (html) => JSON.parse((/window\.__OV_PAGE = (\{.*?\});\n/.exec(h
t('boot: a Vibe chat whose newest task is Actor\'s', async () => {
t('boot: a Vibe chat whose newest task is Actor\'s', async () => {
h = await boot();
h = await boot();
+ require('../server/config').ai.vibeDoIt = true; // This fixture explicitly enables Actor-backed tasks.
workspace = require('../server/rooms/workspace');
workspace = require('../server/rooms/workspace');
workspace._setTokens({ getToken: async () => 'chat-actor-token' });
workspace._setTokens({ getToken: async () => 'chat-actor-token' });
ann = h.addUser('mira', { subject: ids.newId('user') });
ann = h.addUser('mira', { subject: ids.newId('user') });
@@ -12,7 +12,7 @@ const { boot, suite } = require('./helpers');
@@ -12,7 +12,7 @@ const { boot, suite } = require('./helpers');
const t = suite('vibe modes');
const t = suite('vibe modes');
const SITE = 'https://openvibe.chat';
const SITE = 'https://openvibe.chat';
-let h, ann, bob, workspace;
+let h, ann, bob, workspace, config;
const turns = [];
const turns = [];
const actorCalls = [];
const actorCalls = [];
let actorTask = { id: 'tsk_r1', state: 'queued' };
let actorTask = { id: 'tsk_r1', state: 'queued' };
@@ -22,6 +22,8 @@ const realFetch = globalThis.fetch;
@@ -22,6 +22,8 @@ const realFetch = globalThis.fetch;
t('boot', async () => {
t('boot', async () => {
h = await boot({ env: { CHAT_WEB_URL: SITE } });
h = await boot({ env: { CHAT_WEB_URL: SITE } });
+ config = require('../server/config');
+ config.ai.vibeDoIt = false;
workspace = require('../server/rooms/workspace');
workspace = require('../server/rooms/workspace');
workspace._setTokens({ getToken: async () => 'chat-actor-token' });
workspace._setTokens({ getToken: async () => 'chat-actor-token' });
require('../server/ai/client')._setClient({
require('../server/ai/client')._setClient({
@@ -40,6 +42,9 @@ t('boot', async () => {
@@ -40,6 +42,9 @@ t('boot', async () => {
});
});
t('Research in a new chat: a task on Actor as the person, Vibe says it is on it, no instant answer', async () => {
t('Research in a new chat: a task on Actor as the person, Vibe says it is on it, no instant answer', async () => {
+ const disabled = await vibeApi('POST', '', ann, { message: 'Research this first', mode: 'research' });
+ assert.strictEqual(disabled.status, 503, 'the owner switch is off by default');
+ config.ai.vibeDoIt = true;
const r = await vibeApi('POST', '', ann, { message: 'Compare the three cheapest cloud browsers for agents', mode: 'research' });
const r = await vibeApi('POST', '', ann, { message: 'Compare the three cheapest cloud browsers for agents', mode: 'research' });
assert.strictEqual(r.status, 201, r.text);
assert.strictEqual(r.status, 201, r.text);
const slug = r.body.room.slug;
const slug = r.body.room.slug;
@@ -151,6 +156,7 @@ t('refusals: an unknown mode, someone else\'s chat, no sign-in', async () => {
@@ -151,6 +156,7 @@ t('refusals: an unknown mode, someone else\'s chat, no sign-in', async () => {
});
});
t('the composer shows the four modes, Chat chosen, each saying what it does', async () => {
t('the composer shows the four modes, Chat chosen, each saying what it does', async () => {
+ config.ai.vibeDoIt = true;
const page = await h.http('GET', '/vibe', { headers: { cookie: `ov_token=${ann.token}` } });
const page = await h.http('GET', '/vibe', { headers: { cookie: `ov_token=${ann.token}` } });
assert.match(page.text, /<fieldset class="vb-modes" aria-label="What Vibe does with it">/);
assert.match(page.text, /<fieldset class="vb-modes" aria-label="What Vibe does with it">/);
assert.match(page.text, /value="chat" data-placeholder="Message Vibe" checked><span>Chat<\/span>/);
assert.match(page.text, /value="chat" data-placeholder="Message Vibe" checked><span>Chat<\/span>/);