openvibers/openvibe.bot Commits

Programs on a desk (contracts 0.218.0, Node v0.26.0): exec with a desk goes only to a Node that runs it there, and the audit names the desk and detach; openvibe-contracts v0.218.0 (#185)

OP OpenVibers committed 17 days ago · 08e9f29b17 · 6 files, +31 −9

Changes

ddiff --git a/README.md b/README.md
iindex 3d12e50..eec3d37 100644
---- a/README.md
++++ b/README.md
@@@ -187,7 +187,7 @@ Set `BOT_ONVIF_CAMERAS` (JSON, unset by default) to the server-side cameras Bot
OpenVibe.Sites no longer generates `deploy/nginx/openvibe.bot.conf` (sites.json marks the vhost as owned by OpenVibe.Bot), and Bot serves nothing from a Sites checkout: the legal pages, `robots.txt`, `sitemap.xml`, `manifest.webmanifest` and the 404 page all come from the app. A Sites deploy leaves an installed vhost it no longer generates in place, so the current Sites vhost keeps serving openvibe.bot until Bot's first deploy installs this file over it; later Sites deploys never overwrite Bot's `/device` and `/control` routes.
<!-- versions:start -->
--- openvibe-contracts: v0.217.0
++- openvibe-contracts: v0.218.0
- openvibe-sdk: v0.65.0
- openvibe-shared: v4.15.2
<!-- versions:end -->
ddiff --git a/STATUS.json b/STATUS.json
iindex acc53a9..09220ff 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -5,11 +5,11 @@
"runtime": "node 22, express 4, ws; PostgreSQL 18 via openvibe-sdk/db (pg, or embedded PGlite in tests); Valkey optional; no build step",
"port": 4630,
"domain": "openvibe.bot (www -> apex; /device and /control WebSockets; /install)",
-- "contracts": "openvibe-contracts v0.217.0",
++ "contracts": "openvibe-contracts v0.218.0",
"sdk": "openvibe-sdk v0.65.0",
"shared": "openvibe-shared v4.15.2",
"packages": [
-- "openvibe-contracts v0.217.0",
++ "openvibe-contracts v0.218.0",
"openvibe-sdk v0.65.0",
"openvibe-shared v4.15.2"
],
ddiff --git a/package-lock.json b/package-lock.json
iindex 9ffc291..277f9c5 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -13,7 +13,7 @@
"dotenv": "^16.4.7",
"express": "^4.21.2",
"iovalkey": "^0.4.0",
-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.217.0",
++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.218.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.65.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.15.2",
"pg": "^8.23.0",
@@@ -845,9 +845,9 @@
}
},
"node_modules/openvibe-contracts": {
-- "version": "0.217.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.217.0",
-- "integrity": "sha512-hvLStkPZQmUiXH9MSUo7nzLerg13+xOp4Ear4YXQUOEGtMtCDIbBSXhQxfIvgSkGNox2jR94V8WADY6k8gEmcg==",
++ "version": "0.218.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.218.0",
++ "integrity": "sha512-/cgAdxibISceSva51oCYz/382iLsXW5RU6kt8Lyt5JWPiL6QhL0193boUZS9Ls4SxnEgT/fiAtMU3IJY4qMKwg==",
"license": "AGPL-3.0-only",
"dependencies": {
"ajv": "^8.17.1",
ddiff --git a/package.json b/package.json
iindex 9736fb3..a4fd466 100644
---- a/package.json
++++ b/package.json
@@@ -25,7 +25,7 @@
"dotenv": "^16.4.7",
"express": "^4.21.2",
"iovalkey": "^0.4.0",
-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.217.0",
++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.218.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.65.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.15.2",
"pg": "^8.23.0",
ddiff --git a/server/realtime.js b/server/realtime.js
iindex 83bb6b3..e817b6e 100644
---- a/server/realtime.js
++++ b/server/realtime.js
@@@ -45,6 +45,7 @@ const DEVICE_MAX_PAYLOAD = 640 * 1024;
const COMPUTER_CAP = { exec: 'terminal', fs: 'files', screen: 'screen', probe: 'probe', input: 'input', browser: 'browser', desk: 'desk' };
const NO_DESK = 'this machine has no private desk: it needs OpenVibe Node v0.15.0 or newer on Linux, with Xvfb and allow_desk on';
const NO_NAMED_DESKS = 'this machine runs one desk only (private): several named desks need OpenVibe Node v0.19.0 or newer';
++const NO_DESK_PROGRAMS = 'running a program on a desk needs OpenVibe Node v0.26.0 or newer (an older one would run it on the person\'s own screen)';
const NO_BROWSER_DESKS = 'this machine runs no browser desks: they need OpenVibe Node v0.23.0 or newer, with Chromium, Chrome, Edge or Brave';
const NO_WHOLE_DESKS = 'this machine runs browser desks only (a whole desk needs Linux with Xvfb): start it with kind browser';
/** The kinds of desk a machine runs (Node v0.23.0: desk.kinds); an older Node's desks are all whole desks. */
@@@ -66,7 +67,7 @@ const COMPUTER_MAX_MS = 600000;
function computerSummary(kind, value) {
const where = deskOf(value) !== 'main' ? { desk: deskOf(value) } : {};
if (kind === 'desk') return { action: value.action, ...(typeof value.desk === 'string' ? { desk: value.desk } : {}), ...(typeof value.kind === 'string' ? { kind: value.kind } : {}), ...(typeof value.profile === 'string' ? { profile: value.profile } : {}), ...(Number.isInteger(value.width) && Number.isInteger(value.height) ? { width: value.width, height: value.height } : {}) };
-- if (kind === 'exec') return { program: String(value.argv[0]).slice(0, 120), args: value.argv.length - 1 };
++ if (kind === 'exec') return { program: String(value.argv[0]).slice(0, 120), args: value.argv.length - 1, ...where, ...(value.detach === true ? { detach: true } : {}) };
if (kind === 'fs') return { op: value.op, path: String(value.path).slice(0, 300), ...(typeof value.content_b64 === 'string' ? { bytes: Math.floor((value.content_b64.length * 3) / 4) } : {}) };
if (kind === 'probe') { try { const u = new URL(value.target); return { scheme: u.protocol.replace(':', ''), host: u.host }; } catch { return { scheme: null }; } }
// Typed text can be anything (a password the person asked for): the audit keeps its length, never the text.
@@@ -780,6 +781,8 @@ function createRealtime({ config, keys, userAuth, log = console, now = () => Dat
const desk = kind === 'desk' ? (typeof value.desk === 'string' ? value.desk : 'private') : deskOf(value);
if (desk !== 'main' && !(offered && offered.desk)) return refuse('bot.computer_off', NO_DESK, summary);
if (desk !== 'main' && desk !== 'private' && !(offered.desk && offered.desk.named === true)) return refuse('bot.computer_off', NO_NAMED_DESKS, summary);
++ // A program on a desk (Node v0.26.0: terminal.desks): an older Node would run it in the person's own session.
++ if (kind === 'exec' && desk !== 'main' && !(offered.terminal && offered.terminal.desks === true)) return refuse('bot.computer_off', NO_DESK_PROGRAMS, summary);
// A desk starts as a kind the machine runs (Desks plan D2): an older Node would quietly start a whole desk
// for a browser desk, and a Mac runs browser desks only.
if (kind === 'desk' && value.action === 'start') {
ddiff --git a/test/machine-computer.test.js b/test/machine-computer.test.js
iindex 6820c2b..a7cba05 100644
---- a/test/machine-computer.test.js
++++ b/test/machine-computer.test.js
@@@ -235,6 +235,25 @@ const { boot, check, done } = require('./helpers/app');
old.c.close();
});
++ await check('programs on a desk (contracts 0.218.0, Node v0.26.0): sent to a Node that runs them, audited with the desk; an older Node never gets one', async () => {
++ const desk = { level: 'trusted', window_manager: 'openbox', input: true, screen: true, named: true, max_desks: 4, kinds: ['desktop', 'browser'] };
++ const now = await machine(alex, { ...COMPUTER, terminal: { ...COMPUTER.terminal, desks: true }, desk });
++ const old = await machine(alex, { ...COMPUTER, desk });
++ const run = await cmd(now.deviceId, { kind: 'exec', value: { argv: ['libreoffice', '--writer', 'notes.odt'], desk: 'desk-task-a', detach: true } }, { user: alex });
++ assert.strictEqual(run.json.result, 'ack', run.text);
++ assert.deepStrictEqual([seen.at(-1).kind, seen.at(-1).value.desk, seen.at(-1).value.detach], ['exec', 'desk-task-a', true]);
++ const row = await t.db.one('SELECT value FROM command_audit WHERE robot_id = $1 AND kind = $2 ORDER BY id DESC LIMIT 1', [now.deviceId, 'exec']);
++ assert.deepStrictEqual(row.value, { program: 'libreoffice', args: 2, desk: 'desk-task-a', detach: true }, 'never the arguments');
++ const before = seen.length;
++ const refused = await cmd(old.deviceId, { kind: 'exec', value: { argv: ['xterm'], desk: 'desk-task-a' } }, { user: alex });
++ assert.deepStrictEqual([refused.status, refused.json.code], [409, 'bot.computer_off']);
++ assert.match(refused.json.detail || refused.json.reason || '', /Node v0\.26\.0/);
++ assert.strictEqual(seen.length, before, 'never sent');
++ assert.strictEqual((await cmd(old.deviceId, { kind: 'exec', value: { argv: ['echo', 'hi'] } }, { user: alex })).json.result, 'ack', 'its own session as before');
++ now.c.close();
++ old.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;