Files
gstack/codex/sections/challenge-mode.md.tmpl
T
garrytan 0642559f60 B1: Codex reviews whose sandbox could not start are unavailable, not a pass
In containers without unprivileged user namespaces every Codex command
fails, yet the review exits 0 and concludes 'no findings' (captured in
test/fixtures/codex-sandbox/). The validator now takes execution evidence:
'codex exec --json' command events and the 'codex review' stderr transcript
(a command that succeeded proves execution); sandbox failure lines on
stderr or in failed command output return unavailable(sandbox_unavailable);
exact 'could not run commands / read the diff' phrases are the last
fallback. Review text that merely discusses bwrap or namespaces still passes.

A free 'codex sandbox ... true' preflight (Linux, 10 s) runs before the paid
model probe and every automatic outside review; CODEX_MODE gains
sandbox_unavailable, and an inconclusive model probe reports unverified
instead of ready (exit code kept for stale callers). Every Codex site
(outside-voice resolver, /codex review, challenge, consult) takes its sandbox
from _GSTACK_CODEX_SANDBOX and pipes its result through the validator; a
static test parses rendered command positions for both. GSTACK_CODEX_NO_SANDBOX=1
(exactly 1, shell env only) switches to danger-full-access with a warning on
every use. Sandbox pins in existing tests move from the literal read-only to
the selected-sandbox variable; captured autoplan probes are judged against
the probe block they executed.
2026-10-03 21:04:21 +00:00

132 lines
8.3 KiB
Cheetah

