@@ -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
0 commit comments