Skip to content

Commit c99d661

Browse files
Add AI coding agent detection to User-Agent header
Detect when the Python SQL connector is invoked by an AI coding agent (e.g. Claude Code, Cursor, Gemini CLI) by checking well-known environment variables, and append `agent/<product>` to the User-Agent string. This enables Databricks to understand how much driver usage originates from AI coding agents. Detection only succeeds when exactly one agent is detected to avoid ambiguous attribution. Mirrors the approach in databricks/cli#4287. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 9fe7356 commit c99d661

File tree

4 files changed

+114
-0
lines changed

4 files changed

+114
-0
lines changed

src/databricks/sql/common/agent.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"""
2+
Detects whether the Python SQL connector is being invoked by an AI coding agent
3+
by checking for well-known environment variables that agents set in their spawned
4+
shell processes.
5+
6+
Detection only succeeds when exactly one agent environment variable is present,
7+
to avoid ambiguous attribution when multiple agent environments overlap.
8+
9+
Adding a new agent requires only a new entry in KNOWN_AGENTS.
10+
11+
References for each environment variable:
12+
- ANTIGRAVITY_AGENT: Closed source. Google Antigravity sets this variable.
13+
- CLAUDECODE: https://github.com/anthropics/claude-code (sets CLAUDECODE=1)
14+
- CLINE_ACTIVE: https://github.com/cline/cline (shipped in v3.24.0)
15+
- CODEX_CI: https://github.com/openai/codex (part of UNIFIED_EXEC_ENV array in codex-rs)
16+
- CURSOR_AGENT: Closed source. Referenced in a gist by johnlindquist.
17+
- GEMINI_CLI: https://google-gemini.github.io/gemini-cli/docs/tools/shell.html (sets GEMINI_CLI=1)
18+
- OPENCODE: https://github.com/opencode-ai/opencode (sets OPENCODE=1)
19+
"""
20+
21+
import os
22+
23+
KNOWN_AGENTS = [
24+
("ANTIGRAVITY_AGENT", "antigravity"),
25+
("CLAUDECODE", "claude-code"),
26+
("CLINE_ACTIVE", "cline"),
27+
("CODEX_CI", "codex"),
28+
("CURSOR_AGENT", "cursor"),
29+
("GEMINI_CLI", "gemini-cli"),
30+
("OPENCODE", "opencode"),
31+
]
32+
33+
34+
def detect(env=None):
35+
"""Detect which AI coding agent (if any) is driving the current process.
36+
37+
Args:
38+
env: Optional dict-like object for environment variable lookup.
39+
Defaults to os.environ. Exists for testability.
40+
41+
Returns:
42+
The agent product string if exactly one agent is detected,
43+
or an empty string otherwise.
44+
"""
45+
if env is None:
46+
env = os.environ
47+
48+
detected = [product for var, product in KNOWN_AGENTS if env.get(var)]
49+
50+
if len(detected) == 1:
51+
return detected[0]
52+
return ""

src/databricks/sql/session.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from databricks.sql.backend.databricks_client import DatabricksClient
1414
from databricks.sql.backend.types import SessionId, BackendType
1515
from databricks.sql.common.unified_http_client import UnifiedHttpClient
16+
from databricks.sql.common.agent import detect as detect_agent
1617

1718
logger = logging.getLogger(__name__)
1819

@@ -64,6 +65,10 @@ def __init__(
6465
else:
6566
self.useragent_header = "{}/{}".format(USER_AGENT_NAME, __version__)
6667

68+
agent_product = detect_agent()
69+
if agent_product:
70+
self.useragent_header += " agent/{}".format(agent_product)
71+
6772
base_headers = [("User-Agent", self.useragent_header)]
6873
all_headers = (http_headers or []) + base_headers
6974

src/databricks/sql/utils.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -914,12 +914,18 @@ def build_client_context(server_hostname: str, version: str, **kwargs):
914914
)
915915

916916
# Build user agent
917+
from databricks.sql.common.agent import detect as detect_agent
918+
917919
user_agent_entry = kwargs.get("user_agent_entry", "")
918920
if user_agent_entry:
919921
user_agent = f"PyDatabricksSqlConnector/{version} ({user_agent_entry})"
920922
else:
921923
user_agent = f"PyDatabricksSqlConnector/{version}"
922924

925+
agent_product = detect_agent()
926+
if agent_product:
927+
user_agent += f" agent/{agent_product}"
928+
923929
# Explicitly construct ClientContext with proper types
924930
return ClientContext(
925931
hostname=server_hostname,

tests/unit/test_agent_detection.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import pytest
2+
from databricks.sql.common.agent import detect, KNOWN_AGENTS
3+
4+
5+
class TestAgentDetection:
6+
def test_detects_single_agent_claude_code(self):
7+
assert detect({"CLAUDECODE": "1"}) == "claude-code"
8+
9+
def test_detects_single_agent_cursor(self):
10+
assert detect({"CURSOR_AGENT": "1"}) == "cursor"
11+
12+
def test_detects_single_agent_gemini_cli(self):
13+
assert detect({"GEMINI_CLI": "1"}) == "gemini-cli"
14+
15+
def test_detects_single_agent_cline(self):
16+
assert detect({"CLINE_ACTIVE": "1"}) == "cline"
17+
18+
def test_detects_single_agent_codex(self):
19+
assert detect({"CODEX_CI": "1"}) == "codex"
20+
21+
def test_detects_single_agent_opencode(self):
22+
assert detect({"OPENCODE": "1"}) == "opencode"
23+
24+
def test_detects_single_agent_antigravity(self):
25+
assert detect({"ANTIGRAVITY_AGENT": "1"}) == "antigravity"
26+
27+
def test_returns_empty_when_no_agent_detected(self):
28+
assert detect({}) == ""
29+
30+
def test_returns_empty_when_multiple_agents_detected(self):
31+
assert detect({"CLAUDECODE": "1", "CURSOR_AGENT": "1"}) == ""
32+
33+
def test_ignores_empty_env_var_values(self):
34+
assert detect({"CLAUDECODE": ""}) == ""
35+
36+
def test_all_known_agents_are_covered(self):
37+
for env_var, product in KNOWN_AGENTS:
38+
assert detect({env_var: "1"}) == product, (
39+
f"Agent with env var {env_var} should be detected as {product}"
40+
)
41+
42+
def test_defaults_to_os_environ(self, monkeypatch):
43+
monkeypatch.delenv("CLAUDECODE", raising=False)
44+
monkeypatch.delenv("CURSOR_AGENT", raising=False)
45+
monkeypatch.delenv("GEMINI_CLI", raising=False)
46+
monkeypatch.delenv("CLINE_ACTIVE", raising=False)
47+
monkeypatch.delenv("CODEX_CI", raising=False)
48+
monkeypatch.delenv("OPENCODE", raising=False)
49+
monkeypatch.delenv("ANTIGRAVITY_AGENT", raising=False)
50+
# With all agent vars cleared, detect() should return empty
51+
assert detect() == ""

0 commit comments

Comments
 (0)