fix: Docker UID/GID auto-detect from workspace mount + message count tests — v0.50.69

Fixes #569: docker_init.bash auto-detects WANTED_UID/WANTED_GID from the mounted /workspace UID at Phase 1, before usermod remaps the container user. On macOS, host UIDs start at 501 — the default 1024 caused an empty workspace. Guards against root (0). Fallback 1024 preserved. Closes #579: topbar already correctly filters tool messages; sidebar count removed in #584. Regression tests added. Reviewed and approved by @nesquena. 1347 tests passing.
This commit is contained in:
nesquena-hermes
2026-04-16 12:19:25 -07:00
committed by GitHub
parent 6c5911a79f
commit 25d38a467a
4 changed files with 136 additions and 1 deletions

View File

@@ -1,5 +1,11 @@
# Hermes Web UI -- Changelog # Hermes Web UI -- Changelog
## [v0.50.69] — 2026-04-16
### Fixed
- **Docker: workspace file browser no longer appears empty on macOS** — `docker_init.bash` now auto-detects the correct `WANTED_UID` and `WANTED_GID` from the mounted `/workspace` directory at startup. On macOS, host UIDs start at 501 (not 1000), so the default value of 1024 caused the container user to run as a different UID than the files, making the workspace appear empty. The auto-detect reads `stat -c '%u'` on `/workspace` and uses it when no explicit `WANTED_UID` is set — falling back to 1024 if the path doesn't exist or returns 0 (root). Setting `WANTED_UID` explicitly in a `.env` file still takes full precedence. (Closes #569)
- **Session message count inconsistency resolved** — the topbar already correctly shows only visible messages (excluding `role='tool'` tool-call entries). The sidebar previously showed raw `message_count` which included tool messages, but PR #584 removed that display entirely — there is no longer any count displayed in the sidebar. No code change needed; documenting with regression tests. (Closes #579)
## [v0.50.68] — 2026-04-16 ## [v0.50.68] — 2026-04-16
### Fixed ### Fixed

View File

@@ -59,6 +59,20 @@ it=$itdir/hermeswebui_user_uid
if [ -z "${WANTED_UID+x}" ]; then if [ -z "${WANTED_UID+x}" ]; then
if [ -f $it ]; then WANTED_UID=$(cat $it); fi if [ -f $it ]; then WANTED_UID=$(cat $it); fi
fi fi
# Auto-detect from mounted workspace if still unset (#569).
# On macOS, host UIDs start at 501. Using the wrong UID means the container
# user cannot read the bind-mounted files, making the workspace appear empty.
# Prefer the workspace mount UID over the hardcoded default of 1024.
if [ -z "${WANTED_UID+x}" ] || [ "${WANTED_UID}" = "1024" ]; then
# Use /workspace — the standard bind-mount point — to read the host UID.
if [ -d "/workspace" ]; then
_detected_uid=$(stat -c '%u' "/workspace" 2>/dev/null || echo "")
if [ -n "$_detected_uid" ] && [ "$_detected_uid" != "0" ]; then
echo "-- Auto-detected workspace UID: $_detected_uid (from /workspace)"
WANTED_UID=$_detected_uid
fi
fi
fi
WANTED_UID=${WANTED_UID:-1024} WANTED_UID=${WANTED_UID:-1024}
write_worldtmpfile $it "$WANTED_UID" write_worldtmpfile $it "$WANTED_UID"
echo "-- WANTED_UID: \"${WANTED_UID}\"" echo "-- WANTED_UID: \"${WANTED_UID}\""
@@ -67,6 +81,16 @@ it=$itdir/hermeswebui_user_gid
if [ -z "${WANTED_GID+x}" ]; then if [ -z "${WANTED_GID+x}" ]; then
if [ -f $it ]; then WANTED_GID=$(cat $it); fi if [ -f $it ]; then WANTED_GID=$(cat $it); fi
fi fi
# Auto-detect GID from mounted workspace to match (#569)
if [ -z "${WANTED_GID+x}" ] || [ "${WANTED_GID}" = "1024" ]; then
if [ -d "/workspace" ]; then
_detected_gid=$(stat -c '%g' "/workspace" 2>/dev/null || echo "")
if [ -n "$_detected_gid" ] && [ "$_detected_gid" != "0" ]; then
echo "-- Auto-detected workspace GID: $_detected_gid (from /workspace)"
WANTED_GID=$_detected_gid
fi
fi
fi
WANTED_GID=${WANTED_GID:-1024} WANTED_GID=${WANTED_GID:-1024}
write_worldtmpfile $it "$WANTED_GID" write_worldtmpfile $it "$WANTED_GID"
echo "-- WANTED_GID: \"${WANTED_GID}\"" echo "-- WANTED_GID: \"${WANTED_GID}\""

View File

