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:
parent
c476ad35ae
commit
35a9b3a7c2
59
Dockerfile
59
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.
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Reference in New Issue