hermes: skip the auth bootstrap when the credential is unchanged (three Python boots off every wake) - #86
Conversation
…ee Python boots off every wake) Every boot hashed ADMIN_PASSWORD (a Python start that imports the hermes plugin tree) and ran `hermes config set` twice (two more CLI boots) before the dashboard could start -- on a 4 vCPU VM that is most of the ~12 s hermes takes to wake on InstaCloud, and the result was already in config.yaml on the volume from the previous boot. The entrypoint now keeps a marker next to config.yaml holding a SHA-256 of `user:password` (never the values; 0600, next to the bcrypt hash that config.yaml already stores). A boot whose credential digest matches the marker and still finds config.yaml skips straight to serving; anything else -- first boot, a rotated secret, a missing or unreadable marker, a config.yaml that went missing -- takes the full path, and the marker is written last, only after both `config set` calls succeeded, so it can never describe a config that was not written. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G1fBttq231ZKpU5ivCAG2i
jwfing
left a comment
There was a problem hiding this comment.
Summary
This change should not merge as-is because the optimization adds a fast offline password verifier for the Hermes dashboard credential.
Requirements Context
I used the PR description as the main intent: skip the expensive Hermes auth bootstrap on warm boots when the admin credential is unchanged, while still taking the slow path for first boot, rotated credentials, missing config, or bad marker state. I did not find a linked issue in the provided metadata. Repo context confirms the dashboard is protected by an operator-chosen ADMIN_USERNAME/ADMIN_PASSWORD, state persists under HERMES_HOME=/data/.hermes, and the password is the primary gate in front of an agent holding API keys (templates/hermes/README.md:22-40, templates/hermes/README.md:49-80, templates/hermes/insta.template.yaml:27-39).
Findings
Critical
templates/hermes/entrypoint.sh:37-50storessha256(user:password)on the persistent volume. That marker is a fast, unsalted password verifier for the user-chosen dashboard password, while the existingconfig.yamlpath stores a bcrypt hash. Anyone who can read a volume snapshot, backup, diagnostic bundle, or same-container filesystem can test password guesses cheaply by hashingusername:guess; this materially weakens the auth secret and contradicts the claim that the marker “widens nothing.” Avoid persisting a fast password-derived digest; use a non-password-equivalent credential version/fingerprint if the platform exposes one, or keep the stored verifier at least as hard to attack as the existing bcrypt-based config.
Suggestion
templates/hermes/entrypoint.sh:38-50should make digest computation fail closed. Withset -ebut nopipefail, asha256sumfailure can be masked bycut, producing an empty digest that may be written and later treated as a match. Validate a 64-hex digest before compare/write, or enable pipeline failure handling.templates/hermes/entrypoint.sh:49-51does not guarantee the marker is0600if the file already exists, because shell redirection preserves the existing file mode. It also resets the process umask to022rather than restoring the previous value before the dashboard process starts. Force the marker mode and restore the prior umask.templates/hermes/entrypoint.sh:37-52has no automated coverage for the new state machine. Existing template tests cover manifest/entrypoint variable consistency (templates/scripts/variables-lib.test.mjs:103-132), but not first boot, matching marker, rotated secret, missing config, empty marker, or unreadable marker behavior.
Information
templates/hermes/entrypoint.sh:39-46otherwise matches the PR’s functionality/performance shape: it only skips whenconfig.yamlexists and the marker matches, and it leaves the gateway/dashboard flow unchanged. No SQL, HTTP request handling, new dependencies, or request-time performance paths are introduced.git diff --check main...HEAD -- templates/hermes/entrypoint.shpassed; I did not run the full test suite because the review is read-only and the existing tests create/remove fixtures.
Verdict
Request changes due to the password-derived SHA-256 marker.
…of keeping a marker (review round 1) r2d2: a SHA-256 of user:password on the volume is a fast, unsalted verifier -- cheaper to attack than the scrypt hash config.yaml already holds. So no marker at all: the entrypoint now checks the supplied credential against config.yaml directly -- username equal, password re-derived with the stored scrypt parameters (the same routine as upstream's _verify_password: split on "$", base64 salt and digest, hashlib.scrypt with dklen=len(digest), maxmem=0) and compared in constant time. Stdlib Python only, a fraction of a second, and nothing new is stored anywhere. Any failure of the check -- no config.yaml, no hash, malformed, a Python error -- takes the full bootstrap path (exit status, not a pipeline, so nothing can be masked), which also answers the pipefail suggestion. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G1fBttq231ZKpU5ivCAG2i
jwfing
left a comment
There was a problem hiding this comment.
Summary
The fast-path idea is sound, but this revision has release and correctness blockers before merge.
Requirements context
I used the PR description as the primary intent: skip the Hermes dashboard auth bootstrap only when config.yaml already holds the same ADMIN_USERNAME and a matching scrypt hash for ADMIN_PASSWORD; first boot, rotated credentials, missing/malformed hashes, and Python errors must take the slow path. Local template docs confirm Hermes persists state under /data/.hermes, requires operator-supplied dashboard credentials, and starts the dashboard from this entrypoint (templates/hermes/README.md:20-34, templates/hermes/README.md:47-65, templates/hermes/insta.template.yaml:26-39). I also checked the pinned upstream image reference (templates/hermes/Dockerfile:1-2) against upstream Hermes basic-auth behavior: https://github.com/NousResearch/hermes-agent/blob/v2026.8.27/plugins/dashboard_auth/basic/__init__.py#L123-L146.
Findings
Critical:
templates/hermes/entrypoint.sh:39-65,templates/hermes/insta.template.yaml:1-19,templates/scripts/version-guard.mjs:1-5,templates/scripts/version-guard.mjs:58-60- The Hermes template runtime changed but the manifest still advertises version2.3.1and the image tag still points attemplates/hermes:2.3.1. The repo explicitly requires a version bump for any non-doc publishable template change because otherwise the canonical GHCR tag is overwritten and existing deployments can drift. This should block until the Hermes manifest version and image tag are bumped.templates/hermes/entrypoint.sh:49-64- The fast-path verifier does not reject all malformed hashes. It usesstored.split("$", 5)and non-validatingbase64.b64decode, so a stored value with an extra delimiter such as a trailing$can still decode to the same digest and return success here, while upstream Hermes rejects it because its verifier requires exactly six$-separated fields. In that case the entrypoint skipshermes config set, leaves the malformed hash inconfig.yaml, and the dashboard's real login verifier rejects the credential. This violates the stated requirement that malformed hashes take the slow path and regresses the old always-rewrite behavior.
Suggestion:
templates/hermes/entrypoint.sh:39-65,templates/scripts/variables-lib.test.mjs:104-124- Add focused coverage for the new credential fast path, especially matching hash, rotated password, missing hash, malformed extra-field hash, and unreadable YAML. The existing template tests validate env declarations, but they do not exercise this shell/Python verifier behavior.
Information:
templates/hermes/entrypoint.sh:32-43- The comment and PR text call the check stdlib-only, but it importsyamlfrom PyYAML. Hermes already relies on YAML support, so this is not a dependency blocker, just wording worth correcting.templates/hermes/entrypoint.sh:57-64- Security review found no new secret logging in the changed path; the skip log is non-sensitive and the slow path still suppresses theconfig setoutput that would echo the hash.templates/hermes/entrypoint.sh:39-65- Performance review found no separate concern with the intended warm path; the added check is bounded to one config read and one scrypt verification instead of three Hermes CLI startups..github/workflows/templates-lint.yml:29-35- I did not run the local checks because dependencies are not installed in this read-only checkout, and installing them would modify the workspace.
Verdict
Request changes. The version bump is required by repo policy, and the verifier needs to match upstream's malformed-hash rejection before the fast path is safe.
There was a problem hiding this comment.
1 issue found across 1 file (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="templates/hermes/entrypoint.sh">
<violation number="1" location="templates/hermes/entrypoint.sh:53">
P2: When persistent config contains a parseable but resource-intensive scrypt hash, this call performs unbounded work before the fallback can run, so startup can hang or be killed. Bound the scrypt parameters and derived-key length before invoking `hashlib.scrypt`.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| if kind != "scrypt": | ||
| sys.exit(1) | ||
| salt, digest = base64.b64decode(salt), base64.b64decode(digest) | ||
| got = hashlib.scrypt(os.environ["ADMIN_PASSWORD"].encode(), salt=salt, n=int(n), r=int(r), p=int(p), dklen=len(digest), maxmem=0) |
There was a problem hiding this comment.
P2: When persistent config contains a parseable but resource-intensive scrypt hash, this call performs unbounded work before the fallback can run, so startup can hang or be killed. Bound the scrypt parameters and derived-key length before invoking hashlib.scrypt.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At templates/hermes/entrypoint.sh, line 53:
<comment>When persistent config contains a parseable but resource-intensive scrypt hash, this call performs unbounded work before the fallback can run, so startup can hang or be killed. Bound the scrypt parameters and derived-key length before invoking `hashlib.scrypt`.</comment>
<file context>
@@ -25,30 +25,43 @@ mkdir -p "$HERMES_HOME"
+if kind != "scrypt":
+ sys.exit(1)
+salt, digest = base64.b64decode(salt), base64.b64decode(digest)
+got = hashlib.scrypt(os.environ["ADMIN_PASSWORD"].encode(), salt=salt, n=int(n), r=int(r), p=int(p), dklen=len(digest), maxmem=0)
+sys.exit(0 if hmac.compare_digest(got, digest) else 1)
+PY
</file context>
What
The hermes entrypoint skips the dashboard auth bootstrap (hash the password,
hermes config settwice) when the supplied credential already matches whatconfig.yamlholds. That bootstrap is three full Python starts of the hermes CLI before the dashboard can listen, on every wake.Why
Measured on InstaCloud prod (2026-09-02): the plane's share of a hermes wake is about 2 s; the app's own boot is about 12 s, and most of that is the entrypoint re-doing work whose result is already on the volume. The wake path is what tenants feel, and hermes is the slowest template after n8n.
How
Before the bootstrap, a stdlib-only Python check (
yaml,hashlib,base64,hmac) readsconfig.yaml, requires the stored username to equalADMIN_USERNAME, re-derives the storedscrypt$n$r$p$salt$digesthash fromADMIN_PASSWORDwith the stored parameters (the same routine as upstream's_verify_password:dklen=len(digest),maxmem=0) and compares in constant time. A match skips straight tohermes gateway start(first boot only, unchanged) and the dashboard. Anything else takes the full path: first boot, a rotated secret, a missing or malformed hash, any Python error (the decision is the script's exit status, so nothing is masked by a pipeline). Nothing new is stored: the only secret material on the volume stays the salted scrypt hash the dashboard keeps anyway. Everything after the bootstrap is unchanged.Proof (local Docker,
nousresearch/hermes-agent:v2026.8.27, amd64 under emulation on a Mac, so read the deltas, not the absolutes)About 6 s off every warm-volume boot here; on the plane's 4 vCPU VM the three CLI starts are the same fraction of the ~12 s app boot.
Credentials after a fast-path boot, through the dashboard's real login endpoint
POST /auth/password-login(JSON, providerbasic): the current password is accepted ({"ok":true}), the previous one and a wrong one are rejected (401 Invalid credentials).Review log
user:passwordas a marker, a fast unsalted verifier weaker than the scrypt hash already stored. Replaced by verifying against that hash; no marker exists. Thepipefailsuggestion is moot in the same change: the fast path is gated on a Python exit status, not a pipeline.Not in this PR
hermes gateway startseed and the s6 supervision model are untouched.🤖 Generated with Claude Code
https://claude.ai/code/session_01G1fBttq231ZKpU5ivCAG2i