@@ -555,7 +555,7 @@
<div class="settings-section-title">System</div> <div class="settings-section-title">System</div>
<div class="settings-section-meta">Instance version and access controls.</div> <div class="settings-section-meta">Instance version and access controls.</div>
</div> </div>
<span class="settings-version-badge">v0.50.68</span> <span class="settings-version-badge">v0.50.69</span>
</div> </div>
<div class="settings-field" style="border-top:1px solid var(--border);padding-top:12px;margin-top:8px"> <div class="settings-field" style="border-top:1px solid var(--border);padding-top:12px;margin-top:8px">
<label for="settingsPassword" data-i18n="settings_label_password">Access Password</label> <label for="settingsPassword" data-i18n="settings_label_password">Access Password</label>

105
tests/test_issue569_579.py Normal file
View File

@@ -0,0 +1,105 @@
"""
Tests for fixes:
- #569: docker_init.bash auto-detects WANTED_UID/WANTED_GID from mounted workspace
so macOS users (UID 501) don't need to manually set the env var.
- #579: Topbar message count already filters tool messages (role !== 'tool') —
confirmed present. Closing as already fixed by #584 which removed the
sidebar meta row (the only place raw message_count was ever displayed).
"""
import pathlib
import re
REPO_ROOT = pathlib.Path(__file__).parent.parent
INIT_SH = (REPO_ROOT / "docker_init.bash").read_text(encoding="utf-8")
UI_JS = (REPO_ROOT / "static" / "ui.js").read_text(encoding="utf-8")
# ── #569: docker UID/GID auto-detect ─────────────────────────────────────────
def test_569_uid_autodetect_present():
"""docker_init.bash must have workspace-based UID auto-detection (#569)."""
assert "stat -c '%u'" in INIT_SH or 'stat -c \'%u\'' in INIT_SH, (
"docker_init.bash must use stat to read workspace UID (#569)"
)
def test_569_gid_autodetect_present():
"""docker_init.bash must have workspace-based GID auto-detection (#569)."""
assert "stat -c '%g'" in INIT_SH or 'stat -c \'%g\'' in INIT_SH, (
"docker_init.bash must use stat to read workspace GID (#569)"
)
def test_569_autodetect_before_usermod():
"""UID auto-detect must appear before usermod call in docker_init.bash."""
detect_pos = INIT_SH.find("stat -c '%u'")
if detect_pos == -1:
detect_pos = INIT_SH.find("stat -c")
usermod_pos = INIT_SH.find("sudo usermod")
assert detect_pos != -1, "stat UID detection not found"
assert usermod_pos != -1, "sudo usermod not found"
assert detect_pos < usermod_pos, (
"UID auto-detect must occur before 'sudo usermod' so the correct UID "
"is used when remapping the hermeswebui user"
)
def test_569_skips_root_uid():
"""Auto-detect must not use UID 0 (root-owned mount = untrustworthy)."""
detect_block_start = INIT_SH.find("Auto-detect from mounted workspace")
assert detect_block_start != -1, "auto-detect comment block not found"
block = INIT_SH[detect_block_start:detect_block_start + 600]
assert '"0"' in block or "'0'" in block, (
"Auto-detect block must skip UID 0 to avoid incorrectly using root ownership"
)
def test_569_fallback_preserved():
"""Hardcoded default 1024 fallback must still exist after auto-detect."""
assert "WANTED_UID=${WANTED_UID:-1024}" in INIT_SH, (
"WANTED_UID default fallback must remain so explicit env var still works"
)
assert "WANTED_GID=${WANTED_GID:-1024}" in INIT_SH, (
"WANTED_GID default fallback must remain"
)
# ── #579: topbar message count already filters tool messages ──────────────────
def test_579_topbar_filters_tool_messages():
"""ui.js topbar count must filter out role='tool' messages (#579).
The sidebar previously showed raw message_count (which included tool
messages), causing a mismatch with the topbar. PR #584 removed the
sidebar count display entirely; the topbar was already correct.
This test locks in the existing topbar filter so it can't regress.
"""
# Find the topbarMeta assignment
meta_pos = UI_JS.find("topbarMeta")
assert meta_pos != -1, "topbarMeta assignment not found in ui.js"
# Find the filter that precedes it — should exclude role==='tool'
context = UI_JS[max(0, meta_pos - 400):meta_pos + 100]
assert "role" in context and "tool" in context, (
"topbarMeta count must filter by role — "
"messages with role='tool' must be excluded from the displayed count"
)
# The filter must exclude tool messages (not include them)
assert "!=='tool'" in context or "!= 'tool'" in context or "role!=='tool'" in context, (
"topbar count filter must use !== 'tool' to exclude tool messages"
)
def test_579_sidebar_no_longer_shows_raw_count():
"""sessions.js must not reference message_count in the render path (#579).
After PR #584, the sidebar no longer shows message_count at all,
eliminating the inconsistency between sidebar (raw) and topbar (filtered).
"""
sessions_js = (REPO_ROOT / "static" / "sessions.js").read_text(encoding="utf-8")
# message_count should not appear in the client-side session renderer
assert "message_count" not in sessions_js, (
"sessions.js must not reference message_count — "
"the meta row that displayed it was removed in PR #584"
)