fix(qwen): one credential rule for both paths — seven findings said so
Build & push image / build-and-push (pull_request) Successful in 5s
Build & push image / test (pull_request) Successful in 9m40s

Fourteen findings, and seven of them from all four models are the same one:
endpointProvider was missing the no-cross-vendor-fallback guard I had just
added to resolveModel. I fixed a credential leak on one path and left its
sibling leaking, in the commit whose own message argued those two paths must
move together. That is the third time in this PR.

So it is no longer a rule written twice. openAICompatOptions owns it and both
paths call it; builtinCompatProviders names the vendors that must never inherit
OPENAI_API_KEY, replacing a `provider == "kimi" || provider == "qwen"` literal
that was a fourth uncounted copy of the list.

The test drives a real request at a local server and demands two things: that
no request arrives carrying the OpenAI key, AND that the call fails closed
naming the variable to set — the second half because my first draft pointed the
provider at vendor.example, so the server saw nothing and the assertion held
for a reason unrelated to the fix. Break-checked: removing the guard puts
"Bearer sk-openai-must-not-travel" on the wire to the other vendor.

The scrub check failed open. As a bare condition, a grep ERROR (exit >= 2)
reads as "not found" and skips the guard — a credential check that passes
precisely when it cannot see the filesystem it is searching. It now
distinguishes 0/1/>=2 and refuses to continue on error.

A bare "claude-code" spec has no "/", so the provider fell back to ollama-cloud
and the pre-flight would skip a reviewer that authenticates with
CLAUDE_CODE_OAUTH_TOKEN and needs no Ollama key. Engine specs are now exempt.

