mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-29 23:41:35 +08:00
Compare commits
3 Commits
fix/plugin
...
fix/nix-sh
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a239b664e6 | ||
|
|
33c3f0a203 | ||
|
|
f28390d3c8 |
@@ -197,14 +197,44 @@ def _ensure_default_soul_md(home: Path) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def ensure_hermes_home():
|
def ensure_hermes_home():
|
||||||
"""Ensure ~/.hermes directory structure exists with secure permissions."""
|
"""Ensure ~/.hermes directory structure exists with secure permissions.
|
||||||
|
|
||||||
|
In managed mode (NixOS), dirs are created by the activation script with
|
||||||
|
setgid + group-writable (2770). We skip mkdir and set umask(0o007) so
|
||||||
|
any files created (e.g. SOUL.md) are group-writable (0660).
|
||||||
|
"""
|
||||||
home = get_hermes_home()
|
home = get_hermes_home()
|
||||||
home.mkdir(parents=True, exist_ok=True)
|
if is_managed():
|
||||||
_secure_dir(home)
|
old_umask = os.umask(0o007)
|
||||||
|
try:
|
||||||
|
_ensure_hermes_home_managed(home)
|
||||||
|
finally:
|
||||||
|
os.umask(old_umask)
|
||||||
|
else:
|
||||||
|
home.mkdir(parents=True, exist_ok=True)
|
||||||
|
_secure_dir(home)
|
||||||
|
for subdir in ("cron", "sessions", "logs", "memories"):
|
||||||
|
d = home / subdir
|
||||||
|
d.mkdir(parents=True, exist_ok=True)
|
||||||
|
_secure_dir(d)
|
||||||
|
_ensure_default_soul_md(home)
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_hermes_home_managed(home: Path):
|
||||||
|
"""Managed-mode variant: verify dirs exist (activation creates them), seed SOUL.md."""
|
||||||
|
if not home.is_dir():
|
||||||
|
raise RuntimeError(
|
||||||
|
f"HERMES_HOME {home} does not exist. "
|
||||||
|
"Run 'sudo nixos-rebuild switch' first."
|
||||||
|
)
|
||||||
for subdir in ("cron", "sessions", "logs", "memories"):
|
for subdir in ("cron", "sessions", "logs", "memories"):
|
||||||
d = home / subdir
|
d = home / subdir
|
||||||
d.mkdir(parents=True, exist_ok=True)
|
if not d.is_dir():
|
||||||
_secure_dir(d)
|
raise RuntimeError(
|
||||||
|
f"{d} does not exist. "
|
||||||
|
"Run 'sudo nixos-rebuild switch' first."
|
||||||
|
)
|
||||||
|
# Inside umask(0o007) scope — SOUL.md will be created as 0660
|
||||||
_ensure_default_soul_md(home)
|
_ensure_default_soul_md(home)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ secrets are never written to disk.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
from logging.handlers import RotatingFileHandler
|
from logging.handlers import RotatingFileHandler
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
@@ -177,6 +178,38 @@ def setup_verbose_logging() -> None:
|
|||||||
# Internal helpers
|
# Internal helpers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class _ManagedRotatingFileHandler(RotatingFileHandler):
|
||||||
|
"""RotatingFileHandler that ensures group-writable perms in managed mode.
|
||||||
|
|
||||||
|
In managed mode (NixOS), the stateDir uses setgid (2770) so new files
|
||||||
|
inherit the hermes group. However, both _open() (initial creation) and
|
||||||
|
doRollover() create files via open(), which uses the process umask —
|
||||||
|
typically 0022, producing 0644. This subclass applies chmod 0660 after
|
||||||
|
both operations so the gateway and interactive users can share log files.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
from hermes_cli.config import is_managed
|
||||||
|
self._managed = is_managed()
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def _chmod_if_managed(self):
|
||||||
|
if self._managed:
|
||||||
|
try:
|
||||||
|
os.chmod(self.baseFilename, 0o660)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _open(self):
|
||||||
|
stream = super()._open()
|
||||||
|
self._chmod_if_managed()
|
||||||
|
return stream
|
||||||
|
|
||||||
|
def doRollover(self):
|
||||||
|
super().doRollover()
|
||||||
|
self._chmod_if_managed()
|
||||||
|
|
||||||
|
|
||||||
def _add_rotating_handler(
|
def _add_rotating_handler(
|
||||||
logger: logging.Logger,
|
logger: logging.Logger,
|
||||||
path: Path,
|
path: Path,
|
||||||
@@ -198,7 +231,7 @@ def _add_rotating_handler(
|
|||||||
return # already attached
|
return # already attached
|
||||||
|
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
handler = RotatingFileHandler(
|
handler = _ManagedRotatingFileHandler(
|
||||||
str(path), maxBytes=max_bytes, backupCount=backup_count,
|
str(path), maxBytes=max_bytes, backupCount=backup_count,
|
||||||
)
|
)
|
||||||
handler.setLevel(level)
|
handler.setLevel(level)
|
||||||
|
|||||||
@@ -560,10 +560,14 @@
|
|||||||
# ── Directories ───────────────────────────────────────────────────
|
# ── Directories ───────────────────────────────────────────────────
|
||||||
{
|
{
|
||||||
systemd.tmpfiles.rules = [
|
systemd.tmpfiles.rules = [
|
||||||
"d ${cfg.stateDir} 0750 ${cfg.user} ${cfg.group} - -"
|
"d ${cfg.stateDir} 2770 ${cfg.user} ${cfg.group} - -"
|
||||||
"d ${cfg.stateDir}/.hermes 0750 ${cfg.user} ${cfg.group} - -"
|
"d ${cfg.stateDir}/.hermes 2770 ${cfg.user} ${cfg.group} - -"
|
||||||
|
"d ${cfg.stateDir}/.hermes/cron 2770 ${cfg.user} ${cfg.group} - -"
|
||||||
|
"d ${cfg.stateDir}/.hermes/sessions 2770 ${cfg.user} ${cfg.group} - -"
|
||||||
|
"d ${cfg.stateDir}/.hermes/logs 2770 ${cfg.user} ${cfg.group} - -"
|
||||||
|
"d ${cfg.stateDir}/.hermes/memories 2770 ${cfg.user} ${cfg.group} - -"
|
||||||
"d ${cfg.stateDir}/home 0750 ${cfg.user} ${cfg.group} - -"
|
"d ${cfg.stateDir}/home 0750 ${cfg.user} ${cfg.group} - -"
|
||||||
"d ${cfg.workingDirectory} 0750 ${cfg.user} ${cfg.group} - -"
|
"d ${cfg.workingDirectory} 2770 ${cfg.user} ${cfg.group} - -"
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -575,7 +579,21 @@
|
|||||||
mkdir -p ${cfg.stateDir}/home
|
mkdir -p ${cfg.stateDir}/home
|
||||||
mkdir -p ${cfg.workingDirectory}
|
mkdir -p ${cfg.workingDirectory}
|
||||||
chown ${cfg.user}:${cfg.group} ${cfg.stateDir} ${cfg.stateDir}/.hermes ${cfg.stateDir}/home ${cfg.workingDirectory}
|
chown ${cfg.user}:${cfg.group} ${cfg.stateDir} ${cfg.stateDir}/.hermes ${cfg.stateDir}/home ${cfg.workingDirectory}
|
||||||
chmod 0750 ${cfg.stateDir} ${cfg.stateDir}/.hermes ${cfg.stateDir}/home ${cfg.workingDirectory}
|
chmod 2770 ${cfg.stateDir} ${cfg.stateDir}/.hermes ${cfg.workingDirectory}
|
||||||
|
chmod 0750 ${cfg.stateDir}/home
|
||||||
|
|
||||||
|
# Create subdirs, set setgid + group-writable, migrate existing files.
|
||||||
|
# Nix-managed files (config.yaml, .env, .managed) stay 0640/0644.
|
||||||
|
find ${cfg.stateDir}/.hermes -maxdepth 1 \
|
||||||
|
\( -name "*.db" -o -name "*.db-wal" -o -name "*.db-shm" -o -name "SOUL.md" \) \
|
||||||
|
-exec chmod g+rw {} + 2>/dev/null || true
|
||||||
|
for _subdir in cron sessions logs memories; do
|
||||||
|
mkdir -p "${cfg.stateDir}/.hermes/$_subdir"
|
||||||
|
chown ${cfg.user}:${cfg.group} "${cfg.stateDir}/.hermes/$_subdir"
|
||||||
|
chmod 2770 "${cfg.stateDir}/.hermes/$_subdir"
|
||||||
|
find "${cfg.stateDir}/.hermes/$_subdir" -type f \
|
||||||
|
-exec chmod g+rw {} + 2>/dev/null || true
|
||||||
|
done
|
||||||
|
|
||||||
# Merge Nix settings into existing config.yaml.
|
# Merge Nix settings into existing config.yaml.
|
||||||
# Preserves user-added keys (skills, streaming, etc.); Nix keys win.
|
# Preserves user-added keys (skills, streaming, etc.); Nix keys win.
|
||||||
@@ -662,6 +680,10 @@ HERMES_NIX_ENV_EOF
|
|||||||
Restart = cfg.restart;
|
Restart = cfg.restart;
|
||||||
RestartSec = cfg.restartSec;
|
RestartSec = cfg.restartSec;
|
||||||
|
|
||||||
|
# Shared-state: files created by the gateway should be group-writable
|
||||||
|
# so interactive users in the hermes group can read/write them.
|
||||||
|
UMask = "0007";
|
||||||
|
|
||||||
# Hardening
|
# Hardening
|
||||||
NoNewPrivileges = true;
|
NoNewPrivileges = true;
|
||||||
ProtectSystem = "strict";
|
ProtectSystem = "strict";
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import stat
|
||||||
from logging.handlers import RotatingFileHandler
|
from logging.handlers import RotatingFileHandler
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
@@ -300,6 +301,59 @@ class TestAddRotatingHandler:
|
|||||||
logger.removeHandler(h)
|
logger.removeHandler(h)
|
||||||
h.close()
|
h.close()
|
||||||
|
|
||||||
|
def test_managed_mode_initial_open_sets_group_writable(self, tmp_path):
|
||||||
|
log_path = tmp_path / "managed-open.log"
|
||||||
|
logger = logging.getLogger("_test_rotating_managed_open")
|
||||||
|
formatter = logging.Formatter("%(message)s")
|
||||||
|
|
||||||
|
old_umask = os.umask(0o022)
|
||||||
|
try:
|
||||||
|
with patch("hermes_cli.config.is_managed", return_value=True):
|
||||||
|
hermes_logging._add_rotating_handler(
|
||||||
|
logger, log_path,
|
||||||
|
level=logging.INFO, max_bytes=1024, backup_count=1,
|
||||||
|
formatter=formatter,
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
os.umask(old_umask)
|
||||||
|
|
||||||
|
assert log_path.exists()
|
||||||
|
assert stat.S_IMODE(log_path.stat().st_mode) == 0o660
|
||||||
|
|
||||||
|
for h in list(logger.handlers):
|
||||||
|
if isinstance(h, RotatingFileHandler):
|
||||||
|
logger.removeHandler(h)
|
||||||
|
h.close()
|
||||||
|
|
||||||
|
def test_managed_mode_rollover_sets_group_writable(self, tmp_path):
|
||||||
|
log_path = tmp_path / "managed-rollover.log"
|
||||||
|
logger = logging.getLogger("_test_rotating_managed_rollover")
|
||||||
|
formatter = logging.Formatter("%(message)s")
|
||||||
|
|
||||||
|
old_umask = os.umask(0o022)
|
||||||
|
try:
|
||||||
|
with patch("hermes_cli.config.is_managed", return_value=True):
|
||||||
|
hermes_logging._add_rotating_handler(
|
||||||
|
logger, log_path,
|
||||||
|
level=logging.INFO, max_bytes=1, backup_count=1,
|
||||||
|
formatter=formatter,
|
||||||
|
)
|
||||||
|
handler = next(
|
||||||
|
h for h in logger.handlers if isinstance(h, RotatingFileHandler)
|
||||||
|
)
|
||||||
|
logger.info("a" * 256)
|
||||||
|
handler.flush()
|
||||||
|
finally:
|
||||||
|
os.umask(old_umask)
|
||||||
|
|
||||||
|
assert log_path.exists()
|
||||||
|
assert stat.S_IMODE(log_path.stat().st_mode) == 0o660
|
||||||
|
|
||||||
|
for h in list(logger.handlers):
|
||||||
|
if isinstance(h, RotatingFileHandler):
|
||||||
|
logger.removeHandler(h)
|
||||||
|
h.close()
|
||||||
|
|
||||||
|
|
||||||
class TestReadLoggingConfig:
|
class TestReadLoggingConfig:
|
||||||
"""_read_logging_config() reads from config.yaml."""
|
"""_read_logging_config() reads from config.yaml."""
|
||||||
|
|||||||
Reference in New Issue
Block a user