Skip to content

Commit c75266a

Browse files
GuyMosesclaude
andcommitted
test(codex): CI contract for install/uninstall + credential flow
Add Codex Contracts G/H/I to the install-config-contract job, mirroring the Cursor ones (D/E/F) — the Codex install scripts previously had zero CI coverage: G: config-file AND env-var credentials both reach a real OTLP request through codex-on-event.sh. H: install-codex.sh appends the managed block + 10 [hooks.state] pre-trust entries to config.toml, preserving a user-authored hook and setting (asserted with a real TOML parser). I: uninstall-codex.sh strips only the managed block, leaving user content. install-codex.sh now skips the binary/bootstrap download when they're already present — a real idempotency improvement that also lets Contract H pre-stage a locally-built binary and run the merge without a release (Codex has none until M4). Validated locally end-to-end. SIG-171 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 2b8a1cf commit c75266a

2 files changed

Lines changed: 178 additions & 16 deletions

File tree

.github/workflows/ci.yml

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,156 @@ jobs:
391391
[ "$fail" -eq 0 ] || exit 1
392392
echo "PASS: uninstaller stripped Dash0 entries and preserved the user-authored hook"
393393
394+
- name: "Contract G — Codex credential delivery reaches a real OTLP request (config file + env vars)"
395+
run: |
396+
# Mirror of Contract D for the Codex side. Asserts that credentials
397+
# supplied via config file or env vars both reach the wire through
398+
# scripts/codex-on-event.sh. The bootstrap is version-pinned; build the
399+
# binary at that exact path so no release download is needed.
400+
export DASH0_PLUGIN_DATA=/tmp/codex-pdata
401+
VERSION=$(grep '^VERSION=' scripts/codex-on-event.sh | sed 's/VERSION="//;s/"//')
402+
mkdir -p "$DASH0_PLUGIN_DATA/bin"
403+
make build-binary PKG=./cmd/codex-on-event OUT="$DASH0_PLUGIN_DATA/bin/codex-on-event-${VERSION}-linux-amd64"
404+
make build-binary PKG=./test/e2e/mock-otlp-server OUT=/tmp/mock-otlp-codex
405+
/tmp/mock-otlp-codex &
406+
sleep 1
407+
408+
# G1 — credentials from ~/.codex/dash0-agent-plugin.local.md.
409+
export HOME=/tmp/codex-home-cfg
410+
mkdir -p "$HOME/.codex"
411+
cat > "$HOME/.codex/dash0-agent-plugin.local.md" <<'MD'
412+
---
413+
otlp_url: "http://localhost:4319"
414+
auth_token: "codex-cfg-token"
415+
dataset: "codex-cfg-ds"
416+
---
417+
MD
418+
# Clean cwd so the repo's own .codex/ can't shadow the global config
419+
# (codex-on-event.sh checks the project file first).
420+
( cd "$(mktemp -d)" \
421+
&& echo '{"hook_event_name":"SessionStart","session_id":"contract-g1","model":"gpt-5.5","source":"startup"}' \
422+
| bash "$GITHUB_WORKSPACE/scripts/codex-on-event.sh" )
423+
424+
# G2 — credentials from env vars only, no config file present.
425+
export HOME=/tmp/codex-home-env
426+
mkdir -p "$HOME/.codex"
427+
( cd "$(mktemp -d)" \
428+
&& echo '{"hook_event_name":"SessionStart","session_id":"contract-g2","model":"gpt-5.5","source":"startup"}' \
429+
| DASH0_OTLP_URL=http://localhost:4319 \
430+
CODEX_PLUGIN_OPTION_AUTH_TOKEN=codex-env-token \
431+
DASH0_DATASET=codex-env-ds \
432+
bash "$GITHUB_WORKSPACE/scripts/codex-on-event.sh" )
433+
434+
sleep 2
435+
RESULT=$(curl -s http://localhost:4319/requests)
436+
echo "::group::mock requests"; echo "$RESULT" | jq .; echo "::endgroup::"
437+
fail=0
438+
[ "$(echo "$RESULT" | jq '[.requests[]|select(.auth=="Bearer codex-cfg-token")]|length')" -ge 1 ] \
439+
|| { echo "::error::codex config-file token did not reach the OTLP request"; fail=1; }
440+
[ "$(echo "$RESULT" | jq '[.requests[]|select(.auth=="Bearer codex-env-token")]|length')" -ge 1 ] \
441+
|| { echo "::error::codex env-var token did not reach the OTLP request"; fail=1; }
442+
[ "$fail" -eq 0 ] || exit 1
443+
echo "PASS: both config-file and env-var credentials flow through codex-on-event.sh to real OTLP requests"
444+
445+
- name: "Contract H — install-codex.sh merges hooks + pre-trust into config.toml, preserving user content"
446+
run: |
447+
# Codex has no release yet (M4), so pre-stage the version-pinned binary
448+
# and bootstrap; install-codex.sh skips the download when they're
449+
# present. This exercises the real merge: managed block appended,
450+
# [hooks.state] trust entries written, and any user-authored hooks /
451+
# settings preserved. DASH0_VERSION pins the path so no release lookup.
452+
export HOME=/tmp/codex-installer-home
453+
export XDG_STATE_HOME=/tmp/codex-installer-state
454+
rm -rf "$HOME" "$XDG_STATE_HOME"
455+
VERSION=$(grep '^VERSION=' scripts/codex-on-event.sh | sed 's/VERSION="//;s/"//')
456+
STATE_BASE="$XDG_STATE_HOME/dash0-agent-plugin/codex"
457+
mkdir -p "$STATE_BASE/bin" "$HOME/.codex"
458+
make build-binary PKG=./cmd/codex-on-event OUT="$STATE_BASE/bin/codex-on-event-${VERSION}-linux-amd64"
459+
cp scripts/codex-on-event.sh "$STATE_BASE/codex-on-event.sh"
460+
461+
make build-binary PKG=./test/e2e/mock-otlp-server OUT=/tmp/mock-otlp-codex-h
462+
/tmp/mock-otlp-codex-h &
463+
sleep 1
464+
465+
# Seed config.toml with an unrelated setting AND a user-authored hook the
466+
# installer must preserve.
467+
cat > "$HOME/.codex/config.toml" <<'TOML'
468+
model = "gpt-5.5"
469+
470+
[[hooks.PreToolUse]]
471+
matcher = "*"
472+
[[hooks.PreToolUse.hooks]]
473+
type = "command"
474+
command = 'echo user-hook'
475+
TOML
476+
477+
DASH0_VERSION="$VERSION" \
478+
DASH0_OTLP_URL=http://localhost:4319 \
479+
DASH0_AUTH_TOKEN=codex-install-token \
480+
bash "$GITHUB_WORKSPACE/install-codex.sh" 2>&1 | tail -25
481+
482+
CONFIG_TOML="$HOME/.codex/config.toml"
483+
echo "::group::resulting config.toml"; cat "$CONFIG_TOML"; echo "::endgroup::"
484+
fail=0
485+
[ -f "$HOME/.codex/dash0-agent-plugin.local.md" ] \
486+
|| { echo "::error::installer did not write the config .local.md"; fail=1; }
487+
grep -q ">>> dash0-agent-plugin (managed)" "$CONFIG_TOML" \
488+
|| { echo "::error::managed block not appended to config.toml"; fail=1; }
489+
trust_n=$(grep -c 'trusted_hash = "sha256:' "$CONFIG_TOML" || true)
490+
[ "$trust_n" -eq 10 ] \
491+
|| { echo "::error::expected 10 pre-trust entries, found $trust_n"; fail=1; }
492+
493+
# Structural assertions via a real TOML parser: valid TOML, user setting
494+
# and user hook preserved, our hooks + 10 trust-state keys present.
495+
python3 - "$CONFIG_TOML" <<'PY' || fail=1
496+
import sys, tomllib
497+
d = tomllib.load(open(sys.argv[1], "rb"))
498+
assert d.get("model") == "gpt-5.5", "user setting lost"
499+
pre = d["hooks"]["PreToolUse"]
500+
cmds = [h["command"] for g in pre for h in g["hooks"]]
501+
assert any(c == "echo user-hook" for c in cmds), "user hook lost"
502+
assert any("codex-on-event.sh" in c for c in cmds), "dash0 PreToolUse hook missing"
503+
assert len(d["hooks"]["state"]) == 10, f"expected 10 trust keys, got {len(d['hooks']['state'])}"
504+
print("TOML OK: user content preserved, dash0 hooks + trust present")
505+
PY
506+
507+
[ "$fail" -eq 0 ] || exit 1
508+
echo "PASS: install-codex.sh merged hooks + pre-trust and preserved user config"
509+
510+
- name: "Contract I — uninstall-codex.sh strips the managed block, preserves user content"
511+
run: |
512+
# Assumes Contract H ran and left a merged config.toml in place.
513+
export HOME=/tmp/codex-installer-home
514+
export XDG_STATE_HOME=/tmp/codex-installer-state
515+
CONFIG_TOML="$HOME/.codex/config.toml"
516+
[ -f "$CONFIG_TOML" ] || { echo "::error::Contract H did not produce a config.toml"; exit 1; }
517+
518+
bash "$GITHUB_WORKSPACE/uninstall-codex.sh" --yes 2>&1 | tail -20
519+
520+
echo "::group::config.toml after uninstall"; cat "$CONFIG_TOML"; echo "::endgroup::"
521+
fail=0
522+
# config .local.md and state dir gone entirely.
523+
for p in "$HOME/.codex/dash0-agent-plugin.local.md" "$XDG_STATE_HOME/dash0-agent-plugin/codex"; do
524+
[ -e "$p" ] && { echo "::error::uninstaller left behind: $p"; fail=1; }
525+
done
526+
# config.toml still exists (user content), with NO managed block or trust.
527+
grep -q ">>> dash0-agent-plugin (managed)" "$CONFIG_TOML" \
528+
&& { echo "::error::managed block survived uninstall"; fail=1; }
529+
grep -q 'codex-on-event.sh' "$CONFIG_TOML" \
530+
&& { echo "::error::dash0 hook command survived uninstall"; fail=1; }
531+
python3 - "$CONFIG_TOML" <<'PY' || fail=1
532+
import sys, tomllib
533+
d = tomllib.load(open(sys.argv[1], "rb"))
534+
assert d.get("model") == "gpt-5.5", "user setting lost on uninstall"
535+
cmds = [h["command"] for g in d["hooks"]["PreToolUse"] for h in g["hooks"]]
536+
assert cmds == ["echo user-hook"], f"user hook not cleanly preserved: {cmds}"
537+
assert "state" not in d.get("hooks", {}), "trust state survived uninstall"
538+
print("TOML OK: user content intact, dash0 fully removed")
539+
PY
540+
541+
[ "$fail" -eq 0 ] || exit 1
542+
echo "PASS: uninstall-codex.sh stripped the managed block and preserved user config"
543+
394544
e2e:
395545
runs-on: ubuntu-latest
396546
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository

install-codex.sh

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -139,26 +139,38 @@ BASE_URL="https://github.com/${REPO}/releases/download/v${VERSION}"
139139
BIN_ASSET="codex-on-event-${OS}-${ARCH}"
140140
RAW_BASE="https://raw.githubusercontent.com/${REPO}/v${VERSION}"
141141

142-
info "downloading codex-on-event v${VERSION}..."
143-
fetch "$BASE_URL/$BIN_ASSET" "$BIN_PATH" || die "failed to download binary: $BASE_URL/$BIN_ASSET"
144-
CHECKSUMS=$(fetch_stdout "$BASE_URL/checksums.txt" || true)
145-
if [ -n "$CHECKSUMS" ]; then
146-
EXPECTED=$(echo "$CHECKSUMS" | grep " ${BIN_ASSET}\$" | cut -d' ' -f1)
147-
if [ -n "$EXPECTED" ]; then
148-
ACTUAL=$(sha256 "$BIN_PATH")
149-
if [ -n "$ACTUAL" ] && [ "$ACTUAL" != "$EXPECTED" ]; then
150-
rm -f "$BIN_PATH"; die "checksum mismatch for $BIN_ASSET (expected $EXPECTED, got $ACTUAL)"
142+
# The binary path is version-pinned, so an already-present binary is exactly
143+
# this version — skip the download (idempotent re-install; also lets an offline
144+
# or pre-staged binary work). A version bump changes BIN_PATH, forcing a fetch.
145+
if [ -x "$BIN_PATH" ]; then
146+
ok "binary already present → $BIN_PATH"
147+
else
148+
info "downloading codex-on-event v${VERSION}..."
149+
fetch "$BASE_URL/$BIN_ASSET" "$BIN_PATH" || die "failed to download binary: $BASE_URL/$BIN_ASSET"
150+
CHECKSUMS=$(fetch_stdout "$BASE_URL/checksums.txt" || true)
151+
if [ -n "$CHECKSUMS" ]; then
152+
EXPECTED=$(echo "$CHECKSUMS" | grep " ${BIN_ASSET}\$" | cut -d' ' -f1)
153+
if [ -n "$EXPECTED" ]; then
154+
ACTUAL=$(sha256 "$BIN_PATH")
155+
if [ -n "$ACTUAL" ] && [ "$ACTUAL" != "$EXPECTED" ]; then
156+
rm -f "$BIN_PATH"; die "checksum mismatch for $BIN_ASSET (expected $EXPECTED, got $ACTUAL)"
157+
fi
151158
fi
152159
fi
160+
chmod +x "$BIN_PATH"
161+
ok "installed binary → $BIN_PATH"
153162
fi
154-
chmod +x "$BIN_PATH"
155-
ok "installed binary → $BIN_PATH"
156163

157-
# 5b. Install the bootstrap script from the tagged ref.
158-
info "downloading codex-on-event.sh..."
159-
fetch "$RAW_BASE/scripts/codex-on-event.sh" "$SCRIPT_PATH" || die "failed to download: $RAW_BASE/scripts/codex-on-event.sh"
160-
chmod +x "$SCRIPT_PATH"
161-
ok "installed bootstrap → $SCRIPT_PATH"
164+
# 5b. Install the bootstrap script from the tagged ref (skip if already present).
165+
if [ -f "$SCRIPT_PATH" ]; then
166+
chmod +x "$SCRIPT_PATH"
167+
ok "bootstrap already present → $SCRIPT_PATH"
168+
else
169+
info "downloading codex-on-event.sh..."
170+
fetch "$RAW_BASE/scripts/codex-on-event.sh" "$SCRIPT_PATH" || die "failed to download: $RAW_BASE/scripts/codex-on-event.sh"
171+
chmod +x "$SCRIPT_PATH"
172+
ok "installed bootstrap → $SCRIPT_PATH"
173+
fi
162174

163175
# 6. Collect configuration (env var > interactive prompt > skip).
164176
prompt_value() {

0 commit comments

Comments
 (0)