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 --helpnow 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/sessionsand~/.cursor/projects/*/agent-transcripts; - for each new session, writes the same
.metaa 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
setupwith a different--agentslist changes it, and--agents ""turns it off. Disabling an agent pauses its recorded sessions.doctorshows which agents are enabled. Every setup prompt names the enabled agents and what is uploaded, and--uninstallalso 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 theoriginremote of its working directory.gitruns once per directory per run. - Cursor: the folder name under
~/.cursor/projectsis 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/varon macOS, then reads itsoriginremote. Because the name can match the wrong directory, for example whencode/cli-docsandcode/cli/docsboth 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, anddoctorlists them as not synced.
- Codex: the repository comes from the rollout's
- 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
doctorshows 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 | completed, because Codex doesn't record success or failure |
|
| Prompts | ||
| Model and token usage | token_count and turn_context |
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_messageevents 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
originremote 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_HOMEis 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
codexandcursoragent types:- Run a short Codex session and a Cursor agent session in a repository on a project with governance enabled.
- Run
glab govern setup --agents codex,cursor, thenglab govern audit sync --all. - Check that
codexandcursorsessions 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.