fix(security): require explicit consent before uploading debug logs

`hermes debug share` printed a privacy notice and then uploaded the
report to a public paste service in the same breath — the user never got
to say yes or no. Add a consent gate: an interactive [y/N] prompt, a
--yes/-y flag to skip it, and a hard refusal (exit 1) in non-interactive
contexts (no TTY on stdin) so debug data can't be exposed silently in
scripts/CI.

- New _confirm_upload() helper gates the actual upload after the notice.
- Applied to BOTH upload paths: the public paste.rs path and the --nous
  Nous-S3 path (the latter is a sibling site the original PR missed).
- The /debug slash command passes yes=True (typing /debug is itself the
  consent action, and input() would hang inside prompt_toolkit).
- Rewrote the privacy notice for accuracy: secrets (API keys/tokens/
  passwords) ARE force-redacted before upload; PII (display name,
  platform user ID, verbatim message content, filesystem paths) is NOT,
  and that URL is public.

Fixes #22016.

Co-authored-by: liuhao1024 <liuhao1024@users.noreply.github.com>
This commit is contained in:
JezzaHehn 2026-07-01 00:17:16 -07:00 • committed by Teknium
parent 3aebdb1d23
commit 54f32af4a7
4 changed files with 204 additions and 14 deletions

View file

@ -196,15 +196,19 @@ def _best_effort_sweep_expired_pastes() -> None:
# ---------------------------------------------------------------------------
_PRIVACY_NOTICE = """\
⚠️ This will upload the following to a public paste service:
• System info (OS, Python version, Hermes version, provider, which API keys
are configured — NOT the actual keys)
• Recent log lines (agent.log, errors.log, gateway.log, gui.log, desktop.log
— may contain conversation fragments and file paths)
• Full agent.log, gateway.log, gui.log, and desktop.log (up to 512 KB each —
likely contains conversation content, tool outputs, and file paths)
⚠️ This will upload system info + logs to a PUBLIC paste service.
Pastes auto-delete after 6 hours.
Cryptographic secrets (API keys, tokens, passwords) are redacted before
upload, but the following personal data is NOT redacted and will be public:
• Your display name and persistent platform user ID
• Verbatim content of your recent messages (prompts, responses, tool output)
• Local filesystem paths
• Any other PII present in the logs
The resulting URL is public to anyone who has the link. Pastes auto-delete
after 6 hours, but may be archived by third parties in the meantime.
Use --local to view the report without uploading.
"""
_GATEWAY_PRIVACY_NOTICE = (
@ -774,6 +778,38 @@ def build_debug_share(
)
def _confirm_upload(args) -> bool:
"""Require explicit consent before any debug-share upload.
The privacy notice is printed by the caller. This gates the actual
upload: with ``--yes`` (or ``-y``) we proceed unprompted; otherwise we
ask an interactive ``[y/N]`` question. In a non-interactive context
(no TTY on stdin — scripts, CI, piped input) we refuse rather than
hang or upload silently, so debug data can't be exposed without a
deliberate ``--yes``.
Returns True to proceed with the upload, False to abort.
"""
if bool(getattr(args, "yes", False)):
return True
if not sys.stdin.isatty():
print(
"ERROR: Non-interactive mode requires --yes to confirm upload.\n"
" This prevents accidental exposure of personal data.\n"
" Use --local to view the report without uploading.",
file=sys.stderr,
)
sys.exit(1)
try:
answer = input("Upload debug report? [y/N] ").strip().lower()
except (EOFError, KeyboardInterrupt):
answer = ""
if answer not in ("y", "yes"):
print("Aborted.")
return False
return True
def run_debug_share(args):
"""Collect debug report + full logs, upload each, print URLs."""
log_lines = getattr(args, "lines", 200)
@ -805,10 +841,12 @@ def run_debug_share(args):
return
if nous:
_run_debug_share_nous(log_lines=log_lines, redact=redact)
_run_debug_share_nous(args, log_lines=log_lines, redact=redact)
return
print(_PRIVACY_NOTICE)
if not _confirm_upload(args):
return
print("Collecting debug report...")
print("Uploading...")
@ -856,7 +894,7 @@ _NOUS_PRIVACY_NOTICE = """\
"""
def _run_debug_share_nous(*, log_lines: int, redact: bool) -> None:
def _run_debug_share_nous(args, *, log_lines: int, redact: bool) -> None:
"""Handle ``hermes debug share --nous``: upload the bundle to Nous-S3.
Collects the same force-redacted bundle as the paste path, gzips it into
@ -867,6 +905,8 @@ def _run_debug_share_nous(*, log_lines: int, redact: bool) -> None:
from hermes_cli.diagnostics_upload import share_to_nous
print(_NOUS_PRIVACY_NOTICE)
if not _confirm_upload(args):
return
if not redact:
print(
"⚠️ --no-redact is set: secrets in your logs will NOT be redacted "