mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-08 03:01:47 +00:00
Flip the default for HERMES_REDACT_SECRETS from off to on so the redactor already wired into send_message_tool, logs, and tool output actually runs on a fresh install. - agent/redact.py: env-var default "" → "true" - hermes_cli/config.py: DEFAULT_CONFIG security.redact_secrets True; two config-template comments rewritten - gateway/run.py + cli.py: startup log / banner warning when the user has explicitly opted out, so the downgrade is visible in agent.log and at CLI banner time - docs/reference/environment-variables.md: description reconciled - tests: flipped the default-pin, restructured the force=True regression test to explicit-false instead of unset Users who need raw credential values (redactor development) can still opt out via security.redact_secrets: false in config.yaml or HERMES_REDACT_SECRETS=false in .env. Closes #17691. Addresses #20785 (short-term output-pipeline recommendation).
This commit is contained in:
parent
d856f4535d
commit
fb1ce793e6
7 changed files with 81 additions and 28 deletions
|
|
@ -56,12 +56,15 @@ _SENSITIVE_BODY_KEYS = frozenset({
|
|||
})
|
||||
|
||||
# Snapshot at import time so runtime env mutations (e.g. LLM-generated
|
||||
# `export HERMES_REDACT_SECRETS=true`) cannot enable/disable redaction
|
||||
# mid-session. OFF by default — user must opt in via
|
||||
# `security.redact_secrets: true` in config.yaml (bridged to this env var
|
||||
# in hermes_cli/main.py and gateway/run.py) or `HERMES_REDACT_SECRETS=true`
|
||||
# in ~/.hermes/.env.
|
||||
_REDACT_ENABLED = os.getenv("HERMES_REDACT_SECRETS", "").lower() in ("1", "true", "yes", "on")
|
||||
# `export HERMES_REDACT_SECRETS=false`) cannot disable redaction
|
||||
# mid-session. ON by default — secure default per issue #17691. Users who
|
||||
# need raw credential values in tool output (e.g. working on the redactor
|
||||
# itself) can opt out via `security.redact_secrets: false` in config.yaml
|
||||
# (bridged to this env var in hermes_cli/main.py, gateway/run.py, and
|
||||
# cli.py) or `HERMES_REDACT_SECRETS=false` in ~/.hermes/.env. An opt-out
|
||||
# warning is logged at gateway and CLI startup so operators see the
|
||||
# downgrade — see `_log_redaction_status()` in gateway/run.py and cli.py.
|
||||
_REDACT_ENABLED = os.getenv("HERMES_REDACT_SECRETS", "true").lower() in ("1", "true", "yes", "on")
|
||||
|
||||
# Known API key prefixes -- match the prefix + contiguous token chars
|
||||
_PREFIX_PATTERNS = [
|
||||
|
|
|
|||
18
cli.py
18
cli.py
|
|
@ -10213,6 +10213,24 @@ class HermesCLI:
|
|||
_welcome_text = "Welcome to Hermes Agent! Type your message or /help for commands."
|
||||
_welcome_color = "#FFF8DC"
|
||||
self._console_print(f"[{_welcome_color}]{_welcome_text}[/]")
|
||||
|
||||
# Redaction opt-out warning (#17691): ON by default, loud when off.
|
||||
# The redactor snapshots its state at import time so any toggle now
|
||||
# won't affect the running process — we just want the operator to
|
||||
# see that they're running without the safety net.
|
||||
try:
|
||||
_redact_raw = os.getenv("HERMES_REDACT_SECRETS", "true")
|
||||
if _redact_raw.lower() not in ("1", "true", "yes", "on"):
|
||||
self._console_print(
|
||||
"[bold red]⚠ Secret redaction is DISABLED[/] "
|
||||
f"(HERMES_REDACT_SECRETS={_redact_raw}). "
|
||||
"API keys and tokens may appear verbatim in chat output, "
|
||||
"session JSONs, and logs. Set "
|
||||
"[cyan]security.redact_secrets: true[/] in config.yaml "
|
||||
"to re-enable."
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
# First-time OpenClaw-residue banner — fires once if ~/.openclaw/ exists
|
||||
# after an OpenClaw→Hermes migration (especially migrations done by
|
||||
# OpenClaw's own tool, which doesn't archive the source directory).
|
||||
|
|
|
|||
|
|
@ -2860,6 +2860,29 @@ class GatewayRunner:
|
|||
)
|
||||
except Exception:
|
||||
pass
|
||||
# Redaction status: ON by default (#17691). Surface a prominent
|
||||
# warning if an operator has explicitly opted out so they don't
|
||||
# forget the downgrade is active — the redactor snapshots its
|
||||
# state at import time, so this log line is the source of truth
|
||||
# for this process's lifetime.
|
||||
try:
|
||||
_redact_raw = os.getenv("HERMES_REDACT_SECRETS", "true")
|
||||
_redact_on = _redact_raw.lower() in ("1", "true", "yes", "on")
|
||||
if _redact_on:
|
||||
logger.info(
|
||||
"Secret redaction: ENABLED (tool output, logs, and chat "
|
||||
"responses are scrubbed before delivery)"
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
"Secret redaction: DISABLED (HERMES_REDACT_SECRETS=%s). "
|
||||
"API keys and tokens may appear verbatim in chat output, "
|
||||
"session JSONs, and logs. Set security.redact_secrets: true "
|
||||
"in config.yaml to re-enable.",
|
||||
_redact_raw,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
from hermes_cli.profiles import get_active_profile_name
|
||||
_profile = get_active_profile_name()
|
||||
|
|
|
|||
|
|
@ -1191,7 +1191,7 @@ DEFAULT_CONFIG = {
|
|||
# Pre-exec security scanning via tirith
|
||||
"security": {
|
||||
"allow_private_urls": False, # Allow requests to private/internal IPs (for OpenWrt, proxies, VPNs)
|
||||
"redact_secrets": False,
|
||||
"redact_secrets": True,
|
||||
"tirith_enabled": True,
|
||||
"tirith_path": "tirith",
|
||||
"tirith_timeout": 5,
|
||||
|
|
@ -3978,10 +3978,10 @@ def load_config() -> Dict[str, Any]:
|
|||
|
||||
_SECURITY_COMMENT = """
|
||||
# ── Security ──────────────────────────────────────────────────────────
|
||||
# Secret redaction is OFF by default — tool output (terminal stdout,
|
||||
# read_file results, web content) passes through unmodified. Set
|
||||
# redact_secrets to true to mask strings that look like API keys, tokens,
|
||||
# and passwords before they enter the model context and logs.
|
||||
# Secret redaction is ON by default — strings that look like API keys,
|
||||
# tokens, and passwords are masked in tool output, logs, and chat
|
||||
# responses before the model or user ever sees them. Set redact_secrets
|
||||
# to false to disable (e.g. when developing the redactor itself).
|
||||
# tirith pre-exec scanning is enabled by default when the tirith binary
|
||||
# is available. Configure via security.tirith_* keys or env vars
|
||||
# (TIRITH_ENABLED, TIRITH_BIN, TIRITH_TIMEOUT, TIRITH_FAIL_OPEN).
|
||||
|
|
@ -4021,8 +4021,8 @@ _FALLBACK_COMMENT = """
|
|||
|
||||
_COMMENTED_SECTIONS = """
|
||||
# ── Security ──────────────────────────────────────────────────────────
|
||||
# Secret redaction is OFF by default. Set to true to mask strings that
|
||||
# look like API keys, tokens, and passwords in tool output and logs.
|
||||
# Secret redaction is ON by default. Set to false to pass tool output,
|
||||
# logs, and chat responses through unmodified (e.g. for redactor dev).
|
||||
#
|
||||
# security:
|
||||
# redact_secrets: true
|
||||
|
|
|
|||
|
|
@ -291,9 +291,11 @@ class TestCaptureLogSnapshotRedaction:
|
|||
home = tmp_path / ".hermes"
|
||||
home.mkdir()
|
||||
monkeypatch.setenv("HERMES_HOME", str(home))
|
||||
# Critical: ensure the user has NOT opted in to redaction. The whole
|
||||
# point of this PR is that share-time redaction works for users who
|
||||
# never set this env var.
|
||||
# Baseline fixture: no explicit env-var opinion. With the post-#17691
|
||||
# default of ON, the default-path tests below exercise the
|
||||
# secure-default behaviour. The `force=True` regression test
|
||||
# setenvs to "false" inline to prove force=True works even when
|
||||
# the runtime flag is disabled.
|
||||
monkeypatch.delenv("HERMES_REDACT_SECRETS", raising=False)
|
||||
|
||||
logs_dir = home / "logs"
|
||||
|
|
@ -324,21 +326,26 @@ class TestCaptureLogSnapshotRedaction:
|
|||
assert _REDACT_FIXTURE_TOKEN in snap.tail_text
|
||||
assert _REDACT_FIXTURE_TOKEN in (snap.full_text or "")
|
||||
|
||||
def test_force_true_overrides_unset_env_var(self, hermes_home_with_secret):
|
||||
def test_force_true_works_when_redaction_disabled(
|
||||
self, hermes_home_with_secret, monkeypatch
|
||||
):
|
||||
"""Regression test: redact_sensitive_text short-circuits without force=True.
|
||||
|
||||
If a future refactor drops `force=True` from `_redact_log_text`, this
|
||||
test fails immediately. Without `force=True`, the redactor returns the
|
||||
input unchanged when HERMES_REDACT_SECRETS is unset, and the feature
|
||||
ships silently broken for its target audience.
|
||||
input unchanged when HERMES_REDACT_SECRETS=false, and the share-time
|
||||
redaction feature ships silently broken for users who opted out of
|
||||
runtime redaction (e.g. developers working on the redactor itself).
|
||||
"""
|
||||
import os
|
||||
|
||||
# Force the runtime flag off so we're exercising the force=True path,
|
||||
# not the default-on path.
|
||||
monkeypatch.setenv("HERMES_REDACT_SECRETS", "false")
|
||||
|
||||
from hermes_cli.debug import _capture_log_snapshot
|
||||
|
||||
# Belt-and-suspenders: confirm the env var is genuinely unset for this
|
||||
# test so we know we're exercising the force=True path.
|
||||
assert os.environ.get("HERMES_REDACT_SECRETS", "") == ""
|
||||
assert os.environ.get("HERMES_REDACT_SECRETS", "") == "false"
|
||||
|
||||
snap = _capture_log_snapshot("agent", tail_lines=10)
|
||||
|
||||
|
|
|
|||
|
|
@ -72,11 +72,13 @@ def test_redact_secrets_false_in_config_yaml_is_honored(tmp_path):
|
|||
assert "ENV_VAR=false" in result.stdout
|
||||
|
||||
|
||||
def test_redact_secrets_default_false_when_unset(tmp_path):
|
||||
"""Without the config key, redaction stays OFF by default.
|
||||
def test_redact_secrets_default_true_when_unset(tmp_path):
|
||||
"""Without the config key or env var, redaction is ON by default (#17691).
|
||||
|
||||
Secret redaction is opt-in — users who want it must set
|
||||
`security.redact_secrets: true` explicitly (or HERMES_REDACT_SECRETS=true).
|
||||
Secret redaction is a secure default — users who need raw credential
|
||||
values in tool output (e.g. working on the redactor itself) must set
|
||||
`security.redact_secrets: false` explicitly (or
|
||||
`HERMES_REDACT_SECRETS=false`).
|
||||
"""
|
||||
hermes_home = tmp_path / ".hermes"
|
||||
hermes_home.mkdir()
|
||||
|
|
@ -107,7 +109,7 @@ def test_redact_secrets_default_false_when_unset(tmp_path):
|
|||
timeout=30,
|
||||
)
|
||||
assert result.returncode == 0, f"probe failed: {result.stderr}"
|
||||
assert "REDACT_ENABLED=False" in result.stdout
|
||||
assert "REDACT_ENABLED=True" in result.stdout
|
||||
|
||||
|
||||
def test_redact_secrets_true_in_config_yaml_is_honored(tmp_path):
|
||||
|
|
|
|||
|
|
@ -456,7 +456,7 @@ Advanced per-platform knobs for throttling the outbound message batcher. Most us
|
|||
| `HERMES_EPHEMERAL_SYSTEM_PROMPT` | Ephemeral system prompt injected at API-call time (never persisted to sessions) |
|
||||
| `HERMES_PREFILL_MESSAGES_FILE` | Path to a JSON file of ephemeral prefill messages injected at API-call time. |
|
||||
| `HERMES_ALLOW_PRIVATE_URLS` | `true`/`false` — allow tools to fetch localhost/private-network URLs. Off by default in gateway mode. |
|
||||
| `HERMES_REDACT_SECRETS` | `true`/`false` — control secret redaction in logs and shareable outputs (default: `true`). |
|
||||
| `HERMES_REDACT_SECRETS` | `true`/`false` — control secret redaction in tool output, logs, and chat responses (default: `true`). |
|
||||
| `HERMES_WRITE_SAFE_ROOT` | Optional directory prefix that restricts `write_file`/`patch` writes; paths outside require approval. |
|
||||
| `HERMES_DISABLE_FILE_STATE_GUARD` | Set to `1` to turn off the "file changed since you read it" guard on `patch`/`write_file`. |
|
||||
| `HERMES_CORE_TOOLS` | Comma-separated override for the canonical core tool list (advanced; rarely needed). |
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue