Improve agent caller detection across MCP, env, and process tree#885
Merged
Conversation
Restructures `detect_caller` into a six-layer pipeline (RAILWAY_CALLER → strong env → process tree → IDE host → cloud IDE → CI provider → bucketed fallback) and routes JSON-RPC `clientInfo` from `railway mcp` tool events into a new `send_mcp_tool_with_client` so MCP-driven calls get tagged authoritatively from the handshake instead of relying on heuristics. Caller value semantics are extended with colon-suffixed sub-buckets so unattributed events still carry a useful slicing axis: `tty:cursor`, `tty:vscode`, `agent_unknown:vscode`, `agent_unknown:python`, `ci:github_actions`, `cloud_ide:codespaces`, etc. The detector also expands the env-var table (`__COG_BASHRC_SOURCED`, `AI_AGENT`, `CLAUDE_CODE_SESSION_ID` — fixing a long-standing typo where the old code looked for the non-existent `CLAUDECODE_SESSION_ID`), adds a one-shot `ps -A` snapshot in place of N per-hop spawns, matches against full argv (catching `node /path/to/cursor-agent`), increases the ancestor walk to 15 hops, and adds basename matching for short generic agent names (Factory Droid's `droid`, Pi's `pi`, Amp's `amp`). Validated live against 11 agent harnesses (Claude Code CLI/Desktop, Cursor, OpenCode, Amp, Codex CLI/Desktop, Pi, Copilot CLI, Factory Droid, Claude-in-Cursor-terminal): 11/11 detected correctly. Factory Droid is the case the old detector would have leaked into `agent_subprocess`. Diagnostic script for future ground-truth checks lives at `scripts/diagnose-caller.sh`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wraps long || chains and assert_eq! arguments per CI's cargo fmt diff. No behavior change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Walks ancestors signature and MCP client peer_info chain wrapped per cargo fmt. No behavior change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Comprehensive rewrite of the CLI's
callerdetection. Replaces the old single-pass env/ps-walk with a six-layer pipeline and routes JSON-RPCclientInfofromrailway mcptool events into the telemetry payload so MCP-driven calls get tagged authoritatively from the handshake.Validated live against 11 agent harnesses (Claude Code CLI/Desktop, Claude-in-Cursor-terminal, Cursor agent mode, OpenCode, Amp, Codex CLI/Desktop, Pi, Copilot CLI, Factory Droid). 11/11 detected correctly. Factory Droid is the canonical case the old detector would have leaked into
agent_subprocess.Pairs with railwayapp/dbt-analytics#128 which normalizes the new caller taxonomy in
fct_agentic_events/dim_agent_session.What changed
Detection pipeline (
src/telemetry.rs)Six layers, evaluated in order — first match wins:
RAILWAY_CALLERenv override (existing, unchanged)CLAUDECODE,CURSOR_AGENT,CODEX_SANDBOX,OPENCODE,AMP_CURRENT_THREAD_ID,PI_CODING_AGENT,__COG_BASHRC_SOURCED(Devin),AI_AGENT,COPILOT_CLI,AIDER,FACTORY_DROID,GEMINI_CLI,REPLIT_AGENT. Includes a fix for a typo where the old code looked forCLAUDECODE_SESSION_ID(doesn't exist) instead ofCLAUDE_CODE_SESSION_ID.ps -Asnapshot (replaces N per-hop spawns) parsed into aHashMap, walks up to 15 ancestors. Matches against full argv (catches node-bundled agents likenode /path/cursor-agent) plus exact-basename matching for short generic names (droid,pi,amp).__CFBundleIdentifier(Cursorcom.todesktop.230313mzl4w4u92, Windsurf, VS Code, Claude Desktop, JetBrains, Zed),TERM_PROGRAM,TERMINAL_EMULATOR=JetBrains-JediTerm. Combined withisatty(stdout)to disambiguate human vs subprocess in the same IDE:tty:cursorvsagent_unknown:cursor.REPL_ID,CODESPACES,CLOUD_SHELL,MONOSPACE_ENV(Firebase Studio),ANTIGRAVITY_CLI_ALIAS.GITHUB_ACTIONS,GITLAB_CI,CIRCLECI,BUILDKITE,JENKINS_URL,TRAVIS,TF_BUILD,CODEBUILD_BUILD_ID,NETLIFY,VERCEL,RAILWAY_*, etc.tty. Non-interactive subprocess buckets by parent interpreter (agent_unknown:python,agent_unknown:node,agent_unknown:shell,agent_unknown:ruby, ...) so even unidentified harnesses give us a useful axis.MCP
clientInfois now authoritative for MCP eventssrc/commands/mcp/handler.rssnapshotscontext.peer.peer_info().client_infoand threads it into a newsend_mcp_tool_with_client. The clientInfo name (per the MCP JSON-RPC spec, every client must send it duringinitialize) maps to the canonical caller value:claude-ai→claude_code(orclaude_desktopbased on env),codex-mcp-client→codex,Cline→cline,Roo Code→roo_code,kilo→kilo_code,opencode→opencode,continue-client→continue_dev,Visual Studio Code(...)→vscode_copilot/vscode_insiders,windsurf→windsurf, etc. Unknown clients land onmcp_unknownso we can debug new entrants in Hex.Sub-bucketed caller vocabulary
Caller is still a bounded string — backboard accepts colons,
dbt-analyticsnormalizes downstream. New colon-prefixed forms:claude_code,cursor,codex,factory_droid,amp,pi,copilot_cli,aider,windsurf,gemini_cli, ...agent_namedtty:tty:cursor,tty:vscode,tty:zed,tty:jetbrains,tty:trae,tty:ghostty, ...humanagent_unknown:agent_unknown:vscode,agent_unknown:cursor,agent_unknown:python,agent_unknown:node,agent_unknown:shell, ...agent_unknowncloud_ide:cloud_ide:codespaces,cloud_ide:replit,cloud_ide:cloud_shell, ...cloud_ideci:ci:github_actions,ci:gitlab,ci:circle,ci:buildkite,ci:railway, ...cimcp_unknownagent_unknownDiagnostic script
scripts/diagnose-caller.shcollects every signal the new detector reads (env vars, IDE host indicators, CI markers, TTY status, full process ancestry). Drop-in for ground-truth validation against any agent harness — used during this PR to confirm 11/11 attribution.Live validation results
claude_codeclaude_codeclaude_codecursoropencodeampcodexcodexpicopilot_clifactory_droidNotable finding: Copilot CLI is the only tested agent that hands its child a real PTY (
stdout_tty=true). All others pipe stdout. This validates that the TTY check alone is not a reliable agent-vs-human discriminator — we use it only as a tiebreaker for the IDE-host bucket.Test plan
pssnapshot parsingscripts/diagnose-caller.shcallervalues flow throughfct_agentic_events.callerand thatdbt-analytics#128'scaller_class/caller_agent/caller_subkindcolumns populate as expectedBackground
RFC: Agentic Loop Telemetry: MCP + CLI. Companion PRs: railwayapp/dbt-analytics#128 (taxonomy normalization in marts).
🤖 Generated with Claude Code