mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-29 06:31:32 +00:00
Salvages #25872 by @konsisumer against current main. NAS users (UGOS, Synology, unRAID) expect the LinuxServer.io PUID/PGID convention and bind-mount /opt/data from a host directory owned by their own UID. Without this alias those vars are silently ignored and the s6-setuidgid drop to UID 10000 leaves the runtime unable to read the volume. HERMES_UID/HERMES_GID still take precedence when both are set. The original PR targeted docker/entrypoint.sh, which is now a 27-line deprecation shim under s6-overlay (the May 2026 rework moved all bootstrap logic to docker/stage2-hook.sh, installed as /etc/cont-init.d/01-hermes-setup). Re-applied the same 2-line alias resolution at the equivalent spot in stage2-hook.sh just before the existing UID/GID remap block. Test was retargeted at docker/stage2-hook.sh; docs hunk adapted to current main's wording ("stage2 hook" + s6-setuidgid, not the obsolete "entrypoint drops via gosu") with the NAS bind-mount example preserved verbatim. Test-first regression verification: reverted just docker/stage2-hook.sh to origin/main and re-ran the new tests. Result: FAILED test_stage2_hook_resolves_puid_pgid_aliases FAILED test_puid_pgid_populate_hermes_uid_gid AssertionError: assert ':' == '1000:10' That's the exact bug shape — PUID=1000 PGID=10 silently ignored, HERMES_UID/HERMES_GID stay empty. With the salvage applied, all 4 tests pass. Closes #25872 Co-authored-by: konsisumer <11262660+konsisumer@users.noreply.github.com>
This commit is contained in:
parent
a0fc3df878
commit
48083211ef
3 changed files with 106 additions and 1 deletions
|
|
@ -33,6 +33,15 @@ INSTALL_DIR="/opt/hermes"
|
|||
mkdir -p "$HERMES_HOME"
|
||||
|
||||
# --- UID/GID remap ---
|
||||
# Accept PUID/PGID as aliases for HERMES_UID/HERMES_GID. NAS users (UGOS,
|
||||
# Synology, unRAID) expect the LinuxServer.io PUID/PGID convention and
|
||||
# bind-mount /opt/data from a host directory owned by their own UID; without
|
||||
# this alias those vars are silently ignored and the s6-setuidgid drop to
|
||||
# UID 10000 leaves the runtime unable to read the volume. HERMES_UID/
|
||||
# HERMES_GID still win when both are set. See #15290, salvages #25872.
|
||||
HERMES_UID="${HERMES_UID:-${PUID:-}}"
|
||||
HERMES_GID="${HERMES_GID:-${PGID:-}}"
|
||||
|
||||
if [ -n "${HERMES_UID:-}" ] && [ "$HERMES_UID" != "$(id -u hermes)" ]; then
|
||||
echo "[stage2] Changing hermes UID to $HERMES_UID"
|
||||
usermod -u "$HERMES_UID" hermes
|
||||
|
|
|
|||
86
tests/tools/test_stage2_hook_puid_pgid.py
Normal file
86
tests/tools/test_stage2_hook_puid_pgid.py
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
"""Contract test: the s6-overlay stage2 hook accepts PUID/PGID as aliases for
|
||||
HERMES_UID/HERMES_GID.
|
||||
|
||||
Regression guard for #15290. NAS platforms (UGOS, Synology, unRAID) bind-mount
|
||||
/opt/data from a host directory owned by the user's own UID and expect the
|
||||
LinuxServer.io PUID/PGID convention. Without the alias those vars are silently
|
||||
ignored, the s6-setuidgid drop lands on UID 10000, and the runtime cannot read
|
||||
the volume. HERMES_UID/HERMES_GID must still take precedence when both are
|
||||
set.
|
||||
|
||||
The s6-overlay rework moved bootstrap from docker/entrypoint.sh (now a shim)
|
||||
to docker/stage2-hook.sh, which is installed as /etc/cont-init.d/01-hermes-setup
|
||||
by the Dockerfile. This test targets the post-rework location.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
STAGE2_HOOK = REPO_ROOT / "docker" / "stage2-hook.sh"
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def stage2_text() -> str:
|
||||
if not STAGE2_HOOK.exists():
|
||||
pytest.skip("docker/stage2-hook.sh not present in this checkout")
|
||||
return STAGE2_HOOK.read_text()
|
||||
|
||||
|
||||
def _alias_lines(text: str) -> list[str]:
|
||||
"""The stage2 hook lines that resolve HERMES_UID/HERMES_GID from aliases."""
|
||||
return [
|
||||
line.strip()
|
||||
for line in text.splitlines()
|
||||
if line.strip().startswith(("HERMES_UID=", "HERMES_GID="))
|
||||
]
|
||||
|
||||
|
||||
def test_stage2_hook_resolves_puid_pgid_aliases(stage2_text: str) -> None:
|
||||
alias_lines = _alias_lines(stage2_text)
|
||||
assert any("PUID" in line for line in alias_lines), (
|
||||
"docker/stage2-hook.sh must resolve HERMES_UID from a PUID alias; see #15290"
|
||||
)
|
||||
assert any("PGID" in line for line in alias_lines), (
|
||||
"docker/stage2-hook.sh must resolve HERMES_GID from a PGID alias; see #15290"
|
||||
)
|
||||
|
||||
|
||||
def _resolve(stage2_text: str, env: dict[str, str]) -> str:
|
||||
"""Run the stage2 hook's alias-resolution lines in isolation and report the
|
||||
resolved ``HERMES_UID:HERMES_GID`` pair."""
|
||||
bash = shutil.which("bash")
|
||||
if bash is None:
|
||||
pytest.skip("bash not available")
|
||||
script = "\n".join(_alias_lines(stage2_text))
|
||||
script += '\necho "${HERMES_UID:-}:${HERMES_GID:-}"\n'
|
||||
proc = subprocess.run(
|
||||
[bash, "-ec", script],
|
||||
env={"PATH": os.environ.get("PATH", "")} | env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return proc.stdout.strip()
|
||||
|
||||
|
||||
def test_puid_pgid_populate_hermes_uid_gid(stage2_text: str) -> None:
|
||||
assert _resolve(stage2_text, {"PUID": "1000", "PGID": "10"}) == "1000:10"
|
||||
|
||||
|
||||
def test_hermes_uid_gid_take_precedence_over_aliases(stage2_text: str) -> None:
|
||||
resolved = _resolve(
|
||||
stage2_text,
|
||||
{"HERMES_UID": "2000", "HERMES_GID": "2001", "PUID": "1000", "PGID": "10"},
|
||||
)
|
||||
assert resolved == "2000:2001"
|
||||
|
||||
|
||||
def test_no_uid_vars_leaves_values_empty(stage2_text: str) -> None:
|
||||
# An empty resolution means the stage2 hook keeps the default hermes user.
|
||||
assert _resolve(stage2_text, {}) == ":"
|
||||
|
|
@ -710,12 +710,22 @@ Check logs: `docker logs hermes`. Common causes:
|
|||
|
||||
### "Permission denied" errors
|
||||
|
||||
The container's stage2 hook drops privileges to the non-root `hermes` user (UID 10000) via `s6-setuidgid` inside each supervised service. If your host `~/.hermes/` is owned by a different UID, set `HERMES_UID`/`HERMES_GID` to match your host user, or ensure the data directory is writable:
|
||||
The container's stage2 hook drops privileges to the non-root `hermes` user (UID 10000) via `s6-setuidgid` inside each supervised service. If your host `~/.hermes/` is owned by a different UID, set `HERMES_UID`/`HERMES_GID` — or their `PUID`/`PGID` aliases, for parity with LinuxServer.io and NAS images — to match your host user, or ensure the data directory is writable:
|
||||
|
||||
```sh
|
||||
chmod -R 755 ~/.hermes
|
||||
```
|
||||
|
||||
On a NAS (UGOS, Synology, unRAID) the data directory is typically a **bind mount** owned by a host UID the container cannot `chown`. Set `PUID`/`PGID` (or `HERMES_UID`/`HERMES_GID`) to that host user so the runtime runs as the owner of the mount rather than UID 10000:
|
||||
|
||||
```sh
|
||||
docker run -d \
|
||||
--name hermes \
|
||||
-e PUID=1000 -e PGID=10 \
|
||||
-v /volume1/docker/hermes:/opt/data \
|
||||
nousresearch/hermes-agent gateway run
|
||||
```
|
||||
|
||||
`docker exec hermes <cmd>` automatically drops to UID 10000 too — see [`docker exec` automatically drops to the `hermes` user](#docker-exec-automatically-drops-to-the-hermes-user) for details and the per-invocation opt-out.
|
||||
|
||||
### Browser tools not working
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue