fix(hermes): refresh seeded config files on every container boot#27
fix(hermes): refresh seeded config files on every container boot#27EnriqueCanals wants to merge 3 commits into
Conversation
The previous `cp -rn` (no-clobber) seeding meant that once a config file
existed in `~/.hermes-{local,openrouter}`, image rebuilds with updated
defaults were silently ignored. This breaks the config-as-code workflow for
the most common deployment topology — and the one documented in this
README's own fly.io section — where a persistent volume is mounted on
`~/.hermes-openrouter`. Downstream deployers see their custom
`config.yaml`/`system-prompt.md`/`.env` baked into the image, deploy, then
get confused when their changes don't take effect after the first boot.
Distinguish "config" from "state" at the top level of the seed source:
* Top-level files (config.yaml, .env, system-prompt.md, …) → always
refreshed from the image on every boot. This is config-as-code.
* Top-level directories (sessions/, logs/, hooks/, memories/, skills/,
plans/, workspace/) → initialized once on first boot, then preserved
across container restarts. This is runtime state.
Hidden files (.env) are handled correctly via dotglob. The seed source and
destination paths are now parameterized via env vars (HERMES_SEED_SRC_*,
HERMES_SEED_DST_*) so the behavior can be unit-tested without spinning up
a real container; defaults match the prior baked-in paths so this is a
no-op for production use.
First-boot behavior is identical to before. Only subsequent boots change,
and only in the direction users expect.
Made-with: Cursor
…rmes.sh
Four cases:
* first boot: empty volume → top-level files (incl. dotfiles) and
state-scaffolding directories are all seeded
* subsequent boot: stale top-level config files are overwritten from
the image (config-as-code)
* subsequent boot: pre-existing state directories are preserved
untouched, including not letting the image's empty .gitkeep replace
a user's session
* missing seed source dir is a no-op (does not fail)
Style follows tests/e2e/cli.test.mjs: node:test, tempdirs, asserts. The
entrypoint is invoked with /usr/bin/true as the exec target so we exercise
the full seeding code path without needing hermes installed on the test
host.
Made-with: Cursor
Made-with: Cursor
Review Note: Runtime config mutations will be lostHermes writes to The existing Suggestion: Split the file behavior:
The "config-as-code" framing works for truly static seed files, but cc @capotej |
|
Thanks for the careful review and the architectural pointer — both land. You're right on the technical substance: And on the broader point about Refactoring our deployment to follow the claw pattern (use the upstream image directly via Two small follow-ups I'd be happy to send if useful — let me know:
Either, both, or neither — your call. Thanks again for engaging in good faith on this. |
Problem
entrypoint-hermes.shseeds/etc/harness/hermes-defaults/<flavor>/into~/.hermes-<flavor>/usingcp -rn(no-clobber). That works on first boot, but on every subsequent boot the seed is silently a no-op — even if the image was rebuilt with updated defaults. Downstream deployers using a persistent volume on~/.hermes-openrouter(the topology this README's fly.io section documents) end up running stale config forever, with no indication that anything is wrong.This was easy to walk into: I built a custom hermes image on top of
ghcr.io/capotej/harness:hermes-1.5.0, baked in a customconfig.yamlandsystem-prompt.md, deployed to fly with the README's recommended volume mount, watched first-boot work, then spent ~30 minutes wondering why my config edits weren't taking effect on redeploys.Fix
Distinguish "config" from "state" at the top level of the seed source:
config.yaml,.env,system-prompt.md, …) → refreshed from the image on every boot. Config-as-code.sessions/,logs/,hooks/,memories/,skills/,plans/,workspace/) → initialized once on first boot, then preserved across restarts. Runtime state.Hidden files (
.env) are handled correctly via dotglob. Seed paths are parameterized viaHERMES_SEED_SRC_*/HERMES_SEED_DST_*env vars so the behavior is unit-testable without spinning up a real container; defaults match the prior baked-in paths so this is a no-op for production.First-boot behavior is identical. Only subsequent boots change, and only in the direction users expect.
Tests
New
tests/e2e/entrypoint-hermes.test.mjs(4 cases, followscli.test.mjsstyle):.gitkeepreplace a user's sessionshellcheck,hadolint,biome,markdownlintall clean. Pre-existing CLI test failures onmain(#16, #20, #24) confirmed unrelated to this change.