Two findings this round contradicted each other — one asked me to extend the engine-spec exemption to a bare "opencode", the other said opencode should not be exempt at all. The code settles it: that engine drives an ollama-cloud model through the bundled CLI and authenticates with OLLAMA_API_KEY, so it needs exactly the key the pre-flight checks. Exempting it, which I did last round, switched the check off for the one engine it could still help. Only claude-code is exempt now — it carries CLAUDE_CODE_OAUTH_TOKEN and needs no Ollama key — and opencode/open-code get table rows so both spellings are covered. The README told operators to embed the Qwen key in a GADFLY_ENDPOINT_* var, while the workflow that forwards those vars warns in its own comments that vars are NOT masked. Rather than only rewording the docs, a keyless kimi/qwen endpoint now falls back to its own QWEN_API_KEY / KIMI_API_KEY — the same vendor's key, so the no-cross-vendor rule is untouched — which lets the URL live in a var and the credential in a secret. Break-checked by pointing that fallback at OPENAI_API_KEY: the leak test catches it. Smaller: isBuiltinCompatProvider mirrors isOpenAICompatProvider instead of an inline slices.Contains, with a test that every builtin is also in the compat list (a builtin missing from it would never reach the branch that protects it); the preflight.sh rationale is stated once rather than in two comment blocks; the Go test locates the shell script relative to its own source file; and the gofmt step takes GOPROXY=off like its neighbours. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
122 lines
5.8 KiB
Bash
Executable File
122 lines
5.8 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=""
|
|
|
|
# claude-code carries its OWN auth (CLAUDE_CODE_OAUTH_TOKEN, else
|
|
# ANTHROPIC_API_KEY) and needs no Ollama key. A bare "claude-code" has no "/",
|
|
# so the caller's provider falls back to ollama-cloud and the table below
|
|
# would skip a perfectly configured reviewer.
|
|
#
|
|
# opencode is deliberately NOT exempt: that engine drives an ollama-cloud
|
|
# model through the bundled CLI and authenticates with OLLAMA_API_KEY, so it
|
|
# needs exactly the key the table checks. Exempting it — which an earlier
|
|
# version of this guard did — turns the pre-flight off for the one engine
|
|
# whose missing key it could still catch.
|
|
case "$model" in
|
|
claude-code|claude-code/*) 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
|
|
|
|
# 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' \
|
|
'opencode:OLLAMA_API_KEY:OLLAMA_CLOUD_API_KEY' \
|
|
'open-code: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
|
|
}
|