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]>
124 lines
6.0 KiB
Bash
Executable File
124 lines
6.0 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" 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,
|
|
# 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.
|
|
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.
|
|
if [ -n "${!key_env:-}" ]; then
|
|
echo ""
|
|
return 0
|
|
fi
|
|
echo "$key_hint"
|
|
}
|
|
|
|
# _gadfly_preflight_table is the single source for both the credential lookup
|
|
# and the provider list: "<provider>:<env-var-read>:<env-var-to-suggest>".
|
|
#
|
|
# 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.
|
|
#
|
|
# 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() {
|
|
_gadfly_preflight_table | cut -d: -f1
|
|
}
|