feat(telemetry): widen assistant detection from published marker sets

The table previously covered three assistants because the rest were
unverified. They are documented after all: vercel/detect-agent publishes
a machine-readable detection matrix (agents.json), corroborated by the
proposal in agentsmd/agents.md#136 and by microsoft/vscode#311734.

Adds cline, gemini_cli, augment, opencode, antigravity and junie, plus
CLAUDE_CODE alongside CLAUDECODE. Gemini's marker is confirmed by its own
docs, which state that run_shell_command sets GEMINI_CLI=1 in the
subprocess environment.

Rule 2 excluded several entries those sources list. Goose's
GOOSE_PROVIDER and Copilot's COPILOT_MODEL and COPILOT_GITHUB_TOKEN are
user configuration, and a committed .env carrying one would relabel every
ordinary run - the AIDER_MODEL trap the guard test already pins, now
parametrized over all four. Replit's REPL_ID names a hosted environment
rather than an assistant, so it stays a runtime context. Copilot sets no
session marker at all today; that is an open request upstream.

The new assistants are ordered ahead of Cursor, since CURSOR_* is set for
every integrated terminal and would otherwise mask anything spawned
inside it - the same ordering Codex already needed.

Also adds the proposed cross-vendor AI_AGENT marker as a last resort,
reported as "other". It establishes that an assistant is present without
naming one, and its value is an arbitrary vendor string, so the value is
never read.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joao Moura
2026-08-07 15:33:41 -07:00
parent 96e40ed3a4
commit 59c932cd68
2 changed files with 127 additions and 7 deletions

View File

