openvibers/openvibe.bot Commits
A desk host's desks are seen and driven: screen, input and the live view on a desk check the desk's own capabilities (desk.screen, desk.input), not the machine's own screen, which a desk host does not have (#179)
Changes
@@@ -67,10 +67,12 @@ function createMachineScreen({ domain, hub, now = () => Date.now() }) {if (!hub.isOnline(device.id)) throw new BotError(409, 'bot.device_offline', 'the machine is offline');// Before any key is made: the machine says whether it can (Node v0.12.0's computer plugin, a capture tool).const offered = hub.computerOf(device.id);-- if (!(offered && offered.screen && offered.screen.stream === true)) {++ // The person's own screen needs a live capture of it; a desk's is the desk's own (desk.screen, below): a desk++ // host has no screen of its own to show, only its desks.++ if (desk === 'main' && !(offered && offered.screen && offered.screen.stream === true)) {throw new BotError(409, 'bot.computer_off', 'this machine does not show its screen live: it needs OpenVibe Node v0.12.0 or newer, with the computer plugin running in your graphical session');}-- if (desk !== 'main' && !(offered.desk && offered.desk.screen)) {++ if (desk !== 'main' && !(offered && offered.desk && offered.desk.screen)) {throw new BotError(409, 'bot.computer_off', 'this machine has no private desk to show: it needs OpenVibe Node v0.15.0 or newer on Linux, with Xvfb and allow_desk on');}if (desk !== 'main' && desk !== 'private' && offered.desk.named !== true) {@@@ -47,6 +47,9 @@ const NO_DESK = 'this machine has no private desk: it needs OpenVibe Node v0.15.const NO_NAMED_DESKS = 'this machine runs one desk only (private): several named desks need OpenVibe Node v0.19.0 or newer';/** The desk a command names: main (the person's own screen, the default), private or desk-<name> (contracts 0.215.0). */const deskOf = (value) => (typeof value.desk === 'string' && value.desk ? value.desk : 'main');++// The screen and the mouse and keyboard of a desk are the desk's own (Node's desk.screen, desk.input): a desk host with++// no screen of its own (no one signed in, Node v0.20.0 --desk-host) offers them on its desks only.++const onDeskOnly = (kind, value) => (kind === 'screen' || kind === 'input') && deskOf(value) !== 'main';/** A command that acts on a screen (the mouse and keyboard, or the browser doing more than reading it). */const actsOnScreen = (kind, value) => kind === 'input' || (kind === 'browser' && !['read', 'close'].includes(value.action));const COMPUTER_KINDS = Object.keys(COMPUTER_CAP);@@@ -760,10 +763,11 @@ function createRealtime({ config, keys, userAuth, log = console, now = () => Datif (!device || !device.online) return refuse('bot.device_offline', 'the machine is offline', summary);const offered = computerOf(deviceId);if (screen) {-- if (value.action === 'start' && !(offered && offered.screen && offered.screen.stream === true)) {++ // A desk's live view is the desk's own screen (desk.screen, checked below), not the person's.++ if (value.action === 'start' && deskOf(value) === 'main' && !(offered && offered.screen && offered.screen.stream === true)) {return refuse('bot.computer_off', 'this machine does not show its screen live: it needs OpenVibe Node v0.12.0 or newer, with the computer plugin running in your graphical session', summary);}-- } else if (!offered || !offered[COMPUTER_CAP[kind]]) {++ } else if (!offered || !(onDeskOnly(kind, value) ? (offered.desk && offered.desk[kind] === true) : offered[COMPUTER_CAP[kind]])) {return refuse('bot.computer_off', kind === 'desk' ? NO_DESK : `this machine does not offer ${COMPUTER_CAP[kind]}: its computer plugin is off (openvibe-node, --computer)`, summary);}// A desk other than the person's own screen needs a machine that has desks; one other than private (a named desk,@@@ -35,7 +35,7 @@ const { boot, check, done } = require('./helpers/app');else if (m.kind === 'input') c.send(frame('ack', { id: m.id, data: { done: true, action: m.value.action } }));else if (m.kind === 'browser') c.send(frame('ack', { id: m.id, data: { url: m.value.url || 'https://example.org/', title: 'Example', text: 'Example page', truncated: false, links: [] } }));else if (m.kind === 'desk') c.send(frame('ack', { id: m.id, data: { running: m.value.action !== 'stop', display: ':91', width: 1280, height: 800 } }));-- else if (m.kind === 'screen' && m.value.desk === 'private') c.send(frame('ack', { id: m.id, data: { format: 'png', width: 1280, height: 800, content_b64: 'iVBORw0KGgo=' } }));++ else if (m.kind === 'screen' && m.value.desk && m.value.desk !== 'main') c.send(frame('ack', { id: m.id, data: { format: 'png', width: 1280, height: 800, content_b64: 'iVBORw0KGgo=' } }));else c.send(frame('nack', { id: m.id, fault_code: 'not_allowed', message: 'this machine allows observing only' }));});// Beat like a real Node while the socket is open, so a longer check never sees its machine go offline.@@@ -198,6 +198,30 @@ const { boot, check, done } = require('./helpers/app');single.c.close();});++ await check('a desk host (Node v0.20.0 --desk-host): no screen of its own, yet its desks are seen and driven', async () => {++ // Nobody is signed in to a desk host: it offers no screen or input of its own, only its desks' (desk.screen, desk.input).++ const { screen: _s, input: _i, ...headless } = COMPUTER;++ const host = await machine(alex, { ...headless, desk: { level: 'trusted', window_manager: 'openbox', input: true, screen: true, named: true, max_desks: 4 } });++ const look = await cmd(host.deviceId, { kind: 'screen', value: { desk: 'desk-task-a', max_width: 1280 } }, { user: alex });++ assert.strictEqual(look.json.result, 'ack', look.text);++ const click = await cmd(host.deviceId, { kind: 'input', value: { action: 'click', x: 10, y: 10, width: 1280, height: 800, desk: 'desk-task-a' } }, { user: alex });++ assert.strictEqual(click.json.result, 'ack', click.text);++ assert.strictEqual(seen.at(-1).value.desk, 'desk-task-a');++ // Its own screen is still refused: there is none.++ const own = await cmd(host.deviceId, { kind: 'screen', value: { max_width: 1280 } }, { user: alex });++ assert.deepStrictEqual([own.status, own.json.code], [409, 'bot.computer_off']);++ const ownClick = await cmd(host.deviceId, { kind: 'input', value: { action: 'click', x: 1, y: 1, width: 2, height: 2 } }, { user: alex });++ assert.deepStrictEqual([ownClick.status, ownClick.json.code], [409, 'bot.computer_off']);++ // A desk that cannot be seen (no X capture tool) is refused, never sent.++ const blind = await machine(alex, { ...headless, desk: { level: 'trusted', window_manager: 'openbox', input: true, screen: false, named: true, max_desks: 4 } });++ const before = seen.length;++ const dark = await cmd(blind.deviceId, { kind: 'screen', value: { desk: 'desk-task-a' } }, { user: alex });++ assert.deepStrictEqual([dark.status, dark.json.code], [409, 'bot.computer_off']);++ assert.strictEqual(seen.length, before, 'nothing reached the machine');++ host.c.close();++ blind.c.close();++ });++await check('the audit keeps what was asked in short, never stdin, arguments or content', async () => {const rows = await t.db.many('SELECT * FROM command_audit WHERE robot_id = $1 ORDER BY id', [mine.deviceId]);assert.ok(rows.length >= 3);@@@ -174,6 +174,16 @@ const { boot, check, done } = require('./helpers/app');single.c.close();});++ await check('a desk host (Node v0.20.0 --desk-host) shows its desks live though it has no screen of its own', async () => {++ const host = await machine(alex, { files: SCREEN.files, desk: { level: 'trusted', window_manager: 'openbox', input: true, screen: true, named: true, max_desks: 4 } });++ const r = await screen('POST', host.deviceId, { user: alex, body: { seconds: 60, desk: 'desk-task-a' } });++ assert.strictEqual(r.status, 201, r.text);++ assert.strictEqual(seen.at(-1).value.desk, 'desk-task-a');++ const own = await screen('POST', host.deviceId, { user: alex, body: { seconds: 60 } });++ assert.deepStrictEqual([own.status, own.json.code], [409, 'bot.computer_off'], 'its own screen: there is none');++ host.c.close();++ });++await check('watch and take over an agent\'s desk (Desks plan D4): a task\'s link opens its desk; the owner\'s mouse and keyboard go to the desk shown, as the owner; same site only', async () => {const desk = { level: 'trusted', window_manager: 'openbox', input: true, screen: true, named: true, max_desks: 4 };const m = await machine(alex, { ...SCREEN, input: { level: 'trusted', tool: 'xdotool' }, desk });
@@ -67,10 +67,12 @@ function createMachineScreen({ domain, hub, now = () => Date.now() }) {
@@ -67,10 +67,12 @@ function createMachineScreen({ domain, hub, now = () => Date.now() }) {
if (!hub.isOnline(device.id)) throw new BotError(409, 'bot.device_offline', 'the machine is offline');
if (!hub.isOnline(device.id)) throw new BotError(409, 'bot.device_offline', 'the machine is offline');
// Before any key is made: the machine says whether it can (Node v0.12.0's computer plugin, a capture tool).
// Before any key is made: the machine says whether it can (Node v0.12.0's computer plugin, a capture tool).
const offered = hub.computerOf(device.id);
const offered = hub.computerOf(device.id);
- if (!(offered && offered.screen && offered.screen.stream === true)) {
+ // The person's own screen needs a live capture of it; a desk's is the desk's own (desk.screen, below): a desk
+ // host has no screen of its own to show, only its desks.
+ if (desk === 'main' && !(offered && offered.screen && offered.screen.stream === true)) {
throw new BotError(409, 'bot.computer_off', 'this machine does not show its screen live: it needs OpenVibe Node v0.12.0 or newer, with the computer plugin running in your graphical session');
throw new BotError(409, 'bot.computer_off', 'this machine does not show its screen live: it needs OpenVibe Node v0.12.0 or newer, with the computer plugin running in your graphical session');
}
}
- if (desk !== 'main' && !(offered.desk && offered.desk.screen)) {
+ if (desk !== 'main' && !(offered && offered.desk && offered.desk.screen)) {
throw new BotError(409, 'bot.computer_off', 'this machine has no private desk to show: it needs OpenVibe Node v0.15.0 or newer on Linux, with Xvfb and allow_desk on');
throw new BotError(409, 'bot.computer_off', 'this machine has no private desk to show: it needs OpenVibe Node v0.15.0 or newer on Linux, with Xvfb and allow_desk on');
}
}
if (desk !== 'main' && desk !== 'private' && offered.desk.named !== true) {
if (desk !== 'main' && desk !== 'private' && offered.desk.named !== true) {
@@ -47,6 +47,9 @@ const NO_DESK = 'this machine has no private desk: it needs OpenVibe Node v0.15.
@@ -47,6 +47,9 @@ const NO_DESK = 'this machine has no private desk: it needs OpenVibe Node v0.15.
const NO_NAMED_DESKS = 'this machine runs one desk only (private): several named desks need OpenVibe Node v0.19.0 or newer';
const NO_NAMED_DESKS = 'this machine runs one desk only (private): several named desks need OpenVibe Node v0.19.0 or newer';
/** The desk a command names: main (the person's own screen, the default), private or desk-<name> (contracts 0.215.0). */
/** The desk a command names: main (the person's own screen, the default), private or desk-<name> (contracts 0.215.0). */
const deskOf = (value) => (typeof value.desk === 'string' && value.desk ? value.desk : 'main');
const deskOf = (value) => (typeof value.desk === 'string' && value.desk ? value.desk : 'main');
+// The screen and the mouse and keyboard of a desk are the desk's own (Node's desk.screen, desk.input): a desk host with
+// no screen of its own (no one signed in, Node v0.20.0 --desk-host) offers them on its desks only.
+const onDeskOnly = (kind, value) => (kind === 'screen' || kind === 'input') && deskOf(value) !== 'main';
/** A command that acts on a screen (the mouse and keyboard, or the browser doing more than reading it). */
/** A command that acts on a screen (the mouse and keyboard, or the browser doing more than reading it). */
const actsOnScreen = (kind, value) => kind === 'input' || (kind === 'browser' && !['read', 'close'].includes(value.action));
const actsOnScreen = (kind, value) => kind === 'input' || (kind === 'browser' && !['read', 'close'].includes(value.action));
const COMPUTER_KINDS = Object.keys(COMPUTER_CAP);
const COMPUTER_KINDS = Object.keys(COMPUTER_CAP);
@@ -760,10 +763,11 @@ function createRealtime({ config, keys, userAuth, log = console, now = () => Dat
@@ -760,10 +763,11 @@ function createRealtime({ config, keys, userAuth, log = console, now = () => Dat
if (!device || !device.online) return refuse('bot.device_offline', 'the machine is offline', summary);
if (!device || !device.online) return refuse('bot.device_offline', 'the machine is offline', summary);
const offered = computerOf(deviceId);
const offered = computerOf(deviceId);
if (screen) {
if (screen) {
- if (value.action === 'start' && !(offered && offered.screen && offered.screen.stream === true)) {
+ // A desk's live view is the desk's own screen (desk.screen, checked below), not the person's.
+ if (value.action === 'start' && deskOf(value) === 'main' && !(offered && offered.screen && offered.screen.stream === true)) {
return refuse('bot.computer_off', 'this machine does not show its screen live: it needs OpenVibe Node v0.12.0 or newer, with the computer plugin running in your graphical session', summary);
return refuse('bot.computer_off', 'this machine does not show its screen live: it needs OpenVibe Node v0.12.0 or newer, with the computer plugin running in your graphical session', summary);
}
}
- } else if (!offered || !offered[COMPUTER_CAP[kind]]) {
+ } else if (!offered || !(onDeskOnly(kind, value) ? (offered.desk && offered.desk[kind] === true) : offered[COMPUTER_CAP[kind]])) {
return refuse('bot.computer_off', kind === 'desk' ? NO_DESK : `this machine does not offer ${COMPUTER_CAP[kind]}: its computer plugin is off (openvibe-node, --computer)`, summary);
return refuse('bot.computer_off', kind === 'desk' ? NO_DESK : `this machine does not offer ${COMPUTER_CAP[kind]}: its computer plugin is off (openvibe-node, --computer)`, summary);
}
}
// A desk other than the person's own screen needs a machine that has desks; one other than private (a named desk,
// A desk other than the person's own screen needs a machine that has desks; one other than private (a named desk,
@@ -35,7 +35,7 @@ const { boot, check, done } = require('./helpers/app');
@@ -35,7 +35,7 @@ const { boot, check, done } = require('./helpers/app');
else if (m.kind === 'input') c.send(frame('ack', { id: m.id, data: { done: true, action: m.value.action } }));
else if (m.kind === 'input') c.send(frame('ack', { id: m.id, data: { done: true, action: m.value.action } }));
else if (m.kind === 'browser') c.send(frame('ack', { id: m.id, data: { url: m.value.url || 'https://example.org/', title: 'Example', text: 'Example page', truncated: false, links: [] } }));
else if (m.kind === 'browser') c.send(frame('ack', { id: m.id, data: { url: m.value.url || 'https://example.org/', title: 'Example', text: 'Example page', truncated: false, links: [] } }));
else if (m.kind === 'desk') c.send(frame('ack', { id: m.id, data: { running: m.value.action !== 'stop', display: ':91', width: 1280, height: 800 } }));
else if (m.kind === 'desk') c.send(frame('ack', { id: m.id, data: { running: m.value.action !== 'stop', display: ':91', width: 1280, height: 800 } }));
- else if (m.kind === 'screen' && m.value.desk === 'private') c.send(frame('ack', { id: m.id, data: { format: 'png', width: 1280, height: 800, content_b64: 'iVBORw0KGgo=' } }));
+ else if (m.kind === 'screen' && m.value.desk && m.value.desk !== 'main') c.send(frame('ack', { id: m.id, data: { format: 'png', width: 1280, height: 800, content_b64: 'iVBORw0KGgo=' } }));
else c.send(frame('nack', { id: m.id, fault_code: 'not_allowed', message: 'this machine allows observing only' }));
else c.send(frame('nack', { id: m.id, fault_code: 'not_allowed', message: 'this machine allows observing only' }));
});
});
// Beat like a real Node while the socket is open, so a longer check never sees its machine go offline.
// Beat like a real Node while the socket is open, so a longer check never sees its machine go offline.
@@ -198,6 +198,30 @@ const { boot, check, done } = require('./helpers/app');
@@ -198,6 +198,30 @@ const { boot, check, done } = require('./helpers/app');
single.c.close();
single.c.close();
});
});
+ await check('a desk host (Node v0.20.0 --desk-host): no screen of its own, yet its desks are seen and driven', async () => {
+ // Nobody is signed in to a desk host: it offers no screen or input of its own, only its desks' (desk.screen, desk.input).
+ const { screen: _s, input: _i, ...headless } = COMPUTER;
+ const host = await machine(alex, { ...headless, desk: { level: 'trusted', window_manager: 'openbox', input: true, screen: true, named: true, max_desks: 4 } });
+ const look = await cmd(host.deviceId, { kind: 'screen', value: { desk: 'desk-task-a', max_width: 1280 } }, { user: alex });
+ assert.strictEqual(look.json.result, 'ack', look.text);
+ const click = await cmd(host.deviceId, { kind: 'input', value: { action: 'click', x: 10, y: 10, width: 1280, height: 800, desk: 'desk-task-a' } }, { user: alex });
+ assert.strictEqual(click.json.result, 'ack', click.text);
+ assert.strictEqual(seen.at(-1).value.desk, 'desk-task-a');
+ // Its own screen is still refused: there is none.
+ const own = await cmd(host.deviceId, { kind: 'screen', value: { max_width: 1280 } }, { user: alex });
+ assert.deepStrictEqual([own.status, own.json.code], [409, 'bot.computer_off']);
+ const ownClick = await cmd(host.deviceId, { kind: 'input', value: { action: 'click', x: 1, y: 1, width: 2, height: 2 } }, { user: alex });
+ assert.deepStrictEqual([ownClick.status, ownClick.json.code], [409, 'bot.computer_off']);
+ // A desk that cannot be seen (no X capture tool) is refused, never sent.
+ const blind = await machine(alex, { ...headless, desk: { level: 'trusted', window_manager: 'openbox', input: true, screen: false, named: true, max_desks: 4 } });
+ const before = seen.length;
+ const dark = await cmd(blind.deviceId, { kind: 'screen', value: { desk: 'desk-task-a' } }, { user: alex });
+ assert.deepStrictEqual([dark.status, dark.json.code], [409, 'bot.computer_off']);
+ assert.strictEqual(seen.length, before, 'nothing reached the machine');
+ host.c.close();
+ blind.c.close();
+ });
+
await check('the audit keeps what was asked in short, never stdin, arguments or content', async () => {
await check('the audit keeps what was asked in short, never stdin, arguments or content', async () => {
const rows = await t.db.many('SELECT * FROM command_audit WHERE robot_id = $1 ORDER BY id', [mine.deviceId]);
const rows = await t.db.many('SELECT * FROM command_audit WHERE robot_id = $1 ORDER BY id', [mine.deviceId]);
assert.ok(rows.length >= 3);
assert.ok(rows.length >= 3);
@@ -174,6 +174,16 @@ const { boot, check, done } = require('./helpers/app');
@@ -174,6 +174,16 @@ const { boot, check, done } = require('./helpers/app');
single.c.close();
single.c.close();
});
});
+ await check('a desk host (Node v0.20.0 --desk-host) shows its desks live though it has no screen of its own', async () => {
+ const host = await machine(alex, { files: SCREEN.files, desk: { level: 'trusted', window_manager: 'openbox', input: true, screen: true, named: true, max_desks: 4 } });
+ const r = await screen('POST', host.deviceId, { user: alex, body: { seconds: 60, desk: 'desk-task-a' } });
+ assert.strictEqual(r.status, 201, r.text);
+ assert.strictEqual(seen.at(-1).value.desk, 'desk-task-a');
+ const own = await screen('POST', host.deviceId, { user: alex, body: { seconds: 60 } });
+ assert.deepStrictEqual([own.status, own.json.code], [409, 'bot.computer_off'], 'its own screen: there is none');
+ host.c.close();
+ });
+
await check('watch and take over an agent\'s desk (Desks plan D4): a task\'s link opens its desk; the owner\'s mouse and keyboard go to the desk shown, as the owner; same site only', async () => {
await check('watch and take over an agent\'s desk (Desks plan D4): a task\'s link opens its desk; the owner\'s mouse and keyboard go to the desk shown, as the owner; same site only', async () => {
const desk = { level: 'trusted', window_manager: 'openbox', input: true, screen: true, named: true, max_desks: 4 };
const desk = { level: 'trusted', window_manager: 'openbox', input: true, screen: true, named: true, max_desks: 4 };
const m = await machine(alex, { ...SCREEN, input: { level: 'trusted', tool: 'xdotool' }, desk });
const m = await machine(alex, { ...SCREEN, input: { level: 'trusted', tool: 'xdotool' }, desk });