preflight.sh's provider list duplicated its own case arms; both now read one
table. And its comment claimed the Go cross-check fails if either list misses
an entry from the other, when only one direction is checked — the reverse is
not even desirable, since ollama-cloud and anthropic belong in that table and
not in the Go one. The comment now says what is enforced.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
2026-08-12 18:09:18 -04:00
co-authored by Claude Opus 5
parent 274451e89c
commit 3af0f09387
6 changed files with 205 additions and 56 deletions
+51 -24
View File
@@ -24,7 +24,15 @@
# 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=""
local provider="$1" model="${2:-}" key_env="" key_hint=""
# Engine specs are not majordomo providers and carry their own auth. A bare
# "claude-code" has no "/" so the caller's provider falls back to
# ollama-cloud, which would skip a reviewer that authenticates with
# CLAUDE_CODE_OAUTH_TOKEN and needs no Ollama key at all.
case "$model" in
claude-code|claude-code/*|opencode/*) echo ""; return 0 ;;
esac
# Only the registry path has knowable credential rules — see above.
# Trim before testing: resolveModel does strings.TrimSpace on GADFLY_BASE_URL,
@@ -54,22 +62,16 @@ gadfly_preflight_key() {
# 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" ;;
qwen) key_env="QWEN_API_KEY" ;;
kimi) key_env="KIMI_API_KEY" ;;
openai|openai-compatible) key_env="OPENAI_API_KEY" ;;
anthropic) key_env="ANTHROPIC_API_KEY" ;;
esac
# The hint is the variable the operator sets, which equals the one the code
# reads everywhere except ollama-cloud (see the note above).
key_hint="$key_env"
[ "$provider" = "ollama-cloud" ] && key_hint="OLLAMA_CLOUD_API_KEY"
if [ -z "$key_env" ]; then
local row
row="$(_gadfly_preflight_table | awk -F: -v p="$provider" '$1 == p {print; exit}')"
if [ -z "$row" ]; then
echo "" # provider needs no pre-flight
return 0
fi
key_env="$(printf '%s' "$row" | cut -d: -f2)"
key_hint="$(printf '%s' "$row" | cut -d: -f3)"
[ -n "$key_hint" ] || key_hint="$key_env"
# 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.
@@ -80,17 +82,42 @@ gadfly_preflight_key() {
echo "$key_hint"
}
# gadfly_preflight_providers echoes every provider this file has a credential
# arm for, one per line.
# _gadfly_preflight_table is the single source for both the credential lookup
# and the provider list: "<provider>:<env-var-read>:<env-var-to-suggest>".
#
# It exists so callers can ASK which providers are covered instead of parsing
# the case statement. A Go test cross-checks this list against the provider
# table in cmd/gadfly/model.go; having it regex this file would make the shell
# formatting a contract no linter enforces, where a reformat breaks a test in
# another language for no visible reason.
# The third field is normally empty, meaning "same as the second". ollama-cloud
# is the exception: run.sh copies the consumer-facing OLLAMA_CLOUD_API_KEY onto
# the OLLAMA_API_KEY the provider reads BEFORE calling in here, so the check and
# the hint name different variables on purpose. If that copy ever moves after
# the call, this arm reports a missing key for a configured run.
#
# Keep in step with the case arms above — the Go test fails if a provider in
# either list is missing from the other.
# A provider absent from this table is absent for one of TWO reasons — do not
# assume the first and add a row:
# 1. It needs no key, or carries one 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); pre-flighting it needs an
# either-variable check, not this table's one-name shape.
_gadfly_preflight_table() {
printf '%s\n' \
'ollama-cloud:OLLAMA_API_KEY:OLLAMA_CLOUD_API_KEY' \
'qwen:QWEN_API_KEY:' \
'kimi:KIMI_API_KEY:' \
'openai:OPENAI_API_KEY:' \
'openai-compatible:OPENAI_API_KEY:' \
'anthropic:ANTHROPIC_API_KEY:'
}
# gadfly_preflight_providers echoes every provider covered above, one per line.
# Callers ASK rather than parse: a Go test cross-checks this against the
# openai-compat provider table in cmd/gadfly/model.go, and regexing this file
# would make its formatting a contract no linter enforces.
#
# The cross-check runs ONE direction — every openai-compat provider in Go must
# appear here. The reverse is not required and must not be asserted:
# ollama-cloud and anthropic belong in this table and are deliberately not in
# that Go list.
gadfly_preflight_providers() {
printf '%s\n' ollama-cloud qwen kimi openai openai-compatible anthropic
_gadfly_preflight_table | cut -d: -f1
}
+12 -2
View File
@@ -25,11 +25,11 @@ check() { # description, want, got
# under the same shell options production uses (set -u), so an unset-variable
# bug surfaces here instead of in a live review.
probe() {
local provider="$1"; shift
local provider="$1" model="${GADFLY_TEST_MODEL:-}"; shift
env -i PATH="$PATH" HOME="$HOME" "$@" bash -c "
set -u
. '$SCRIPT_DIR/preflight.sh'
gadfly_preflight_key '$provider'
gadfly_preflight_key '$provider' '$model'
"
}
@@ -84,6 +84,16 @@ echo "== a whitespace-only GADFLY_BASE_URL counts as unset, as it does in Go =="
# disagreed, the missing key would arrive as a bare 401 with no skip notice.
check "qwen + blank BASE_URL" "QWEN_API_KEY" "$(probe qwen GADFLY_BASE_URL=" ")"
echo "== engine specs carry their own auth and are never pre-flighted =="
# A bare "claude-code" has no "/", so the caller's provider falls back to
# ollama-cloud; judging it by that would skip a reviewer using
# CLAUDE_CODE_OAUTH_TOKEN, which needs no Ollama key.
check "bare claude-code, no ollama key" "" "$(GADFLY_TEST_MODEL=claude-code probe ollama-cloud)"
check "claude-code/opus, no ollama key" "" "$(GADFLY_TEST_MODEL=claude-code/opus probe ollama-cloud)"
check "opencode/x, no ollama key" "" "$(GADFLY_TEST_MODEL=opencode/x probe ollama-cloud)"
# ...but a genuine ollama-cloud model still is.
check "ollama-cloud model, no key" "OLLAMA_CLOUD_API_KEY" "$(GADFLY_TEST_MODEL=glm-5.2:cloud probe ollama-cloud)"
if [ "$fail" -ne 0 ]; then
echo "RESULT: preflight table FAILED"
exit 1
+3 -1
View File
@@ -168,7 +168,9 @@ case "$PROVIDER" in
GADFLY_PROVIDER_EFF="$MODEL_PROVIDER"
# Credential pre-flight — one definition, shared with preflight_test.sh.
MISSING_KEY="$(gadfly_preflight_key "$GADFLY_PROVIDER_EFF")"
# Pass the raw spec too: engine specs (claude-code/opencode) carry their
# own auth and must not be judged by the provider fallback.
MISSING_KEY="$(gadfly_preflight_key "$GADFLY_PROVIDER_EFF" "$MODEL")"
if [ -n "$MISSING_KEY" ]; then
REVIEW="⚠️ No API key configured for provider \`${GADFLY_PROVIDER_EFF}\` (set \`${MISSING_KEY}\`); this reviewer was skipped."
else