Round 5, and the best findings are again about the fix from round 4. The scrub only ran on success. `set -e` aborts the step when `go mod download` fails, so the cleanup line after it never executed — leaving a push-capable credential on a long-lived self-hosted runner for whatever job landed there next. It is now a `trap ... EXIT`, verified against a simulated failure. It also scrubbed the wrong file in principle: `git config --global` writes to GIT_CONFIG_GLOBAL, else $XDG_CONFIG_HOME/git/config when that exists, else ~/.gitconfig — so deleting ~/.gitconfig can scrub a path the credential was never in. The step now names GIT_CONFIG_GLOBAL itself, leaving exactly one file to remove. And the verification failed open in the case that matters most: `grep -F ""` matches every file, so a run WITHOUT the secret — a fork PR, the threat model — failed the check with a message accusing it of leaking a credential it never had. Guarded on a non-empty secret. Credentials move to an Authorization header instead of being embedded in the URL, so a password containing @ : / or # can no longer break URL parsing in a way that reads as a bad password. Two list-drift holes closed with one test that reads across languages: TestOpenAICompatProvidersAreFullyWired asserts every openAICompatProviders entry is both advertised in endpointProviderNames and has a credential arm in scripts/preflight.sh. Adding a compat provider touches three places in two languages and nothing connected them. Break-checked in both directions. Finally, a whitespace-only GADFLY_BASE_URL disagreed across the boundary: Go TrimSpaces it and takes the registry path, bash called it "set" and skipped the pre-flight, so the missing key arrived as a bare 401 with no notice. Both now agree on what unset means. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
78 lines
3.9 KiB
Bash
Executable File
78 lines
3.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Credential pre-flight for the agentic reviewer, in ONE definition.
|
|
#
|
|
# Sourced by run.sh (production) and by preflight_test.sh (the table test), so
|
|
# the tested bytes and the running bytes are the same. Keep it that way: a test
|
|
# that reimplements this logic can agree with a stale copy of it.
|
|
#
|
|
# Why pre-flight at all, when majordomo already fails closed with a 401:
|
|
# without it a missing key surfaces as five identical per-lens agent failures
|
|
# that name no variable, and the operator reads a stack trace to learn which
|
|
# secret they forgot to forward.
|
|
|
|
# gadfly_preflight_key <provider> -> echoes "" when the run may proceed, or the
|
|
# name of the environment variable the operator must set.
|
|
#
|
|
# Scope: the REGISTRY path only — GADFLY_BASE_URL unset — and deliberately so.
|
|
# The two resolution paths have DIFFERENT credential rules: with an explicit
|
|
# endpoint the credential is GADFLY_API_KEY (falling back to the client's own
|
|
# default, OPENAI_API_KEY for the openai family) and a built-in's own variable
|
|
# is never consulted; without one, the reverse. Applying either path's rule to
|
|
# the other yields a check that passes a run which then 401s — the precise
|
|
# failure this exists to prevent. So it covers the path whose rules it can state
|
|
# exactly and stays silent on the other. That is also the useful half: an
|
|
# override-path config is hand-written, while the registry path is what somebody
|
|
# hits by adding a model id to a var and forgetting the secret.
|
|
gadfly_preflight_key() {
|
|
local provider="$1" key_env="" key_hint=""
|
|
|
|
# Only the registry path has knowable credential rules — see above.
|
|
# Trim before testing: resolveModel does strings.TrimSpace on GADFLY_BASE_URL,
|
|
# so a whitespace-only value takes the REGISTRY path there. Testing the raw
|
|
# value here would call it "set", skip the check, and let the missing key
|
|
# arrive as a 401 with no notice — the two must agree on what "unset" means.
|
|
local base_url
|
|
base_url="$(printf '%s' "${GADFLY_BASE_URL:-}" | tr -d '[:space:]')"
|
|
if [ -n "$base_url" ]; then
|
|
echo ""
|
|
return 0
|
|
fi
|
|
|
|
# A provider is absent from this table for one of TWO different reasons — do
|
|
# not assume the first one and add an arm:
|
|
# 1. It needs no key, or carries it in its endpoint/DSN: local ollama,
|
|
# llama-swap, foreman.
|
|
# 2. It needs a key but accepts more than one variable, so a single-name
|
|
# check would skip a correctly-configured run. **google** is this case:
|
|
# GOOGLE_API_KEY *or* GEMINI_API_KEY. Adding
|
|
# `google) key_env="GOOGLE_API_KEY"` would silently skip every reviewer
|
|
# configured with GEMINI_API_KEY. Pre-flighting google needs an
|
|
# either-variable check, not this table's one-name shape.
|
|
# ollama-cloud is checked on OLLAMA_API_KEY but hinted as OLLAMA_CLOUD_API_KEY:
|
|
# run.sh copies the consumer-facing OLLAMA_CLOUD_API_KEY secret into the
|
|
# OLLAMA_API_KEY the provider reads, BEFORE calling this. The hint names the
|
|
# variable the operator actually sets; the check reads the one the code uses.
|
|
# If that copy ever moves after this call, this arm reports a missing key for
|
|
# a configured run.
|
|
case "$provider" in
|
|
ollama-cloud) key_env="OLLAMA_API_KEY"; key_hint="OLLAMA_CLOUD_API_KEY" ;;
|
|
qwen) key_env="QWEN_API_KEY"; key_hint="QWEN_API_KEY" ;;
|
|
kimi) key_env="KIMI_API_KEY"; key_hint="KIMI_API_KEY" ;;
|
|
openai|openai-compatible) key_env="OPENAI_API_KEY"; key_hint="OPENAI_API_KEY" ;;
|
|
anthropic) key_env="ANTHROPIC_API_KEY"; key_hint="ANTHROPIC_API_KEY" ;;
|
|
esac
|
|
|
|
if [ -z "$key_env" ]; then
|
|
echo "" # provider needs no pre-flight
|
|
return 0
|
|
fi
|
|
# Indirect expansion (bash). Each majordomo built-in reads ONLY its own
|
|
# variable — cross-provider fallback is refused by design — so the named hint
|
|
# is always the actual fix.
|
|
if [ -n "${!key_env:-}" ]; then
|
|
echo ""
|
|
return 0
|
|
fi
|
|
echo "$key_hint"
|
|
}
|