Skip to content

Commit eb471c9

Browse files
GodsBoyluyao618
authored andcommitted
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 NousResearch#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 NousResearch#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 NousResearch#19316
1 parent e3df125 commit eb471c9

4 files changed

Lines changed: 308 additions & 7 deletions

File tree

hermes_cli/debug.py

Lines changed: 82 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
1-
"""``hermes debug`` debug tools for Hermes Agent.
1+
"""``hermes debug`` debug tools for Hermes Agent.
22
33
Currently supports:
44
hermes debug share Upload debug report (system info + logs) to a
55
paste service and print a shareable URL.
6+
By default, log content is run through
7+
``agent.redact.redact_sensitive_text`` with
8+
``force=True`` before upload so credentials in
9+
``~/.hermes/logs/*.log`` are not leaked into
10+
the public paste service. Pass ``--no-redact``
11+
to disable.
612
"""
713

814
import io
915
import json
16+
import logging
1017
import sys
1118
import time
1219
import urllib.error
@@ -19,6 +26,16 @@
1926
from hermes_constants import get_hermes_home
2027
from utils import atomic_replace
2128

29+
logger = logging.getLogger(__name__)
30+
31+
# Banner prepended to upload-bound log content when redaction is enabled.
32+
# Visible in the public paste so reviewers know the content was sanitized.
33+
# Kept short; the trailing newline guarantees the banner sits on its own line.
34+
_REDACTION_BANNER = (
35+
"[hermes debug share: log content redacted at upload time. "
36+
"run with --no-redact to disable]\n"
37+
)
38+
2239

2340
# ---------------------------------------------------------------------------
2441
# Paste services — try paste.rs first, dpaste.com as fallback.
@@ -368,17 +385,40 @@ def _resolve_log_path(log_name: str) -> Optional[Path]:
368385
return None
369386

370387

388+
def _redact_log_text(text: str) -> str:
389+
"""Run ``redact_sensitive_text`` with ``force=True`` over upload-bound text.
390+
391+
Uses ``force=True`` so redaction fires regardless of the operator's
392+
``security.redact_secrets`` setting. The local on-disk log file is
393+
not modified; only the in-memory copy headed for the public paste
394+
service is sanitized. Returns the redacted text (or the original
395+
when empty / non-string).
396+
"""
397+
if not text:
398+
return text
399+
from agent.redact import redact_sensitive_text
400+
401+
return redact_sensitive_text(text, force=True)
402+
403+
371404
def _capture_log_snapshot(
372405
log_name: str,
373406
*,
374407
tail_lines: int,
375408
max_bytes: int = _MAX_LOG_BYTES,
409+
redact: bool = True,
376410
) -> LogSnapshot:
377411
"""Capture a log once and derive summary/full-log views from it.
378412
379413
The report tail and standalone log upload must come from the same file
380414
snapshot. Otherwise a rotation/truncate between reads can make the report
381415
look newer than the uploaded ``agent.log`` paste.
416+
417+
When ``redact`` is True (the default), both ``tail_text`` and
418+
``full_text`` are run through ``_redact_log_text`` so the snapshot
419+
returned is upload-safe. The on-disk log file is never modified.
420+
Pass ``redact=False`` to capture original log content (used by
421+
``hermes debug share --no-redact``).
382422
"""
383423
log_path = _resolve_log_path(log_name)
384424
if log_path is None:
@@ -438,18 +478,34 @@ def _capture_log_snapshot(
438478
if truncated:
439479
full_text = f"[... truncated — showing last ~{max_bytes // 1024}KB ...]\n{full_text}"
440480

481+
if redact:
482+
tail_text = _redact_log_text(tail_text)
483+
full_text = _redact_log_text(full_text)
484+
441485
return LogSnapshot(path=log_path, tail_text=tail_text, full_text=full_text)
442486
except Exception as exc:
443487
return LogSnapshot(path=log_path, tail_text=f"(error reading: {exc})", full_text=None)
444488

445489

446-
def _capture_default_log_snapshots(log_lines: int) -> dict[str, LogSnapshot]:
447-
"""Capture all logs used by debug-share exactly once."""
490+
def _capture_default_log_snapshots(
491+
log_lines: int, *, redact: bool = True
492+
) -> dict[str, LogSnapshot]:
493+
"""Capture all logs used by debug-share exactly once.
494+
495+
``redact`` is forwarded to each ``_capture_log_snapshot`` call so all
496+
captured logs share the same redaction policy for a given run.
497+
"""
448498
errors_lines = min(log_lines, 100)
449499
return {
450-
"agent": _capture_log_snapshot("agent", tail_lines=log_lines),
451-
"errors": _capture_log_snapshot("errors", tail_lines=errors_lines),
452-
"gateway": _capture_log_snapshot("gateway", tail_lines=errors_lines),
500+
"agent": _capture_log_snapshot(
501+
"agent", tail_lines=log_lines, redact=redact
502+
),
503+
"errors": _capture_log_snapshot(
504+
"errors", tail_lines=errors_lines, redact=redact
505+
),
506+
"gateway": _capture_log_snapshot(
507+
"gateway", tail_lines=errors_lines, redact=redact
508+
),
453509
}
454510

455511

@@ -532,15 +588,24 @@ def run_debug_share(args):
532588
log_lines = getattr(args, "lines", 200)
533589
expiry = getattr(args, "expire", 7)
534590
local_only = getattr(args, "local", False)
591+
redact = not getattr(args, "no_redact", False)
535592

536593
if not local_only:
537594
print(_PRIVACY_NOTICE)
538595

539596
print("Collecting debug report...")
540597

541598
# Capture dump once — prepended to every paste for context.
599+
# The dump is already redacted at extract time via dump.py:_redact;
600+
# log_snapshots are redacted by _capture_default_log_snapshots when
601+
# redact=True so credentials never reach the public paste service.
542602
dump_text = _capture_dump()
543-
log_snapshots = _capture_default_log_snapshots(log_lines)
603+
log_snapshots = _capture_default_log_snapshots(log_lines, redact=redact)
604+
605+
if redact:
606+
logger.info(
607+
"hermes debug share: applied force-mode redaction to log snapshots before upload"
608+
)
544609

545610
report = collect_debug_report(
546611
log_lines=log_lines,
@@ -556,6 +621,15 @@ def run_debug_share(args):
556621
if gateway_log:
557622
gateway_log = dump_text + "\n\n--- full gateway.log ---\n" + gateway_log
558623

624+
# Visible banner so reviewers reading the public paste know redaction
625+
# was applied at upload time. Banner is omitted under --no-redact.
626+
if redact:
627+
report = _REDACTION_BANNER + report
628+
if agent_log:
629+
agent_log = _REDACTION_BANNER + agent_log
630+
if gateway_log:
631+
gateway_log = _REDACTION_BANNER + gateway_log
632+
559633
if local_only:
560634
print(report)
561635
if agent_log:
@@ -666,6 +740,7 @@ def run_debug(args):
666740
print(" --lines N Number of log lines to include (default: 200)")
667741
print(" --expire N Paste expiry in days (default: 7)")
668742
print(" --local Print report locally instead of uploading")
743+
print(" --no-redact Disable upload-time secret redaction (default: redact)")
669744
print()
670745
print("Options (delete):")
671746
print(" <url> ... One or more paste URLs to delete")

hermes_cli/main.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8891,6 +8891,7 @@ def main():
88918891
hermes debug share --lines 500 Include more log lines
88928892
hermes debug share --expire 30 Keep paste for 30 days
88938893
hermes debug share --local Print report locally (no upload)
8894+
hermes debug share --no-redact Disable upload-time secret redaction
88948895
hermes debug delete <url> Delete a previously uploaded paste
88958896
""",
88968897
)
@@ -8916,6 +8917,16 @@ def main():
89168917
action="store_true",
89178918
help="Print the report locally instead of uploading",
89188919
)
8920+
share_parser.add_argument(
8921+
"--no-redact",
8922+
action="store_true",
8923+
help=(
8924+
"Disable upload-time secret redaction (default: redact). Logs "
8925+
"are normally run through agent.redact.redact_sensitive_text "
8926+
"with force=True before upload so credentials are not leaked "
8927+
"into the public paste service."
8928+
),
8929+
)
89198930
delete_parser = debug_sub.add_parser(
89208931
"delete",
89218932
help="Delete a paste uploaded by 'hermes debug share'",

scripts/release.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -679,6 +679,8 @@
679679
"ztzheng@163.com": "chengoak", # PR #17467
680680
"24110240104@m.fudan.edu.cn": "YuShu", # co-author only
681681
"charliekerfoot@gmail.com": "CharlieKerfoot", # PR #18951
682+
# Debug share upload-time redaction (May 2026)
683+
"dhuysamen@gmail.com": "GodsBoy", # PR #19318
682684
}
683685

684686

0 commit comments

Comments
 (0)