openvibers/openvibe.bot Commits
Bot: every owner switch from the machine card; lend desks to other people's agents (#431)
Changes
@@@ -704,10 +704,10 @@ function v1Router({ domain, apiAuth, limits, hub, jobs: jobService, config, submconst 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, submif (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) => {@@@ -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);
@@ -704,10 +704,10 @@ function v1Router({ domain, apiAuth, limits, hub, jobs: jobService, config, subm
@@ -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);
const updated = await domain.machines.setWatchProbe(device.id, req.body.on);
res.json({ machine: machineOut(updated) });
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) => {
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');
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);
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');
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
@@ -715,14 +715,30 @@ function v1Router({ domain, apiAuth, limits, hub, jobs: jobService, config, subm
if (hub && hub.machineChanged) await hub.machineChanged(updated);
if (hub && hub.machineChanged) await hub.machineChanged(updated);
res.json({ machine: machineOut(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) => {
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');
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);
const updated = await domain.machines.setAiModels(device.id, req.body.on);
res.json({ machine: machineOut(updated) });
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 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.
// 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) => {
r.post('/machines/:id/agent-computer', manage, wrap(async (req, res) => {
@@ -82,6 +82,21 @@ const { boot, check, done } = require('./helpers/app');
@@ -82,6 +82,21 @@ const { boot, check, done } = require('./helpers/app');
assert.strictEqual(other.status, 404, `${method} ${path}: ${other.text}`);
assert.strictEqual(other.status, 404, `${method} ${path}: ${other.text}`);
assert.strictEqual(other.json.code, 'bot.device_not_found');
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.
// Another service with the capability cannot act for a person.
const stranger = await t.call('POST', `/api/v1/machines/${mine.deviceId}/agent-computer`, {
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 },
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');
@@ -90,6 +105,20 @@ const { boot, check, done } = require('./helpers/app');
assert.strictEqual(stranger.json.code, 'bot.forbidden');
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 () => {
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: {} });
const start = await hub('POST', `/api/v1/machines/${mine.deviceId}/screen`, { body: {} });
assert.strictEqual(start.status, 201, start.text);
assert.strictEqual(start.status, 201, start.text);