1- """``hermes debug`` — debug tools for Hermes Agent.
1+ """``hermes debug`` debug tools for Hermes Agent.
22
33Currently 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
814import io
915import json
16+ import logging
1017import sys
1118import time
1219import urllib .error
1926from hermes_constants import get_hermes_home
2027from 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+
371404def _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" )
0 commit comments