From 35a9b3a7c2346e932408e906cac44aed48012e55 Mon Sep 17 00:00:00 2001 From: izumi0uu Date: Sun, 26 Jul 2026 18:50:33 +0800 Subject: [PATCH] fix(docker): ship a WAL-reset-safe SQLite runtime Compile and checksum-pin SQLite 3.53.4 in the published image, preserve Hermes' required SQLite features, and assert the final Python linkage plus FTS5 trigram behavior during image builds.\n\nMake doctor remediation install-aware so Docker users pull and recreate every Hermes container instead of running the inapplicable git updater.\n\nFixes #70480 --- Dockerfile | 59 ++++++++++++++++++++++++++++ hermes_cli/doctor.py | 27 +++++++++++-- tests/docker/test_sqlite_runtime.py | 60 +++++++++++++++++++++++++++++ tests/hermes_cli/test_doctor.py | 20 ++++++++++ 4 files changed, 163 insertions(+), 3 deletions(-) create mode 100644 tests/docker/test_sqlite_runtime.py diff --git a/Dockerfile b/Dockerfile index 388056faacde..42870d737761 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,45 @@ +# Debian 13 still ships SQLite 3.46.1, which contains the upstream WAL-reset +# corruption bug. Build a pinned shared library for the runtime image instead +# of relying on a distro backport that trixie does not currently provide. +# See #70480 and https://sqlite.org/wal.html#walresetbug. +FROM debian:13.4 AS sqlite_build +ARG SQLITE_AUTOCONF_VERSION=3530400 +ARG SQLITE_SHA256=0e9483900e92cd5de8fd48d16bf9200145a61f7fd5be542a5ac81d8a9516eb9c +RUN apt-get -o Acquire::Retries=3 update && \ + apt-get -o Acquire::Retries=3 install -y --no-install-recommends \ + build-essential ca-certificates curl && \ + rm -rf /var/lib/apt/lists/* && \ + (curl -fsSL --retry 1 --retry-all-errors --connect-timeout 15 --max-time 60 \ + -o /tmp/sqlite.tar.gz \ + "https://sqlite.org/2026/sqlite-autoconf-${SQLITE_AUTOCONF_VERSION}.tar.gz" || \ + curl -fsSL --retry 3 --retry-all-errors --connect-timeout 15 --max-time 120 \ + -o /tmp/sqlite.tar.gz \ + "https://sources.buildroot.net/sqlite/sqlite-autoconf-${SQLITE_AUTOCONF_VERSION}.tar.gz") && \ + printf '%s %s\n' "${SQLITE_SHA256}" /tmp/sqlite.tar.gz > /tmp/sqlite.sha256 && \ + sha256sum -c /tmp/sqlite.sha256 && \ + tar -xzf /tmp/sqlite.tar.gz -C /tmp && \ + cd "/tmp/sqlite-autoconf-${SQLITE_AUTOCONF_VERSION}" && \ + CFLAGS="-O2 \ + -DSQLITE_ENABLE_FTS3 \ + -DSQLITE_ENABLE_FTS3_PARENTHESIS \ + -DSQLITE_ENABLE_FTS4 \ + -DSQLITE_ENABLE_FTS5 \ + -DSQLITE_ENABLE_RTREE \ + -DSQLITE_ENABLE_GEOPOLY \ + -DSQLITE_ENABLE_COLUMN_METADATA \ + -DSQLITE_ENABLE_UNLOCK_NOTIFY \ + -DSQLITE_ENABLE_DBSTAT_VTAB \ + -DSQLITE_ENABLE_DBPAGE_VTAB \ + -DSQLITE_ENABLE_MATH_FUNCTIONS \ + -DSQLITE_ENABLE_PREUPDATE_HOOK \ + -DSQLITE_ENABLE_SESSION \ + -DSQLITE_SECURE_DELETE \ + -DSQLITE_THREADSAFE=1 \ + -DSQLITE_MAX_VARIABLE_NUMBER=250000" \ + ./configure --prefix=/opt/sqlite-fixed --disable-static && \ + make -j"$(nproc)" && \ + make install + FROM ghcr.io/astral-sh/uv:0.11.6-python3.13-trixie@sha256:b3c543b6c4f23a5f2df22866bd7857e5d304b67a564f4feab6ac22044dde719b AS uv_source # Node 22 LTS source stage. Debian trixie's bundled nodejs is pinned to 20.x # which reached EOL in April 2026 — we copy node + npm + corepack from the @@ -31,6 +73,23 @@ RUN apt-get -o Acquire::Retries=3 update && \ ca-certificates curl iputils-ping python3 python-is-python3 ripgrep ffmpeg gcc g++ make cmake python3-dev python3-venv libffi-dev libolm-dev procps git openssh-client docker-cli xz-utils && \ rm -rf /var/lib/apt/lists/* +# Prefer the fixed SQLite over Debian's vulnerable libsqlite3.so.0. Keep the +# public library name stable so both the system interpreter and the uv-created +# venv resolve the replacement without changing Python import paths. +COPY --from=sqlite_build /opt/sqlite-fixed/lib/libsqlite3.so.3.53.4 /usr/local/lib/ +RUN ln -sf libsqlite3.so.3.53.4 /usr/local/lib/libsqlite3.so.0 && \ + ln -sf libsqlite3.so.3.53.4 /usr/local/lib/libsqlite3.so && \ + printf '/usr/local/lib\n' > /etc/ld.so.conf.d/000-sqlite-fixed.conf && \ + ldconfig && \ + python3 -c "import sqlite3, sys; \ +v = sqlite3.sqlite_version_info; \ +sys.exit(f'linked SQLite {sqlite3.sqlite_version} still has the WAL-reset bug') if v < (3, 51, 3) else None; \ +db = sqlite3.connect(':memory:'); \ +db.execute(\"CREATE VIRTUAL TABLE docs USING fts5(content, tokenize='trigram')\"); \ +db.execute(\"INSERT INTO docs VALUES ('hermes')\"); \ +sys.exit('SQLite FTS5 trigram self-test failed') if db.execute(\"SELECT count(*) FROM docs WHERE docs MATCH 'erm'\").fetchone()[0] != 1 else None; \ +db.close()" + # ---------- s6-overlay install ---------- # s6-overlay provides supervision for the main hermes process, the dashboard, # and per-profile gateways. /init becomes PID 1 below — see ENTRYPOINT. diff --git a/hermes_cli/doctor.py b/hermes_cli/doctor.py index 7c89e75adcbc..9fd241630ae0 100644 --- a/hermes_cli/doctor.py +++ b/hermes_cli/doctor.py @@ -10,7 +10,13 @@ import subprocess import shutil from pathlib import Path -from hermes_cli.config import get_project_root, get_hermes_home, get_env_path +from hermes_cli.config import ( + detect_install_method, + get_env_path, + get_hermes_home, + get_project_root, + recommended_update_command_for_method, +) from hermes_cli.env_loader import load_hermes_dotenv from hermes_constants import display_hermes_home from hermes_constants import agent_browser_runnable @@ -72,6 +78,22 @@ def _system_package_install_cmd(pkg: str) -> str: return f"sudo apt install {pkg}" +def _sqlite_upgrade_hint(install_method: str | None = None) -> str: + """Return an actionable SQLite upgrade hint for this install layout.""" + method = install_method or detect_install_method(PROJECT_ROOT) + if method == "docker": + command = recommended_update_command_for_method(method) + action = f"run `{command}`, then recreate all Hermes containers" + elif method in {"nix", "nixos"}: + action = recommended_update_command_for_method(method) + else: + action = "run `hermes update`" + return ( + f"({action}; fixed versions: 3.51.3+ / 3.50.7 / 3.44.6 — " + "see https://sqlite.org/wal.html#walresetbug)" + ) + + def _safe_which(cmd: str) -> str | None: """shutil.which wrapper resilient to platform monkeypatching in tests.""" try: @@ -827,8 +849,7 @@ def run_doctor(args): # best-effort and unsupported installs may need manual action. check_warn( f"SQLite {_sqlite_ver} (WAL-reset bug)", - "(run `hermes update`; fixed versions: 3.51.3+ / 3.50.7 / " - "3.44.6 — see https://sqlite.org/wal.html#walresetbug)", + _sqlite_upgrade_hint(), ) else: check_ok(f"SQLite {_sqlite_ver}") diff --git a/tests/docker/test_sqlite_runtime.py b/tests/docker/test_sqlite_runtime.py new file mode 100644 index 000000000000..235a6a2c20eb --- /dev/null +++ b/tests/docker/test_sqlite_runtime.py @@ -0,0 +1,60 @@ +"""Runtime qualification for SQLite in the published Docker image.""" + +from __future__ import annotations + +import json +import subprocess + + +_SQLITE_PROBE = r""" +import json +import sqlite3 + +from hermes_cli.sqlite_runtime import is_sqlite_wal_reset_vulnerable + +db = sqlite3.connect(":memory:") +try: + db.execute("CREATE VIRTUAL TABLE docs USING fts5(content, tokenize='trigram')") + db.execute("INSERT INTO docs VALUES ('hermes')") + matches = db.execute( + "SELECT count(*) FROM docs WHERE docs MATCH 'erm'" + ).fetchone()[0] +finally: + db.close() + +print(json.dumps({ + "sqlite_version": sqlite3.sqlite_version, + "wal_reset_vulnerable": is_sqlite_wal_reset_vulnerable( + sqlite3.sqlite_version_info + ), + "trigram_matches": matches, +})) +""" + + +def test_image_links_fixed_sqlite_with_fts5_trigram(built_image: str) -> None: + result = subprocess.run( + [ + "docker", + "run", + "--rm", + "--user", + "hermes", + "--entrypoint", + "/opt/hermes/.venv/bin/python", + built_image, + "-c", + _SQLITE_PROBE, + ], + capture_output=True, + text=True, + timeout=60, + ) + + assert result.returncode == 0, ( + f"SQLite runtime probe failed: stdout={result.stdout!r} " + f"stderr={result.stderr!r}" + ) + payload = json.loads(result.stdout) + assert payload["wal_reset_vulnerable"] is False, payload + assert payload["trigram_matches"] == 1, payload diff --git a/tests/hermes_cli/test_doctor.py b/tests/hermes_cli/test_doctor.py index c439aaf0cee5..76ba52110416 100644 --- a/tests/hermes_cli/test_doctor.py +++ b/tests/hermes_cli/test_doctor.py @@ -32,6 +32,26 @@ class TestDoctorPlatformHints: assert doctor._python_install_cmd() == "uv pip install" assert doctor._system_package_install_cmd("ripgrep") == "sudo apt install ripgrep" + def test_sqlite_upgrade_hint_recreates_docker_containers(self, monkeypatch): + monkeypatch.setattr(doctor, "detect_install_method", lambda _root: "docker") + + hint = doctor._sqlite_upgrade_hint() + + assert "docker pull nousresearch/hermes-agent:latest" in hint + assert "recreate all Hermes containers" in hint + assert "hermes update" not in hint + + def test_sqlite_upgrade_hint_keeps_git_runtime_repair(self): + hint = doctor._sqlite_upgrade_hint("git") + + assert "run `hermes update`" in hint + + def test_sqlite_upgrade_hint_uses_nix_package_manager(self): + hint = doctor._sqlite_upgrade_hint("nix") + + assert "Nix source that installed it" in hint + assert "hermes update" not in hint + class TestProviderEnvDetection: def test_detects_openai_api_key(self):