feat(#83): variables readout, two clear move actions, args->env relocation
CI / Lint (ruff) (pull_request) Successful in 11s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 30s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 19s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 20s
CI / Catalog signature (pull_request) Successful in 13s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 18s
CI / Lint (ruff) (pull_request) Successful in 11s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 30s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 19s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 20s
CI / Catalog signature (pull_request) Successful in 13s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 18s
Field testing showed the feature was doing the right thing but describing it
wrong, and left the moved variable invisible. Reworked per that feedback:
Naming. "Move to environment variable" read like "move into the Environment
variables table"; it actually creates a ${VAR} reference to the shell/OS
environment. Renamed the action to "Replace with a ${VAR} reference (out of
file)…" everywhere, and the dialog now says plainly the secret goes to your
shell environment -- not this file, not the table below.
Visibility (the real gap). A lone ${SECRET_KEY} with nothing saying whether
it's wired up isn't much better than a mystery. New Variables… button opens
ReferencedVarsDialog: every ${VAR} the loaded server references, each with
✓ set / ✓ default / ✗ not set and the exact export/setx line to set it.
Backed by pure core.referenced_env_vars (dedupes across fields, resolves
against a given environment or a default).
Two actions on an args secret, because the user may want either:
* "Replace with a ${VAR} reference (out of file)…" -- secret leaves the
file (needs a client that expands refs; disabled with reason on Desktop).
* "Move into Environment variables (kept in this config)…" -- relocates the
arg into the env block where it's visible and editable. Works on any
client (no ${VAR} needed). core.move_arg_to_env_block drops the flag+value
and sets env[VAR]; the dialog warns it changes how the server launches.
Both args actions run through ServerEditor (moving into env touches args AND
the env table), which reloads the form from the transformed data.
Tests: +10 core (referenced_env_vars dedupe/status/default; move_arg_to_env_block
flag+value removal, bare positional, None on bad target). 488 passed, ruff
clean. New dialogs/menus are GUI, untestable in CI as before.
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:
+82
@@ -1960,6 +1960,88 @@ def is_env_var_set(var_name: str, environ: dict | None = None) -> bool:
|
||||
return bool(env.get(var_name))
|
||||
|
||||
|
||||
class EnvVarUsage(NamedTuple):
|
||||
"""One ${VAR} referenced by a server definition, and whether it resolves.
|
||||
|
||||
`resolved` is best-effort: a variable is considered resolvable if it's set
|
||||
in the checked environment OR any occurrence carries a `:-default`. Since
|
||||
BCC's environment isn't necessarily the client's, this is advisory (the UI
|
||||
says so).
|
||||
"""
|
||||
|
||||
name: str
|
||||
fields: tuple[str, ...] # which config fields it appears in (sorted)
|
||||
has_default: bool
|
||||
resolved: bool
|
||||
|
||||
|
||||
def referenced_env_vars(data: dict, environ: dict | None = None) -> list[EnvVarUsage]:
|
||||
"""Every distinct ${VAR} a server definition references, with its status.
|
||||
|
||||
This is the "where did my secret go / is it wired up?" readout for #83:
|
||||
after a value becomes `${VAR}`, the variable lives in the user's
|
||||
environment, not the config, so BCC surfaces the list and whether each one
|
||||
currently resolves. Sorted by name; deduped across fields.
|
||||
"""
|
||||
env = os.environ if environ is None else environ
|
||||
by_name: dict[str, dict] = {}
|
||||
for ref in server_env_refs(data):
|
||||
slot = by_name.setdefault(ref.name, {"fields": set(), "has_default": False})
|
||||
slot["fields"].add(ref.field)
|
||||
slot["has_default"] = slot["has_default"] or ref.has_default
|
||||
out: list[EnvVarUsage] = []
|
||||
for name in sorted(by_name):
|
||||
slot = by_name[name]
|
||||
has_default = slot["has_default"]
|
||||
out.append(
|
||||
EnvVarUsage(
|
||||
name=name,
|
||||
fields=tuple(sorted(slot["fields"])),
|
||||
has_default=has_default,
|
||||
resolved=has_default or name in env,
|
||||
)
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def move_arg_to_env_block(data: dict, index: int, var_name=None) -> dict | None:
|
||||
"""Relocate one secret arg into the `env` block, keeping the value in-file.
|
||||
|
||||
The "managed in-file" alternative to a ${VAR} reference: the secret leaves
|
||||
the args (where it's visible in process listings) and becomes an env entry
|
||||
the user can see and edit in BCC's table. Returns a NEW data dict, or None
|
||||
if the target isn't a movable string.
|
||||
|
||||
When the arg follows a `--flag`, the flag is removed too, since a server
|
||||
that reads the secret from an env var no longer needs the switch. This
|
||||
changes how the server is launched -- the GUI warns before doing it.
|
||||
"""
|
||||
new = copy.deepcopy(data)
|
||||
args = new.get("args")
|
||||
if not isinstance(args, list) or not (0 <= index < len(args)):
|
||||
return None
|
||||
value = args[index]
|
||||
if not isinstance(value, str) or is_env_ref(value):
|
||||
return None
|
||||
|
||||
vn = sanitize_env_var_name(
|
||||
var_name if var_name is not None else suggested_env_var_for_arg(args, index)
|
||||
)
|
||||
|
||||
# Drop the value, and the preceding flag if there is one (--api-key SECRET).
|
||||
remove_from = index
|
||||
if index > 0 and isinstance(args[index - 1], str) and args[index - 1].startswith("-"):
|
||||
remove_from = index - 1
|
||||
del args[remove_from : index + 1]
|
||||
|
||||
env = new.get("env")
|
||||
if not isinstance(env, dict):
|
||||
env = {}
|
||||
new["env"] = env
|
||||
env[vn] = value
|
||||
return new
|
||||
|
||||
|
||||
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 ""))
|
||||
|
||||
Reference in New Issue
Block a user