B3: Codex auth probe accepts custom providers' env_key credentials

The auth probe only knew CODEX_API_KEY, OPENAI_API_KEY and auth.json, so a
working self-hosted or OpenAI-compatible provider configured with
[model_providers.<id>] env_key = "X" reported AUTH_FAILED and /codex
stopped. The probe now reads env_key from model_providers tables only
(comments ignored, the name must be a shell identifier before it is
dereferenced) and accepts a set variable; when none is set it names the
missing key on stderr. Ported from #2192 with the table scoping added.

Co-authored-by: hardy <932821449@qq.com>
This commit is contained in:
garrytanandhardy committed 2026-10-03 21:06:20 +00:00
1 parent ee70becad5
commit ebab87bb11
2 files changed
+60 -2

No files matched your search

+22 -2
View File
@@ -21,8 +21,9 @@
# --- Auth probe -------------------------------------------------------------
_gstack_codex_auth_probe() {
# Multi-signal: env vars OR auth file. Avoids false negatives for env-auth
# users (CI, platform engineers) that a file-only check would reject.
# Multi-signal: env vars OR auth file OR a custom provider's env_key. Avoids
# false negatives for env-auth users (CI, platform engineers, self-hosted
# OpenAI-compatible providers) that a file-only check would reject.
local _codex_home="${CODEX_HOME:-$HOME/.codex}"
# Use `-n` which returns true only for non-empty non-whitespace. Bash's [ -n ]
# alone allows whitespace; pair with a whitespace strip for robustness.
@@ -33,7 +34,26 @@ _gstack_codex_auth_probe() {
echo "AUTH_OK"
return 0
fi
# Custom OpenAI-compatible providers (#2192): a [model_providers.<id>]
# table names its credential variable in env_key. Only keys inside those
# tables count, comments are ignored, and the name must be a plain shell
# identifier before it is dereferenced.
local _env_key _resolved _missing=""
if [ -f "$_codex_home/config.toml" ]; then
while IFS= read -r _env_key; do
case "$_env_key" in ''|[0-9]*|*[!A-Za-z0-9_]*) continue ;; esac
_resolved=$(printf '%s' "${!_env_key:-}" | tr -d '[:space:]')
if [ -n "$_resolved" ]; then
echo "AUTH_OK"
return 0
fi
_missing="${_missing:+$_missing, }$_env_key"
done < <(awk '/^[[:space:]]*#/ { next }
/^[[:space:]]*\[/ { in_provider = ($0 ~ /^[[:space:]]*\[model_providers\.[^]]+\][[:space:]]*(#.*)?$/); next }
in_provider && /^[[:space:]]*env_key[[:space:]]*=/ { v = $0; sub(/^[^=]*=[[:space:]]*/, "", v); sub(/[[:space:]]*#.*$/, "", v); gsub(/["\047[:space:]]/, "", v); print v }' "$_codex_home/config.toml" 2>/dev/null)
fi
echo "AUTH_FAILED"
[ -n "$_missing" ] && echo "HINT: $_codex_home/config.toml names a custom provider key ($_missing), but it is not set in this shell. Export it, or run codex login." >&2
return 1
}
+38
View File
@@ -51,6 +51,44 @@ function tempHome(): string {
}
describe('gstack-codex-probe: auth probe', () => {
// #2192: custom OpenAI-compatible providers name their credential variable
// in config.toml [model_providers.<id>] env_key; a set variable is auth.
const withConfig = (toml: string, env: Record<string, string | undefined> = {}) => {
const home = tempHome();
try {
fs.mkdirSync(path.join(home, '.codex'), { recursive: true });
fs.writeFileSync(path.join(home, '.codex', 'config.toml'), toml);
return runProbe({ snippet: '_gstack_codex_auth_probe', env, home });
} finally { fs.rmSync(home, { recursive: true, force: true }); }
};
const MIMO = '[model_providers.mimo]\nenv_key = "MIMO_API_KEY"\n';
test('#2192: a provider env_key whose variable is set → AUTH_OK', () => {
const r = withConfig(MIMO, { MIMO_API_KEY: 'tp-test-key' });
expect([r.stdout.trim(), r.status]).toEqual(['AUTH_OK', 0]);
const several = withConfig('[model_providers.a]\nenv_key = "MISSING_A"\n\n[model_providers.b]\nenv_key = \'MIMO_API_KEY\' # inline comment\n', { MIMO_API_KEY: 'k' });
expect(several.stdout.trim()).toBe('AUTH_OK');
});
test('#2192: a provider env_key that is unset or blank → AUTH_FAILED naming the key', () => {
for (const value of [undefined, ' \t\n']) {
const r = withConfig(MIMO, { MIMO_API_KEY: value });
expect([r.stdout.trim(), r.status]).toEqual(['AUTH_FAILED', 1]);
expect(r.stderr).toContain('custom provider key (MIMO_API_KEY)');
}
});
test('#2192: commented env_key lines and env_key outside [model_providers.*] never count', () => {
const toml = '[model_providers.mimo]\n# env_key = "DEPRECATED_KEY"\nenv_key = "MIMO_API_KEY"\n[profiles.work]\nenv_key = "PROFILE_KEY"\n';
expect(withConfig(toml, { DEPRECATED_KEY: 'old', PROFILE_KEY: 'p' }).stdout.trim()).toBe('AUTH_FAILED');
});
test('#2192: malformed config.toml or a non-identifier env_key → AUTH_FAILED, no crash', () => {
expect(withConfig('\x00\x01 garbage \xff[broken\n=no=\n').stdout.trim()).toBe('AUTH_FAILED');
const injected = withConfig('[model_providers.x]\nenv_key = "A;touch /tmp/never"\n');
expect([injected.stdout.trim(), injected.status]).toEqual(['AUTH_FAILED', 1]);
});
test('CODEX_API_KEY set → AUTH_OK', () => {
const home = tempHome();
try {