openvibers/openvibe.bot Commits

Bot: every owner switch from the machine card; lend desks to other people's agents (#431)

OP OpenVibers committed 7 h ago · 3706bff88a · 2 files, +51 −6

POST /api/v1/machines/:id/shared-desks: the owner (or the network's hub, for them) lends this computer's desks to
agents other people run, only where its Link keeps every desk in its own sandbox (409 bot.desks_not_sandboxed
otherwise); until now nothing but the database could set it. The hub may also set ai-models and voice, as it
already set agent-computer and watch-probe; OpenVibe.AI itself still may not.

Changes

ddiff --git a/server/api/v1.js b/server/api/v1.js
iindex d59de3a..970c7bf 100644
---- a/server/api/v1.js
++++ b/server/api/v1.js
@@@ -704,10 +704,10 @@ function v1Router({ domain, apiAuth, limits, hub, jobs: jobService, config, subm
const updated = await domain.machines.setWatchProbe(device.id, req.body.on);
res.json({ machine: machineOut(updated) });
}));
-- // The owner lets Vibe answer their voice on the machine, with a call word, or stops it (never an agent or a service).
++ // The owner lets Vibe answer their voice on the machine, with a call word, or stops it (never an agent or another
++ // service). The network's hub does the same for the person from their own session (2026-11-12: the machine card).
r.post('/machines/:id/voice', manage, wrap(async (req, res) => {
-- if (req.principal.kind !== 'user') fail(403, 'bot.forbidden', 'only the machine\'s owner turns this on or off');
-- const { device } = await computerMachine(req, { agentNeedsOptIn: false });
++ const { device } = req.principal.kind === 'user' ? await computerMachine(req, { agentNeedsOptIn: false }) : await hubMachine(req);
if (typeof req.body?.on !== 'boolean') fail(422, 'bot.invalid_input', 'on is true or false');
const word = req.body.call_word == null ? null : validCallWord(req.body.call_word);
if (req.body.call_word != null && !word) fail(422, 'bot.invalid_input', 'a call word is 1 to 4 words, 3 to 40 characters, with one word of three letters or more');
@@@ -715,14 +715,30 @@ function v1Router({ domain, apiAuth, limits, hub, jobs: jobService, config, subm
if (hub && hub.machineChanged) await hub.machineChanged(updated);
res.json({ machine: machineOut(updated) });
}));
-- // The owner lets their AI run models on this machine, or stops it (never the AI itself).
++ // The owner lets their AI run models on this machine, or stops it (never the AI itself). The network's hub does the
++ // same for the person from their own session (2026-11-12: the machine card).
r.post('/machines/:id/ai-models', manage, wrap(async (req, res) => {
-- if (req.principal.kind !== 'user') fail(403, 'bot.forbidden', 'only the machine\'s owner turns this on or off');
-- const { device } = await computerMachine(req, { agentNeedsOptIn: false });
++ const { device } = req.principal.kind === 'user' ? await computerMachine(req, { agentNeedsOptIn: false }) : await hubMachine(req);
if (typeof req.body?.on !== 'boolean') fail(422, 'bot.invalid_input', 'on is true or false');
const updated = await domain.machines.setAiModels(device.id, req.body.on);
res.json({ machine: machineOut(updated) });
}));
++ // The owner lends this machine's desks to agents other people run, or stops (never an agent or another service). A
++ // machine may lend them only where its Link keeps every desk in its own sandbox (installed with --desk-host
++ // --shared): browser desks with the public internet only, nothing of the machine itself. Desk leases check it again
++ // for every session (desk-leases.js usable()). Until 2026-11-12 nothing could set it but the database.
++ r.post('/machines/:id/shared-desks', manage, wrap(async (req, res) => {
++ const { device } = req.principal.kind === 'user' ? await computerMachine(req, { agentNeedsOptIn: false }) : await hubMachine(req);
++ if (typeof req.body?.on !== 'boolean') fail(422, 'bot.invalid_input', 'on is true or false');
++ if (req.body.on) {
++ const offered = hub.computerOf(device.id) || computerCaps(device.capabilities);
++ if (!(offered && offered.desk && offered.desk.isolation === 'sandbox')) {
++ fail(409, 'bot.desks_not_sandboxed', 'this computer does not keep each desk in a sandbox of its own, so it cannot lend them; set it up again as a shared desk host (openvibe-link install --desk-host --shared)');
++ }
++ }
++ const updated = await domain.machines.setSharedDesks(device.id, req.body.on);
++ res.json({ machine: machineOut(updated) });
++ }));
// The owner turns their agent's use of the machine on or off (never the agent itself). The network's hub does
// the same for the person from their own session (plan L34), never for an app acting for them.
r.post('/machines/:id/agent-computer', manage, wrap(async (req, res) => {
ddiff --git a/test/machine-controls-hub.test.js b/test/machine-controls-hub.test.js
iindex b3aea81..0608199 100644
---- a/test/machine-controls-hub.test.js
++++ b/test/machine-controls-hub.test.js
@@@ -82,6 +82,21 @@ const { boot, check, done } = require('./helpers/app');
assert.strictEqual(other.status, 404, `${method} ${path}: ${other.text}`);
assert.strictEqual(other.json.code, 'bot.device_not_found');
}
++ // 2026-11-12: every switch of the machine card: the models, voice, and lending the desks to other people's
++ // agents (only where every desk is sandboxed; this fake Link's are).
++ for (const [route, col] of [['ai-models', 'ai_models'], ['voice', 'voice_on'], ['shared-desks', 'shared_desks']]) {
++ const r = await hub('POST', `/api/v1/machines/${mine.deviceId}/${route}`, { body: { on: true } });
++ assert.strictEqual(r.status, 200, `${route}: ${r.text}`);
++ assert.strictEqual((await t.db.maybe(`SELECT ${col} FROM devices WHERE id = $1`, [mine.deviceId]))[col], true, route);
++ const other = await hub('POST', `/api/v1/machines/${theirs.deviceId}/${route}`, { body: { on: true } });
++ assert.strictEqual(other.status, 404, `${route} on another person's machine: ${other.text}`);
++ const bad = await hub('POST', `/api/v1/machines/${mine.deviceId}/${route}`, { body: { on: 'yes' } });
++ assert.strictEqual(bad.status, 422, `${route}: on is true or false`);
++ }
++ assert.strictEqual((await hub('GET', '/api/v1/machines')).json.machines.find((m) => m.id === mine.deviceId).shared_desks, true, 'the card reads it back');
++ const lentOff = await t.call('POST', `/api/v1/machines/${mine.deviceId}/shared-desks`, { user: alex, body: { on: false } });
++ assert.strictEqual(lentOff.status, 200, lentOff.text);
++ assert.strictEqual(lentOff.json.machine.shared_desks, false, 'the person themselves stops lending');
// Another service with the capability cannot act for a person.
const stranger = await t.call('POST', `/api/v1/machines/${mine.deviceId}/agent-computer`, {
body: { on: true }, sub: 'svc:codes', cap: ['bot.machine.manage'], headers: { 'X-OV-Subject': alex.subject },
@@@ -90,6 +105,20 @@ const { boot, check, done } = require('./helpers/app');
assert.strictEqual(stranger.json.code, 'bot.forbidden');
});
++ await check('a computer whose desks are not each in a sandbox cannot lend them to other people', async () => {
++ // The same machine, now saying its desks share one browser profile (no sandbox of their own).
++ mine.c.send(frame('status', { capabilities: { computer: { ...COMPUTER, desk: { ...COMPUTER.desk, isolation: 'profile' } } }, drivers: [], faults: [] }));
++ await poll(() => { const o = t.hub.computerOf(mine.deviceId); return o && o.desk && o.desk.isolation === 'profile'; });
++ const r = await hub('POST', `/api/v1/machines/${mine.deviceId}/shared-desks`, { body: { on: true } });
++ assert.strictEqual(r.status, 409, r.text);
++ assert.strictEqual(r.json.code, 'bot.desks_not_sandboxed');
++ assert.strictEqual((await t.db.maybe('SELECT shared_desks FROM devices WHERE id = $1', [mine.deviceId])).shared_desks, false);
++ const off = await hub('POST', `/api/v1/machines/${mine.deviceId}/shared-desks`, { body: { on: false } });
++ assert.strictEqual(off.status, 200, 'turning it off always works');
++ mine.c.send(frame('status', { capabilities: { computer: COMPUTER }, drivers: [], faults: [] }));
++ await poll(() => { const o = t.hub.computerOf(mine.deviceId); return o && o.desk && o.desk.isolation === 'sandbox'; });
++ });
++
await check('the live screen: the hub starts it for ten minutes, reads it, takes the input and stops it', async () => {
const start = await hub('POST', `/api/v1/machines/${mine.deviceId}/screen`, { body: {} });
assert.strictEqual(start.status, 201, start.text);