Skip to content

fix(cli): reject corrupt config in noninteractive runs - #81988

Open
embwl0x wants to merge 3 commits into
NousResearch:mainfrom
embwl0x:agent/fail-close-corrupt-noninteractive-81952
Open

fix(cli): reject corrupt config in noninteractive runs#81988
embwl0x wants to merge 3 commits into
NousResearch:mainfrom
embwl0x:agent/fail-close-corrupt-noninteractive-81952

Conversation

@embwl0x

@embwl0x embwl0x commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Summary

  • reject existing malformed or non-mapping profile config before quiet, single-query, or oneshot startup
  • log the refusal at ERROR and preserve the existing corrupt-config backup behavior
  • keep missing/empty config, interactive repair, safe mode, and explicit --ignore-user-config behavior intact

Why

A fresh process currently falls back to defaults after a config parse failure. If the profile .env contains OPENROUTER_API_KEY, a noninteractive run can then resolve OpenRouter and silently incur spend against a provider the broken config never selected.

The guard runs before plugin discovery or provider initialization and covers the full parser, Termux fast path, and direct cmd_chat callers.

Verification

  • focused and adjacent regression after review follow-up: 45 passed
  • focused and adjacent CLI suites before the edge-case-only follow-up: 112 passed
  • full tests/hermes_cli matrix: 4,610 passed; one unrelated macOS service-manager mode test was flaky and passed standalone on both exact base and this branch
  • isolated cron-style subprocess: exit 2, refusal present in stderr and errors.log, corrupt file preserved and backed up, no provider initialization or API call
  • ruff, git diff --check, publish-range gitleaks, and metadata checks passed

Fixes #81952

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/cli CLI entry point, hermes_cli/, setup wizard area/config Config system, migrations, profiles sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades labels Aug 8, 2026

@DavidMetcalfe DavidMetcalfe 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.

Thanks for the fast, well-scoped fix. The guard placement and test coverage look solid:

  • Profile resolution runs at import time (_apply_profile_override()), before cmd_chat, so the guard reads the profile config.yaml — the exact path from the issue.
  • All non-interactive CLI paths pass through the guard: top-level oneshot (-z), -q/-Q query, --quiet, and direct cmd_chat callers (_prepare_agent_startup at main.py:10960/10984/12597 + top of cmd_chat).
  • The guard uses fast_safe_load, the same parser _load_config_impl uses, so parse semantics match the normal load path.
  • Safe mode is double-covered: _apply_safe_mode already sets HERMES_IGNORE_USER_CONFIG=1 (main.py:10898), and the guard also passes the flag explicitly.

One substantive gap to consider — follow-up, not a blocker for this PR:

  • Gateway-scheduled agent runs (Hermes' own cron jobs, not shell-level hermes chat -Q) execute in-process and never pass through cmd_chat/_prepare_agent_startup. A corrupt config at gateway startup still falls back to defaults for those jobs — the same spend class as the issue, one layer up. Worth a follow-up that fails closed at gateway startup (or refuses to schedule) when the profile config is unparseable; the gateway's last-known-good in-process retention also means config edits stay silently ignored until restart.

Nits:

  • -q "" (empty query): the guard treats query is not None as non-interactive, while interactive_prompt uses truthiness (not query). An empty-string query would now fail closed instead of dropping to the interactive prompt. Degenerate usage, but the two checks disagree — consider bool(getattr(args, "query", None)) for consistency.
  • Coverage suggestions: (a) env-var-only bypass (HERMES_IGNORE_USER_CONFIG=1, no CLI flag) isn't exercised — the test fixture deletes it; (b) a Namespace-reuse case (same args object, config repaired between calls) for the _noninteractive_config_validated flag.
  • PermissionError/unreadable config now fails closed too — I think that's the right default (silently running with defaults is the bug), just noting it's a behavior change beyond parse errors.

Scope notes for the issue's expected behaviors: fail-closed exit + ERROR + backup covers #1 and #4; credential-ingestion logging (#3) is moot here since the guard exits before provider/credential init; the paid silent default (#2) is a separate maintainer policy decision (#64635) unaffected by this PR.

The fix is solid — exit 2, ERROR log, and backup behavior all match the issue's ask. @embwl0x nice work.

@embwl0x
embwl0x force-pushed the agent/fail-close-corrupt-noninteractive-81952 branch from 2ce4a73 to 80798a7 Compare August 8, 2026 20:08
@embwl0x

embwl0x commented Aug 8, 2026

Copy link
Copy Markdown
Contributor Author

@DavidMetcalfe Thanks for the careful review. Updated current head 80798a764 to cover the non-blocking edge cases:

  • empty -q "" now retains the existing interactive-prompt behavior
  • added env-only HERMES_IGNORE_USER_CONFIG=1 bypass coverage
  • added a reused-Namespace regression proving a failed validation can retry after config repair

The focused regression file passes all 16 tests after rebasing onto current main, and the publish gate passed again. I agree the in-process gateway/cron path is a distinct follow-up boundary rather than part of this CLI-scoped patch.

@spfcraze

spfcraze commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

This was generated by AI during triage.

Summary:
The guard's quiet clause contradicts the CLI's own interactivity test: hermes chat -Q without -q is dispatched interactive (interactive_prompt at hermes_cli/main.py:10976 and cli.py's if query or image: gate both ignore quiet), yet is_noninteractive includes quiet and exits 2 on a corrupt config.

Problems:

  • is_noninteractive in _guard_noninteractive_user_config is bool(oneshot) or bool(query) or bool(quiet), while the dispatcher's interactive_prompt = not query and not image (hermes_cli/main.py:10976) and cli.py's single-query gate if query or image: -> cli.run() (cli.py:18475/18703) never treat quiet as non-interactive - so a query-less -Q session (interactive, operator present) exits 2 before the repair path the summary says stays intact.
  • The -q "" fix (bool(getattr(args, "query", None)), test_empty_query_keeps_interactive_repair_behavior) holds only without -Q: -Q -q "" re-trips the guard via the quiet clause, so the empty-query interactive behavior the fix restores depends on -Q not being set.

Solution:
Align the predicate with the dispatcher: is_noninteractive = bool(getattr(args, "oneshot", None)) or bool(getattr(args, "query", None)) - -Q -q "..." with a real query still fails closed via the query clause, and -Q without -q keeps the interactive repair path.


Checked against 80798a7 — the tip of agent/fail-close-corrupt-noninteractive-81952 when this was written — and 1792e75, main at the same moment.

@embwl0x
embwl0x force-pushed the agent/fail-close-corrupt-noninteractive-81952 branch from 80798a7 to ca9a55e Compare August 9, 2026 07:55
@embwl0x

embwl0x commented Aug 9, 2026

Copy link
Copy Markdown
Contributor Author

Addressed in ca9a55e. I removed quiet from the noninteractive guard predicate so hermes -Q and hermes -Q -q "" retain the operator-present repair path, while hermes -Q -q "hello" still fails closed through the real query signal.

Regression coverage now exercises all three cases. I rebased onto current main and reran the guard, single-query finalization, argument propagation, and relaunch suites: 45 tests passed; ruff and git diff --check also pass.

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

Labels

area/config Config system, migrations, profiles comp/cli CLI entry point, hermes_cli/, setup wizard P2 Medium — degraded but workaround exists sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades type/bug Something isn't working

Projects

None yet

4 participants