Skip to content

fix(hermes): fall back to ruamel.yaml when the image ships no PyYAML - #289

Open
brandon-nuttall wants to merge 1 commit into
av:mainfrom
brandon-nuttall:fix/hermes-provider-yaml-fallback
Open

brandon-nuttall wants to merge 1 commit into
av:mainfrom
brandon-nuttall:fix/hermes-provider-yaml-fallback

Conversation

@brandon-nuttall

Copy link
Copy Markdown

Summary

configure-provider.sh runs before the image entrypoint, so its imports sit on hermes' critical path:

/bin/sh /configure-provider.sh gateway run
  import yaml            # ModuleNotFoundError on hermes-agent >= 2026-10-08
  set -e -> exit 1       # entrypoint-dispatch.sh is never exec'd

The hermes-agent image dropped PyYAML from /opt/hermes/.venv in its 2026-10-08 rebuild. That venv is Python 3.14.7 with ruamel.yaml 0.18.16 only — which is also what Hermes itself reads config.yaml with. There is no fallback interpreter (python3 resolves to the venv; /usr/bin/python3 has no yaml either) and no pip in the image, so nothing can self-heal at boot.

Symptom: harbor.hermes exits 1 ~170 ms after start, and the entire container log is the traceback — s6-overlay never boots, so there is no gateway and nothing else to read.

 if [ -n "${HERMES_PROVIDER_BASE_URL:-}" ]; then
     python3 - <<'EOF'
 import os
-import yaml
+try:
+    import yaml
+except ImportError:
+    from ruamel.yaml import YAML

Reachable only when a cross-integration file sets HERMES_PROVIDER_BASE_URL (hermes + llamacpp/ollama/prismml/litellm). A bare harbor up hermes skips the block and starts fine, which is why this stayed hidden.

Adds a # services > hermes fact asserting the writer is resolved at runtime.

Evidence

  • Before: upstream bytes on the current image → ModuleNotFoundError: No module named 'yaml', EXIT=1, entrypoint never reached.
    After: patched bytes on the same image → provider step succeeds, entrypoint-dispatch.sh exec'd, model block written.
  • No regression on older images: patched bytes on a pre-Oct-8 image (PyYAML present) → green, output byte-for-byte unchanged.
  • Fidelity: against a real 619-line config.yaml, the rewrite changes 4 quoting-only lines ('on'→on, 'off'→off, 'cache-capy', a cursor glyph); all four re-parse to identical strings under Hermes' own reader. _yaml.indent(mapping=2, sequence=4, offset=2) is load-bearing — without it the fallback restyles the entire file (56 changed lines instead of 4).

Considered shelling out to hermes config set model.provider custom instead, which would drop the YAML dependency entirely. It works pre-entrypoint, but three sets cost 5.2 s at boot and it performs a schema migration as a side effect (_config_version 46 → 50, dropping the tirith_* keys), so it is not a drop-in for a boot-time wrapper.

Merge Danger

Door: two-way — one script, no interface or config change, single-commit revert.

Blast Radius: hermes startup, only when a provider cross-file is active.

On images that still ship PyYAML, output is unchanged. On ruamel-only images config.yaml gains 4 quoting-only diffs. One consequence worth stating: bare on/off are booleans to a YAML 1.1 parser, so anything outside the image that parses this file with PyYAML would read them differently. Nothing inside the image does — PyYAML is no longer present, and Hermes reads the file with ruamel.

configure-provider.sh imports PyYAML to persist HERMES_PROVIDER_BASE_URL
into config.yaml. The hermes-agent image dropped PyYAML from its venv in
the 2026-10-08 rebuild and ships ruamel.yaml only — which is also what
Hermes itself uses to read the file. There is no fallback interpreter
(python3 resolves to the venv, /usr/bin/python3 has no yaml either) and
no pip in the image.

Because the wrapper runs before the image entrypoint under `set -e`, the
failed import aborts the script before `exec entrypoint-dispatch.sh`, so
the gateway never starts: the container exits 1 ~170ms after launch with
nothing on stderr but the traceback.

Resolve the writer at runtime instead — PyYAML when present, so older
images keep byte-for-byte output, ruamel.yaml otherwise. The indent call
reproduces the block-sequence indentation Hermes writes, so switching
writers does not restyle the file.

Only reachable when a cross-integration file sets HERMES_PROVIDER_BASE_URL
(hermes + llamacpp/ollama/prismml/litellm); a bare `harbor up hermes`
skips the block and starts fine.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant