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 "