fix(debug): redact log content at upload time in hermes debug share
Apply agent.redact.redact_sensitive_text with force=True to log content captured by _capture_log_snapshot before it reaches upload_to_pastebin. On-disk logs are untouched. Compatible with the off-by-default local redaction policy from #16794: this is upload-time-only and applies regardless of security.redact_secrets because the public paste service is the leak surface. A visible banner is prepended to each uploaded log paste so reviewers know redaction was applied. --no-redact preserves deliberate unredacted sharing for maintainer-coordinated cases. The bug-report, setup-help, and feature-request issue templates direct users to run hermes debug share and paste the resulting public URLs. With redaction off by default per #16794, those uploads have been carrying credentials onto paste.rs and dpaste.com. force=True is non-negotiable: without it, redact_sensitive_text short-circuits at agent/redact.py:322 when the env var is unset, so the fix would silently be a no-op for its target audience. A regression test pins this down. Fixes #19316
This commit is contained in:
@@ -1,12 +1,19 @@
|
|||||||
"""``hermes debug`` — debug tools for Hermes Agent.
|
"""``hermes debug`` debug tools for Hermes Agent.
|
||||||
|
|
||||||
Currently supports:
|
Currently supports:
|
||||||
hermes debug share Upload debug report (system info + logs) to a
|
hermes debug share Upload debug report (system info + logs) to a
|
||||||
paste service and print a shareable URL.
|
paste service and print a shareable URL.
|
||||||
|
By default, log content is run through
|
||||||
|
``agent.redact.redact_sensitive_text`` with
|
||||||
|
``force=True`` before upload so credentials in
|
||||||
|
``~/.hermes/logs/*.log`` are not leaked into
|
||||||
|
the public paste service. Pass ``--no-redact``
|
||||||
|
to disable.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import urllib.error
|
import urllib.error
|
||||||
@@ -19,6 +26,16 @@ from typing import Optional
|
|||||||
from hermes_constants import get_hermes_home
|
from hermes_constants import get_hermes_home
|
||||||
from utils import atomic_replace
|
from utils import atomic_replace
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Banner prepended to upload-bound log content when redaction is enabled.
|
||||||
|
# Visible in the public paste so reviewers know the content was sanitized.
|
||||||
|
# Kept short; the trailing newline guarantees the banner sits on its own line.
|
||||||
|
_REDACTION_BANNER = (
|
||||||
|
"[hermes debug share: log content redacted at upload time. "
|
||||||
|
"run with --no-redact to disable]\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Paste services — try paste.rs first, dpaste.com as fallback.
|
# Paste services — try paste.rs first, dpaste.com as fallback.
|
||||||
@@ -368,17 +385,40 @@ def _resolve_log_path(log_name: str) -> Optional[Path]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _redact_log_text(text: str) -> str:
|
||||||
|
"""Run ``redact_sensitive_text`` with ``force=True`` over upload-bound text.
|
||||||
|
|
||||||
|
Uses ``force=True`` so redaction fires regardless of the operator's
|
||||||
|
``security.redact_secrets`` setting. The local on-disk log file is
|
||||||
|
not modified; only the in-memory copy headed for the public paste
|
||||||
|
service is sanitized. Returns the redacted text (or the original
|
||||||
|
when empty / non-string).
|
||||||
|
"""
|
||||||
|
if not text:
|
||||||
|
return text
|
||||||
|
from agent.redact import redact_sensitive_text
|
||||||
|
|
||||||
|
return redact_sensitive_text(text, force=True)
|
||||||
|
|
||||||
|
|
||||||
def _capture_log_snapshot(
|
def _capture_log_snapshot(
|
||||||
log_name: str,
|
log_name: str,
|
||||||
*,
|
*,
|
||||||
tail_lines: int,
|
tail_lines: int,
|
||||||
max_bytes: int = _MAX_LOG_BYTES,
|
max_bytes: int = _MAX_LOG_BYTES,
|
||||||
|
redact: bool = True,
|
||||||
) -> LogSnapshot:
|
) -> LogSnapshot:
|
||||||
"""Capture a log once and derive summary/full-log views from it.
|
"""Capture a log once and derive summary/full-log views from it.
|
||||||
|
|
||||||
The report tail and standalone log upload must come from the same file
|
The report tail and standalone log upload must come from the same file
|
||||||
snapshot. Otherwise a rotation/truncate between reads can make the report
|
snapshot. Otherwise a rotation/truncate between reads can make the report
|
||||||
look newer than the uploaded ``agent.log`` paste.
|
look newer than the uploaded ``agent.log`` paste.
|
||||||
|
|
||||||
|
When ``redact`` is True (the default), both ``tail_text`` and
|
||||||
|
``full_text`` are run through ``_redact_log_text`` so the snapshot
|
||||||
|
returned is upload-safe. The on-disk log file is never modified.
|
||||||
|
Pass ``redact=False`` to capture original log content (used by
|
||||||
|
``hermes debug share --no-redact``).
|
||||||
"""
|
"""
|
||||||
log_path = _resolve_log_path(log_name)
|
log_path = _resolve_log_path(log_name)
|
||||||
if log_path is None:
|
if log_path is None:
|
||||||
@@ -438,18 +478,34 @@ def _capture_log_snapshot(
|
|||||||
if truncated:
|
if truncated:
|
||||||
full_text = f"[... truncated — showing last ~{max_bytes // 1024}KB ...]\n{full_text}"
|
full_text = f"[... truncated — showing last ~{max_bytes // 1024}KB ...]\n{full_text}"
|
||||||
|
|
||||||
|
if redact:
|
||||||
|
tail_text = _redact_log_text(tail_text)
|
||||||
|
full_text = _redact_log_text(full_text)
|
||||||
|
|
||||||
return LogSnapshot(path=log_path, tail_text=tail_text, full_text=full_text)
|
return LogSnapshot(path=log_path, tail_text=tail_text, full_text=full_text)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return LogSnapshot(path=log_path, tail_text=f"(error reading: {exc})", full_text=None)
|
return LogSnapshot(path=log_path, tail_text=f"(error reading: {exc})", full_text=None)
|
||||||
|
|
||||||
|
|
||||||
def _capture_default_log_snapshots(log_lines: int) -> dict[str, LogSnapshot]:
|
def _capture_default_log_snapshots(
|
||||||
"""Capture all logs used by debug-share exactly once."""
|
log_lines: int, *, redact: bool = True
|
||||||
|
) -> dict[str, LogSnapshot]:
|
||||||
|
"""Capture all logs used by debug-share exactly once.
|
||||||
|
|
||||||
|
``redact`` is forwarded to each ``_capture_log_snapshot`` call so all
|
||||||
|
captured logs share the same redaction policy for a given run.
|
||||||
|
"""
|
||||||
errors_lines = min(log_lines, 100)
|
errors_lines = min(log_lines, 100)
|
||||||
return {
|
return {
|
||||||
"agent": _capture_log_snapshot("agent", tail_lines=log_lines),
|
"agent": _capture_log_snapshot(
|
||||||
"errors": _capture_log_snapshot("errors", tail_lines=errors_lines),
|
"agent", tail_lines=log_lines, redact=redact
|
||||||
"gateway": _capture_log_snapshot("gateway", tail_lines=errors_lines),
|
),
|
||||||
|
"errors": _capture_log_snapshot(
|
||||||
|
"errors", tail_lines=errors_lines, redact=redact
|
||||||
|
),
|
||||||
|
"gateway": _capture_log_snapshot(
|
||||||
|
"gateway", tail_lines=errors_lines, redact=redact
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -532,6 +588,7 @@ def run_debug_share(args):
|
|||||||
log_lines = getattr(args, "lines", 200)
|
log_lines = getattr(args, "lines", 200)
|
||||||
expiry = getattr(args, "expire", 7)
|
expiry = getattr(args, "expire", 7)
|
||||||
local_only = getattr(args, "local", False)
|
local_only = getattr(args, "local", False)
|
||||||
|
redact = not getattr(args, "no_redact", False)
|
||||||
|
|
||||||
if not local_only:
|
if not local_only:
|
||||||
print(_PRIVACY_NOTICE)
|
print(_PRIVACY_NOTICE)
|
||||||
@@ -539,8 +596,16 @@ def run_debug_share(args):
|
|||||||
print("Collecting debug report...")
|
print("Collecting debug report...")
|
||||||
|
|
||||||
# Capture dump once — prepended to every paste for context.
|
# Capture dump once — prepended to every paste for context.
|
||||||
|
# The dump is already redacted at extract time via dump.py:_redact;
|
||||||
|
# log_snapshots are redacted by _capture_default_log_snapshots when
|
||||||
|
# redact=True so credentials never reach the public paste service.
|
||||||
dump_text = _capture_dump()
|
dump_text = _capture_dump()
|
||||||
log_snapshots = _capture_default_log_snapshots(log_lines)
|
log_snapshots = _capture_default_log_snapshots(log_lines, redact=redact)
|
||||||
|
|
||||||
|
if redact:
|
||||||
|
logger.info(
|
||||||
|
"hermes debug share: applied force-mode redaction to log snapshots before upload"
|
||||||
|
)
|
||||||
|
|
||||||
report = collect_debug_report(
|
report = collect_debug_report(
|
||||||
log_lines=log_lines,
|
log_lines=log_lines,
|
||||||
@@ -556,6 +621,15 @@ def run_debug_share(args):
|
|||||||
if gateway_log:
|
if gateway_log:
|
||||||
gateway_log = dump_text + "\n\n--- full gateway.log ---\n" + gateway_log
|
gateway_log = dump_text + "\n\n--- full gateway.log ---\n" + gateway_log
|
||||||
|
|
||||||
|
# Visible banner so reviewers reading the public paste know redaction
|
||||||
|
# was applied at upload time. Banner is omitted under --no-redact.
|
||||||
|
if redact:
|
||||||
|
report = _REDACTION_BANNER + report
|
||||||
|
if agent_log:
|
||||||
|
agent_log = _REDACTION_BANNER + agent_log
|
||||||
|
if gateway_log:
|
||||||
|
gateway_log = _REDACTION_BANNER + gateway_log
|
||||||
|
|
||||||
if local_only:
|
if local_only:
|
||||||
print(report)
|
print(report)
|
||||||
if agent_log:
|
if agent_log:
|
||||||
@@ -666,6 +740,7 @@ def run_debug(args):
|
|||||||
print(" --lines N Number of log lines to include (default: 200)")
|
print(" --lines N Number of log lines to include (default: 200)")
|
||||||
print(" --expire N Paste expiry in days (default: 7)")
|
print(" --expire N Paste expiry in days (default: 7)")
|
||||||
print(" --local Print report locally instead of uploading")
|
print(" --local Print report locally instead of uploading")
|
||||||
|
print(" --no-redact Disable upload-time secret redaction (default: redact)")
|
||||||
print()
|
print()
|
||||||
print("Options (delete):")
|
print("Options (delete):")
|
||||||
print(" <url> ... One or more paste URLs to delete")
|
print(" <url> ... One or more paste URLs to delete")
|
||||||
|
|||||||
@@ -8891,6 +8891,7 @@ Examples:
|
|||||||
hermes debug share --lines 500 Include more log lines
|
hermes debug share --lines 500 Include more log lines
|
||||||
hermes debug share --expire 30 Keep paste for 30 days
|
hermes debug share --expire 30 Keep paste for 30 days
|
||||||
hermes debug share --local Print report locally (no upload)
|
hermes debug share --local Print report locally (no upload)
|
||||||
|
hermes debug share --no-redact Disable upload-time secret redaction
|
||||||
hermes debug delete <url> Delete a previously uploaded paste
|
hermes debug delete <url> Delete a previously uploaded paste
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
@@ -8916,6 +8917,16 @@ Examples:
|
|||||||
action="store_true",
|
action="store_true",
|
||||||
help="Print the report locally instead of uploading",
|
help="Print the report locally instead of uploading",
|
||||||
)
|
)
|
||||||
|
share_parser.add_argument(
|
||||||
|
"--no-redact",
|
||||||
|
action="store_true",
|
||||||
|
help=(
|
||||||
|
"Disable upload-time secret redaction (default: redact). Logs "
|
||||||
|
"are normally run through agent.redact.redact_sensitive_text "
|
||||||
|
"with force=True before upload so credentials are not leaked "
|
||||||
|
"into the public paste service."
|
||||||
|
),
|
||||||
|
)
|
||||||
delete_parser = debug_sub.add_parser(
|
delete_parser = debug_sub.add_parser(
|
||||||
"delete",
|
"delete",
|
||||||
help="Delete a paste uploaded by 'hermes debug share'",
|
help="Delete a paste uploaded by 'hermes debug share'",
|
||||||
|
|||||||
@@ -679,6 +679,8 @@ AUTHOR_MAP = {
|
|||||||
"ztzheng@163.com": "chengoak", # PR #17467
|
"ztzheng@163.com": "chengoak", # PR #17467
|
||||||
"24110240104@m.fudan.edu.cn": "YuShu", # co-author only
|
"24110240104@m.fudan.edu.cn": "YuShu", # co-author only
|
||||||
"charliekerfoot@gmail.com": "CharlieKerfoot", # PR #18951
|
"charliekerfoot@gmail.com": "CharlieKerfoot", # PR #18951
|
||||||
|
# Debug share upload-time redaction (May 2026)
|
||||||
|
"dhuysamen@gmail.com": "GodsBoy", # PR #19318
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -273,6 +273,101 @@ class TestCaptureLogSnapshot:
|
|||||||
assert "rotated agent data" in snap.full_text
|
assert "rotated agent data" in snap.full_text
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Capture log redaction (force=True applies regardless of HERMES_REDACT_SECRETS)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# A vendor-prefixed token used across redaction tests. Long enough to clear
|
||||||
|
# the redactor's `floor` parameter so it actually masks rather than fully blanks.
|
||||||
|
_REDACT_FIXTURE_TOKEN = "sk-proj-A1B2C3D4E5F6G7H8I9J0aA"
|
||||||
|
|
||||||
|
|
||||||
|
class TestCaptureLogSnapshotRedaction:
|
||||||
|
"""Pin upload-time redaction at the _capture_log_snapshot boundary."""
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def hermes_home_with_secret(self, tmp_path, monkeypatch):
|
||||||
|
"""Isolated HERMES_HOME whose agent.log contains a vendor-prefixed token."""
|
||||||
|
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.
|
||||||
|
monkeypatch.delenv("HERMES_REDACT_SECRETS", raising=False)
|
||||||
|
|
||||||
|
logs_dir = home / "logs"
|
||||||
|
logs_dir.mkdir()
|
||||||
|
(logs_dir / "agent.log").write_text(
|
||||||
|
f"2026-04-12 17:00:00 INFO config: api_key={_REDACT_FIXTURE_TOKEN} loaded\n"
|
||||||
|
)
|
||||||
|
(logs_dir / "errors.log").write_text("")
|
||||||
|
(logs_dir / "gateway.log").write_text("")
|
||||||
|
return home
|
||||||
|
|
||||||
|
def test_default_redacts_tail_and_full_text(self, hermes_home_with_secret):
|
||||||
|
from hermes_cli.debug import _capture_log_snapshot
|
||||||
|
|
||||||
|
snap = _capture_log_snapshot("agent", tail_lines=10)
|
||||||
|
|
||||||
|
# Both views the upload uses must be sanitized.
|
||||||
|
assert _REDACT_FIXTURE_TOKEN not in snap.tail_text
|
||||||
|
assert snap.full_text is not None
|
||||||
|
assert _REDACT_FIXTURE_TOKEN not in snap.full_text
|
||||||
|
|
||||||
|
def test_redact_false_passes_through(self, hermes_home_with_secret):
|
||||||
|
from hermes_cli.debug import _capture_log_snapshot
|
||||||
|
|
||||||
|
snap = _capture_log_snapshot("agent", tail_lines=10, redact=False)
|
||||||
|
|
||||||
|
# Original token survives when the caller opts out.
|
||||||
|
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):
|
||||||
|
"""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.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
|
||||||
|
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", "") == ""
|
||||||
|
|
||||||
|
snap = _capture_log_snapshot("agent", tail_lines=10)
|
||||||
|
|
||||||
|
assert _REDACT_FIXTURE_TOKEN not in snap.tail_text
|
||||||
|
assert snap.full_text is not None
|
||||||
|
assert _REDACT_FIXTURE_TOKEN not in snap.full_text
|
||||||
|
|
||||||
|
def test_capture_default_log_snapshots_threads_redact(
|
||||||
|
self, hermes_home_with_secret
|
||||||
|
):
|
||||||
|
from hermes_cli.debug import _capture_default_log_snapshots
|
||||||
|
|
||||||
|
snaps = _capture_default_log_snapshots(50)
|
||||||
|
|
||||||
|
# Default threads redact=True to all three captured logs.
|
||||||
|
assert _REDACT_FIXTURE_TOKEN not in snaps["agent"].tail_text
|
||||||
|
assert _REDACT_FIXTURE_TOKEN not in (snaps["agent"].full_text or "")
|
||||||
|
|
||||||
|
def test_capture_default_log_snapshots_no_redact_passes_through(
|
||||||
|
self, hermes_home_with_secret
|
||||||
|
):
|
||||||
|
from hermes_cli.debug import _capture_default_log_snapshots
|
||||||
|
|
||||||
|
snaps = _capture_default_log_snapshots(50, redact=False)
|
||||||
|
|
||||||
|
assert _REDACT_FIXTURE_TOKEN in snaps["agent"].tail_text
|
||||||
|
assert _REDACT_FIXTURE_TOKEN in (snaps["agent"].full_text or "")
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Debug report collection
|
# Debug report collection
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -556,6 +651,124 @@ class TestRunDebugShare:
|
|||||||
assert "all failed" in out.err
|
assert "all failed" in out.err
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Share-time redaction wiring + visible banner
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TestRunDebugShareRedaction:
|
||||||
|
"""End-to-end: --no-redact flag, banner injection, default behavior."""
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def hermes_home_with_secret(self, tmp_path, monkeypatch):
|
||||||
|
"""Isolated HERMES_HOME whose agent.log contains a vendor-prefixed token."""
|
||||||
|
home = tmp_path / ".hermes"
|
||||||
|
home.mkdir()
|
||||||
|
monkeypatch.setenv("HERMES_HOME", str(home))
|
||||||
|
monkeypatch.delenv("HERMES_REDACT_SECRETS", raising=False)
|
||||||
|
|
||||||
|
logs_dir = home / "logs"
|
||||||
|
logs_dir.mkdir()
|
||||||
|
(logs_dir / "agent.log").write_text(
|
||||||
|
f"2026-04-12 17:00:00 INFO config: api_key={_REDACT_FIXTURE_TOKEN} loaded\n"
|
||||||
|
)
|
||||||
|
(logs_dir / "errors.log").write_text("")
|
||||||
|
(logs_dir / "gateway.log").write_text(
|
||||||
|
f"2026-04-12 17:00:01 INFO gateway.run: token {_REDACT_FIXTURE_TOKEN}\n"
|
||||||
|
)
|
||||||
|
return home
|
||||||
|
|
||||||
|
def test_default_share_redacts_uploaded_content(
|
||||||
|
self, hermes_home_with_secret, capsys
|
||||||
|
):
|
||||||
|
"""The uploaded report and full-log pastes do not contain the raw token."""
|
||||||
|
from hermes_cli.debug import run_debug_share
|
||||||
|
|
||||||
|
args = MagicMock()
|
||||||
|
args.lines = 50
|
||||||
|
args.expire = 7
|
||||||
|
args.local = False
|
||||||
|
args.no_redact = False
|
||||||
|
|
||||||
|
captured: list[str] = []
|
||||||
|
|
||||||
|
def fake_upload(content, expiry_days=7):
|
||||||
|
captured.append(content)
|
||||||
|
return f"https://paste.rs/{len(captured)}"
|
||||||
|
|
||||||
|
with patch("hermes_cli.dump.run_dump"), \
|
||||||
|
patch("hermes_cli.debug._sweep_expired_pastes", return_value=(0, 0)), \
|
||||||
|
patch("hermes_cli.debug.upload_to_pastebin", side_effect=fake_upload):
|
||||||
|
run_debug_share(args)
|
||||||
|
|
||||||
|
# At least the report plus one full log paste reached the upload path.
|
||||||
|
assert len(captured) >= 2
|
||||||
|
for content in captured:
|
||||||
|
assert _REDACT_FIXTURE_TOKEN not in content, (
|
||||||
|
"raw token leaked into upload-bound content"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_default_share_includes_redaction_banner(
|
||||||
|
self, hermes_home_with_secret, capsys
|
||||||
|
):
|
||||||
|
"""Each upload-bound paste carries the visible redaction banner."""
|
||||||
|
from hermes_cli.debug import run_debug_share
|
||||||
|
|
||||||
|
args = MagicMock()
|
||||||
|
args.lines = 50
|
||||||
|
args.expire = 7
|
||||||
|
args.local = False
|
||||||
|
args.no_redact = False
|
||||||
|
|
||||||
|
captured: list[str] = []
|
||||||
|
|
||||||
|
def fake_upload(content, expiry_days=7):
|
||||||
|
captured.append(content)
|
||||||
|
return f"https://paste.rs/{len(captured)}"
|
||||||
|
|
||||||
|
with patch("hermes_cli.dump.run_dump"), \
|
||||||
|
patch("hermes_cli.debug._sweep_expired_pastes", return_value=(0, 0)), \
|
||||||
|
patch("hermes_cli.debug.upload_to_pastebin", side_effect=fake_upload):
|
||||||
|
run_debug_share(args)
|
||||||
|
|
||||||
|
for content in captured:
|
||||||
|
assert "redacted at upload time" in content, (
|
||||||
|
"redaction banner missing from upload-bound content"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_no_redact_flag_disables_redaction_and_banner(
|
||||||
|
self, hermes_home_with_secret, capsys
|
||||||
|
):
|
||||||
|
"""--no-redact preserves original log content and omits the banner."""
|
||||||
|
from hermes_cli.debug import run_debug_share
|
||||||
|
|
||||||
|
args = MagicMock()
|
||||||
|
args.lines = 50
|
||||||
|
args.expire = 7
|
||||||
|
args.local = False
|
||||||
|
args.no_redact = True
|
||||||
|
|
||||||
|
captured: list[str] = []
|
||||||
|
|
||||||
|
def fake_upload(content, expiry_days=7):
|
||||||
|
captured.append(content)
|
||||||
|
return f"https://paste.rs/{len(captured)}"
|
||||||
|
|
||||||
|
with patch("hermes_cli.dump.run_dump"), \
|
||||||
|
patch("hermes_cli.debug._sweep_expired_pastes", return_value=(0, 0)), \
|
||||||
|
patch("hermes_cli.debug.upload_to_pastebin", side_effect=fake_upload):
|
||||||
|
run_debug_share(args)
|
||||||
|
|
||||||
|
# The agent.log paste should now contain the raw token.
|
||||||
|
assert any(_REDACT_FIXTURE_TOKEN in c for c in captured), (
|
||||||
|
"expected raw token in --no-redact upload"
|
||||||
|
)
|
||||||
|
# No banner anywhere when redaction is disabled.
|
||||||
|
for content in captured:
|
||||||
|
assert "redacted at upload time" not in content, (
|
||||||
|
"banner present with --no-redact"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# run_debug router
|
# run_debug router
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user