@@ -12,19 +12,27 @@ from pydantic_core import CoreSchema
__all__ = [
"ANTIGRAVITY_ENV_VARS",
"AUGMENT_ENV_VARS",
"CC_ENV_VAR",
"CC_ENV_VARS",
"CI_ENV_VARS",
"CLINE_ENV_VARS",
"CODEX_ENV_VARS",
"CODING_AGENT_ENV_MARKERS",
"CONTAINER_ENV_VARS",
"CREWAI_TRAINED_AGENTS_FILE_ENV",
"CURSOR_ENV_VARS",
"EMITTER_COLOR",
"GEMINI_CLI_ENV_VARS",
"GENERIC_AGENT_ENV_VARS",
"HOSTED_IDE_ENV_VARS",
"JUNIE_ENV_VARS",
"KNOWLEDGE_DIRECTORY",
"MAX_FILE_NAME_LENGTH",
"NOTEBOOK_ENV_VARS",
"NOT_SPECIFIED",
"OPENCODE_ENV_VARS",
"PAAS_ENV_VARS",
"RUNTIME_CONTEXT_ENV_MARKERS",
"SERVERLESS_ENV_VARS",
@@ -42,6 +50,7 @@ CODEX_ENV_VARS: Final[tuple[str, ...]] = (
"CODEX_SANDBOX_NETWORK_DISABLED",
"CODEX_THREAD_ID",
)
CC_ENV_VARS: Final[tuple[str, ...]] = (CC_ENV_VAR, "CLAUDE_CODE")
CURSOR_ENV_VARS: Final[tuple[str, ...]] = (
"CURSOR_AGENT",
"CURSOR_EXTENSION_HOST_ROLE",
@@ -49,6 +58,20 @@ CURSOR_ENV_VARS: Final[tuple[str, ...]] = (
"CURSOR_TRACE_ID",
"CURSOR_WORKSPACE_LABEL",
)
ANTIGRAVITY_ENV_VARS: Final[tuple[str, ...]] = (
"ANTIGRAVITY_AGENT",
"ANTIGRAVITY_CLI_ALIAS",
)
AUGMENT_ENV_VARS: Final[tuple[str, ...]] = ("AUGMENT_AGENT",)
CLINE_ENV_VARS: Final[tuple[str, ...]] = ("CLINE_ACTIVE",)
GEMINI_CLI_ENV_VARS: Final[tuple[str, ...]] = ("GEMINI_CLI",)
JUNIE_ENV_VARS: Final[tuple[str, ...]] = ("JUNIE_DATA", "JUNIE_SHIM_PATH")
OPENCODE_ENV_VARS: Final[tuple[str, ...]] = ("OPENCODE", "OPENCODE_CLIENT")
# Proposed cross-vendor marker (agentsmd/agents.md#136). Checked last and
# reported as "other": it says an assistant is present without naming one, and
# reading its value to find out would put an arbitrary string in telemetry.
GENERIC_AGENT_ENV_VARS: Final[tuple[str, ...]] = ("AI_AGENT",)
# Ordered (name, env vars) pairs for identifying the AI coding assistant a
# process is running under. Reuses the sets above and keeps the same precedence
@@ -69,11 +92,33 @@ CURSOR_ENV_VARS: Final[tuple[str, ...]] = (
# a leftover config value would mislabel ordinary human executions.
#
# Extend the shared sets above rather than adding a parallel tuple here, so both
# detection paths pick the new markers up together.
# detection paths pick the new markers up together. Entries past the first three
# have no ``get_env_context()`` event of their own yet and fall to
# ``DefaultEnvEvent`` there.
#
# Markers below the first three were taken from the published detection matrix
# at vercel/detect-agent (agents.json), cross-checked against the proposal in
# agentsmd/agents.md#136 and microsoft/vscode#311734. Rule 2 excluded several
# entries those sources list: Goose's ``GOOSE_PROVIDER`` and Copilot's
# ``COPILOT_MODEL`` / ``COPILOT_GITHUB_TOKEN`` are user configuration, and a
# committed ``.env`` carrying one would relabel every ordinary run. Replit's
# ``REPL_ID`` is a hosted environment rather than an assistant, so it stays in
# HOSTED_IDE_ENV_VARS.
#
# The assistants that spawn inside another editor's terminal are ordered ahead
# of Cursor for the same reason Codex is: CURSOR_* is set for every integrated
# terminal, so checking Cursor first would mask anything running inside it.
CODING_AGENT_ENV_MARKERS: Final[tuple[tuple[str, tuple[str, ...]], ...]] = (
("claude_code", (CC_ENV_VAR,)),
("claude_code", CC_ENV_VARS),
("codex", CODEX_ENV_VARS),
("cline", CLINE_ENV_VARS),
("gemini_cli", GEMINI_CLI_ENV_VARS),
("augment", AUGMENT_ENV_VARS),
("opencode", OPENCODE_ENV_VARS),
("antigravity", ANTIGRAVITY_ENV_VARS),
("junie", JUNIE_ENV_VARS),
("cursor", CURSOR_ENV_VARS),
("other", GENERIC_AGENT_ENV_VARS),
)
# Markers for *where* a process runs, kept separate from which assistant is

View File

@@ -12,10 +12,18 @@ from crewai.telemetry.utils import (
detect_runtime_context,
)
from crewai.utilities.constants import (
ANTIGRAVITY_ENV_VARS,
AUGMENT_ENV_VARS,
CC_ENV_VAR,
CC_ENV_VARS,
CLINE_ENV_VARS,
CODEX_ENV_VARS,
CODING_AGENT_ENV_MARKERS,
CURSOR_ENV_VARS,
GEMINI_CLI_ENV_VARS,
GENERIC_AGENT_ENV_VARS,
JUNIE_ENV_VARS,
OPENCODE_ENV_VARS,
RUNTIME_CONTEXT_ENV_MARKERS,
)
@@ -161,15 +169,33 @@ def test_precedence_matches_get_env_context(clean_env):
assert detect_coding_agent() == expected, markers
def test_config_style_variables_are_not_used_as_markers():
@pytest.mark.parametrize(
"config_var",
[
"AIDER_MODEL",
"COPILOT_GITHUB_TOKEN",
"COPILOT_MODEL",
"GOOSE_PROVIDER",
],
)
def test_config_style_variables_are_not_used_as_markers(config_var):
"""Persistent user config must never be treated as a session marker.
crewai loads dotenv files on normal runs, so a committed AIDER_MODEL or
similar would mislabel ordinary human executions.
GOOSE_PROVIDER would mislabel ordinary human executions. Published
detection matrices list several of these; they are deliberately excluded
here rather than copied wholesale.
"""
all_vars = {var for _, env_vars in CODING_AGENT_ENV_MARKERS for var in env_vars}
assert "AIDER_MODEL" not in all_vars
assert config_var not in all_vars
def test_hosted_environments_are_not_reported_as_assistants():
"""REPL_ID marks a hosted environment, not an assistant driving the run."""
all_vars = {var for _, env_vars in CODING_AGENT_ENV_MARKERS for var in env_vars}
assert "REPL_ID" not in all_vars
def test_every_marker_comes_from_a_verified_set():
@@ -180,15 +206,64 @@ def test_every_marker_comes_from_a_verified_set():
Adding an assistant means extending the canonical sets, which keeps both
detection paths in sync.
"""
verified = {CC_ENV_VAR, *CODEX_ENV_VARS, *CURSOR_ENV_VARS}
verified = {
*ANTIGRAVITY_ENV_VARS,
*AUGMENT_ENV_VARS,
*CC_ENV_VARS,
*CLINE_ENV_VARS,
*CODEX_ENV_VARS,
*CURSOR_ENV_VARS,
*GEMINI_CLI_ENV_VARS,
*GENERIC_AGENT_ENV_VARS,
*JUNIE_ENV_VARS,
*OPENCODE_ENV_VARS,
}
declared = {var for _, env_vars in CODING_AGENT_ENV_MARKERS for var in env_vars}
assert declared == verified, (
"markers must come from CC_ENV_VAR / CODEX_ENV_VARS / CURSOR_ENV_VARS; "
"markers must come from the canonical per-assistant sets; "
f"unverified names present: {sorted(declared - verified)}"
)
def test_generic_marker_is_the_last_resort(clean_env):
"""AI_AGENT says an assistant is present without naming which one.
A named marker must win, so the generic entry cannot mask a specific one.
"""
clean_env.setenv("AI_AGENT", "1")
assert detect_coding_agent() == "other"
clean_env.setenv("CLINE_ACTIVE", "true")
assert detect_coding_agent() == "cline"
def test_generic_marker_value_is_never_reported(clean_env):
"""Its value is an arbitrary vendor string, so it is never read."""
clean_env.setenv("AI_AGENT", "some-unreleased-tool/2.0")
assert detect_coding_agent() == "other"
def test_terminal_bound_assistants_outrank_cursor(clean_env):
"""CURSOR_* is set for every integrated terminal.
Checking Cursor first would report cursor for anything spawned inside it,
the same trap Codex already had to be ordered around.
"""
clean_env.setenv("CURSOR_TRACE_ID", "t-1")
for marker, expected in (
("CLINE_ACTIVE", "cline"),
("GEMINI_CLI", "gemini_cli"),
("AUGMENT_AGENT", "augment"),
("OPENCODE_CLIENT", "opencode"),
):
clean_env.setenv(marker, "1")
assert detect_coding_agent() == expected, marker
clean_env.delenv(marker)
def test_concurrent_attach_registers_the_processor_once(isolated_telemetry, clean_env):
"""Check-then-act on the provider set must be locked.