Files
hermes-agent/hermes_cli/approval_mode.py
T
kshitijk4poor 93a29d110d fix(hermes_cli): surface fail-closed config write refusals cleanly
Follow-ups to the salvaged #71385 guard (which raises RuntimeError from
require_readable_config_before_write on unparseable / non-mapping YAML):

- config_command: catch RuntimeError for set/unset and print a clean
  one-line error + exit(1) instead of a raw traceback on the primary
  'hermes config set/unset' CLI path.
- console_engine._capture_output: convert escaping RuntimeError into a
  ConsoleCommandError so 'hermes console' and the dashboard console
  report the refusal instead of crashing the REPL/websocket session.
- _warn_config_parse_failure: add a dedicated 'refuse-write' wording
  branch — the old fallthrough claimed 'falling back to default config'
  even though the write was refused and the file preserved.
- approval_mode: update the stale SystemExit-only comment.
- Regression tests for the console path and both config_command paths.
2026-08-27 13:27:24 +05:30

89 lines
2.8 KiB
Python

"""Shared persistent approval-mode command logic.
Approval mode is profile-scoped configuration, not conversation state. Changing
it affects subsequent terminal guard checks immediately because approval.py
loads config on each check; it must not rebuild a live agent or mutate its
system prompt/tool schema, preserving the prompt-cache prefix.
"""
from __future__ import annotations
from contextlib import redirect_stderr, redirect_stdout
from dataclasses import dataclass
from io import StringIO
from typing import Optional
VALID_APPROVAL_MODES = ("manual", "smart", "off")
@dataclass(frozen=True)
class ApprovalModeResult:
ok: bool
mode: str
changed: bool
message: str
def _effective_mode() -> str:
"""Return the exact mode enforced by the terminal approval guard."""
from tools.approval import _get_approval_mode
return _get_approval_mode()
def run_approval_mode_command(requested_mode: Optional[str]) -> ApprovalModeResult:
"""Inspect or persist ``approvals.mode`` through canonical config APIs."""
current = _effective_mode()
requested = (requested_mode or "").strip().lower()
if not requested:
return ApprovalModeResult(
True,
current,
False,
f"Approval mode: {current} (persistent profile setting).",
)
if requested not in VALID_APPROVAL_MODES:
return ApprovalModeResult(
False,
current,
False,
"Usage: /approvals [manual|smart|off]",
)
# set_config_value is the canonical managed-scope/write-safety chokepoint.
# It reports managed policy through stderr + SystemExit, and the fail-closed
# write guard raises RuntimeError on an unparseable config.yaml; capture both
# for slash-command output instead of terminating the interactive worker.
from hermes_cli.config import set_config_value
output = StringIO()
try:
with redirect_stdout(output), redirect_stderr(output):
set_config_value("approvals.mode", requested)
except SystemExit:
detail = output.getvalue().strip() or "Approval mode is managed and cannot be changed."
return ApprovalModeResult(False, current, False, detail)
except Exception as exc:
return ApprovalModeResult(
False,
current,
False,
f"Failed to save approval mode: {exc}",
)
effective = _effective_mode()
if effective != requested:
return ApprovalModeResult(
False,
effective,
False,
f"Approval mode remains {effective}; the requested value did not become effective.",
)
return ApprovalModeResult(
True,
effective,
effective != current,
f"Approval mode: {effective} (persistent profile setting).",
)