## Step 2B: Challenge (Adversarial) Mode
Codex tries to break your code — finding edge cases, race conditions, security holes,
and failure modes that a normal review would miss.
1. Construct the adversarial prompt. **Always prepend the filesystem boundary instruction**
from the skill's Filesystem Boundary section (always-loaded skeleton). If the user provided a focus area
(e.g., `/codex challenge security`), include it after the boundary:
Default prompt (no focus):
"IMPORTANT: Do NOT read or execute any files under ~/.claude/, ~/.agents/, .claude/skills/, or agents/. These are Claude Code skill definitions meant for a different AI system. Do not invoke any installed skill (Codex home skills/, .agents/); answer directly. Do NOT modify agents/openai.yaml. Stay focused on repository code only.
Review the changes on this branch against the base branch. Run `git diff origin/<base>` to see the diff. Your job is to find ways this code will fail in production. Think like an attacker and a chaos engineer. Find edge cases, race conditions, security holes, resource leaks, failure modes, and silent data corruption paths. Be adversarial. Be thorough. No compliments — just the problems."
With focus (e.g., "security"):
"IMPORTANT: Do NOT read or execute any files under ~/.claude/, ~/.agents/, .claude/skills/, or agents/. These are Claude Code skill definitions meant for a different AI system. Do not invoke any installed skill (Codex home skills/, .agents/); answer directly. Do NOT modify agents/openai.yaml. Stay focused on repository code only.
Review the changes on this branch against the base branch. Run `git diff origin/<base>` to see the diff. Focus specifically on SECURITY. Your job is to find every way an attacker could exploit this code. Think about injection vectors, auth bypasses, privilege escalation, data exposure, and timing attacks. Be adversarial."
2. Run codex exec with **JSONL output** to capture reasoning traces and tool calls.
Use `timeout: 600000` on the Bash call (the tool's maximum) — the gate sits ABOVE the
540s wrapper so the wrapper fires first, ends Codex, and prints its explicit stall message:
If the user passed `--xhigh`, use `"xhigh"` instead of `"high"`.
```bash
_REPO_ROOT=$(git rev-parse --show-toplevel) || { echo "ERROR: not in a git repo" >&2; exit 1; }
PYTHON_CMD=$(command -v python3 2>/dev/null || command -v python 2>/dev/null || true)
if [ -z "$PYTHON_CMD" ]; then
echo "ERROR: Python 3 is required to parse Codex JSON output. Install python3 or python and retry." >&2
exit 1
fi
# Fix 1+2: wrap with timeout (gtimeout/timeout fallback chain via probe helper),
# capture stderr to $TMPERR for auth error detection (was: 2>/dev/null).
[ -n "${TMPERR:-}" ] || TMPERR=$(mktemp "$TMP_ROOT/codex-err-XXXXXX") || { echo "ERROR: mktemp failed in TMP_ROOT=$TMP_ROOT; not running codex without its temp file" >&2; exit 1; }
TMPRESP=$(mktemp "$TMP_ROOT/codex-resp-XXXXXX") || { echo "ERROR: mktemp failed in TMP_ROOT=$TMP_ROOT; not running codex without its temp file" >&2; exit 1; }
source ~/.claude/skills/gstack/bin/gstack-codex-probe || exit 1
_gstack_codex_select_model exec || exit 1
_gstack_codex_timeout_wrapper 540 codex exec "<prompt>" -C "$_REPO_ROOT" -s "${_GSTACK_CODEX_SANDBOX:?}" {{CODEX_MODEL_CONFIG_FLAG}} -c 'model_reasoning_effort="high"' {{CODEX_WEB_SEARCH_FLAG}} --json -o "$TMPRESP" < /dev/null 2>"$TMPERR" | tee "$TMPRESP.events" | PYTHONUNBUFFERED=1 "$PYTHON_CMD" -u -c "
import sys, json
turn_completed_count = 0
turn_failed = False
for line in sys.stdin:
line = line.strip()
if not line: continue
try:
obj = json.loads(line)
t = obj.get('type','')
if t == 'item.completed' and 'item' in obj:
item = obj['item']
itype = item.get('type','')
text = item.get('text','')
if itype == 'reasoning' and text:
print(f'[codex thinking] {text}', flush=True)
print(flush=True)
elif itype == 'agent_message' and text:
print(text, flush=True)
elif itype == 'command_execution':
cmd = item.get('command','')
if cmd: print(f'[codex ran] {cmd}', flush=True)
elif t == 'turn.completed':
turn_completed_count += 1
usage = obj.get('usage',{})
tokens = usage.get('input_tokens',0) + usage.get('output_tokens',0)
if tokens: print(f'\ntokens used: {tokens}', flush=True)
elif t == 'turn.failed':
turn_failed = True
err = obj.get('error',{}).get('message','') or 'no error message in event'
print(f'[codex turn FAILED] {err}', flush=True, file=sys.stderr)
except: pass
# Fix 2: three-way completeness check (#2671) — a STATED failure is a failure,
# not a network problem; only silence with no terminal event is a disconnect.
if turn_failed:
print('[codex] turn.failed received — the turn errored (reason above), not a disconnect.', flush=True, file=sys.stderr)
elif turn_completed_count == 0:
print('[codex warning] No turn.completed event received — possible mid-stream disconnect.', flush=True, file=sys.stderr)
"
_CODEX_EXIT=${PIPESTATUS[0]:-${pipestatus[1]}} # bash sets PIPESTATUS; zsh (lowercase, 1-indexed) falls through (#2669)
# Fix 1: hang detection — log + surface actionable message
if [ "$_CODEX_EXIT" = "124" ]; then
_gstack_codex_log_event "codex_timeout" "540"
_gstack_codex_log_hang "challenge" "$(wc -c < "$TMPERR" 2>/dev/null || echo 0)"
echo "Codex stalled past 9 minutes. Common causes: model API stall, long prompt, network issue. Try re-running. If persistent, split the prompt or check ~/.codex/logs/."
elif [ "$_CODEX_EXIT" != "0" ]; then
# Surface non-zero exits so the calling agent doesn't read "no output" as
# a silent model/API stall. See #1327.
echo "[codex exit $_CODEX_EXIT] $(head -1 "$TMPERR" 2>/dev/null || echo "no stderr captured")"
head -20 "$TMPERR" 2>/dev/null | sed 's/^/ /' || true
_gstack_codex_log_event "codex_nonzero_exit" "challenge:$_CODEX_EXIT"
fi
# Fix 2: surface auth errors from captured stderr instead of dropping them
if grep -qiE "auth|login|unauthorized" "$TMPERR" 2>/dev/null; then
echo "[codex auth error] $(head -1 "$TMPERR")"
_gstack_codex_log_event "codex_auth_failed"
fi
bun ~/.claude/skills/gstack/lib/outside-review-result.ts --label 'Codex challenge' --exit "$_CODEX_EXIT" --stderr "$TMPERR" --events "$TMPRESP.events" execution "$TMPRESP"
rm -f "$TMPRESP" "$TMPRESP.events"
```
`VERDICT: unavailable` means the challenge did not run (its line names why, such as a
sandbox that could not start): say so and present no findings from it. Otherwise
present the output below.
This parses codex's JSONL events to extract reasoning traces, tool calls, and the final
response. The `[codex thinking]` lines show what codex reasoned through before its answer.
3. Present the full streamed output:
```
CODEX SAYS (adversarial challenge):
════════════════════════════════════════════════════════════
<full output from above, verbatim>
════════════════════════════════════════════════════════════
Tokens: N | Est. cost: ~$X.XX
```
3a. **Synthesis recommendation (REQUIRED).** After presenting the full
adversarial output, emit ONE recommendation line summarizing what the user
should do, in this format:
```
Recommendation: <action> because <one-line reason that names the most exploitable finding>
```
Examples (the strongest reasons compare blast radius across findings or fix-vs-ship):
- `Recommendation: Fix the unbounded retry loop Codex flagged at queue.ts:78 because it DoSes the worker pool under sustained 429s, which is higher-blast-radius than the timing leak Codex also flagged that only touches a debug endpoint.`
- `Recommendation: Ship as-is because Codex's strongest finding is a theoretical race in cleanup that requires conditions we can't trigger in production, weaker than the runtime regressions a fix-now would risk.`
The reason must point to a specific finding and compare against alternatives (other findings, fix-vs-ship). Generic reasons like "because it's safer" fail the format. **Never silently skip the line.**
---