openvibers/openvibe.link Commits

v0.20.0: install.sh --desk-host: a used machine hosts desks for its owner's agents (its own account, the desk tools installed, max_desks sized to its memory) (#66)

OP OpenVibers committed 17 days ago · 5ae2943886 · 7 files, +102 −13

Changes

ddiff --git a/STATUS.json b/STATUS.json
iindex d33d395..4f647c4 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -5,8 +5,8 @@
"wave": "T15",
"plan": "Plan track T15 (Bot complete) and ADR-043 (Bot: devices, pairing, control and safety); the computer plugin and the job worker serve OpenVibe.Actor and the Codes job runner (tracks T14/T17)",
"kind": "client",
-- "version": "0.19.0",
-- "release": "v0.19.0, published 2026-10-27 as a GitHub release (several desks per machine, the Desks plan's step D1: the computer plugin runs named desks (desk-<name>, private kept), each with its own X display, D-Bus, browser profile, downloads and temp folder, up to max_desks (one per 2 GiB of memory by default), so two agents work at once without seeing or driving each other's screen; proven with two real Chrome desks that never share a cookie; v0.18.0: a motion command's validity also counts the time it waits in the queue to its plugin and in the pipe: the core fixes the instant it expires at receipt, writes what is left at the moment it writes the line with expires_at_mono_ns on the clock it shares with the plugin, and never writes one that has run out, and the Python runtime refuses one that ran out in the pipe before handle(); the clock bound is kept in nanoseconds and counts Bot's whole-millisecond stamps, rounded up from nanoseconds; a Node booting latched sets the e-stop on its plugins before starting them; the Python SDK package reports the Node release; v0.17.0: deadlines are checked against the latest the server's clock can be: the estimate plus its bound, the connection's fastest round trip, and motion waits for that bound on every new connection; every stop and estop names its cause, hello carries the newest halt epoch, and the Python SDK gives drivers on_stop(StopInfo) and on_resume to tell an ordinary stop from an emergency without re-arming anything; v0.16.1: the Python runtime refuses a motion command whose deadline passed while it waited behind a slow handler, so a driver never moves on a stale command; plugins.md answers where deadlines come from, re-stamping, order across a change of driver, stop versus estop, and that nothing is held past its validity; v0.16.0: openvibe-node quit ends a running node cleanly with nothing latched, for launchers; each driver command carries its provenance (link session, seq, Bot send time and deadline) and a halt's stop its turn_epoch, and the Python runtime refuses a replay on a session and a command from an ended turn; the no-redelivery contract written down in plugins.md): binaries for linux amd64/arm64/armv7, darwin amd64/arm64 and windows amd64, the plugins tarball, install.sh and SHA256SUMS",
++ "version": "0.20.0",
++ "release": "v0.20.0, published 2026-10-27 as a GitHub release (desk hosts, the Desks plan's step D5: install.sh --desk-host turns an old laptop, a mini PC or a spare workstation into a host of desks for its owner's agents, run as its own openvibe-desk account with Xvfb, openbox, D-Bus, xdotool, ImageMagick and Chromium installed and max_desks sized to its memory; v0.19.0: several desks per machine, the Desks plan's step D1: the computer plugin runs named desks (desk-<name>, private kept), each with its own X display, D-Bus, browser profile, downloads and temp folder, up to max_desks (one per 2 GiB of memory by default), so two agents work at once without seeing or driving each other's screen; proven with two real Chrome desks that never share a cookie; v0.18.0: a motion command's validity also counts the time it waits in the queue to its plugin and in the pipe: the core fixes the instant it expires at receipt, writes what is left at the moment it writes the line with expires_at_mono_ns on the clock it shares with the plugin, and never writes one that has run out, and the Python runtime refuses one that ran out in the pipe before handle(); the clock bound is kept in nanoseconds and counts Bot's whole-millisecond stamps, rounded up from nanoseconds; a Node booting latched sets the e-stop on its plugins before starting them; the Python SDK package reports the Node release; v0.17.0: deadlines are checked against the latest the server's clock can be: the estimate plus its bound, the connection's fastest round trip, and motion waits for that bound on every new connection; every stop and estop names its cause, hello carries the newest halt epoch, and the Python SDK gives drivers on_stop(StopInfo) and on_resume to tell an ordinary stop from an emergency without re-arming anything; v0.16.1: the Python runtime refuses a motion command whose deadline passed while it waited behind a slow handler, so a driver never moves on a stale command; plugins.md answers where deadlines come from, re-stamping, order across a change of driver, stop versus estop, and that nothing is held past its validity; v0.16.0: openvibe-node quit ends a running node cleanly with nothing latched, for launchers; each driver command carries its provenance (link session, seq, Bot send time and deadline) and a halt's stop its turn_epoch, and the Python runtime refuses a replay on a session and a command from an ended turn; the no-redelivery contract written down in plugins.md): binaries for linux amd64/arm64/armv7, darwin amd64/arm64 and windows amd64, the plugins tarball, install.sh and SHA256SUMS",
"runtime": "go 1.24 (one static binary per platform); plugins in Python 3; an ESP32 Arduino library",
"database": "none",
"deployed": false,
ddiff --git a/docs/computer.md b/docs/computer.md
iindex 0131247..66b5fba 100644
---- a/docs/computer.md
++++ b/docs/computer.md
@@@ -156,6 +156,24 @@ An agent can work on a desk of its own instead of your screen (plan track K: Vib
- **Needs** Linux with `Xvfb` (Debian/Ubuntu: `apt install xvfb`), `xdotool` for input and ImageMagick `import` or `scrot`
for screenshots. Not offered elsewhere; `allow_desk: false` turns it off.
++### Desk hosts (`--desk-host`, Node v0.20.0)
++
++An old laptop, a mini PC or a spare workstation can host desks for your agents: no screen, keyboard or login needed.
++```
++curl -fsSL https://openvibe.bot/install | sh -s -- --network … --pairing pair_… --code … --desk-host
++```
++- **Its own account.** The Node runs as `openvibe-desk` (home `/var/lib/openvibe-desk`), never as a person. An agent
++ there never reaches anyone's own files, browser or sessions. The computer plugin runs at `trusted` for that account's
++ folder only (`--computer` sets another level).
++- **What desks need.** On apt systems the installer adds Xvfb, openbox, D-Bus, xdotool, ImageMagick, fonts and
++ Chromium. Elsewhere it lists them.
++- **Sized to the machine.** `max_desks` defaults to one desk per 2 GiB of memory (1 to 8), and the installer says how
++ many. Each agent task gets a desk of its own (OpenVibe.Actor with OpenVibe.Bot, Node v0.19.0): its own screen,
++ browser profile, downloads and temp folder. A task beyond `max_desks` is told the machine is busy (its desk does not
++ start); it never shares another task's desk.
++- **Then** turn on "Let my agent use this computer" for the machine on openvibe.bot/machines. Watch any desk live from
++ there, and take it over the same way.
++
## Config
| key | default | meaning |
ddiff --git a/install/install.sh b/install/install.sh
iindex 8b71c15..937a1bc 100755
---- a/install/install.sh
++++ b/install/install.sh
@@@ -36,6 +36,12 @@
# as the unprivileged user openvibe-job in the worker's sandbox with public egress (the service
# runs as root, which run_as and the egress need), and the OpenVibe.Codes job runner is added when
# Node.js 20+ is installed system-wide. Linux only. docs/worker.md has the details.
++# --desk-host this machine hosts desks for its owner's agents (docs/computer.md "Desk hosts"; the Desks plan's
++# step D5): an old laptop, a mini PC or a spare workstation, no screen or login needed. The
++# service runs as its own account (openvibe-desk), the installer adds what desks need (Xvfb, a
++# window manager, D-Bus, xdotool, ImageMagick, Chromium) and sizes them to the machine's memory
++# (max_desks: one per 2 GiB); each agent task gets a desk of its own. Implies --computer trusted
++# (for that account's own files only); give another level with --computer.
# --computer LEVEL this machine's terminal, files and screen for its owner's agents (the computer plugin,
# docs/computer.md), at LEVEL observe, ask, trusted or full. The service then runs as YOUR account
# (the one that ran sudo), never root, and its folders are your home. Linux only; not with --worker.
@@@ -72,6 +78,7 @@ LOCAL=""
TAG="latest"
WORKER=0
COMPUTER=""
++DESK_HOST=0
CLAIM_OK=1
DRIVER_GIVEN=0
SETUP="${OPENVIBE_SETUP_KEY:-}"
@@@ -82,7 +89,7 @@ CODES_DIR=/opt/openvibe-codes
usage() {
echo "usage: install.sh [--robot rob_… | --network URL --pairing pair_…] [--code CODE | CODE] [--name NAME]"
echo " [--driver adeept|adeept-mecanum|cozmo|looi|ros2|ros2-holonomic|none] [--worker]"
-- echo " [--computer observe|ask|trusted|full] [--no-service] [--no-claim] [--setup osk_…]"
++ echo " [--computer observe|ask|trusted|full] [--desk-host] [--no-service] [--no-claim] [--setup osk_…]"
echo " [--local DIR] [--version TAG]"
echo " curl -fsSL https://openvibe.bot/install | sh (finds the kit, then shows a code to approve on openvibe.bot/link)"
echo " curl -fsSL https://openvibe.bot/install | sh -s -- --robot rob_… --code ABCD-1234 --driver adeept"
@@@ -113,6 +120,7 @@ while [ $# -gt 0 ]; do
--pairing) PAIRING="${2:-}"; shift 2 ;;
--pairing=*) PAIRING="${1#*=}"; shift ;;
--worker) WORKER=1; shift ;;
++ --desk-host) DESK_HOST=1; shift ;;
--computer) COMPUTER="${2:-}"; shift 2 ;;
--computer=*) COMPUTER="${1#*=}"; shift ;;
--no-service) SERVICE=0; shift ;;
@@@ -138,7 +146,7 @@ case "$NETWORK" in ""|https://*|http://localhost*|http://127.0.0.1*) ;; *) die "
if [ -n "$SETUP" ]; then
printf '%s' "$SETUP" | grep -Eq '^osk_[A-Za-z0-9_-]{43}$' || die "--setup takes the osk_… key from your card files (openvibe.bot/setup/sd-card)"
[ -z "$CODE$ROBOT_ID$NETWORK$PAIRING" ] || die "--setup is used on its own (no --robot, --code, --network or --pairing)"
-- if [ "$CLAIM_OK" = 0 ] || [ "$WORKER" = 1 ] || [ -n "$COMPUTER" ]; then die "--setup cannot be used with --no-claim, --worker or --computer"; fi
++ if [ "$CLAIM_OK" = 0 ] || [ "$WORKER" = 1 ] || [ -n "$COMPUTER" ] || [ "$DESK_HOST" = 1 ]; then die "--setup cannot be used with --no-claim, --worker, --computer or --desk-host"; fi
fi
# No robot, no code, no Network pairing, not a worker or a computer: the one command (openvibe.bot/link, see the top).
CLAIM=0
@@@ -295,16 +303,23 @@ fi
# write_worker_config <config.json> <job uid> <job gid> [<node.js> <codes dir>]: turn the job worker on in that config
# (kept as it is otherwise, created when missing): jobs as the job user, public egress, caps sized for a coding task,
# and, with a Node.js and a Codes install, the `[email protected]` job runner. Keys stay empty for the owner to add.
--# write_computer_config <config.json> <level> <home>: add the computer plugin at that level (replacing an earlier entry),
--# its folder the owner's home; the rest of the config is kept.
++# write_computer_config <config.json> <level> <home> [desk-host]: add the computer plugin at that level (replacing an
++# earlier entry), its folder the owner's home; the rest of the config is kept. A desk host (4th argument 1) keeps its
++# desks under <home>/desks, shows no notifications (nobody sits at it) and turns the camera off.
write_computer_config() {
${SUDO:-} python3 - "$@" <<'PY'
import json, os, sys
path, level, home = sys.argv[1], sys.argv[2], sys.argv[3]
++desk_host = len(sys.argv) > 4 and sys.argv[4] == '1'
with open(path) as f:
conf = json.load(f)
plugins = [p for p in conf.get('plugins') or [] if p.get('name') not in ('computer', 'dryrun')]
--plugins.append({'name': 'computer', 'config': {'level': level, 'roots': [home]}})
++computer = {'level': level, 'roots': [home]}
++if desk_host:
++ computer.update({'notify': False, 'allow_desk': True, 'desk_dir': os.path.join(home, 'desks'),
++ 'browser_profile': os.path.join(home, 'browser')})
++ conf['video'] = {'source': 'off'}
++plugins.append({'name': 'computer', 'config': computer})
conf['plugins'] = plugins
old = os.umask(0o077)
with open(path + '.tmp', 'w') as f:
@@@ -376,8 +391,9 @@ fi
# OPENVIBE_INSTALL_COMPUTER_CONFIG_ONLY=<config.json> with --computer LEVEL and OPENVIBE_COMPUTER_HOME adds the computer
# plugin to that config and exits (scripts/test-install-computer-config.sh).
if [ -n "${OPENVIBE_INSTALL_COMPUTER_CONFIG_ONLY:-}" ]; then
++ [ "$DESK_HOST" = 0 ] || [ -n "$COMPUTER" ] || COMPUTER=trusted
case "$COMPUTER" in observe|ask|trusted|full) ;; *) die "--computer is observe, ask, trusted or full" ;; esac
-- write_computer_config "$OPENVIBE_INSTALL_COMPUTER_CONFIG_ONLY" "$COMPUTER" "${OPENVIBE_COMPUTER_HOME:?}"
++ write_computer_config "$OPENVIBE_INSTALL_COMPUTER_CONFIG_ONLY" "$COMPUTER" "${OPENVIBE_COMPUTER_HOME:?}" "$DESK_HOST"
exit 0
fi
@@@ -633,7 +649,22 @@ say "installed $BIN_DIR/openvibe-node ($("$BIN_DIR/openvibe-node" version))"
SVC_USER=""
if [ "$WORKER" = 1 ] && [ "$OS" != linux ]; then die "--worker is for Linux"; fi
COMPUTER_HOME=""
--if [ -n "$COMPUTER" ]; then
++if [ "$DESK_HOST" = 1 ]; then
++ # A desk host: the desks run as an account of their own, never the person's, so an agent there never reaches the
++ # owner's files, browser or sessions. --computer still sets the level (trusted by default).
++ [ "$OS" = linux ] || die "--desk-host is for Linux (desks are Xvfb displays)"
++ [ "$WORKER" = 0 ] || die "--desk-host and --worker cannot share a machine yet (the worker's service runs as root)"
++ [ -n "$COMPUTER" ] || COMPUTER=trusted
++ SVC_USER=openvibe-desk
++ COMPUTER_HOME=/var/lib/openvibe-desk
++ if ! id "$SVC_USER" >/dev/null 2>&1; then
++ $SUDO useradd --system --create-home --home-dir "$COMPUTER_HOME" --shell /usr/sbin/nologin --user-group "$SVC_USER" 2>/dev/null ||
++ $SUDO adduser --system --home "$COMPUTER_HOME" --shell /usr/sbin/nologin --group "$SVC_USER"
++ fi
++ $SUDO mkdir -p "$COMPUTER_HOME/desks"
++ $SUDO chown -R "$SVC_USER": "$COMPUTER_HOME"
++ $SUDO chmod 700 "$COMPUTER_HOME"
++elif [ -n "$COMPUTER" ]; then
case "$COMPUTER" in observe|ask|trusted|full) ;; *) die "--computer is observe, ask, trusted or full" ;; esac
[ "$OS" = linux ] || die "--computer is for Linux in this installer (docs/computer.md: run the Node in your own session elsewhere)"
[ "$WORKER" = 0 ] || die "--computer and --worker cannot share a machine yet (the worker's service runs as root)"
@@@ -642,6 +673,7 @@ if [ -n "$COMPUTER" ]; then
COMPUTER_HOME=$(getent passwd "$SVC_USER" | cut -d: -f6)
if [ -z "$COMPUTER_HOME" ] || [ ! -d "$COMPUTER_HOME" ]; then die "no home folder for $SVC_USER"; fi
fi
++case "$COMPUTER" in ""|observe|ask|trusted|full) ;; *) die "--computer is observe, ask, trusted or full" ;; esac
if [ "$OS" = linux ] && [ "$SERVICE" = 1 ] && [ "$WORKER" = 0 ] && [ -z "$COMPUTER" ]; then
SVC_USER=openvibe-node
if ! id "$SVC_USER" >/dev/null 2>&1; then
@@@ -714,6 +746,20 @@ if [ -n "$COMPUTER" ]; then
$SUDO "$PIP" install --quiet "$STATE/plugins/computer[screen]" 2>/dev/null || $SUDO "$PIP" install --quiet "$STATE/plugins/computer"
[ "$DRIVER" = none ] && VIDEO='"source": "off"'
fi
++if [ "$DESK_HOST" = 1 ]; then
++ # What a desk needs: a display (Xvfb), a window manager, its own D-Bus, input (xdotool), screenshots (ImageMagick),
++ # fonts, and a Chromium-family browser.
++ if command -v apt-get >/dev/null 2>&1; then
++ say "installing what desks need (Xvfb, openbox, D-Bus, xdotool, ImageMagick, fonts, Chromium)"
++ $SUDO apt-get install -y xvfb openbox dbus-x11 xdotool imagemagick fonts-dejavu-core fonts-noto-color-emoji >/dev/null 2>&1 ||
++ say "warning: apt-get could not install every desk tool; install xvfb openbox dbus-x11 xdotool imagemagick yourself"
++ if ! command -v chromium >/dev/null 2>&1 && ! command -v chromium-browser >/dev/null 2>&1 && ! command -v google-chrome >/dev/null 2>&1; then
++ $SUDO apt-get install -y chromium >/dev/null 2>&1 || say "tip: no Chromium package here: install Google Chrome (a .deb from google.com/chrome) for the desks' browser"
++ fi
++ else
++ say "install Xvfb, openbox, D-Bus, xdotool, ImageMagick and Chromium with your package manager: desks need them"
++ fi
++fi
if [ ! -f "$CONF/config.json" ]; then
$SUDO sh -c "umask 077; cat > '$CONF/config.json'" <<EOF
{
@@@ -729,7 +775,14 @@ else
# The one command sets up the robot it found: the driver's plugins, kind and video replace the old ones.
[ "$CLAIM" = 0 ] || set_driver_config "$CONF/config.json"
fi
--if [ -n "$COMPUTER" ]; then
++if [ "$DESK_HOST" = 1 ]; then
++ write_computer_config "$CONF/config.json" "$COMPUTER" "$COMPUTER_HOME" 1
++ mem_kb=$(awk '/^MemTotal:/ {print $2}' /proc/meminfo 2>/dev/null || echo 0)
++ desks=$(( mem_kb / 2097152 )); [ "$desks" -ge 1 ] || desks=1; [ "$desks" -le 8 ] || desks=8
++ say "this machine is a desk host: up to $desks desks at once for your agents, each its own screen, browser and files"
++ say "(max_desks in $CONF/config.json changes it). On https://openvibe.bot/machines turn on \"Let my agent use this"
++ say "computer\" for it: your agents' tasks then each get a desk of their own here."
++elif [ -n "$COMPUTER" ]; then
write_computer_config "$CONF/config.json" "$COMPUTER" "$COMPUTER_HOME"
say "the computer plugin is on at level $COMPUTER for $SVC_USER (folder $COMPUTER_HOME); change it in $CONF/config.json"
# The tray starts when they sign in: what waits for approval with Approve and Deny, the stop and resume.
ddiff --git a/plugins/sdk/openvibe_plugin/__init__.py b/plugins/sdk/openvibe_plugin/__init__.py
iindex a73474f..90e2520 100644
---- a/plugins/sdk/openvibe_plugin/__init__.py
++++ b/plugins/sdk/openvibe_plugin/__init__.py
@@@ -24,7 +24,7 @@ __all__ = ["Plugin", "Fault", "Deferred", "Command", "StopInfo", "Runtime", "run
"RUNTIME_CAUSES", "SHARED_CLOCK", "__version__"]
# The Node release this runtime ships with (pyproject.toml carries the same).
--__version__ = "0.19.0"
++__version__ = "0.20.0"
PROTOCOL_VERSION = 1
HEARTBEAT_TIMEOUT_S = 1.0
DEFAULT_DEADLINE_MS = 300
ddiff --git a/plugins/sdk/pyproject.toml b/plugins/sdk/pyproject.toml
iindex 1fc2544..f1bab3e 100644
---- a/plugins/sdk/pyproject.toml
++++ b/plugins/sdk/pyproject.toml
@@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "openvibe-plugin"
--version = "0.19.0"
++version = "0.20.0"
description = "Runtime for OpenVibe Node plugins (JSON lines over stdin/stdout, deadlines, heartbeat, e-stop)"
requires-python = ">=3.9"
license = { text = "AGPL-3.0-or-later" }
ddiff --git a/plugins/sdk/tests/test_runtime.py b/plugins/sdk/tests/test_runtime.py
iindex 18a4eb6..9602c56 100644
---- a/plugins/sdk/tests/test_runtime.py
++++ b/plugins/sdk/tests/test_runtime.py
@@@ -439,7 +439,7 @@ class RuntimeTest(unittest.TestCase):
self.assertEqual(rt.stop_infos[-1].halt_epoch, 1792900000500)
def test_the_package_reports_the_node_release(self):
-- self.assertEqual(openvibe_plugin.__version__, "0.19.0")
++ self.assertEqual(openvibe_plugin.__version__, "0.20.0")
with open(os.path.join(os.path.dirname(HERE), "pyproject.toml")) as f:
self.assertIn('version = "%s"' % openvibe_plugin.__version__, f.read())
ddiff --git a/scripts/test-install-computer-config.sh b/scripts/test-install-computer-config.sh
iindex 93fe491..01acea8 100644
---- a/scripts/test-install-computer-config.sh
++++ b/scripts/test-install-computer-config.sh
@@@ -23,4 +23,22 @@ PY
if OPENVIBE_INSTALL_COMPUTER_CONFIG_ONLY="$conf" OPENVIBE_COMPUTER_HOME=/home/someone sh "$installer" --computer root 2>/dev/null; then
fail "an unknown level was accepted"
fi
++# A desk host (--desk-host, the Desks plan's step D5): trusted by default, its desks under its own account's home, no
++# notifications (nobody sits at it), the camera off; --computer still sets the level.
++host="$tmp/host.json"
++printf '{"server": "https://openvibe.bot", "plugins": [{"name": "dryrun"}], "video": {"source": "auto"}}\n' > "$host"
++OPENVIBE_INSTALL_COMPUTER_CONFIG_ONLY="$host" OPENVIBE_COMPUTER_HOME=/var/lib/openvibe-desk sh "$installer" --desk-host
++python3 - "$host" <<'PY' || fail "desk host config"
++import json, sys
++c = json.load(open(sys.argv[1]))
++assert c['video'] == {'source': 'off'}, c['video']
++assert c['plugins'] == [{'name': 'computer', 'config': {'level': 'trusted', 'roots': ['/var/lib/openvibe-desk'], 'notify': False,
++ 'allow_desk': True, 'desk_dir': '/var/lib/openvibe-desk/desks', 'browser_profile': '/var/lib/openvibe-desk/browser'}}], c['plugins']
++PY
++OPENVIBE_INSTALL_COMPUTER_CONFIG_ONLY="$host" OPENVIBE_COMPUTER_HOME=/var/lib/openvibe-desk sh "$installer" --desk-host --computer ask
++python3 - "$host" <<'PY' || fail "desk host level"
++import json, sys
++c = json.load(open(sys.argv[1]))
++assert [p['config']['level'] for p in c['plugins']] == ['ask'], c['plugins']
++PY
echo "ok: install.sh --computer config"