openvibers/openvibe.link Commits
v0.28.0: install.sh --ai sets up your own models: the ai plugin (kept when a driver is chosen later), which model server answers or how to get one, --ai-endpoint for another port (#75)
Changes
@@@ -63,7 +63,7 @@ minutes, and `openvibe-node stop` ends it at once ([the live screen](docs/comput**Your own models for your OpenVibe work:** the `ai` plugin lets OpenVibe use a model you already run at home (Ollama,LM Studio or llama.cpp's server) for your own work, at no cost and with no open port, once you turn on "Let my AI use--this computer's models" on openvibe.bot/machines ([docs/ai.md](docs/ai.md)).++this computer's models" on openvibe.bot/machines ([docs/ai.md](docs/ai.md)). The installer sets it up with `--ai`.**A machine that runs your jobs:** add it on openvibe.bot/machines, which prints the same installer with `--worker`: thejob worker runs each job as an unprivileged user in a sandbox, and the OpenVibe.Codes job runner lets you start a coding@@@ -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.27.0",-- "release": "v0.27.0, published 2026-10-27 as a GitHub release (your own models for your OpenVibe work: the ai plugin relays kind ai (contracts 0.219.0), one chat completion, to a local OpenAI-compatible model server (Ollama, LM Studio, llama.cpp), lists its models, runs one at a time, and reaches nothing else; the core routes ai to it like a computer command that never acts; v0.26.0: programs on a desk: exec takes desk, running in that desk's session so a program's windows open there, and detach, starting a program that keeps running on a whole desk (an office suite, a terminal) and ends with its desk; desk templates follow: research, office and dev desks; v0.25.0: the egress guard: every connection every agent browser makes, a page's own scripts included, goes through a proxy the plugin runs on 127.0.0.1 that resolves, checks and connects to the address it checked, so a page never reaches the machine's own network below level full; browser_egress: open turns it off; v0.24.1: a kept profile carries session cookies too: Chrome drops cookies that end with the browser as it quits, and a sign-in is often one, so a desk keeps them beside the profile and the next desk's browser gets them back as it starts, found by the production proof; v0.24.0: kept desk profiles: desk start takes profile, so a desk's browser starts from the person's kept profile (sign in once, on any desk) and saves it back when it stops, closing the browser gracefully so its cookies are written, the last desk to stop wins, caches never kept, and each desk's own copy removed; the browser follows a page its page opens (a sign-in popup, a new tab) and comes back when it closes; the agent's clicks count as a person's, so sites open their sign-in popups; v0.23.0: browser desks, the Desks plan's step D2: desk start takes kind browser, a headless browser alone with no display at about half the cost of a whole desk, on Linux or macOS; its page is its screen, mouse and keyboard and live view over the browser's pipe, exactly the desk's size; a desk host, which has no screen or input of its own, gets screen, screen_stream and input on its desks; a click on a named desk scales to that desk's size; v0.22.0: desks on a desk host, found by the first production proof: a desk's temp folder is short (Chrome's SingletonSocket must fit a Unix socket path, and the desks folder on a desk host was too deep, so Chrome closed as it started), and a desk no command used for desk_idle_s (15 minutes) stops by itself, so a desk an agent left behind when it crashed or restarted ends; v0.21.0: element refs and receipts, the Desks plan's step D3: the agent's browser lists a page's interactive elements as compact refs (e7 button \"Sign in\"), kept on the element while it stays, and click, press and back answer with what changed (url, lines that appeared, lines gone), so agents act on the page's structure before screenshots; v0.20.0: 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",++ "version": "0.28.0",++ "release": "v0.28.0, published 2026-10-27 as a GitHub release (install.sh --ai sets up your own models: it adds the ai plugin, which a driver chosen later keeps, says which model server answers (Ollama, LM Studio, llama.cpp) or how to get one, and --ai-endpoint names another port; v0.27.0: your own models for your OpenVibe work: the ai plugin relays kind ai (contracts 0.219.0), one chat completion, to a local OpenAI-compatible model server (Ollama, LM Studio, llama.cpp), lists its models, runs one at a time, and reaches nothing else; the core routes ai to it like a computer command that never acts; v0.26.0: programs on a desk: exec takes desk, running in that desk's session so a program's windows open there, and detach, starting a program that keeps running on a whole desk (an office suite, a terminal) and ends with its desk; desk templates follow: research, office and dev desks; v0.25.0: the egress guard: every connection every agent browser makes, a page's own scripts included, goes through a proxy the plugin runs on 127.0.0.1 that resolves, checks and connects to the address it checked, so a page never reaches the machine's own network below level full; browser_egress: open turns it off; v0.24.1: a kept profile carries session cookies too: Chrome drops cookies that end with the browser as it quits, and a sign-in is often one, so a desk keeps them beside the profile and the next desk's browser gets them back as it starts, found by the production proof; v0.24.0: kept desk profiles: desk start takes profile, so a desk's browser starts from the person's kept profile (sign in once, on any desk) and saves it back when it stops, closing the browser gracefully so its cookies are written, the last desk to stop wins, caches never kept, and each desk's own copy removed; the browser follows a page its page opens (a sign-in popup, a new tab) and comes back when it closes; the agent's clicks count as a person's, so sites open their sign-in popups; v0.23.0: browser desks, the Desks plan's step D2: desk start takes kind browser, a headless browser alone with no display at about half the cost of a whole desk, on Linux or macOS; its page is its screen, mouse and keyboard and live view over the browser's pipe, exactly the desk's size; a desk host, which has no screen or input of its own, gets screen, screen_stream and input on its desks; a click on a named desk scales to that desk's size; v0.22.0: desks on a desk host, found by the first production proof: a desk's temp folder is short (Chrome's SingletonSocket must fit a Unix socket path, and the desks folder on a desk host was too deep, so Chrome closed as it started), and a desk no command used for desk_idle_s (15 minutes) stops by itself, so a desk an agent left behind when it crashed or restarted ends; v0.21.0: element refs and receipts, the Desks plan's step D3: the agent's browser lists a page's interactive elements as compact refs (e7 button \"Sign in\"), kept on the element while it stays, and click, press and back answer with what changed (url, lines that appeared, lines gone), so agents act on the page's structure before screenshots; v0.20.0: 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,@@@ -19,7 +19,17 @@ queued. The audit on Bot keeps the model and the token counts, never the text.## Set it up--Add the plugin to the Node's config (`config.json`):++The installer does it with `--ai` (Node v0.28.0), added to the command openvibe.bot/machines gives you or to an++install you already have. It says which model server answers, or how to get one:++++```sh++curl -fsSL https://openvibe.bot/install | sh -s -- --network … --pairing pair_… --code … --ai++```++++`--ai-endpoint http://127.0.0.1:8090` names a server on another port (it implies `--ai`). `--ai` works with `--worker`,++`--computer` and `--desk-host` too, and a driver chosen later keeps it.++++By hand, add the plugin to the Node's config (`config.json`):```json{ "plugins": [{ "name": "ai" }] }@@@ -70,6 +70,10 @@ Its output is in `journalctl -u openvibe-setup` on the Pi.names the device on openvibe.bot (default: the hostname).3. Confirm the new device on openvibe.bot.++**Your own models:** add `--ai` to any of these commands. It adds the `ai` plugin, which finds Ollama, LM Studio or++llama.cpp's server on the machine, and the installer says which one answers. `--ai-endpoint URL` names another port.++[ai.md](ai.md) has the rest.++**A machine that runs your jobs, not a robot:** openvibe.bot/machines prints the Network form with `--worker` (and no`--driver`). It turns the job worker on with a dedicated `openvibe-job` user and the OpenVibe.Codes job runner, so acoding task you start on openvibe.codes/run runs on this machine. [worker.md](worker.md#a-machine-that-runs-your-coding-tasks---worker)@@@ -45,6 +45,11 @@# --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.++# --ai this machine's own models for its owner's OpenVibe work (docs/ai.md): adds the ai plugin, which++# finds Ollama, LM Studio or llama.cpp's server on this machine and relays its owner's questions to++# it, with no port opened. The installer says which one answers. Works with any of the above; on++# openvibe.bot/machines turn on "Let my AI use this computer's models" for it.++# --ai-endpoint URL the model server's http(s) base when it is not on one of the usual ports (implies --ai).# --no-service install and pair, but do not install the service.# --no-claim with no robot and no code: install without pairing (no code is asked for on openvibe.bot).# --setup KEY the osk_… setup key from card files made on openvibe.bot/setup/sd-card (or OPENVIBE_SETUP_KEY):@@@ -62,6 +67,7 @@# OPENVIBE_CRONTAB_USER=FILE edit FILE as the invoking user's crontab instead of the real one.# OPENVIBE_RC_LOCAL=FILE edit FILE as rc.local instead of /etc/rc.local.# OPENVIBE_INSTALL_PAIR_ARGS_ONLY=1 prints the `openvibe-node pair` arguments the flags make, one per line, and exits.++# OPENVIBE_INSTALL_AI_CONFIG_ONLY=<config.json> adds the --ai plugin entry to that file and exits.# OPENVIBE_INSTALL_WORKER_CONFIG_ONLY=<config.json> writes the --worker section into that file (OPENVIBE_JOB_UID,# OPENVIBE_JOB_GID, OPENVIBE_NODEJS and OPENVIBE_CODES_DIR stand in for the job user and the runner) and exits.set -eu@@@ -79,6 +85,8 @@ TAG="latest"WORKER=0COMPUTER=""DESK_HOST=0++AI=0++AI_ENDPOINT=""CLAIM_OK=1DRIVER_GIVEN=0SETUP="${OPENVIBE_SETUP_KEY:-}"@@@ -89,7 +97,8 @@ CODES_DIR=/opt/openvibe-codesusage() {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] [--desk-host] [--no-service] [--no-claim] [--setup osk_…]"++ echo " [--computer observe|ask|trusted|full] [--desk-host] [--ai] [--ai-endpoint URL]"++ echo " [--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"@@@ -121,6 +130,9 @@ while [ $# -gt 0 ]; do--pairing=*) PAIRING="${1#*=}"; shift ;;--worker) WORKER=1; shift ;;--desk-host) DESK_HOST=1; shift ;;++ --ai) AI=1; shift ;;++ --ai-endpoint) AI=1; AI_ENDPOINT="${2:-}"; shift 2 ;;++ --ai-endpoint=*) AI=1; AI_ENDPOINT="${1#*=}"; shift ;;--computer) COMPUTER="${2:-}"; shift 2 ;;--computer=*) COMPUTER="${1#*=}"; shift ;;--no-service) SERVICE=0; shift ;;@@@ -137,6 +149,7 @@ donecase "$DRIVER" in adeept|adeept-mecanum|cozmo|looi|ros2|ros2-holonomic|none) ;; *) die "--driver must be adeept, adeept-mecanum, cozmo, looi, ros2, ros2-holonomic or none" ;; esaccase "$ROBOT_ID" in ""|rob_*) ;; *) die "--robot must be a rob_… id" ;; esac++case "$AI_ENDPOINT" in ""|http://*|https://*) ;; *) die "--ai-endpoint is the model server's http(s) base, for example http://127.0.0.1:11434" ;; esac# --pairing takes the pair_… id; with no --code, anything else there is the code itself.case "$PAIRING" in ""|pair_*) ;; *) [ -z "$CODE" ] || die "--pairing must be a pair_… id"; CODE="$PAIRING"; PAIRING="" ;; esacif [ -n "$PAIRING" ] && [ -z "$NETWORK" ]; then die "--pairing needs --network, the OpenVibe.Network URL"; fi@@@ -146,11 +159,12 @@ case "$NETWORK" in ""|https://*|http://localhost*|http://127.0.0.1*) ;; *) die "if [ -n "$SETUP" ]; thenprintf '%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" ] || [ "$DESK_HOST" = 1 ]; then die "--setup cannot be used with --no-claim, --worker, --computer or --desk-host"; fi++ if [ "$CLAIM_OK" = 0 ] || [ "$WORKER" = 1 ] || [ -n "$COMPUTER" ] || [ "$DESK_HOST" = 1 ] || [ "$AI" = 1 ]; then die "--setup cannot be used with --no-claim, --worker, --computer, --desk-host or --ai"; fifi--# No robot, no code, no Network pairing, not a worker or a computer: the one command (openvibe.bot/link, see the top).++# No robot, no code, no Network pairing, not a worker, a computer or a model machine: the one command (openvibe.bot/link,++# see the top).CLAIM=0--if [ "$CLAIM_OK" = 1 ] && [ -z "$CODE$ROBOT_ID$NETWORK$PAIRING" ] && [ "$WORKER" = 0 ] && [ -z "$COMPUTER" ]; then CLAIM=1; fi++if [ "$CLAIM_OK" = 1 ] && [ -z "$CODE$ROBOT_ID$NETWORK$PAIRING" ] && [ "$WORKER" = 0 ] && [ -z "$COMPUTER" ] && [ "$AI" = 0 ]; then CLAIM=1; fi# Run "$@" pair … with the pairing the flags gave: through Network with --network, else Bot's legacy code.pair_with() {@@@ -171,7 +185,7 @@ if [ "${OPENVIBE_INSTALL_PAIR_ARGS_ONLY:-0}" = 1 ]; thenfi# set_driver_config <config.json>: $PLUGINS, $KIND and $VIDEO (driver_setup's) in that config, the rest kept (a computer--# plugin too). The one command writes the driver the bus suggested, then the one its owner chose, if that differs.++# or ai plugin too). The one command writes the driver the bus suggested, then the one its owner chose, if that differs.set_driver_config() {${SUDO:-} env OV_PLUGINS="$PLUGINS" OV_KIND="$KIND" OV_VIDEO="{$VIDEO}" python3 - "$1" <<'PY'import json, os, sys@@@ -181,7 +195,7 @@ try:conf = json.load(f)except (OSError, ValueError):conf = {}--keep = [p for p in conf.get('plugins') or [] if p.get('name') == 'computer']++keep = [p for p in conf.get('plugins') or [] if p.get('name') in ('computer', 'ai')]conf['plugins'] = json.loads(os.environ['OV_PLUGINS']) + keepconf['device_kind'] = os.environ['OV_KIND']conf['video'] = {**(conf.get('video') or {}), **json.loads(os.environ['OV_VIDEO'])}@@@ -330,6 +344,49 @@ os.replace(path + '.tmp', path)PY}++# write_ai_config <config.json> [endpoint]: add the ai plugin (replacing an earlier entry; the dry-run plugin goes), with++# the model server's base when one was given, else the plugin looks on the usual ports; the rest of the config is kept.++write_ai_config() {++ ${SUDO:-} python3 - "$@" <<'PY'++import json, os, sys++path = sys.argv[1]++endpoint = sys.argv[2] if len(sys.argv) > 2 else ''++try:++ with open(path) as f:++ conf = json.load(f)++except FileNotFoundError:++ conf = {}++plugins = [p for p in conf.get('plugins') or [] if p.get('name') not in ('ai', 'dryrun')]++plugins.append({'name': 'ai', 'config': {'endpoint': endpoint}} if endpoint else {'name': 'ai'})++conf['plugins'] = plugins++old = os.umask(0o077)++with open(path + '.tmp', 'w') as f:++ json.dump(conf, f, indent=2)++ f.write('\n')++os.umask(old)++os.replace(path + '.tmp', path)++PY++}++++# ai_found [endpoint]: the first model server that answers (the one given, else Ollama, LM Studio, llama.cpp's server) and++# how many models it offers, as one line; nothing when none answers.++ai_found() {++ python3 - "$@" <<'PY' 2>/dev/null || true++import json, sys, urllib.request++given = sys.argv[1] if len(sys.argv) > 1 else ''++servers = [(given, 'the model server')] if given else [('http://127.0.0.1:11434', 'Ollama'), ('http://127.0.0.1:1234', 'LM Studio'), ('http://127.0.0.1:8080', "llama.cpp's server")]++for base, name in servers:++ try:++ with urllib.request.urlopen(base.rstrip('/') + '/v1/models', timeout=3) as r:++ data = json.load(r).get('data') or []++ except Exception:++ continue++ models = [str(m.get('id')) for m in data if isinstance(m, dict) and m.get('id')]++ print('%s at %s with %d model%s%s' % (name, base, len(models), '' if len(models) == 1 else 's', (': ' + ', '.join(models[:5]) + (', …' if len(models) > 5 else '')) if models else ''))++ break++PY++}++write_worker_config() {${SUDO:-} python3 - "$@" <<'PY'import json, os, sys@@@ -388,6 +445,11 @@ if [ -n "${OPENVIBE_INSTALL_WORKER_CONFIG_ONLY:-}" ]; then${OPENVIBE_NODEJS:+"$OPENVIBE_NODEJS"} ${OPENVIBE_CODES_DIR:+"$OPENVIBE_CODES_DIR"}exit 0fi++if [ -n "${OPENVIBE_INSTALL_AI_CONFIG_ONLY:-}" ]; then++ [ "$AI" = 1 ] || die "OPENVIBE_INSTALL_AI_CONFIG_ONLY needs --ai or --ai-endpoint"++ write_ai_config "$OPENVIBE_INSTALL_AI_CONFIG_ONLY" ${AI_ENDPOINT:+"$AI_ENDPOINT"}++ exit 0++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@@@ -797,6 +859,20 @@ elif [ -n "$COMPUTER" ]; thenfifi++# ---- your own models (--ai): the ai plugin, and which model server answers now ----++if [ "$AI" = 1 ]; then++ write_ai_config "$CONF/config.json" ${AI_ENDPOINT:+"$AI_ENDPOINT"}++ found="$(ai_found ${AI_ENDPOINT:+"$AI_ENDPOINT"})"++ if [ -n "$found" ]; then++ say "your own models: found $found"++ else++ say "your own models: no model server answers yet. Install one (Ollama: https://ollama.com, then: ollama pull llama3.1:8b),"++ say " or start LM Studio's or llama.cpp's server; the Node finds it on its usual port (--ai-endpoint URL for another)."++ fi++ say "on https://openvibe.bot/machines turn on \"Let my AI use this computer's models\" for this machine: OpenVibe then"++ say " asks them for your own work only (your Vibe, your AI members), and nobody else's."++fi++# ---- the job worker (--worker): the job user, the egress tools, the Codes job runner, the worker config ----if [ "$WORKER" = 1 ]; thenif ! id openvibe-job >/dev/null 2>&1; then@@@ -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.27.0"++__version__ = "0.28.0"PROTOCOL_VERSION = 1HEARTBEAT_TIMEOUT_S = 1.0DEFAULT_DEADLINE_MS = 300@@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"[project]name = "openvibe-plugin"--version = "0.27.0"++version = "0.28.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" }@@@ -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.27.0")++ self.assertEqual(openvibe_plugin.__version__, "0.28.0")with open(os.path.join(os.path.dirname(HERE), "pyproject.toml")) as f:self.assertIn('version = "%s"' % openvibe_plugin.__version__, f.read())@@@ -41,4 +41,21 @@ import json, sysc = json.load(open(sys.argv[1]))assert [p['config']['level'] for p in c['plugins']] == ['ask'], c['plugins']PY++# --ai (AI on your own machines): the ai plugin, in place of the dry-run plugin and of an earlier ai entry, the rest kept;++# --ai-endpoint gives the server's base, and a base that is not http(s) is refused. A driver rewrite keeps it.++ai="$tmp/ai.json"++printf '{"server": "https://openvibe.bot", "plugins": [{"name": "dryrun"}, {"name": "computer", "config": {"level": "ask"}}]}\n' > "$ai"++OPENVIBE_INSTALL_AI_CONFIG_ONLY="$ai" sh "$installer" --ai++OPENVIBE_INSTALL_AI_CONFIG_ONLY="$ai" sh "$installer" --ai-endpoint http://127.0.0.1:8090++python3 - "$ai" <<'PY' || fail "ai config"++import json, sys++c = json.load(open(sys.argv[1]))++assert c['server'] == 'https://openvibe.bot'++assert c['plugins'] == [{'name': 'computer', 'config': {'level': 'ask'}}, {'name': 'ai', 'config': {'endpoint': 'http://127.0.0.1:8090'}}], c['plugins']++PY++if OPENVIBE_INSTALL_AI_CONFIG_ONLY="$ai" sh "$installer" --ai-endpoint ftp://x 2>/dev/null; then++ fail "a base that is not http(s) was accepted"++fi++grep -q "if p.get('name') in ('computer', 'ai')" "$installer" || fail "a driver rewrite must keep the ai plugin"echo "ok: install.sh --computer config"++echo "ok: install.sh --ai config"
@@ -63,7 +63,7 @@ minutes, and `openvibe-node stop` ends it at once ([the live screen](docs/comput
@@ -63,7 +63,7 @@ minutes, and `openvibe-node stop` ends it at once ([the live screen](docs/comput
**Your own models for your OpenVibe work:** the `ai` plugin lets OpenVibe use a model you already run at home (Ollama,
**Your own models for your OpenVibe work:** the `ai` plugin lets OpenVibe use a model you already run at home (Ollama,
LM Studio or llama.cpp's server) for your own work, at no cost and with no open port, once you turn on "Let my AI use
LM Studio or llama.cpp's server) for your own work, at no cost and with no open port, once you turn on "Let my AI use
-this computer's models" on openvibe.bot/machines ([docs/ai.md](docs/ai.md)).
+this computer's models" on openvibe.bot/machines ([docs/ai.md](docs/ai.md)). The installer sets it up with `--ai`.
**A machine that runs your jobs:** add it on openvibe.bot/machines, which prints the same installer with `--worker`: the
**A machine that runs your jobs:** add it on openvibe.bot/machines, which prints the same installer with `--worker`: the
job worker runs each job as an unprivileged user in a sandbox, and the OpenVibe.Codes job runner lets you start a coding
job worker runs each job as an unprivileged user in a sandbox, and the OpenVibe.Codes job runner lets you start a coding
@@ -5,8 +5,8 @@
@@ -5,8 +5,8 @@
"wave": "T15",
"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)",
"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",
"kind": "client",
- "version": "0.27.0",
- "release": "v0.27.0, published 2026-10-27 as a GitHub release (your own models for your OpenVibe work: the ai plugin relays kind ai (contracts 0.219.0), one chat completion, to a local OpenAI-compatible model server (Ollama, LM Studio, llama.cpp), lists its models, runs one at a time, and reaches nothing else; the core routes ai to it like a computer command that never acts; v0.26.0: programs on a desk: exec takes desk, running in that desk's session so a program's windows open there, and detach, starting a program that keeps running on a whole desk (an office suite, a terminal) and ends with its desk; desk templates follow: research, office and dev desks; v0.25.0: the egress guard: every connection every agent browser makes, a page's own scripts included, goes through a proxy the plugin runs on 127.0.0.1 that resolves, checks and connects to the address it checked, so a page never reaches the machine's own network below level full; browser_egress: open turns it off; v0.24.1: a kept profile carries session cookies too: Chrome drops cookies that end with the browser as it quits, and a sign-in is often one, so a desk keeps them beside the profile and the next desk's browser gets them back as it starts, found by the production proof; v0.24.0: kept desk profiles: desk start takes profile, so a desk's browser starts from the person's kept profile (sign in once, on any desk) and saves it back when it stops, closing the browser gracefully so its cookies are written, the last desk to stop wins, caches never kept, and each desk's own copy removed; the browser follows a page its page opens (a sign-in popup, a new tab) and comes back when it closes; the agent's clicks count as a person's, so sites open their sign-in popups; v0.23.0: browser desks, the Desks plan's step D2: desk start takes kind browser, a headless browser alone with no display at about half the cost of a whole desk, on Linux or macOS; its page is its screen, mouse and keyboard and live view over the browser's pipe, exactly the desk's size; a desk host, which has no screen or input of its own, gets screen, screen_stream and input on its desks; a click on a named desk scales to that desk's size; v0.22.0: desks on a desk host, found by the first production proof: a desk's temp folder is short (Chrome's SingletonSocket must fit a Unix socket path, and the desks folder on a desk host was too deep, so Chrome closed as it started), and a desk no command used for desk_idle_s (15 minutes) stops by itself, so a desk an agent left behind when it crashed or restarted ends; v0.21.0: element refs and receipts, the Desks plan's step D3: the agent's browser lists a page's interactive elements as compact refs (e7 button \"Sign in\"), kept on the element while it stays, and click, press and back answer with what changed (url, lines that appeared, lines gone), so agents act on the page's structure before screenshots; v0.20.0: 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",
+ "version": "0.28.0",
+ "release": "v0.28.0, published 2026-10-27 as a GitHub release (install.sh --ai sets up your own models: it adds the ai plugin, which a driver chosen later keeps, says which model server answers (Ollama, LM Studio, llama.cpp) or how to get one, and --ai-endpoint names another port; v0.27.0: your own models for your OpenVibe work: the ai plugin relays kind ai (contracts 0.219.0), one chat completion, to a local OpenAI-compatible model server (Ollama, LM Studio, llama.cpp), lists its models, runs one at a time, and reaches nothing else; the core routes ai to it like a computer command that never acts; v0.26.0: programs on a desk: exec takes desk, running in that desk's session so a program's windows open there, and detach, starting a program that keeps running on a whole desk (an office suite, a terminal) and ends with its desk; desk templates follow: research, office and dev desks; v0.25.0: the egress guard: every connection every agent browser makes, a page's own scripts included, goes through a proxy the plugin runs on 127.0.0.1 that resolves, checks and connects to the address it checked, so a page never reaches the machine's own network below level full; browser_egress: open turns it off; v0.24.1: a kept profile carries session cookies too: Chrome drops cookies that end with the browser as it quits, and a sign-in is often one, so a desk keeps them beside the profile and the next desk's browser gets them back as it starts, found by the production proof; v0.24.0: kept desk profiles: desk start takes profile, so a desk's browser starts from the person's kept profile (sign in once, on any desk) and saves it back when it stops, closing the browser gracefully so its cookies are written, the last desk to stop wins, caches never kept, and each desk's own copy removed; the browser follows a page its page opens (a sign-in popup, a new tab) and comes back when it closes; the agent's clicks count as a person's, so sites open their sign-in popups; v0.23.0: browser desks, the Desks plan's step D2: desk start takes kind browser, a headless browser alone with no display at about half the cost of a whole desk, on Linux or macOS; its page is its screen, mouse and keyboard and live view over the browser's pipe, exactly the desk's size; a desk host, which has no screen or input of its own, gets screen, screen_stream and input on its desks; a click on a named desk scales to that desk's size; v0.22.0: desks on a desk host, found by the first production proof: a desk's temp folder is short (Chrome's SingletonSocket must fit a Unix socket path, and the desks folder on a desk host was too deep, so Chrome closed as it started), and a desk no command used for desk_idle_s (15 minutes) stops by itself, so a desk an agent left behind when it crashed or restarted ends; v0.21.0: element refs and receipts, the Desks plan's step D3: the agent's browser lists a page's interactive elements as compact refs (e7 button \"Sign in\"), kept on the element while it stays, and click, press and back answer with what changed (url, lines that appeared, lines gone), so agents act on the page's structure before screenshots; v0.20.0: 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",
"runtime": "go 1.24 (one static binary per platform); plugins in Python 3; an ESP32 Arduino library",
"database": "none",
"database": "none",
"deployed": false,
"deployed": false,
@@ -19,7 +19,17 @@ queued. The audit on Bot keeps the model and the token counts, never the text.
@@ -19,7 +19,17 @@ queued. The audit on Bot keeps the model and the token counts, never the text.
## Set it up
## Set it up
-Add the plugin to the Node's config (`config.json`):
+The installer does it with `--ai` (Node v0.28.0), added to the command openvibe.bot/machines gives you or to an
+install you already have. It says which model server answers, or how to get one:
+
+```sh
+curl -fsSL https://openvibe.bot/install | sh -s -- --network … --pairing pair_… --code … --ai
+```
+
+`--ai-endpoint http://127.0.0.1:8090` names a server on another port (it implies `--ai`). `--ai` works with `--worker`,
+`--computer` and `--desk-host` too, and a driver chosen later keeps it.
+
+By hand, add the plugin to the Node's config (`config.json`):
```json
```json
{ "plugins": [{ "name": "ai" }] }
{ "plugins": [{ "name": "ai" }] }
@@ -70,6 +70,10 @@ Its output is in `journalctl -u openvibe-setup` on the Pi.
@@ -70,6 +70,10 @@ Its output is in `journalctl -u openvibe-setup` on the Pi.
names the device on openvibe.bot (default: the hostname).
names the device on openvibe.bot (default: the hostname).
3. Confirm the new device on openvibe.bot.
3. Confirm the new device on openvibe.bot.
+**Your own models:** add `--ai` to any of these commands. It adds the `ai` plugin, which finds Ollama, LM Studio or
+llama.cpp's server on the machine, and the installer says which one answers. `--ai-endpoint URL` names another port.
+[ai.md](ai.md) has the rest.
+
**A machine that runs your jobs, not a robot:** openvibe.bot/machines prints the Network form with `--worker` (and no
**A machine that runs your jobs, not a robot:** openvibe.bot/machines prints the Network form with `--worker` (and no
`--driver`). It turns the job worker on with a dedicated `openvibe-job` user and the OpenVibe.Codes job runner, so a
`--driver`). It turns the job worker on with a dedicated `openvibe-job` user and the OpenVibe.Codes job runner, so a
coding task you start on openvibe.codes/run runs on this machine. [worker.md](worker.md#a-machine-that-runs-your-coding-tasks---worker)
coding task you start on openvibe.codes/run runs on this machine. [worker.md](worker.md#a-machine-that-runs-your-coding-tasks---worker)
@@ -45,6 +45,11 @@
@@ -45,6 +45,11 @@
# --computer LEVEL this machine's terminal, files and screen for its owner's agents (the computer plugin,
# --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
# 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.
# (the one that ran sudo), never root, and its folders are your home. Linux only; not with --worker.
+# --ai this machine's own models for its owner's OpenVibe work (docs/ai.md): adds the ai plugin, which
+# finds Ollama, LM Studio or llama.cpp's server on this machine and relays its owner's questions to
+# it, with no port opened. The installer says which one answers. Works with any of the above; on
+# openvibe.bot/machines turn on "Let my AI use this computer's models" for it.
+# --ai-endpoint URL the model server's http(s) base when it is not on one of the usual ports (implies --ai).
# --no-service install and pair, but do not install the service.
# --no-service install and pair, but do not install the service.
# --no-claim with no robot and no code: install without pairing (no code is asked for on openvibe.bot).
# --no-claim with no robot and no code: install without pairing (no code is asked for on openvibe.bot).
# --setup KEY the osk_… setup key from card files made on openvibe.bot/setup/sd-card (or OPENVIBE_SETUP_KEY):
# --setup KEY the osk_… setup key from card files made on openvibe.bot/setup/sd-card (or OPENVIBE_SETUP_KEY):
@@ -62,6 +67,7 @@
@@ -62,6 +67,7 @@
# OPENVIBE_CRONTAB_USER=FILE edit FILE as the invoking user's crontab instead of the real one.
# OPENVIBE_CRONTAB_USER=FILE edit FILE as the invoking user's crontab instead of the real one.
# OPENVIBE_RC_LOCAL=FILE edit FILE as rc.local instead of /etc/rc.local.
# OPENVIBE_RC_LOCAL=FILE edit FILE as rc.local instead of /etc/rc.local.
# OPENVIBE_INSTALL_PAIR_ARGS_ONLY=1 prints the `openvibe-node pair` arguments the flags make, one per line, and exits.
# OPENVIBE_INSTALL_PAIR_ARGS_ONLY=1 prints the `openvibe-node pair` arguments the flags make, one per line, and exits.
+# OPENVIBE_INSTALL_AI_CONFIG_ONLY=<config.json> adds the --ai plugin entry to that file and exits.
# OPENVIBE_INSTALL_WORKER_CONFIG_ONLY=<config.json> writes the --worker section into that file (OPENVIBE_JOB_UID,
# OPENVIBE_INSTALL_WORKER_CONFIG_ONLY=<config.json> writes the --worker section into that file (OPENVIBE_JOB_UID,
# OPENVIBE_JOB_GID, OPENVIBE_NODEJS and OPENVIBE_CODES_DIR stand in for the job user and the runner) and exits.
# OPENVIBE_JOB_GID, OPENVIBE_NODEJS and OPENVIBE_CODES_DIR stand in for the job user and the runner) and exits.
set -eu
set -eu
@@ -79,6 +85,8 @@ TAG="latest"
@@ -79,6 +85,8 @@ TAG="latest"
WORKER=0
WORKER=0
COMPUTER=""
COMPUTER=""
DESK_HOST=0
DESK_HOST=0
+AI=0
+AI_ENDPOINT=""
CLAIM_OK=1
CLAIM_OK=1
DRIVER_GIVEN=0
DRIVER_GIVEN=0
SETUP="${OPENVIBE_SETUP_KEY:-}"
SETUP="${OPENVIBE_SETUP_KEY:-}"
@@ -89,7 +97,8 @@ CODES_DIR=/opt/openvibe-codes
@@ -89,7 +97,8 @@ CODES_DIR=/opt/openvibe-codes
usage() {
usage() {
echo "usage: install.sh [--robot rob_… | --network URL --pairing pair_…] [--code CODE | CODE] [--name NAME]"
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 " [--driver adeept|adeept-mecanum|cozmo|looi|ros2|ros2-holonomic|none] [--worker]"
- echo " [--computer observe|ask|trusted|full] [--desk-host] [--no-service] [--no-claim] [--setup osk_…]"
+ echo " [--computer observe|ask|trusted|full] [--desk-host] [--ai] [--ai-endpoint URL]"
+ echo " [--no-service] [--no-claim] [--setup osk_…]"
echo " [--local DIR] [--version TAG]"
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 (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"
echo " curl -fsSL https://openvibe.bot/install | sh -s -- --robot rob_… --code ABCD-1234 --driver adeept"
@@ -121,6 +130,9 @@ while [ $# -gt 0 ]; do
@@ -121,6 +130,9 @@ while [ $# -gt 0 ]; do
--pairing=*) PAIRING="${1#*=}"; shift ;;
--pairing=*) PAIRING="${1#*=}"; shift ;;
--worker) WORKER=1; shift ;;
--worker) WORKER=1; shift ;;
--desk-host) DESK_HOST=1; shift ;;
--desk-host) DESK_HOST=1; shift ;;
+ --ai) AI=1; shift ;;
+ --ai-endpoint) AI=1; AI_ENDPOINT="${2:-}"; shift 2 ;;
+ --ai-endpoint=*) AI=1; AI_ENDPOINT="${1#*=}"; shift ;;
--computer) COMPUTER="${2:-}"; shift 2 ;;
--computer) COMPUTER="${2:-}"; shift 2 ;;
--computer=*) COMPUTER="${1#*=}"; shift ;;
--computer=*) COMPUTER="${1#*=}"; shift ;;
--no-service) SERVICE=0; shift ;;
--no-service) SERVICE=0; shift ;;
@@ -137,6 +149,7 @@ done
@@ -137,6 +149,7 @@ done
case "$DRIVER" in adeept|adeept-mecanum|cozmo|looi|ros2|ros2-holonomic|none) ;; *) die "--driver must be adeept, adeept-mecanum, cozmo, looi, ros2, ros2-holonomic or none" ;; esac
case "$DRIVER" in adeept|adeept-mecanum|cozmo|looi|ros2|ros2-holonomic|none) ;; *) die "--driver must be adeept, adeept-mecanum, cozmo, looi, ros2, ros2-holonomic or none" ;; esac
case "$ROBOT_ID" in ""|rob_*) ;; *) die "--robot must be a rob_… id" ;; esac
case "$ROBOT_ID" in ""|rob_*) ;; *) die "--robot must be a rob_… id" ;; esac
+case "$AI_ENDPOINT" in ""|http://*|https://*) ;; *) die "--ai-endpoint is the model server's http(s) base, for example http://127.0.0.1:11434" ;; esac
# --pairing takes the pair_… id; with no --code, anything else there is the code itself.
# --pairing takes the pair_… id; with no --code, anything else there is the code itself.
case "$PAIRING" in ""|pair_*) ;; *) [ -z "$CODE" ] || die "--pairing must be a pair_… id"; CODE="$PAIRING"; PAIRING="" ;; esac
case "$PAIRING" in ""|pair_*) ;; *) [ -z "$CODE" ] || die "--pairing must be a pair_… id"; CODE="$PAIRING"; PAIRING="" ;; esac
if [ -n "$PAIRING" ] && [ -z "$NETWORK" ]; then die "--pairing needs --network, the OpenVibe.Network URL"; fi
if [ -n "$PAIRING" ] && [ -z "$NETWORK" ]; then die "--pairing needs --network, the OpenVibe.Network URL"; fi
@@ -146,11 +159,12 @@ case "$NETWORK" in ""|https://*|http://localhost*|http://127.0.0.1*) ;; *) die "
@@ -146,11 +159,12 @@ case "$NETWORK" in ""|https://*|http://localhost*|http://127.0.0.1*) ;; *) die "
if [ -n "$SETUP" ]; then
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)"
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)"
[ -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" ] || [ "$DESK_HOST" = 1 ]; then die "--setup cannot be used with --no-claim, --worker, --computer or --desk-host"; fi
+ if [ "$CLAIM_OK" = 0 ] || [ "$WORKER" = 1 ] || [ -n "$COMPUTER" ] || [ "$DESK_HOST" = 1 ] || [ "$AI" = 1 ]; then die "--setup cannot be used with --no-claim, --worker, --computer, --desk-host or --ai"; fi
fi
fi
-# No robot, no code, no Network pairing, not a worker or a computer: the one command (openvibe.bot/link, see the top).
+# No robot, no code, no Network pairing, not a worker, a computer or a model machine: the one command (openvibe.bot/link,
+# see the top).
CLAIM=0
CLAIM=0
-if [ "$CLAIM_OK" = 1 ] && [ -z "$CODE$ROBOT_ID$NETWORK$PAIRING" ] && [ "$WORKER" = 0 ] && [ -z "$COMPUTER" ]; then CLAIM=1; fi
+if [ "$CLAIM_OK" = 1 ] && [ -z "$CODE$ROBOT_ID$NETWORK$PAIRING" ] && [ "$WORKER" = 0 ] && [ -z "$COMPUTER" ] && [ "$AI" = 0 ]; then CLAIM=1; fi
# Run "$@" pair … with the pairing the flags gave: through Network with --network, else Bot's legacy code.
# Run "$@" pair … with the pairing the flags gave: through Network with --network, else Bot's legacy code.
pair_with() {
pair_with() {
@@ -171,7 +185,7 @@ if [ "${OPENVIBE_INSTALL_PAIR_ARGS_ONLY:-0}" = 1 ]; then
@@ -171,7 +185,7 @@ if [ "${OPENVIBE_INSTALL_PAIR_ARGS_ONLY:-0}" = 1 ]; then
fi
fi
# set_driver_config <config.json>: $PLUGINS, $KIND and $VIDEO (driver_setup's) in that config, the rest kept (a computer
# set_driver_config <config.json>: $PLUGINS, $KIND and $VIDEO (driver_setup's) in that config, the rest kept (a computer
-# plugin too). The one command writes the driver the bus suggested, then the one its owner chose, if that differs.
+# or ai plugin too). The one command writes the driver the bus suggested, then the one its owner chose, if that differs.
set_driver_config() {
set_driver_config() {
${SUDO:-} env OV_PLUGINS="$PLUGINS" OV_KIND="$KIND" OV_VIDEO="{$VIDEO}" python3 - "$1" <<'PY'
${SUDO:-} env OV_PLUGINS="$PLUGINS" OV_KIND="$KIND" OV_VIDEO="{$VIDEO}" python3 - "$1" <<'PY'
import json, os, sys
import json, os, sys
@@ -181,7 +195,7 @@ try:
@@ -181,7 +195,7 @@ try:
conf = json.load(f)
conf = json.load(f)
except (OSError, ValueError):
except (OSError, ValueError):
conf = {}
conf = {}
-keep = [p for p in conf.get('plugins') or [] if p.get('name') == 'computer']
+keep = [p for p in conf.get('plugins') or [] if p.get('name') in ('computer', 'ai')]
conf['plugins'] = json.loads(os.environ['OV_PLUGINS']) + keep
conf['plugins'] = json.loads(os.environ['OV_PLUGINS']) + keep
conf['device_kind'] = os.environ['OV_KIND']
conf['device_kind'] = os.environ['OV_KIND']
conf['video'] = {**(conf.get('video') or {}), **json.loads(os.environ['OV_VIDEO'])}
conf['video'] = {**(conf.get('video') or {}), **json.loads(os.environ['OV_VIDEO'])}
@@ -330,6 +344,49 @@ os.replace(path + '.tmp', path)
@@ -330,6 +344,49 @@ os.replace(path + '.tmp', path)
PY
PY
}
}
+# write_ai_config <config.json> [endpoint]: add the ai plugin (replacing an earlier entry; the dry-run plugin goes), with
+# the model server's base when one was given, else the plugin looks on the usual ports; the rest of the config is kept.
+write_ai_config() {
+ ${SUDO:-} python3 - "$@" <<'PY'
+import json, os, sys
+path = sys.argv[1]
+endpoint = sys.argv[2] if len(sys.argv) > 2 else ''
+try:
+ with open(path) as f:
+ conf = json.load(f)
+except FileNotFoundError:
+ conf = {}
+plugins = [p for p in conf.get('plugins') or [] if p.get('name') not in ('ai', 'dryrun')]
+plugins.append({'name': 'ai', 'config': {'endpoint': endpoint}} if endpoint else {'name': 'ai'})
+conf['plugins'] = plugins
+old = os.umask(0o077)
+with open(path + '.tmp', 'w') as f:
+ json.dump(conf, f, indent=2)
+ f.write('\n')
+os.umask(old)
+os.replace(path + '.tmp', path)
+PY
+}
+
+# ai_found [endpoint]: the first model server that answers (the one given, else Ollama, LM Studio, llama.cpp's server) and
+# how many models it offers, as one line; nothing when none answers.
+ai_found() {
+ python3 - "$@" <<'PY' 2>/dev/null || true
+import json, sys, urllib.request
+given = sys.argv[1] if len(sys.argv) > 1 else ''
+servers = [(given, 'the model server')] if given else [('http://127.0.0.1:11434', 'Ollama'), ('http://127.0.0.1:1234', 'LM Studio'), ('http://127.0.0.1:8080', "llama.cpp's server")]
+for base, name in servers:
+ try:
+ with urllib.request.urlopen(base.rstrip('/') + '/v1/models', timeout=3) as r:
+ data = json.load(r).get('data') or []
+ except Exception:
+ continue
+ models = [str(m.get('id')) for m in data if isinstance(m, dict) and m.get('id')]
+ print('%s at %s with %d model%s%s' % (name, base, len(models), '' if len(models) == 1 else 's', (': ' + ', '.join(models[:5]) + (', …' if len(models) > 5 else '')) if models else ''))
+ break
+PY
+}
+
write_worker_config() {
write_worker_config() {
${SUDO:-} python3 - "$@" <<'PY'
${SUDO:-} python3 - "$@" <<'PY'
import json, os, sys
import json, os, sys
@@ -388,6 +445,11 @@ if [ -n "${OPENVIBE_INSTALL_WORKER_CONFIG_ONLY:-}" ]; then
@@ -388,6 +445,11 @@ if [ -n "${OPENVIBE_INSTALL_WORKER_CONFIG_ONLY:-}" ]; then
${OPENVIBE_NODEJS:+"$OPENVIBE_NODEJS"} ${OPENVIBE_CODES_DIR:+"$OPENVIBE_CODES_DIR"}
${OPENVIBE_NODEJS:+"$OPENVIBE_NODEJS"} ${OPENVIBE_CODES_DIR:+"$OPENVIBE_CODES_DIR"}
exit 0
exit 0
fi
fi
+if [ -n "${OPENVIBE_INSTALL_AI_CONFIG_ONLY:-}" ]; then
+ [ "$AI" = 1 ] || die "OPENVIBE_INSTALL_AI_CONFIG_ONLY needs --ai or --ai-endpoint"
+ write_ai_config "$OPENVIBE_INSTALL_AI_CONFIG_ONLY" ${AI_ENDPOINT:+"$AI_ENDPOINT"}
+ exit 0
+fi
# OPENVIBE_INSTALL_COMPUTER_CONFIG_ONLY=<config.json> with --computer LEVEL and OPENVIBE_COMPUTER_HOME adds the computer
# 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).
# plugin to that config and exits (scripts/test-install-computer-config.sh).
if [ -n "${OPENVIBE_INSTALL_COMPUTER_CONFIG_ONLY:-}" ]; then
if [ -n "${OPENVIBE_INSTALL_COMPUTER_CONFIG_ONLY:-}" ]; then
@@ -797,6 +859,20 @@ elif [ -n "$COMPUTER" ]; then
@@ -797,6 +859,20 @@ elif [ -n "$COMPUTER" ]; then
fi
fi
fi
fi
+# ---- your own models (--ai): the ai plugin, and which model server answers now ----
+if [ "$AI" = 1 ]; then
+ write_ai_config "$CONF/config.json" ${AI_ENDPOINT:+"$AI_ENDPOINT"}
+ found="$(ai_found ${AI_ENDPOINT:+"$AI_ENDPOINT"})"
+ if [ -n "$found" ]; then
+ say "your own models: found $found"
+ else
+ say "your own models: no model server answers yet. Install one (Ollama: https://ollama.com, then: ollama pull llama3.1:8b),"
+ say " or start LM Studio's or llama.cpp's server; the Node finds it on its usual port (--ai-endpoint URL for another)."
+ fi
+ say "on https://openvibe.bot/machines turn on \"Let my AI use this computer's models\" for this machine: OpenVibe then"
+ say " asks them for your own work only (your Vibe, your AI members), and nobody else's."
+fi
+
# ---- the job worker (--worker): the job user, the egress tools, the Codes job runner, the worker config ----
# ---- the job worker (--worker): the job user, the egress tools, the Codes job runner, the worker config ----
if [ "$WORKER" = 1 ]; then
if [ "$WORKER" = 1 ]; then
if ! id openvibe-job >/dev/null 2>&1; then
if ! id openvibe-job >/dev/null 2>&1; then
@@ -24,7 +24,7 @@ __all__ = ["Plugin", "Fault", "Deferred", "Command", "StopInfo", "Runtime", "run
@@ -24,7 +24,7 @@ __all__ = ["Plugin", "Fault", "Deferred", "Command", "StopInfo", "Runtime", "run
"RUNTIME_CAUSES", "SHARED_CLOCK", "__version__"]
"RUNTIME_CAUSES", "SHARED_CLOCK", "__version__"]
# The Node release this runtime ships with (pyproject.toml carries the same).
# The Node release this runtime ships with (pyproject.toml carries the same).
-__version__ = "0.27.0"
+__version__ = "0.28.0"
PROTOCOL_VERSION = 1
PROTOCOL_VERSION = 1
HEARTBEAT_TIMEOUT_S = 1.0
HEARTBEAT_TIMEOUT_S = 1.0
DEFAULT_DEADLINE_MS = 300
DEFAULT_DEADLINE_MS = 300
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
[project]
name = "openvibe-plugin"
name = "openvibe-plugin"
-version = "0.27.0"
+version = "0.28.0"
description = "Runtime for OpenVibe Node plugins (JSON lines over stdin/stdout, deadlines, heartbeat, e-stop)"
description = "Runtime for OpenVibe Node plugins (JSON lines over stdin/stdout, deadlines, heartbeat, e-stop)"
requires-python = ">=3.9"
requires-python = ">=3.9"
license = { text = "AGPL-3.0-or-later" }
license = { text = "AGPL-3.0-or-later" }
@@ -439,7 +439,7 @@ class RuntimeTest(unittest.TestCase):
@@ -439,7 +439,7 @@ class RuntimeTest(unittest.TestCase):
self.assertEqual(rt.stop_infos[-1].halt_epoch, 1792900000500)
self.assertEqual(rt.stop_infos[-1].halt_epoch, 1792900000500)
def test_the_package_reports_the_node_release(self):
def test_the_package_reports_the_node_release(self):
- self.assertEqual(openvibe_plugin.__version__, "0.27.0")
+ self.assertEqual(openvibe_plugin.__version__, "0.28.0")
with open(os.path.join(os.path.dirname(HERE), "pyproject.toml")) as f:
with open(os.path.join(os.path.dirname(HERE), "pyproject.toml")) as f:
self.assertIn('version = "%s"' % openvibe_plugin.__version__, f.read())
self.assertIn('version = "%s"' % openvibe_plugin.__version__, f.read())
@@ -41,4 +41,21 @@ import json, sys
@@ -41,4 +41,21 @@ import json, sys
c = json.load(open(sys.argv[1]))
c = json.load(open(sys.argv[1]))
assert [p['config']['level'] for p in c['plugins']] == ['ask'], c['plugins']
assert [p['config']['level'] for p in c['plugins']] == ['ask'], c['plugins']
PY
PY
+# --ai (AI on your own machines): the ai plugin, in place of the dry-run plugin and of an earlier ai entry, the rest kept;
+# --ai-endpoint gives the server's base, and a base that is not http(s) is refused. A driver rewrite keeps it.
+ai="$tmp/ai.json"
+printf '{"server": "https://openvibe.bot", "plugins": [{"name": "dryrun"}, {"name": "computer", "config": {"level": "ask"}}]}\n' > "$ai"
+OPENVIBE_INSTALL_AI_CONFIG_ONLY="$ai" sh "$installer" --ai
+OPENVIBE_INSTALL_AI_CONFIG_ONLY="$ai" sh "$installer" --ai-endpoint http://127.0.0.1:8090
+python3 - "$ai" <<'PY' || fail "ai config"
+import json, sys
+c = json.load(open(sys.argv[1]))
+assert c['server'] == 'https://openvibe.bot'
+assert c['plugins'] == [{'name': 'computer', 'config': {'level': 'ask'}}, {'name': 'ai', 'config': {'endpoint': 'http://127.0.0.1:8090'}}], c['plugins']
+PY
+if OPENVIBE_INSTALL_AI_CONFIG_ONLY="$ai" sh "$installer" --ai-endpoint ftp://x 2>/dev/null; then
+ fail "a base that is not http(s) was accepted"
+fi
+grep -q "if p.get('name') in ('computer', 'ai')" "$installer" || fail "a driver rewrite must keep the ai plugin"
echo "ok: install.sh --computer config"
echo "ok: install.sh --computer config"
+echo "ok: install.sh --ai config"