fix(state-db): keep non-UTF-8 POSIX paths readable in the read-only URI

state_db_file_uri() quoted the POSIX path as a str; an undecodable filename
byte is carried as a surrogateescape code point, which quote() rejects with
UnicodeEncodeError, and get_cli_sessions() swallowed that as an empty
session list. Quote os.fsencode() bytes instead (what Path.as_uri() did on
master). Regression test: a b'\xff' HERMES_HOME component opens read-only.

Maintainer fold-in during release gating (Codex re-gate finding).

Co-authored-by: ruizanthony <ruizanthony@users.noreply.github.com>
This commit is contained in:
nesquena-hermesandruizanthony committed 2026-09-22 22:56:02 +00:00
1 parent c8b1f87583
commit 8635487d9b
2 files changed
+36 -2

No files matched your search

+9 -2
View File
@@ -1,11 +1,12 @@
"""Shared helpers for reading Hermes Agent sessions from state.db.""" """Shared helpers for reading Hermes Agent sessions from state.db."""
import json import json
import logging import logging
import os
import sqlite3 import sqlite3
import sys import sys
from contextlib import closing from contextlib import closing
from pathlib import Path, PurePosixPath, PureWindowsPath from pathlib import Path, PurePosixPath, PureWindowsPath
from urllib.parse import quote from urllib.parse import quote, quote_from_bytes
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -56,7 +57,13 @@ def state_db_file_uri(db_path, platform: str | None = None) -> str:
# ``file:///C:/...`` shape (``Path.as_uri()`` leaves it unescaped too). # ``file:///C:/...`` shape (``Path.as_uri()`` leaves it unescaped too).
return "file://" + quote(posix_path, safe="/:") return "file://" + quote(posix_path, safe="/:")
posix_path = PurePosixPath(str(db_path)).as_posix() posix_path = PurePosixPath(str(db_path)).as_posix()
return "file://" + quote(posix_path, safe="/") # Quote the filesystem BYTES, not the str: a POSIX path component that is
# not valid UTF-8 is carried in the str as ``surrogateescape`` code points,
# which ``quote(str)`` rejects with UnicodeEncodeError (the caller then
# treats the db as unreadable and every agent-backed session vanishes).
# ``Path.as_uri()`` — what master used — percent-encodes os.fsencode()
# bytes; this keeps that behavior.
return "file://" + quote_from_bytes(os.fsencode(posix_path), safe="/")
def state_db_readonly_uri(db_path, platform: str | None = None) -> str: def state_db_readonly_uri(db_path, platform: str | None = None) -> str:
@@ -207,6 +207,33 @@ def test_empty_authority_uri_opens_on_this_platform(tmp_path):
conn.execute("INSERT INTO t VALUES (1)") conn.execute("INSERT INTO t VALUES (1)")
@pytest.mark.skipif(sys.platform == "win32", reason="POSIX filesystem-bytes path")
def test_non_utf8_posix_path_still_opens_readonly(tmp_path):
"""A non-UTF-8 path component must not make every agent session vanish.
Python carries undecodable POSIX filename bytes as ``surrogateescape``
code points; ``quote(str)`` rejects those with UnicodeEncodeError, which
the listing path swallows as "no sessions". ``Path.as_uri()`` (the pre-PR
shape) percent-encoded the fsencode() bytes, so the URI builder must too.
"""
try:
odd_dir = tmp_path / os.fsdecode(b"hermes-\xff-home")
odd_dir.mkdir()
except (OSError, UnicodeError):
pytest.skip("filesystem rejects non-UTF-8 names")
path = odd_dir / "state.db"
with closing(sqlite3.connect(str(path))) as conn:
conn.execute("CREATE TABLE t(x)")
conn.execute("INSERT INTO t VALUES (1)")
conn.commit()
uri = agent_sessions.state_db_readonly_uri(path.resolve())
assert "%FF" in uri
with closing(agent_sessions.open_state_db_readonly(path)) as conn:
assert conn.execute("SELECT COUNT(*) FROM t").fetchone() == (1,)
with pytest.raises(sqlite3.OperationalError):
conn.execute("INSERT INTO t VALUES (2)")
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
# 3. The index maintenance script imports and runs without fcntl. # 3. The index maintenance script imports and runs without fcntl.
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------