This commit is contained in:
hsuperman 2026-07-14 18:28:35 -07:00 committed by GitHub
commit c6a9558c43
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 169 additions and 12 deletions

View File

@ -1123,7 +1123,7 @@ elif ! command -v codex >/dev/null 2>&1; then
_CODEX_AVAILABLE=false
elif ! _gstack_codex_auth_probe >/dev/null; then
_gstack_codex_log_event "codex_auth_failed"
echo "[codex-unavailable: auth missing] — proceeding with Claude subagent only. Run \`codex login\` or set \$CODEX_API_KEY to enable dual-voice review."
echo "[codex-unavailable: auth missing] — proceeding with Claude subagent only. Run \`codex login\`, set \$CODEX_API_KEY / \$OPENAI_API_KEY, or export the env var named by ~/.codex/config.toml env_key to enable dual-voice review."
_CODEX_AVAILABLE=false
else
_gstack_codex_version_check # non-blocking warn if known-bad

View File

@ -259,7 +259,7 @@ elif ! command -v codex >/dev/null 2>&1; then
_CODEX_AVAILABLE=false
elif ! _gstack_codex_auth_probe >/dev/null; then
_gstack_codex_log_event "codex_auth_failed"
echo "[codex-unavailable: auth missing] — proceeding with Claude subagent only. Run \`codex login\` or set \$CODEX_API_KEY to enable dual-voice review."
echo "[codex-unavailable: auth missing] — proceeding with Claude subagent only. Run \`codex login\`, set \$CODEX_API_KEY / \$OPENAI_API_KEY, or export the env var named by ~/.codex/config.toml env_key to enable dual-voice review."
_CODEX_AVAILABLE=false
else
_gstack_codex_version_check # non-blocking warn if known-bad

View File

