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
|
# Snapshot at import time so runtime env mutations (e.g. LLM-generated
|
||||||
# `export HERMES_REDACT_SECRETS=true`) cannot enable/disable redaction
|
# `export HERMES_REDACT_SECRETS=false`) cannot disable redaction
|
||||||
# mid-session. OFF by default — user must opt in via
|
# mid-session. ON by default — secure default per issue #17691. Users who
|
||||||
# `security.redact_secrets: true` in config.yaml (bridged to this env var
|
# need raw credential values in tool output (e.g. working on the redactor
|
||||||
# in hermes_cli/main.py and gateway/run.py) or `HERMES_REDACT_SECRETS=true`
|
# itself) can opt out via `security.redact_secrets: false` in config.yaml
|
||||||
# in ~/.hermes/.env.
|
# (bridged to this env var in hermes_cli/main.py, gateway/run.py, and
|
||||||
_REDACT_ENABLED = os.getenv("HERMES_REDACT_SECRETS", "").lower() in ("1", "true", "yes", "on")
|
# 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
|
# Known API key prefixes -- match the prefix + contiguous token chars
|
||||||
_PREFIX_PATTERNS = [
|
_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_text = "Welcome to Hermes Agent! Type your message or /help for commands."
|
||||||
_welcome_color = "#FFF8DC"
|
_welcome_color = "#FFF8DC"
|
||||||
self._console_print(f"[{_welcome_color}]{_welcome_text}[/]")
|
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
|
# First-time OpenClaw-residue banner — fires once if ~/.openclaw/ exists
|
||||||
# after an OpenClaw→Hermes migration (especially migrations done by
|
# after an OpenClaw→Hermes migration (especially migrations done by
|
||||||
# OpenClaw's own tool, which doesn't archive the source directory).
|
# OpenClaw's own tool, which doesn't archive the source directory).
|
||||||
|
|
|
||||||
|
|
@ -2860,6 +2860,29 @@ class GatewayRunner:
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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:
|
try:
|
||||||
from hermes_cli.profiles import get_active_profile_name
|
from hermes_cli.profiles import get_active_profile_name
|
||||||
_profile = get_active_profile_name()
|
_profile = get_active_profile_name()
|
||||||
|
|
|
||||||
|
|
@ -1191,7 +1191,7 @@ DEFAULT_CONFIG = {
|
||||||
# Pre-exec security scanning via tirith
|
# Pre-exec security scanning via tirith
|
||||||
"security": {
|
"security": {
|
||||||
"allow_private_urls": False, # Allow requests to private/internal IPs (for OpenWrt, proxies, VPNs)
|
"allow_private_urls": False, # Allow requests to private/internal IPs (for OpenWrt, proxies, VPNs)
|
||||||
"redact_secrets": False,
|
"redact_secrets": True,
|
||||||
"tirith_enabled": True,
|
"tirith_enabled": True,
|
||||||
"tirith_path": "tirith",
|
"tirith_path": "tirith",
|
||||||
"tirith_timeout": 5,
|
"tirith_timeout": 5,
|
||||||
|
|
@ -3978,10 +3978,10 @@ def load_config() -> Dict[str, Any]:
|
||||||
|
|
||||||
_SECURITY_COMMENT = """
|
_SECURITY_COMMENT = """
|
||||||
# ── Security ──────────────────────────────────────────────────────────
|
# ── Security ──────────────────────────────────────────────────────────
|
||||||
# Secret redaction is OFF by default — tool output (terminal stdout,
|
# Secret redaction is ON by default — strings that look like API keys,
|
||||||
# read_file results, web content) passes through unmodified. Set
|
# tokens, and passwords are masked in tool output, logs, and chat
|
||||||
# redact_secrets to true to mask strings that look like API keys, tokens,
|
# responses before the model or user ever sees them. Set redact_secrets
|
||||||
# and passwords before they enter the model context and logs.
|
# to false to disable (e.g. when developing the redactor itself).
|
||||||
# tirith pre-exec scanning is enabled by default when the tirith binary
|
# tirith pre-exec scanning is enabled by default when the tirith binary
|
||||||
# is available. Configure via security.tirith_* keys or env vars
|
# is available. Configure via security.tirith_* keys or env vars
|
||||||
# (TIRITH_ENABLED, TIRITH_BIN, TIRITH_TIMEOUT, TIRITH_FAIL_OPEN).
|
# (TIRITH_ENABLED, TIRITH_BIN, TIRITH_TIMEOUT, TIRITH_FAIL_OPEN).
|
||||||
|
|
@ -4021,8 +4021,8 @@ _FALLBACK_COMMENT = """
|
||||||
|
|
||||||
_COMMENTED_SECTIONS = """
|
_COMMENTED_SECTIONS = """
|
||||||
# ── Security ──────────────────────────────────────────────────────────
|
# ── Security ──────────────────────────────────────────────────────────
|
||||||
# Secret redaction is OFF by default. Set to true to mask strings that
|
# Secret redaction is ON by default. Set to false to pass tool output,
|
||||||
# look like API keys, tokens, and passwords in tool output and logs.
|
# logs, and chat responses through unmodified (e.g. for redactor dev).
|
||||||
#
|
#
|
||||||
# security:
|
# security:
|
||||||
# redact_secrets: true
|
# redact_secrets: true
|
||||||
|
|
|
||||||
|
|
@ -291,9 +291,11 @@ class TestCaptureLogSnapshotRedaction:
|
||||||
home = tmp_path / ".hermes"
|
home = tmp_path / ".hermes"
|
||||||
home.mkdir()
|
home.mkdir()
|
||||||
monkeypatch.setenv("HERMES_HOME", str(home))
|
monkeypatch.setenv("HERMES_HOME", str(home))
|
||||||
# Critical: ensure the user has NOT opted in to redaction. The whole
|
# Baseline fixture: no explicit env-var opinion. With the post-#17691
|
||||||
# point of this PR is that share-time redaction works for users who
|
# default of ON, the default-path tests below exercise the
|
||||||
# never set this env var.
|
# 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)
|
monkeypatch.delenv("HERMES_REDACT_SECRETS", raising=False)
|
||||||
|
|
||||||
logs_dir = home / "logs"
|
logs_dir = home / "logs"
|
||||||
|
|
@ -324,21 +326,26 @@ class TestCaptureLogSnapshotRedaction:
|
||||||
assert _REDACT_FIXTURE_TOKEN in snap.tail_text
|
assert _REDACT_FIXTURE_TOKEN in snap.tail_text
|
||||||
assert _REDACT_FIXTURE_TOKEN in (snap.full_text or "")
|
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.
|
"""Regression test: redact_sensitive_text short-circuits without force=True.
|
||||||
|
|
||||||
If a future refactor drops `force=True` from `_redact_log_text`, this
|
If a future refactor drops `force=True` from `_redact_log_text`, this
|
||||||
test fails immediately. Without `force=True`, the redactor returns the
|
test fails immediately. Without `force=True`, the redactor returns the
|
||||||
input unchanged when HERMES_REDACT_SECRETS is unset, and the feature
|
input unchanged when HERMES_REDACT_SECRETS=false, and the share-time
|
||||||
ships silently broken for its target audience.
|
redaction feature ships silently broken for users who opted out of
|
||||||
|
runtime redaction (e.g. developers working on the redactor itself).
|
||||||
"""
|
"""
|
||||||
import os
|
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
|
from hermes_cli.debug import _capture_log_snapshot
|
||||||
|
|
||||||
# Belt-and-suspenders: confirm the env var is genuinely unset for this
|
assert os.environ.get("HERMES_REDACT_SECRETS", "") == "false"
|
||||||
# test so we know we're exercising the force=True path.
|
|
||||||
assert os.environ.get("HERMES_REDACT_SECRETS", "") == ""
|
|
||||||
|
|
||||||
snap = _capture_log_snapshot("agent", tail_lines=10)
|
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
|
assert "ENV_VAR=false" in result.stdout
|
||||||
|
|
||||||
|
|
||||||
def test_redact_secrets_default_false_when_unset(tmp_path):
|
def test_redact_secrets_default_true_when_unset(tmp_path):
|
||||||
"""Without the config key, redaction stays OFF by default.
|
"""Without the config key or env var, redaction is ON by default (#17691).
|
||||||
|
|
||||||
Secret redaction is opt-in — users who want it must set
|
Secret redaction is a secure default — users who need raw credential
|
||||||
`security.redact_secrets: true` explicitly (or HERMES_REDACT_SECRETS=true).
|
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 = tmp_path / ".hermes"
|
||||||
hermes_home.mkdir()
|
hermes_home.mkdir()
|
||||||
|
|
@ -107,7 +109,7 @@ def test_redact_secrets_default_false_when_unset(tmp_path):
|
||||||
timeout=30,
|
timeout=30,
|
||||||
)
|
)
|
||||||
assert result.returncode == 0, f"probe failed: {result.stderr}"
|
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):
|
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_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_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_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_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_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). |
|
| `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