diff --git a/agents/hermes/start.sh b/agents/hermes/start.sh index d51eef4120..44d3a9c94f 100755 --- a/agents/hermes/start.sh +++ b/agents/hermes/start.sh @@ -217,6 +217,22 @@ remove_stale_gateway_file() { fi } +hermes_config_path_is_locked() { + local path="$1" + local owner mode + + [ -f "$path" ] || return 1 + [ ! -L "$path" ] || return 1 + + owner="$(stat -c '%U:%G' "$path" 2>/dev/null || stat -f '%Su:%Sg' "$path" 2>/dev/null || true)" + mode="$(stat -c '%a' "$path" 2>/dev/null || stat -f '%Lp' "$path" 2>/dev/null || true)" + mode="${mode#0}" + [ -n "$mode" ] || return 1 + + [ "$owner" = "root:root" ] || return 1 + (((8#$mode & 0222) == 0)) +} + hermes_config_root_is_locked() { local owner mode @@ -224,9 +240,12 @@ hermes_config_root_is_locked() { mode="$(stat -c '%a' "$HERMES_DIR" 2>/dev/null || stat -f '%Lp' "$HERMES_DIR" 2>/dev/null || true)" case "${owner} ${mode}" in - "root:root 755" | "root:root 0755") return 0 ;; + "root:root 755" | "root:root 0755") ;; + *) return 1 ;; esac - return 1 + + hermes_config_path_is_locked "${HERMES_DIR}/config.yaml" \ + && hermes_config_path_is_locked "${HERMES_DIR}/.env" } ensure_hermes_config_root_mode() { diff --git a/test/hermes-start.test.ts b/test/hermes-start.test.ts index fe0569407f..7177b5e18b 100644 --- a/test/hermes-start.test.ts +++ b/test/hermes-start.test.ts @@ -117,6 +117,7 @@ function runHermesGatewayRuntimeCleanup(opts: { staleLock?: boolean; stalePid?: boolean; lockedConfigRoot?: boolean; + rootOwnedConfigRoot?: boolean; }) { const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-hermes-runtime-cleanup-")); const hermesHome = path.join(tmpDir, ".hermes"); @@ -130,9 +131,13 @@ function runHermesGatewayRuntimeCleanup(opts: { fs.mkdirSync(runtimeDir, { recursive: true }); fs.mkdirSync(procRoot, { recursive: true }); - if (opts.lockedConfigRoot) { + if (opts.lockedConfigRoot || opts.rootOwnedConfigRoot) { fs.chmodSync(hermesHome, 0o755); } + if (opts.lockedConfigRoot) { + fs.writeFileSync(path.join(hermesHome, "config.yaml"), "model: test\n"); + fs.writeFileSync(path.join(hermesHome, ".env"), "HERMES_TEST=1\n"); + } fs.symlinkSync("runtime/gateway.pid", legacyPid); if (opts.stalePid !== false) fs.writeFileSync(runtimePid, "999999\n"); if (opts.staleLock !== false) fs.writeFileSync(runtimeLock, "stale lock"); @@ -157,6 +162,7 @@ function runHermesGatewayRuntimeCleanup(opts: { extractShellFunctionFromSource(src, "has_live_hermes_gateway"), extractShellFunctionFromSource(src, "cleanup_orphan_socat_forwarders"), extractShellFunctionFromSource(src, "remove_stale_gateway_file"), + extractShellFunctionFromSource(src, "hermes_config_path_is_locked"), extractShellFunctionFromSource(src, "hermes_config_root_is_locked"), extractShellFunctionFromSource(src, "ensure_hermes_config_root_mode"), extractShellFunctionFromSource(src, "ensure_hermes_state_dir"), @@ -164,15 +170,23 @@ function runHermesGatewayRuntimeCleanup(opts: { extractShellFunctionFromSource(src, "cleanup_stale_hermes_gateway_runtime"), `KILL_LOG=${shellQuote(killLog)}`, 'kill() { printf "%s\\n" "$*" >>"$KILL_LOG"; return 0; }', + 'id() { if [ "${1:-}" = "-u" ]; then printf "1000\\n"; else command id "$@"; fi; }', `HERMES_DIR=${shellQuote(hermesHome)}`, `NEMOCLAW_PROC_ROOT=${shellQuote(procRoot)}`, - opts.lockedConfigRoot + opts.lockedConfigRoot || opts.rootOwnedConfigRoot ? [ 'stat() {', ' if [ "${1:-}" = "-c" ] && [ "${2:-}" = "%U:%G" ] && [ "${3:-}" = "$HERMES_DIR" ]; then printf "root:root\\n"; return 0; fi', ' if [ "${1:-}" = "-c" ] && [ "${2:-}" = "%a" ] && [ "${3:-}" = "$HERMES_DIR" ]; then printf "755\\n"; return 0; fi', ' if [ "${1:-}" = "-f" ] && [ "${2:-}" = "%Su:%Sg" ] && [ "${3:-}" = "$HERMES_DIR" ]; then printf "root:root\\n"; return 0; fi', ' if [ "${1:-}" = "-f" ] && [ "${2:-}" = "%Lp" ] && [ "${3:-}" = "$HERMES_DIR" ]; then printf "755\\n"; return 0; fi', + ' case "${3:-}" in "$HERMES_DIR/config.yaml"|"$HERMES_DIR/.env")', + ' if [ "${1:-}" = "-c" ] && [ "${2:-}" = "%U:%G" ]; then printf "root:root\\n"; return 0; fi', + ' if [ "${1:-}" = "-c" ] && [ "${2:-}" = "%a" ]; then printf "444\\n"; return 0; fi', + ' if [ "${1:-}" = "-f" ] && [ "${2:-}" = "%Su:%Sg" ]; then printf "root:root\\n"; return 0; fi', + ' if [ "${1:-}" = "-f" ] && [ "${2:-}" = "%Lp" ]; then printf "444\\n"; return 0; fi', + ' ;;', + ' esac', ' command stat "$@"', '}', ].join("\n") @@ -316,7 +330,11 @@ describe("agents/hermes/start.sh gateway runtime cleanup", () => { }); it("repairs the Hermes v0.14 writable directory layout before launch", () => { - const run = runHermesGatewayRuntimeCleanup({ staleLock: false, stalePid: false }); + const run = runHermesGatewayRuntimeCleanup({ + staleLock: false, + stalePid: false, + rootOwnedConfigRoot: true, + }); expect(run.result.status).toBe(0); expect(run.hermesDirMode).toBe("3770");