fix(hermesagent): follow the platform default profile root and stabilize the shared-file key - #2483
Merged
Merged
Conversation
…ize the shared-file key Four follow-ups from the #2394 review (issue #2439, findings 1, 2, 9, 10): - Upstream Hermes defaults its profile root to %LOCALAPPDATA%\hermes on win32 and ~/.hermes elsewhere, but rulesync wrote ~/.hermes on every platform when HERMES_HOME was unset, so a global generate on Windows landed where Hermes never reads. getHermesagentRelativeDirPath now re-anchors the de-prefixed path on the platform default, and the false docs sentence is corrected. - The shared-file key derived from getSettablePaths depended on HERMES_HOME (and now the platform), so the gateway drift guards went blind in exactly the configuration this feature targets. Every Hermes config.yaml writer now declares all three spellings through getExtraSharedWritePaths, and the ownership table declares them alongside .hermes/config.yaml. - resolveToolOutputRoot validates a substituted home override with validateOutputRoot, closing the same hole for KIMI_CODE_HOME. - The de-prefix guard uses checkPathTraversal semantics on both the input and the de-prefixed result, rejecting any .. segment rather than a bare prefix. The unit suite is now hermetic with respect to HERMES_HOME and KIMI_CODE_HOME (finding 5): both are cleared in vitest.config.ts before any module loads, since they change paths resolved at module load. E2E specs use the platform-aware directory instead of a hardcoded .hermes for global scope. Part of #2439 (findings 1, 2, 5, 9, 10). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…the path written Review follow-ups on PR #2483: - The three Hermes config.yaml ownership entries (and now the two kimi-code config.toml entries) share one hoisted declaration, so a policy edit cannot land on a single spelling while the others keep the old semantics. - Writers pass the key of the file they are actually writing via getHermesagentConfigSharedFileKey / getKimiCodeConfigSharedFileKey instead of a constant that only names the non-HERMES_HOME, non-win32 spelling. This is the sharedConfigFileKey convention the Zed and Codex adapters already follow, and it makes the new declarations live rather than guard-only data. - kimi-code gets the same two-spelling treatment: KIMI_CODE_HOME made its derived key collapse to a bare config.toml, so both drift guards broke for it exactly as they did for hermes. Fixing it here keeps the vitest env clearing from masking a live bug. - resolveToolOutputRoot re-throws a rejected home override naming the variable (the user never passed --output-roots), and gains a unit test file. - vitest.e2e.config.ts clears the same two overrides; e2e helpers spread process.env into the CLI child, so an exported one would redirect global output away from the pseudo-home the specs assert against. - Records why the win32 profile dir is home-relative rather than read from LOCALAPPDATA, matching the Zed and Warp constants. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lper signature
Review round two (all low):
- getKimiCodeConfigSharedFileKey takes { global } like its hermes counterpart
instead of hardcoding true, so the write-time key cannot drift from the
settable path the day kimi-code gains a project scope.
- Direct unit coverage for both key helpers: they exist precisely because the
answer depends on the home override and the platform, and the previous
protection was indirect (the gateway throwing on an undeclared key in e2e).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #2439 (findings 1, 2, 5, 9, 10) — the refactor bundle the maintainer asked for, with finding 5 falling out of it. Findings 3, 4, 6, 7, 8 follow in a separate PR.
Finding 1 (mid) — Windows default profile root
Upstream
_get_platform_default_hermes_home()returns%LOCALAPPDATA%\hermeson win32 and~/.hermesonly elsewhere, butgetHermesagentHome()fell back to~/.hermeson every platform, so a global generate on native Windows withHERMES_HOMEunset wrote where Hermes never reads.getHermesagentRelativeDirPathnow strips the canonical.hermesprefix and re-anchors it: nothing is prepended whenHERMES_HOMEis set (it is the profile root itself), otherwise the platform default from the newgetHermesagentGlobalDir()takes its place. The path constants stay canonical, so this is one funnel rather than a de-prefixing of every constant.docs/reference/supported-tools.mdloses the sentence claiming "the default remains~/.hermes".Finding 2 (mid) — env-dependent shared-file key
sharedFileKey()collapsed to a bareconfig.yamlwheneverHERMES_HOMEwas set, while writers passed the constant.hermes/config.yaml— so the ownership lock-step guard and the shared-file contract test went blind in exactly the configuration this feature is about (reproducible before this PR withHERMES_HOME=/tmp/x npx vitest run src/features/shared/shared-config-gateway.test.ts).Rather than make the writers' key env-dependent too, all five Hermes writers of
config.yaml(commands, subagents, mcp, hooks, permissions) now declare every spelling the profile root can take throughgetExtraSharedWritePaths—.hermes/,AppData/Local/hermes/, and the root itself — so the derived key set is identical on every platform and with or withoutHERMES_HOME.SHARED_CONFIG_OWNERSHIPdeclares all three with the same policies. This is the shape #2450 used for the two Zed global paths.Finding 9 (low) — env-derived outputRoot bypassed validation
resolveToolOutputRootnow runsvalidateOutputRooton a substituted home override, soHERMES_HOME=/is rejected instead of becoming the output root. Closes the same pre-existing hole forKIMI_CODE_HOME.Finding 10 (low) — weak de-prefix guard
The guard rejected only an exact
.., a..-prefix, and absolute paths. It now usescheckPathTraversalon both the input and the de-prefixed result — checking both matters becauserelative()normalizes..away, so a path that walks out of.hermesand back in would pass a check on the result alone.Finding 5 (low) — hermetic tests, resolved as a side effect
HERMES_HOMEandKIMI_CODE_HOMEare cleared invitest.config.tsbefore any test module loads. They have to be neutralized at that level rather than in abeforeEach, because the shared-write derivation runs at module load. Verified:HERMES_HOME=/tmp/x KIMI_CODE_HOME=/tmp/y npx vitest runover the previously-affected specs (hermesagent-mcp, hermesagent-skill, hermesagent-subagent, shared-config-gateway, shared-file-contract, utils/hermesagent) is green, where it produced 12 failures before.Tests
New unit coverage for the platform default, the traversal guard, and the env-independence of the declared write paths. E2E specs now derive Hermes global paths from
getHermesagentGlobalDir()instead of hardcoding.hermes, which is what makes the win32 default observable on the windows-latest E2E run. Fullpnpm cicheckis green, as are every Hermes-touching e2e spec locally (mcp, hooks, permissions, commands, subagents, skills, checks, ignore, hermesagent-home).🤖 Generated with Claude Code