feat: mask secret values in the UI and redact them from diagnostics

- Env/header values whose key looks secret (TOKEN, API_KEY, PASSWORD,
  AUTH, ...) render as •••••••• via a display-only delegate; a
  'Show secrets' toggle reveals them. Underlying data, editing, and
  save are untouched.
- The Add dialog switches the value field to password echo when the
  key name looks secret.
- Copy-diagnostics now redacts secrets from args (--token <v>,
  --api-key=<v>, and well-known token prefixes like ghp_/sk-/xoxb-),
  since those reports get pasted into public bug reports. Env and
  header values were already omitted from diagnostics.
This commit is contained in:
AJ
2026-07-02 00:31:34 -04:00
parent b16c0fd526
commit 6dee318976
3 changed files with 167 additions and 2 deletions
+80 -1
View File
@@ -596,6 +596,84 @@ def parse_pasted_json(text: str) -> dict[str, dict]:
return servers
# --------------------------------------------------------------------------- #
# Secrets: detection + redaction
# --------------------------------------------------------------------------- #
_SECRET_KEY_RE = re.compile(
r"(?i)(token|secret|passw|api[-_]?key|apikey|auth|credential|bearer|private[-_]?key|access[-_]?key)"
)
# Well-known token prefixes that identify a bare value as a secret even
# without a telling key/flag name next to it.
_TOKEN_PREFIXES = (
"ghp_",
"gho_",
"ghu_",
"ghs_",
"github_pat_", # GitHub
"sk-",
"sk_live_",
"sk_test_",
"rk_live_", # OpenAI / Stripe
"xoxb-",
"xoxp-",
"xoxc-",
"xoxs-",
"xapp-", # Slack
"glpat-",
"gldt-", # GitLab
"AKIA",
"ASIA", # AWS access key IDs
"ya29.",
"AIza", # Google
"pypi-",
"npm_",
"dop_v1_",
"figd_",
)
MASK = "••••••••"
def is_secret_key(name: str) -> bool:
"""Does this env-var / header / flag name look like it holds a secret?"""
return bool(_SECRET_KEY_RE.search(name or ""))
def _is_secret_value(value: str) -> bool:
return isinstance(value, str) and value.startswith(_TOKEN_PREFIXES)
def redact_args(args: list[str]) -> list[str]:
"""
Mask secret values in an args list for display/diagnostics:
--token abc123 -> --token •••••••• (value after a secret flag)
--api-key=abc123 -> --api-key=•••••••• (inline flag=value)
ghp_abc123 -> •••••••• (well-known token prefix)
Everything else passes through untouched.
"""
out: list[str] = []
mask_next = False
for a in args:
s = str(a)
if mask_next:
out.append(MASK)
mask_next = False
continue
if s.startswith("-") and "=" in s and is_secret_key(s.split("=", 1)[0]):
out.append(s.split("=", 1)[0] + "=" + MASK)
continue
if s.startswith("-") and is_secret_key(s):
out.append(s)
mask_next = True
continue
if _is_secret_value(s):
out.append(MASK)
continue
out.append(s)
return out
def split_suspicious_args(args: list[str]) -> tuple[list[str], list[str]]:
"""
Detect the classic argument-entry mistake: several argv tokens typed on one
@@ -944,7 +1022,8 @@ def diagnostics_text(name: str, data: dict) -> str:
L.append(f"Server: {name or '(unnamed)'} [local / stdio]")
L.append(f"Command: {data.get('command', '')}")
if data.get("args"):
L.append("Args: " + " ".join(str(a) for a in data["args"]))
# Redacted: this report is designed to be pasted into bug reports.
L.append("Args: " + " ".join(redact_args(list(data["args"]))))
if data.get("env"):
L.append("Env keys: " + ", ".join(data["env"].keys()))
L.append(f"Status: {res['status'].upper()}")