@ -16,16 +16,97 @@
# --- Auth probe -------------------------------------------------------------
_gstack_codex_trim_env_value() {
local _value="$1"
_value=$(printf '%s' "$_value" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')
case "$_value" in
\"*\") _value="${_value#\"}"; _value="${_value%\"}" ;;
\'*\') _value="${_value#\'}"; _value="${_value%\'}" ;;
esac
printf '%s' "$_value"
}
_gstack_codex_read_env_file_key() {
local _key="$1"
local _repo_top _file _line _value
case "$_key" in
''|[0-9]*|*[!A-Za-z0-9_]*)
return 1
;;
esac
_repo_top=$(git rev-parse --show-toplevel 2>/dev/null || echo "")
for _file in ".env" ".env.local" "$_repo_top/.env" "$_repo_top/.env.local" "$HOME/.config/recruitmagic/cli.env"; do
[ -n "$_file" ] || continue
[ -f "$_file" ] || continue
_line=$(grep -E "^[[:space:]]*(export[[:space:]]+)?${_key}=" "$_file" 2>/dev/null | tail -1)
[ -n "$_line" ] || continue
_value="${_line#*=}"
_value="${_value%%#*}"
_value=$(_gstack_codex_trim_env_value "$_value")
if [ -n "$(printf '%s' "$_value" | tr -d '[:space:]')" ]; then
printf '%s' "$_value"
return 0
fi
done
return 1
}
_gstack_codex_config_env_key() {
local _codex_home="$1"
local _config="$_codex_home/config.toml"
[ -f "$_config" ] || return 1
sed -n -E 's/^[[:space:]]*env_key[[:space:]]*=[[:space:]]*["'\'']?([A-Za-z_][A-Za-z0-9_]*)["'\'']?.*$/\1/p' "$_config" 2>/dev/null | head -1
}
_gstack_codex_ensure_env_key() {
local _key="$1"
local _current _from_file
case "$_key" in
''|[0-9]*|*[!A-Za-z0-9_]*)
return 1
;;
esac
eval "_current=\${$_key:-}"
if [ -n "$(printf '%s' "$_current" | tr -d '[:space:]')" ]; then
return 0
fi
_from_file=$(_gstack_codex_read_env_file_key "$_key" 2>/dev/null || echo "")
if [ -n "$(printf '%s' "$_from_file" | tr -d '[:space:]')" ]; then
export "$_key=$_from_file"
return 0
fi
return 1
}
_gstack_codex_load_auth_env() {
local _codex_home="${CODEX_HOME:-$HOME/.codex}"
local _configured_key
_gstack_codex_ensure_env_key "CODEX_API_KEY" >/dev/null 2>&1 || true
_gstack_codex_ensure_env_key "OPENAI_API_KEY" >/dev/null 2>&1 || true
_configured_key=$(_gstack_codex_config_env_key "$_codex_home" 2>/dev/null || echo "")
if [ -n "$_configured_key" ]; then
_gstack_codex_ensure_env_key "$_configured_key" >/dev/null 2>&1 || true
fi
}
_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.
local _codex_home="${CODEX_HOME:-$HOME/.codex}"
local _configured_key _k3
_gstack_codex_load_auth_env
_configured_key=$(_gstack_codex_config_env_key "$_codex_home" 2>/dev/null || echo "")
# Use `-n` which returns true only for non-empty non-whitespace. Bash's [ -n ]
# alone allows whitespace; pair with a whitespace strip for robustness.
local _k1 _k2
_k1=$(printf '%s' "${CODEX_API_KEY:-}" | tr -d '[:space:]')
_k2=$(printf '%s' "${OPENAI_API_KEY:-}" | tr -d '[:space:]')
if [ -n "$_k1" ] || [ -n "$_k2" ] || [ -f "$_codex_home/auth.json" ]; then
_k3=""
if [ -n "$_configured_key" ]; then
eval "_k3=\${$_configured_key:-}"
_k3=$(printf '%s' "$_k3" | tr -d '[:space:]')
fi
if [ -n "$_k1" ] || [ -n "$_k2" ] || [ -n "$_k3" ] || [ -f "$_codex_home/auth.json" ]; then
echo "AUTH_OK"
return 0
fi
@ -58,6 +139,7 @@ _gstack_codex_timeout_wrapper() {
local _duration="$1"
shift
local _to
_gstack_codex_load_auth_env
_to=$(command -v gtimeout 2>/dev/null || command -v timeout 2>/dev/null || echo "")
if [ -n "$_to" ]; then
"$_to" "$_duration" "$@"

View File

@ -884,14 +884,17 @@ _gstack_codex_version_check # warns if known-bad, non-blocking
```
If the output contains `AUTH_FAILED`, stop and tell the user:
"No Codex authentication found. Run `codex login` or set `$CODEX_API_KEY` / `$OPENAI_API_KEY`, then re-run this skill."
"No Codex authentication found. Run `codex login`, set `$CODEX_API_KEY` / `$OPENAI_API_KEY`, or export the env var named by `~/.codex/config.toml` `env_key`, then re-run this skill."
If the version check printed a `WARN:` line, pass it through to the user verbatim
(non-blocking — Codex may still work, but the user should upgrade).
The probe multi-signal auth logic accepts: `$CODEX_API_KEY` set, `$OPENAI_API_KEY`
set, or `${CODEX_HOME:-~/.codex}/auth.json` exists. Avoids false-negatives for
env-auth users (CI, platform engineers) that file-only checks would reject.
set, the custom provider `env_key` from `${CODEX_HOME:-~/.codex}/config.toml`,
or `${CODEX_HOME:-~/.codex}/auth.json` exists. It also loads simple key/value
entries from `.env`, `.env.local`, and `~/.config/recruitmagic/cli.env` for
worktree automation. Avoids false-negatives for env-auth users (CI, platform
engineers) that file-only checks would reject.
**Update the known-bad list** in `bin/gstack-codex-probe` when a new Codex CLI version
regresses. Current entries (`0.120.0`, `0.120.1`, `0.120.2`) trace to the stdin

View File

@ -75,14 +75,17 @@ _gstack_codex_version_check # warns if known-bad, non-blocking
```
If the output contains `AUTH_FAILED`, stop and tell the user:
"No Codex authentication found. Run `codex login` or set `$CODEX_API_KEY` / `$OPENAI_API_KEY`, then re-run this skill."
"No Codex authentication found. Run `codex login`, set `$CODEX_API_KEY` / `$OPENAI_API_KEY`, or export the env var named by `~/.codex/config.toml` `env_key`, then re-run this skill."
If the version check printed a `WARN:` line, pass it through to the user verbatim
(non-blocking — Codex may still work, but the user should upgrade).
The probe multi-signal auth logic accepts: `$CODEX_API_KEY` set, `$OPENAI_API_KEY`
set, or `${CODEX_HOME:-~/.codex}/auth.json` exists. Avoids false-negatives for
env-auth users (CI, platform engineers) that file-only checks would reject.
set, the custom provider `env_key` from `${CODEX_HOME:-~/.codex}/config.toml`,
or `${CODEX_HOME:-~/.codex}/auth.json` exists. It also loads simple key/value
entries from `.env`, `.env.local`, and `~/.config/recruitmagic/cli.env` for
worktree automation. Avoids false-negatives for env-auth users (CI, platform
engineers) that file-only checks would reject.
**Update the known-bad list** in `bin/gstack-codex-probe` when a new Codex CLI version
regresses. Current entries (`0.120.0`, `0.120.1`, `0.120.2`) trace to the stdin

View File

@ -91,6 +91,70 @@ describe('gstack-codex-probe: auth probe', () => {
}
});
test('configured env_key set → AUTH_OK', () => {
const home = tempHome();
try {
fs.mkdirSync(path.join(home, '.codex'), { recursive: true });
fs.writeFileSync(
path.join(home, '.codex', 'config.toml'),
'model_provider = "azure"\n[model_providers.azure]\nenv_key = "AZURE_OPENAI_API_KEY_RECRUIT_MAGIC_RESOURCE"\n',
);
const r = runProbe({
snippet: '_gstack_codex_auth_probe',
env: { AZURE_OPENAI_API_KEY_RECRUIT_MAGIC_RESOURCE: 'azure-test' },
home,
});
expect(r.stdout.trim()).toBe('AUTH_OK');
expect(r.status).toBe(0);
} finally {
fs.rmSync(home, { recursive: true, force: true });
}
});
test('OPENAI_API_KEY in cwd .env is loaded for worktree automation', () => {
const home = tempHome();
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-codex-env-cwd-'));
try {
fs.writeFileSync(path.join(cwd, '.env'), 'OPENAI_API_KEY=sk-from-env-file\n');
const escapedCwd = cwd.replaceAll('"', '\\"');
const r = runProbe({
snippet: `cd "${escapedCwd}"\n_gstack_codex_auth_probe\nprintf "loaded:%s\\n" "\${OPENAI_API_KEY:-}"`,
home,
});
expect(r.stdout).toContain('AUTH_OK');
expect(r.stdout).toContain('loaded:sk-from-env-file');
expect(r.status).toBe(0);
} finally {
fs.rmSync(home, { recursive: true, force: true });
fs.rmSync(cwd, { recursive: true, force: true });
}
});
test('configured env_key can be loaded from machine-local cli.env', () => {
const home = tempHome();
try {
fs.mkdirSync(path.join(home, '.codex'), { recursive: true });
fs.writeFileSync(
path.join(home, '.codex', 'config.toml'),
'[model_providers.azure]\nenv_key = "AZURE_OPENAI_API_KEY_RECRUIT_MAGIC_RESOURCE"\n',
);
fs.mkdirSync(path.join(home, '.config/recruitmagic'), { recursive: true });
fs.writeFileSync(
path.join(home, '.config/recruitmagic/cli.env'),
'AZURE_OPENAI_API_KEY_RECRUIT_MAGIC_RESOURCE=azure-from-cli-env\n',
);
const r = runProbe({
snippet: '_gstack_codex_auth_probe\nprintf "loaded:%s\\n" "${AZURE_OPENAI_API_KEY_RECRUIT_MAGIC_RESOURCE:-}"',
home,
});
expect(r.stdout).toContain('AUTH_OK');
expect(r.stdout).toContain('loaded:azure-from-cli-env');
expect(r.status).toBe(0);
} finally {
fs.rmSync(home, { recursive: true, force: true });
}
});
test('no env + no file → AUTH_FAILED with exit 1', () => {
const home = tempHome();
try {

View File

@ -99,10 +99,15 @@ describe('generateAskUserFormat — v1.7.0.0 Pros/Cons format', () => {
expect(out).toMatch(/options differ in kind, not coverage/);
});
test('documents tool_use mandate (rule 11)', () => {
test('documents tool_use preference with prose fallback', () => {
expect(out).toMatch(/tool_use/);
// "not a question" spans a newline in the rendered text
expect(out).toMatch(/not a[\s\S]*question|not[\s\S]*interactive/i);
expect(out).toMatch(/prose fallback/i);
expect(out).toMatch(/reply with a letter/i);
});
test('routes interactive hosts without AskUserQuestion tools to prose fallback', () => {
expect(out).toMatch(/interactive.*prose fallback/i);
expect(out).toMatch(/headless.*BLOCKED\s+—\s+AskUserQuestion unavailable/i);
});
test('includes self-check before emitting', () => {