Skip to content

fix(acp): honour --profile from CLI and hermes-acp direct entry (#30571) - #30612

Closed
briandevans wants to merge 4 commits into
NousResearch:mainfrom
briandevans:fix/acp-profile-propagation-30571
Closed

fix(acp): honour --profile from CLI and hermes-acp direct entry (#30571)#30612
briandevans wants to merge 4 commits into
NousResearch:mainfrom
briandevans:fix/acp-profile-propagation-30571

Conversation

@briandevans

Copy link
Copy Markdown
Contributor

What does this PR do?

Plumbs --profile propagation through both ACP entrypoints so hermes -p <name> acp AND direct hermes-acp -p <name> invocations (used by editor configs like Zed agent commands) run ACP under the requested profile — config.yaml, .env, skills, memory, gateway state, and the canonical profile name all visible to downstream callers.

Root cause split:

  1. hermes_cli.main._apply_profile_override set HERMES_HOME for hermes -p <name> acp but never published the canonical profile name. Downstream code that branches on HERMES_PROFILE (kanban, gateway adapters) couldn't see the active profile.
  2. acp_adapter.entry._parse_args had no --profile flag at all, so hermes-acp -p code-reviewer — the form an editor config would use — failed with "unrecognized arguments".

Mirrors _apply_profile_override precedence: explicit --profile/-p arg wins, then falls through to the inherited HERMES_HOME/HERMES_PROFILE from the parent shell. Uses setdefault for HERMES_PROFILE so a deliberately-exported value from the spawning shell is preserved.

Sibling code paths I considered: lsaether's PR #30560 (open) wires --skills from CLI → acp_mainHermesACPAgentSessionManager for the same family of bugs. This PR uses an env-var-based propagation instead because profile activation is a process-wide concern (HERMES_HOME flips multiple subsystems at once), whereas --skills is per-session ephemeral state. Both PRs touch acp_adapter/entry.py but in independent ways and should merge cleanly side by side.

Related Issue

Fixes #30571

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

  • hermes_cli/main.py_apply_profile_override now publishes HERMES_PROFILE (canonical, setdefault-only) alongside HERMES_HOME; cmd_acp forwards the active profile to acp_main via argv so the two invocation paths converge on the same environment.
  • acp_adapter/entry.py — adds --profile/-p to the argument parser and a new _apply_profile_override(profile_name) helper that calls resolve_profile_env, sets HERMES_HOME, and seeds HERMES_PROFILE. Runs BEFORE every subcommand branch (--version, --check, --setup, --setup-browser, server start) so all of them observe the profile-scoped environment.
  • tests/acp/test_entry.py — six new tests covering: --profile X sets HERMES_HOME before _load_env; -p equivalent to --profile; --check short-circuit still honours --profile; absence of --profile leaves inherited env untouched; unknown profile exits with status 1; explicit HERMES_PROFILE from the shell wins over setdefault.
  • tests/hermes_cli/test_apply_profile_override.py — four new tests covering the parent _apply_profile_override: explicit flag sets canonical name; sticky active_profile sets canonical name; existing HERMES_PROFILE is preserved; default profile does not publish either env var.

How to Test

  1. Create two profiles with different default models:

    hermes profile create code-reviewer
    hermes profile create fast-coder
    # Edit ~/.hermes/profiles/code-reviewer/config.yaml + .env
  2. Direct ACP entrypoint (the form editor configs use):

    hermes-acp -p code-reviewer --check  # exits "Hermes ACP check OK" under code-reviewer
    HERMES_PROFILE=  hermes-acp --profile code-reviewer  # picks up code-reviewer's config
  3. CLI entrypoint:

    hermes -p fast-coder acp  # cmd_acp forwards --profile fast-coder into acp_argv
  4. Focused tests:

    uv run --with pytest --with pytest-xdist --with pytest-asyncio --with pytest-timeout --with 'agent-client-protocol==0.9.0' python3 -m pytest tests/acp/test_entry.py tests/hermes_cli/test_apply_profile_override.py -v

    24 tests pass locally (10 pre-existing + 10 new across the two files; 4 of the new tests fail on a baseline-revert check, confirming the regression guard).

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(acp): …)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix (no unrelated commits)
  • I've run focused tests for the touched code and all pass
  • I've added tests for my changes (10 new tests across tests/acp/test_entry.py and tests/hermes_cli/test_apply_profile_override.py)
  • I've tested on my platform: macOS 15.x

Documentation & Housekeeping

  • I've updated relevant documentation — N/A (the --help text on the new --profile flag is self-describing)
  • I've updated cli-config.yaml.example — N/A (no new config keys)
  • I've updated CONTRIBUTING.md or AGENTS.md — N/A (no architecture change)
  • I've considered cross-platform impact (Windows, macOS) — env-var-based, no platform-specific paths
  • I've updated tool descriptions/schemas — N/A

Contract Protected

Invariant: Both hermes -p <name> acp and hermes-acp -p <name> (and hermes-acp --profile=<name>) MUST leave the process with HERMES_HOME pointing at the profile directory and HERMES_PROFILE set to the canonical name, before any session manager, runtime provider, or .env loader runs.

Known-bad inputs covered: unknown profile name (exits 1, doesn't silently fall back); empty --profile (no env mutation); operator-set HERMES_PROFILE from the shell (preserved via setdefault).

Negative case: test_main_profile_flag_rejects_unknown_profile proves the bad path fails loudly instead of silently using the default profile.

Repro case from the issue: test_main_profile_flag_sets_hermes_home_before_load_env exercises the exact scenario from #30571's "Steps to Reproduce" — launch with --profile <name>, observe _load_env sees the profile-scoped HERMES_HOME.

Related / Positioning

Sibling code paths that may need the same fix: none identified for this PR — _apply_profile_override in both hermes_cli/main.py and acp_adapter/entry.py are the two profile-activation entrypoints in the codebase, and both are covered here. The runtime resolver chain (get_hermes_home()HERMES_HOME env var) is the single source of truth, and this PR feeds it from both entry points.

Copilot AI review requested due to automatic review settings May 22, 2026 21:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR ensures the active Hermes profile name is propagated via HERMES_PROFILE (alongside HERMES_HOME) and is forwarded into the ACP adapter so direct hermes-acp usage and hermes -p <name> acp behave consistently.

Changes:

  • Publish canonical HERMES_PROFILE when a profile override is applied in hermes_cli.main._apply_profile_override.
  • Add --profile/-p support to hermes-acp startup and apply it before env loading and subcommand short-circuits.
  • Add regression tests covering HERMES_PROFILE publication and ACP profile propagation.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

File Description
tests/hermes_cli/test_apply_profile_override.py Adds regression tests asserting HERMES_PROFILE is published (and preserved) when profile overrides apply.
tests/acp/test_entry.py Adds ACP entrypoint tests verifying --profile/-p sets env before _load_env and works with --check.
hermes_cli/main.py Publishes HERMES_PROFILE and forwards it into hermes-acp argv when running hermes ... acp.
acp_adapter/entry.py Adds --profile/-p flag and applies profile override early to scope env/config loading.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

tmp_path,
monkeypatch,
hermes_home=None,
active_profile="Coder",
Comment thread hermes_cli/main.py Outdated
Comment on lines +175 to +198
@@ -189,6 +190,12 @@ def _apply_profile_override() -> None:
)
return
os.environ["HERMES_HOME"] = hermes_home
# Also publish the canonical profile name so downstream code that
# branches on profile identity (kanban, gateway adapters, ACP entry)
# can pick it up without re-parsing HERMES_HOME. Honour any value the
# caller already exported — a deliberate ``HERMES_PROFILE=...`` on
# the spawning shell should win over this best-effort default.
os.environ.setdefault("HERMES_PROFILE", canonical_name)
Comment thread acp_adapter/entry.py Outdated
Comment on lines +181 to +184
# Profile resolution must never prevent ACP from starting; surface
# the issue and fall back to whatever environment the caller had.
print(
f"Warning: profile override failed ({exc}), using default",
Comment thread hermes_cli/main.py Outdated
Comment on lines +13268 to +13275
# Forward the active profile name so ACP can announce it in logs
# and so direct ``hermes-acp`` invocations from editor configs
# behave the same as ``hermes -p <name> acp``. The env var is
# populated by ``_apply_profile_override`` when ``-p/--profile``
# was passed; absence means the default profile.
active_profile = os.environ.get("HERMES_PROFILE", "").strip()
if active_profile and active_profile != "default":
acp_argv.extend(["--profile", active_profile])
Comment thread acp_adapter/entry.py
Comment on lines +157 to +191
"""Resolve ``--profile`` and publish HERMES_HOME / HERMES_PROFILE.

Runs at ACP startup so that ``hermes-acp -p <name>`` direct invocations
(used by editor configs like Zed agent commands) end up with the same
environment as ``hermes -p <name> acp`` — i.e. ``get_hermes_home()``
returns the profile directory and downstream callers that branch on
``HERMES_PROFILE`` see the canonical name.

When the env is *already* pointing at this profile (because the parent
``hermes_cli.main._apply_profile_override`` ran first), the resolver is
a no-op: ``setdefault`` preserves the inherited HERMES_PROFILE and the
HERMES_HOME assignment writes the same string back.
"""
if not profile_name:
return
try:
from hermes_cli.profiles import normalize_profile_name, resolve_profile_env

hermes_home = resolve_profile_env(profile_name)
canonical_name = normalize_profile_name(profile_name)
except (ValueError, FileNotFoundError) as exc:
print(f"Error: {exc}", file=sys.stderr)
sys.exit(1)
except Exception as exc:
# Profile resolution must never prevent ACP from starting; surface
# the issue and fall back to whatever environment the caller had.
print(
f"Warning: profile override failed ({exc}), using default",
file=sys.stderr,
)
return
os.environ["HERMES_HOME"] = hermes_home
os.environ.setdefault("HERMES_PROFILE", canonical_name)


@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/acp Agent Communication Protocol adapter comp/cli CLI entry point, hermes_cli/, setup wizard labels May 22, 2026
@briandevans

Copy link
Copy Markdown
Contributor Author

@copilot Findings addressed:

  • Test fixture (tests/hermes_cli/test_apply_profile_override.py:162): commit faad50e — helper now creates the canonical lowercased dir name to match get_profile_dir's lookup. Fixes the CI failure on test_explicit_flag_sets_hermes_profile_canonical_name.
  • Inherited-HERMES_HOME early-return path (hermes_cli/main.py:158): commit c553e38 — mirror the late-path os.environ.setdefault("HERMES_PROFILE", ...) from the basename so the contract holds on every execution path. Two new tests (test_inherited_profile_hermes_home_publishes_hermes_profile, test_inherited_profile_does_not_override_existing_hermes_profile) cover both the default and the caller-set cases.
  • Misleading "using default" warning (acp_adapter/entry.py:184): commit c553e38 — reworded to "using existing environment" since the fallback preserves whatever the caller already had (possibly a non-default profile).
  • cmd_acp argv-forwarding test gap (hermes_cli/main.py:13275): the forwarding logic itself is straightforward and is exercised end-to-end by the existing ACP tests once HERMES_PROFILE is published. Adding a focused unit test would require either extracting the nested helper or building an argparse fixture; I've left this out to keep PR scope tight, happy to add it as a follow-up if preferred.
  • Helper duplication between hermes_cli.main and acp_adapter.entry (acp_adapter/entry.py:191): real concern, but the two callers have intentionally different policies (the CLI version pre-parses -p from raw argv and strips the flag; the ACP version takes a pre-parsed name from argparse). Factoring a shared helper is a reasonable follow-up but widens this PR's scope — happy to do it in a separate PR if you'd prefer that shape.

@briandevans

Copy link
Copy Markdown
Contributor Author

CI note — the single test failure on c553e38 is unrelated to this PR:

FAILED tests/tools/test_browser_secret_exfil.py::TestBrowserSecretExfil::test_allows_normal_url - Failed: Timeout (>30.0s) from pytest-timeout.

That test is a browser-secret-exfil subprocess test (added in 712aa44, security:); it has no code path overlap with this PR's hermes_cli/main.py / acp_adapter/entry.py ACP-profile changes. The most recent main run (26314620271 on a84cec6) hit a similar 30s timeout flake but on a different file (test_web_server.py::test_pub_broadcasts_to_events_subscribers) — both look like infrastructure-side timeout flakes on subprocess/threading tests, not regressions. Happy to rebase and re-trigger if a re-run helps.

@briandevans

Copy link
Copy Markdown
Contributor Author

@copilot All review nits addressed in commit 7061eadfb:

  • Test for cmd_acp argv-forwarding — pulled the argv-building out of the nested closure into a module-level _build_acp_argv(args, env=None) helper. New test file tests/hermes_cli/test_build_acp_argv.py covers: HERMES_PROFILE set → forwards --profile <name>, HERMES_PROFILE=default does NOT forward, unset/empty/whitespace does NOT forward, env defaults to os.environ for the production call site (16 cases, all pass).
  • _apply_profile_override duplication — extracted shared apply_profile_env(profile_name) -> bool into hermes_cli/profiles.py. Both hermes_cli.main and acp_adapter.entry now delegate, so resolve / sys.exit / env writes can't drift between the two ACP entry points. Bonus: main.py's warning text now matches entry.py's 'using existing environment' wording (your earlier nit on the misleading 'using default').

@briandevans
briandevans force-pushed the fix/acp-profile-propagation-30571 branch from 7061ead to 0636edb Compare May 25, 2026 03:12
@briandevans
briandevans force-pushed the fix/acp-profile-propagation-30571 branch from 0636edb to a6825cd Compare May 27, 2026 12:15
@briandevans
briandevans force-pushed the fix/acp-profile-propagation-30571 branch from a6825cd to bef4ebd Compare May 28, 2026 00:18
@briandevans
briandevans force-pushed the fix/acp-profile-propagation-30571 branch from bef4ebd to 7554fd3 Compare May 29, 2026 03:14
@briandevans
briandevans force-pushed the fix/acp-profile-propagation-30571 branch from 7554fd3 to a8b73ae Compare May 30, 2026 04:11
…Research#30571)

`hermes -p <profile> acp` already set `HERMES_HOME` via the top-level
profile override, but ACP-created sessions could not see the canonical
profile name (HERMES_PROFILE was never published) and `hermes-acp -p
<profile>` direct invocations — used by editor configs like Zed agent
commands — had no `--profile` flag at all.

This change plumbs profile activation through both entrypoints:

1. `hermes_cli.main._apply_profile_override` now also publishes
   `HERMES_PROFILE` (canonical, setdefault-only so a shell-set value
   wins). Downstream callers that branch on profile name (kanban,
   gateway adapters, ACP entry) can read it without re-parsing
   `HERMES_HOME`.
2. `acp_adapter.entry` gains a `--profile/-p` argument that runs
   `resolve_profile_env` before `_load_env`, so direct
   `hermes-acp -p code-reviewer` invocations land in the same profile
   environment as `hermes -p code-reviewer acp`.
3. `hermes_cli.main.cmd_acp` forwards the active profile to `acp_main`
   via argv, keeping the two invocation paths consistent.

Mirrors lsaether's `--skills` plumbing pattern in PR NousResearch#30560 so the two
follow-ups land cleanly side by side.

Tests cover: both entrypoints set HERMES_HOME + HERMES_PROFILE before
`_load_env`; `-p` is equivalent to `--profile`; `--check` short-circuit
still honours `--profile`; missing `--profile` leaves inherited env
untouched; unknown profile name exits cleanly; explicit
`HERMES_PROFILE` from the spawning shell is preserved.
The fixture creates ``profiles/<active_profile>`` literally, but
production resolves ``-p Coder`` to ``profiles/coder`` via
``normalize_profile_name``. When ``active_profile="Coder"`` was passed,
the resolver couldn't find ``profiles/coder`` and raised
FileNotFoundError, causing test_explicit_flag_sets_hermes_profile_canonical_name
to fail on CI.

Lowercase the directory name to match what production looks up.
Addresses Copilot review on NousResearch#30612.

When ``HERMES_HOME`` is already a profile directory (set by an outer
``hermes -p <name>`` invocation that re-execs into the gateway, or by
a child process inheriting the resolved env), ``_apply_profile_override``
returns early. The new ``HERMES_PROFILE`` contract was unset on that
branch, so downstream callers that branch on profile identity still
couldn't see the active profile when re-entering this layer.

Mirror the late-path ``setdefault`` from the basename of HERMES_HOME so
the contract holds on every execution path.

Also clarify the warning text in ``acp_adapter.entry`` — the fallback
preserves whatever HERMES_HOME / HERMES_PROFILE the caller already had,
which may itself be a non-default profile, so "using default" was
misleading during debugging.

Adds two regression tests for the inherited-HERMES_HOME path: one for
the default setdefault behaviour and one proving the caller-set
HERMES_PROFILE still wins.
…_argv

Two Copilot review nits on NousResearch#30612, fixed together because they share a
seam:

1. ``_apply_profile_override`` was duplicated across
   ``hermes_cli.main`` and ``acp_adapter.entry`` with very similar
   resolve / setenv / exit logic. Drift risk: the entry-point that
   diverges first (e.g. on a new env var) silently desyncs the two ACP
   call paths. Extracted ``apply_profile_env(profile_name) -> bool`` to
   ``hermes_cli.profiles``; both call sites now delegate. Behaviour
   preserved verbatim, including the existing-environment warning
   wording (main.py previously said "using default", now matches
   entry.py's "using existing environment" — the misleading "default"
   string was Copilot's NousResearch#3 on the original PR).

2. ``cmd_acp``'s argv-forwarding logic (the NousResearch#30571 fix surface) had
   no test coverage because the function is nested inside ``main()``
   and not directly importable. Pulled the argv-building into a
   module-level ``_build_acp_argv(args, env=None)`` helper and added
   ``tests/hermes_cli/test_build_acp_argv.py`` covering: profile set
   forwards ``--profile <name>``, ``HERMES_PROFILE=default`` does
   NOT forward, unset/empty/whitespace does NOT forward, flag order
   stable, env defaults to ``os.environ`` for the production call.

No behaviour change for ``hermes acp`` or ``hermes-acp``; both still
mirror ``--profile`` through to the ACP server and resolve via the
same code path.
@briandevans
briandevans force-pushed the fix/acp-profile-propagation-30571 branch from a8b73ae to 748242c Compare June 2, 2026 23:20
@briandevans

Copy link
Copy Markdown
Contributor Author

Closing to focus the queue on security/file-safety work where civilian merges are landing. Happy to reopen if maintainers want this picked up.

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

Labels

comp/acp Agent Communication Protocol adapter comp/cli CLI entry point, hermes_cli/, setup wizard P2 Medium — degraded but workaround exists type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: --profile is not respected by acp command

3 participants