mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-08-10 08:21:54 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user