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
This commit is contained in:
izumi0uu 2026-07-26 18:50:33 +08:00 committed by Teknium
parent c476ad35ae
commit 35a9b3a7c2
4 changed files with 163 additions and 3 deletions

View File

@ -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.

View File

@ -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}")

View File

@ -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

View File

@ -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):