feat(#83): offer move-to-env on args rows; explain the gate instead of an empty menu
CI / Lint (ruff) (pull_request) Successful in 19s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 24s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 29s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 29s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 31s
CI / Catalog signature (pull_request) Successful in 22s

Two things surfaced testing the GUI:

1. Args rows showed the secret warning but no move action -- the args
   editor is a free-text widget, not a table, and was deliberately left out
   of the first cut. Wired it up: ArgsEdit gains a context menu that offers
   "Move to environment variable…" on exactly the args that look like a
   credential. New pure core: secret_arg_indices (which args are secrets,
   mirroring args_secret_warning per-index) and suggested_env_var_for_arg
   (default var name from the preceding flag -- `--api-key <secret>` ->
   API_KEY, else SECRET). The move replaces that one arg line with ${VAR}
   and copies the secret to the clipboard, same contract as the tables.

2. On a Claude Desktop profile (or any non-expanding client) the menu showed
   NOTHING, so it read as broken. Now a real stored secret always shows the
   item -- enabled on a client that expands references, or disabled with the
   reason ("unavailable for Claude Desktop -- it doesn't expand ${VAR}") so
   the gate is visible rather than silent. Applies to env, headers and args.

Not a change: after converting, env_ref_warnings still notes a variable that
isn't set in the environment. That's #82's advisory doing its job -- the user
runs the export line the dialog handed them; auto-adding a ':-default' would
bake a fallback back into the config and defeat moving the secret out.

Tests: +5 core (secret_arg_indices for token/flag-value/embedded-URL/
reference-excluded, suggested_env_var_for_arg with and without a flag).
483 passed, ruff clean. GUI wiring (context menus) remains untestable in CI.

Refs #83

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EKwBecy6N83jnqQmw8ezwE
This commit is contained in:
2026-08-04 04:03:47 +00:00
co-authored by Claude Opus 4.8
parent 8fdbe90b37
commit 4743c4a995
4 changed files with 191 additions and 5 deletions
+48
View File
@@ -2051,6 +2051,54 @@ def args_secret_warning(data: dict) -> str | None:
return None
def secret_arg_indices(args: list) -> list[int]:
"""Indices of args that look like a raw credential.
Same detection as `args_secret_warning`, but per-arg so the "move to
environment variable" action (#83) knows exactly which arg to offer on.
Flags a token-prefixed positional (ghp_..., sk-...), an embedded-credential
URL, or the value following a secret-named flag (`--token abc`). A `${VAR}`
reference is never flagged -- it's the fix, not the problem.
"""
out: list[int] = []
mask_next = False
for i, a in enumerate(str(x) for x in args):
if is_env_ref(a):
mask_next = False
continue
if mask_next:
mask_next = False
if not a.startswith("-"):
out.append(i)
continue
if a.startswith("-") and "=" in a:
continue
if a.startswith("-") and is_secret_key(a):
mask_next = True
continue
if a.startswith("-"):
continue
if _is_secret_value(a) or _EMBEDDED_CRED_RE.search(a):
out.append(i)
return out
def suggested_env_var_for_arg(args: list, index: int) -> str:
"""A default variable name for moving `args[index]` to a reference.
Uses the preceding flag when there is one (`--api-key <secret>` ->
API_KEY), since that names what the value is; otherwise falls back to a
generic SECRET. Always a legal shell name.
"""
if 0 < index <= len(args):
prev = str(args[index - 1]) if index - 1 < len(args) else ""
if prev.startswith("-"):
base = prev.lstrip("-").split("=", 1)[0]
if base:
return sanitize_env_var_name(base)
return "SECRET"
def split_suspicious_args(args: list[str]) -> tuple[list[str], list[str]]:
"""
Detect the classic argument-entry mistake: several argv tokens typed on one