openvibers/openvibe.link Commits

v0.24.1: kept profiles carry session cookies too (found by the production proof) (#71)

OP OpenVibers committed 17 days ago · 562e8ee83a · 11 files, +110 −9

* v0.24.1: a kept profile carries session cookies too (Chrome drops them as it quits, and a sign-in is often one), found by the production proof

* A browser desk's browser has no session bus and starts none (DBUS_SESSION_BUS_ADDRESS=disabled:): without an address Chrome auto-launches one, which hung its start on a CI runner

Changes

ddiff --git a/STATUS.json b/STATUS.json
iindex c707128..ed5788d 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.24.0",
-- "release": "v0.24.0, published 2026-10-27 as a GitHub release (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.24.1",
++ "release": "v0.24.1, published 2026-10-27 as a GitHub release (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,
ddiff --git a/docs/computer.md b/docs/computer.md
iindex 8f67b46..2f477f7 100644
---- a/docs/computer.md
++++ b/docs/computer.md
@@@ -201,6 +201,9 @@ task's desk from the profile `agent`.
- **Saved when the desk stops.** The browser is asked to quit first, so it writes its cookies down. The desk's profile
is then saved back (`profile_saved` in the answer), and the desk's own copy is removed, so a desk per task leaves
nothing behind. When two desks with the same profile stop, the last one wins.
++- **Session sign-ins too** (Node v0.24.1). Many sites sign people in with a cookie that ends when the browser quits,
++ and Chrome throws those away as it closes. Before the desk's browser quits, its session cookies are kept beside the
++ profile (`ov-session-cookies.json`, 0600), and the next desk's browser gets them back as soon as it starts.
- **Not kept:** caches, crash reports, locks and the last session's tabs. A profile larger than `desk_profile_max_mb`
(256 MB) is not saved, and an event `desk_profile_too_big` says so. The kill switch saves nothing.
- **Where.** `browser_profile` + `-kept-<name>`, 0700, on this machine only. Profiles that move between machines are
ddiff --git a/plugins/computer/openvibe_computer/__init__.py b/plugins/computer/openvibe_computer/__init__.py
iindex 62a171f..2cf430c 100644
---- a/plugins/computer/openvibe_computer/__init__.py
++++ b/plugins/computer/openvibe_computer/__init__.py
@@@ -68,6 +68,7 @@ Command kinds and their answers (the ack's ``data``):
import base64
import contextlib
++import json
import http.client
import ipaddress
import os
@@@ -117,6 +118,7 @@ BROWSER_READS = frozenset({"read", "screenshot", "close"})
BROWSER_ACTS = frozenset({"open", "click", "fill", "press", "back"})
MAX_URL = 2048
PROFILE_NAME = re.compile(r"^[a-z0-9][a-z0-9-]{0,23}$")
++SESSION_COOKIES = "ov-session-cookies.json" # in a kept profile: the cookies that end with the browser
# What a kept profile leaves out: caches, crash reports, locks and the last session's tabs (a desk opens on a blank page).
PROFILE_SKIP = shutil.ignore_patterns("Cache", "Code Cache", "GPUCache", "GrShaderCache", "GraphiteDawnCache", "ShaderCache",
"DawnCache", "DawnGraphiteCache", "DawnWebGPUCache", "CacheStorage", "ScriptCache",
@@@ -175,7 +177,7 @@ def _int(v, name, lo, hi, default):
class Computer(Plugin):
driver = "computer"
-- version = "0.12.0"
++ version = "0.12.1"
motion_kinds = frozenset()
telemetry_interval_s = 2.0
@@@ -534,6 +536,9 @@ class Computer(Plugin):
profile = self._desk_profile_dir(desk)
self._restore_profile(desk)
b = self.desk_browsers[desk] = self.browser_factory(binary, profile, page, env=env)
++ kept = self._take_session_cookies(desk)
++ if kept:
++ b.after_start = lambda: self._put_cookies(desk, b, kept)
b.download_dir = self.desks.dir(desk, "downloads")
if page:
st = self.desks.status(desk)
@@@ -586,11 +591,45 @@ class Computer(Plugin):
shutil.copytree(kept, own, symlinks=True, ignore=PROFILE_SKIP)
os.chmod(own, 0o700)
++ def _take_session_cookies(self, desk):
++ """The session cookies a desk's kept profile brought (read once, then the file goes: it is written again on save)."""
++ if desk not in self.desk_profiles:
++ return None
++ path = os.path.join(self._desk_profile_dir(desk), SESSION_COOKIES)
++ try:
++ with open(path) as f:
++ cookies = json.load(f)
++ except (OSError, ValueError):
++ return None
++ finally:
++ try:
++ os.unlink(path)
++ except OSError:
++ pass
++ return cookies if isinstance(cookies, list) else None
++
++ def _put_cookies(self, desk, b, cookies):
++ try:
++ b.restore_cookies(cookies)
++ except BrowserError as e:
++ self.log("desk %s: its kept session cookies did not go back in: %s", desk, e.message)
++
def _end_desk_browser(self, desk):
"""Close a desk's browser so it writes everything down, save its profile back when it started from a kept one
(the last desk to stop wins), and remove the desk's own copy. → True when saved, False when not (too large or
a failed copy), None for a desk without a kept profile."""
b = self.desk_browsers.pop(desk, None)
++ profile = self.desk_profiles.get(desk)
++ if b is not None and profile and b.alive():
++ # Chrome drops cookies that end with the browser when it quits, and a sign-in is often one: keep them.
++ try:
++ cookies = b.session_cookies()
++ path = os.path.join(self._desk_profile_dir(desk), SESSION_COOKIES)
++ fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
++ with os.fdopen(fd, "w") as f:
++ json.dump(cookies, f)
++ except (BrowserError, OSError) as e:
++ self.log("desk %s: its session cookies were not kept: %s", desk, getattr(e, "message", e))
if b is not None:
b.close(graceful=True)
profile = self.desk_profiles.pop(desk, None)
ddiff --git a/plugins/computer/openvibe_computer/browser.py b/plugins/computer/openvibe_computer/browser.py
iindex d1a140c..4c1cca1 100644
---- a/plugins/computer/openvibe_computer/browser.py
++++ b/plugins/computer/openvibe_computer/browser.py
@@@ -27,7 +27,11 @@ STARTUP_S = 45.0
MAX_LINKS = 60
MAX_ELEMENTS = 120
MAX_APPEARED = 8
--MAX_FOLLOWED = 5 # pages opened from pages (a sign-in popup, a new tab) followed at most this deep
++MAX_FOLLOWED = 5
++MAX_SESSION_COOKIES = 3000
++# What Storage.setCookies takes back of what Storage.getCookies gives.
++COOKIE_FIELDS = ("name", "value", "domain", "path", "secure", "httpOnly", "sameSite", "priority", "sourceScheme",
++ "sourcePort", "partitionKey") # pages opened from pages (a sign-in popup, a new tab) followed at most this deep
KEY_CODES = {
"Enter": (13, "\r"), "Tab": (9, "\t"), "Escape": (27, ""), "Backspace": (8, ""), "Delete": (46, ""),
"ArrowUp": (38, ""), "ArrowDown": (40, ""), "ArrowLeft": (37, ""), "ArrowRight": (39, ""),
@@@ -203,6 +207,7 @@ class Browser:
self.tab_events = [] # Target.targetCreated / targetDestroyed seen since the last _follow
self.tab_change = None # "opened" or "closed" since the last _mark: said in the action's receipt
self.follow_mu = threading.Lock()
++ self.after_start = None # called once the browser runs (a kept profile's session cookies go back in)
# ---- the process and the pipe ------------------------------------------------------------------------------------
def alive(self):
@@@ -250,6 +255,9 @@ class Browser:
self.call("Target.setDiscoverTargets", {"discover": True}, session=False)
if self.download_dir:
self.call("Browser.setDownloadBehavior", {"behavior": "allow", "downloadPath": self.download_dir}, session=False)
++ if self.after_start:
++ hook, self.after_start = self.after_start, None
++ hook()
def _attach(self, target):
"""Drive that page: a session of its own, its events on, and a browser desk's exact size."""
@@@ -610,6 +618,23 @@ class Browser:
f.write(data)
os.replace(path + ".part", path)
++ # ---- cookies a kept profile carries (Node v0.24.1) ------------------------------------------------------------
++ def session_cookies(self):
++ """The cookies that end with the browser (no expiry): Chrome drops them when it quits, and a sign-in is often
++ one. A kept profile keeps them alongside, for the next desk (restore_cookies)."""
++ if not self.alive():
++ return []
++ got = self.call("Storage.getCookies", {}, session=False).get("cookies") or []
++ return [{k: c[k] for k in COOKIE_FIELDS if k in c} for c in got if c.get("session")][:MAX_SESSION_COOKIES]
++
++ def restore_cookies(self, cookies):
++ """Put session cookies back (from session_cookies, on another run of the browser)."""
++ ok = [c for c in cookies if isinstance(c, dict) and isinstance(c.get("name"), str) and isinstance(c.get("value"), str)
++ and isinstance(c.get("domain"), str)][:MAX_SESSION_COOKIES]
++ if ok:
++ self.call("Storage.setCookies", {"cookies": [{k: c[k] for k in COOKIE_FIELDS if k in c} for c in ok]}, session=False)
++ return len(ok)
++
def close(self, graceful=False):
"""End the browser. graceful: ask it to quit first (a few seconds), so it writes its cookies and storage to the
profile (a desk's profile is kept, Node v0.24.0); else, and when it does not quit, it is killed."""
ddiff --git a/plugins/computer/openvibe_computer/desk.py b/plugins/computer/openvibe_computer/desk.py
iindex fa3aedb..b6bc32d 100644
---- a/plugins/computer/openvibe_computer/desk.py
++++ b/plugins/computer/openvibe_computer/desk.py
@@@ -235,7 +235,11 @@ class BrowserDesk:
with self.mu:
if not self.on:
raise Fault("not_ready", "start the desk first (desk start)")
-- return {k: v for k, v in self.base_env.items() if k not in OWNER_SESSION}
++ env = {k: v for k, v in self.base_env.items() if k not in OWNER_SESSION}
++ # No session bus at all, and none started: without an address Chrome tries to auto-launch one, which can
++ # hang its start on a machine with no display (a CI runner, a desk host's service).
++ env["DBUS_SESSION_BUS_ADDRESS"] = "disabled:"
++ return env
# A browser desk counts as half a desk against max_desks (a whole desk counts 2 of 2 * max_desks).
ddiff --git a/plugins/computer/pyproject.toml b/plugins/computer/pyproject.toml
iindex 826c07e..de5937b 100644
---- a/plugins/computer/pyproject.toml
++++ b/plugins/computer/pyproject.toml
@@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "openvibe-computer"
--version = "0.12.0"
++version = "0.12.1"
description = "OpenVibe Node computer plugin: the machine's terminal, files and screen under the owner's control level"
requires-python = ">=3.9"
license = { text = "AGPL-3.0-or-later" }
ddiff --git a/plugins/computer/tests/test_browser.py b/plugins/computer/tests/test_browser.py
iindex 88f9586..07e4567 100644
---- a/plugins/computer/tests/test_browser.py
++++ b/plugins/computer/tests/test_browser.py
@@@ -215,6 +215,7 @@ class Handler(http.server.SimpleHTTPRequestHandler):
# Node v0.24.0: a page that opens another (a sign-in popup), and one that sets a cookie.
"/pop": b"<!doctype html><title>Pop</title><button id='open' onclick=\"window.open('/two', 'signin', 'popup')\">Sign in</button>",
"/cookie": b"<!doctype html><title>Cookie</title><script>if (!document.cookie.includes('kept=yes')) document.cookie = 'kept=yes; max-age=3600; path=/';</script>",
++ "/session": b"<!doctype html><title>Session</title><script>if (!document.cookie.includes('sid=')) document.cookie = 'sid=s1; path=/';</script>",
# Desks plan D3: a page whose button changes the page without going anywhere, and things that are not elements.
"/app": b"<!doctype html><title>App</title><p>Ready</p><button id='go' aria-label='Add a row'>+</button>"
b"<input type='email' placeholder='Email'><input type='checkbox' checked><a href='/two'>Second page</a>"
@@@ -298,6 +299,16 @@ class RealBrowserTest(unittest.TestCase):
self.addCleanup(again.close)
again.open(base + "/cookie")
self.assertIn("kept=yes", again._eval("document.cookie"))
++ # A session cookie (no expiry) ends with the browser; it is kept and put back by hand (Node v0.24.1).
++ again.open(base + "/session")
++ session = again.session_cookies()
++ self.assertEqual([c["name"] for c in session], ["sid"])
++ again.close(graceful=True)
++ third = Browser(find_binary(shutil.which, sys.platform), profile, extra_args=extra)
++ self.addCleanup(third.close)
++ third.after_start = lambda: third.restore_cookies(session)
++ third.open(base + "/two")
++ self.assertIn("sid=s1", third._eval("document.cookie"), "the session sign-in is back")
def test_a_browser_desk_its_page_is_the_screen(self):
# Desks plan D2 in a real browser: no display; the page is the desk's screen, mouse and keyboard, and live view.
ddiff --git a/plugins/computer/tests/test_desk.py b/plugins/computer/tests/test_desk.py
iindex 5d3c771..50de155 100644
---- a/plugins/computer/tests/test_desk.py
++++ b/plugins/computer/tests/test_desk.py
@@@ -4,6 +4,7 @@ machine's level (refused at observe), reports it at every level, and sends scree
desk: private (refused until it runs), never to the owner's screen. A real desk runs where Xvfb is installed."""
import base64
import io
++import json
import os
import shutil
import sys
@@@ -181,6 +182,7 @@ class ProfileBrowser:
self.on, self.graceful = True, None
path = os.path.join(profile, "Default", "Cookies")
self.found = open(path).read() if os.path.exists(path) else None # what the desk started with
++ self.after_start, self.session, self.restored = None, [], None
ProfileBrowser.made.append(self)
def alive(self):
@@@ -194,8 +196,18 @@ class ProfileBrowser:
f.write(b"x" * (cache_mb * 1024 * 1024 or 10))
def open(self, url):
++ if self.after_start:
++ hook, self.after_start = self.after_start, None
++ hook()
return {"url": url}
++ def session_cookies(self):
++ return self.session
++
++ def restore_cookies(self, cookies):
++ self.restored = cookies
++ return len(cookies)
++
def close(self, graceful=False):
self.on, self.graceful = False, graceful
@@@ -378,6 +390,7 @@ class DeskPluginTest(unittest.TestCase):
self.assertEqual((a.headless, a.window), (True, (1000, 500)))
self.assertTrue(a.profile.endswith("-desk-a"))
self.assertNotIn("DISPLAY", a.env)
++ self.assertEqual(a.env["DBUS_SESSION_BUS_ADDRESS"], "disabled:", "never the owner's session bus, and none started")
self.assertTrue(os.path.basename(a.env["TMPDIR"]).startswith("ov-desk-a-"))
# The mouse and keyboard are the page's: a point in a 100×100 picture lands on the 1000×500 page.
r = reply(self.out, self.cmd("input", {"action": "click", "x": 50, "y": 50, "width": 100, "height": 100, "desk": "desk-a"}))
@@@ -460,7 +473,11 @@ class DeskPluginTest(unittest.TestCase):
self.assertIsNone(a.found, "nothing kept yet")
self.assertEqual(a.profile, os.path.join(home, "browser-desk-a"))
a.sign_in("one")
++ a.session = [{"name": "sid", "value": "s1", "domain": "example.org", "path": "/"}] # a sign-in that ends with the browser
self.assertEqual(stop("desk-a")["profile_saved"], True)
++ with open(os.path.join(os.path.dirname(kept), "ov-session-cookies.json")) as f:
++ self.assertEqual(json.load(f)[0]["name"], "sid", "session cookies are kept beside the profile")
++ self.assertEqual(os.stat(os.path.join(os.path.dirname(kept), "ov-session-cookies.json")).st_mode & 0o777, 0o600)
self.assertTrue(a.graceful, "closed so it writes its cookies down")
with open(os.path.join(kept, "Cookies")) as f:
self.assertEqual(f.read(), "one")
@@@ -472,6 +489,8 @@ class DeskPluginTest(unittest.TestCase):
start("desk-c", profile="agent")
b, c = use("desk-b"), use("desk-c")
self.assertEqual((b.found, c.found), ("one", "one"), "signed in on both")
++ self.assertEqual([x["name"] for x in b.restored], ["sid"], "the session cookie is back as the browser starts")
++ self.assertFalse(os.path.exists(os.path.join(b.profile, "ov-session-cookies.json")), "read once, not left in the copy")
c.sign_in("three")
b.sign_in("two")
stop("desk-c")
ddiff --git a/plugins/sdk/openvibe_plugin/__init__.py b/plugins/sdk/openvibe_plugin/__init__.py
iindex c6b199f..bc437f9 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.24.0"
++__version__ = "0.24.1"
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 1af4ee8..c40d6d4 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.24.0"
++version = "0.24.1"
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 11fbaef..1165da3 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.24.0")
++ self.assertEqual(openvibe_plugin.__version__, "0.24.1")
with open(os.path.join(os.path.dirname(HERE), "pyproject.toml")) as f:
self.assertIn('version = "%s"' % openvibe_plugin.__version__, f.read())