feat(govern): sync Codex and Cursor sessions through the fallback job

NOTE: glab govern is currently an experimental command

What does this MR do?

Syncs Codex and Cursor sessions through the fallback periodic sync from !4013 (merged), without hooks.

It also sends richer audit events for every agent, including Claude Code and OpenCode sessions synced by the hooks. Until now, each tool call sent only its tool name. See Richer audit events.

Depends on a GitLab change adding codex and cursor to Ai::ExternalAgents::AgentIdentity::AGENT_TYPES. Today the API accepts only claude-code and opencode, so GitLab rejects these sessions until that change is deployed.

Richer audit events

These apply to all agents. Users who already set up the hooks get them when they upgrade glab, without running setup again.

Event names and detail keys follow the audit events GitLab's own MCP server records (ee/app/services/mcp/audit/capture_event_service.rb), so the governance views read them the same way. Every event carries agent_name.

Event When Details
ai_user_input_received Each prompt the user types input, truncated to 8 KB with input_truncated: true
ai_tool_invoked Each tool call tool_name, arguments, outcome: "pending", tool_call_id, agent_initiated: true
ai_tool_response_received / ai_tool_execution_failed Each tool result outcome (success or error), duration_s, error_message (up to 255 characters), tool_name, tool_call_id
ai_llm_response_received Each model response model, input_tokens, output_tokens, and cache token counts

What is and isn't sent

  • Sent: prompts as typed, and tool arguments. Arguments include Bash commands, file paths, and the content of file edits, and are replaced with {"omitted": "arguments exceeded size limit"} above 8 KB, as the MCP server does.
  • Not sent: tool output, and the text of model responses.
  • glab govern audit sync --help now describes what is uploaded.

Compatibility and limits

  • Tool call events keep the IDs they had before, so calls already synced by the current release aren't duplicated.
  • GitLab rejects an event whose details exceed 10 KB, so prompts and arguments are capped at 8 KB of encoded JSON, measured with the escaping GitLab applies, and the details as a whole are checked too.
  • GitLab rejects a whole batch if one event is more than 90 days old, so such events are dropped instead.
  • OpenCode: sends arguments, and outcomes for calls that had already finished when first synced (the usual case, since syncs follow each turn). Its prompts and responses aren't sent yet, because the OpenCode cursor counts tool calls, and changing what it counts would misread existing cursors.

How it works

glab govern setup --agents codex,cursor enables it. Each glab govern audit sync --all run then:

  • scans ~/.codex/sessions and ~/.cursor/projects/*/agent-transcripts;
  • for each new session, writes the same .meta a hook would write.

Discovered sessions then follow the existing path: syncing, completion after 24 hours idle, cleanup once the transcript is deleted, the run log, and doctor.

  • Opt-in: nothing is discovered until an agent is enabled. Re-running setup with a different --agents list changes it, and --agents "" turns it off. Disabling an agent pauses its recorded sessions. doctor shows which agents are enabled. Every setup prompt names the enabled agents and what is uploaded, and --uninstall also stops syncing them.
  • Backfill: existing sessions are uploaded too, and the setup prompt says so. A session that has never synced and has had no activity in the last 89 days is skipped, because GitLab rejects events older than 90 days. A session that has synced before still goes through idle completion.
  • Only GitLab projects: only repositories on GitLab hosts glab is logged in to are synced.
    • Codex: the repository comes from the rollout's git.repository_url, falling back to the origin remote of its working directory. git runs once per directory per run.
    • Cursor: the folder name under ~/.cursor/projects is the workspace path with every non-alphanumeric character replaced by -, so it can't be decoded directly. glab finds the workspace by matching the name against directories that exist, including directories starting with . or _ and symlinks such as /var on macOS, then reads its origin remote. Because the name can match the wrong directory, for example when code/cli-docs and code/cli/docs both exist, or when the workspace has moved, the absolute paths in the sessions' tool calls must point inside the directory. Otherwise the sessions are skipped, and doctor lists them as not synced.
  • Partial lines: both parsers read only complete lines, so a line still being written is read on the next run.
  • GitLab versions without these agent types: GitLab rejects the sessions with a 400. glab records the agent as not supported by that host, which doctor shows without failing, and marks the sessions completed locally, so they aren't retried or uploaded in a burst after an upgrade.

What each agent sends

Codex Cursor
Tool calls with arguments ✅ ✅
Tool results ✅ duration; outcome completed, because Codex doesn't record success or failure ❌ not recorded by Cursor
Prompts ✅ ✅
Model and token usage ✅ from token_count and turn_context ❌ not recorded by Cursor

The formats come from Codex's source (codex-rs/protocol and codex-rs/rollout/src/policy.rs) and from several open-source Cursor transcript parsers.

  • Codex history modes: Codex writes tool calls and outputs in every history mode, but not user_message events in its newer paginated mode. Prompts there come from user message items instead, skipping injected AGENTS.md and environment context.
  • Cursor IDs and times: Cursor records no tool call IDs or timestamps, so event IDs come from each call's position in the transcript, and times are the transcript's last modification.

Limitations

  • Subagent transcripts (Codex sub-threads and Cursor subagents/) are skipped.
  • Sessions arrive up to 30 minutes after activity, and are completed only by the 24-hour idle rule.
  • Only the origin remote is used. A Cursor session whose tool calls read or write no files can't be checked against its workspace, so it is trusted when only one directory matches the workspace's name.
  • A custom CODEX_HOME is followed only if the scheduled job's environment has it, which launchd and systemd jobs usually don't.

How to test

  • go test ./internal/commands/govern/...
  • End to end, against a GitLab that accepts the codex and cursor agent types:
    1. Run a short Codex session and a Cursor agent session in a repository on a project with governance enabled.
    2. Run glab govern setup --agents codex,cursor, then glab govern audit sync --all.
    3. Check that codex and cursor sessions with events appear in the project.

Who is affected

Users who run glab govern setup (an experimental command). The richer audit events apply to their Claude Code and OpenCode sessions as soon as they upgrade, without running setup again. Codex and Cursor sessions are synced only for users who also pass --agents.

Edited by Jay McCure

Merge request reports

Loading
Loading