mirror of https://github.com/garrytan/gstack.git
Merge 4e74b27b59 into 2beb636f7c
This commit is contained in:
commit
c6a9558c43
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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" "$@"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue