Skip to content

Commit 2c3d129

Browse files
[PECOBLR-1928] Add AI coding agent detection to User-Agent header (#1230)
## Summary - Adds `AgentDetector` utility class that detects 7 AI coding agents (Claude Code, Cursor, Gemini CLI, Cline, Codex, OpenCode, Antigravity) by checking well-known environment variables they set in spawned shell processes - Integrates detection into `UserAgentManager` to append `agent/<product>` to the User-Agent header on both SDK-based and connector-service UA paths - Uses exactly-one detection rule: if zero or multiple agent env vars are set, no agent is attributed (avoids ambiguity) ## Approach Mirrors the implementation in [databricks/cli#4287](databricks/cli#4287) and aligns with the latest agent list in [`libs/agent/agent.go`](https://github.com/databricks/cli/blob/main/libs/agent/agent.go#L35). | Agent | Product String | Environment Variable | |-------|---------------|---------------------| | Google Antigravity | `antigravity` | `ANTIGRAVITY_AGENT` | | Claude Code | `claude-code` | `CLAUDECODE` | | Cline | `cline` | `CLINE_ACTIVE` | | OpenAI Codex | `codex` | `CODEX_CI` | | Cursor | `cursor` | `CURSOR_AGENT` | | Gemini CLI | `gemini-cli` | `GEMINI_CLI` | | OpenCode | `opencode` | `OPENCODE` | Adding a new agent requires only a new constant and a new entry in the `KNOWN_AGENTS` array. ## Changes - **New**: `AgentDetector.java` — environment-variable-based agent detection with injectable env lookup for testability - **Modified**: `UserAgentManager.java` — calls `AgentDetector.detect()` in both `setUserAgent()` and `buildUserAgentForConnectorService()` - **New**: `AgentDetectorTest.java` — 12 test cases covering single agent, no agent, multiple agents, empty values, null values, and full coverage of all known agents ## Test plan - [x] `AgentDetectorTest` — 12 tests pass (single agent detection for all 7 agents, no agent, multiple agents, empty/null values) - [x] `UserAgentManagerTest` — all 7 existing tests continue to pass - [x] Manual: verify User-Agent header contains `agent/claude-code` when run from Claude Code 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Vikrant Puppala <vikrant.puppala@databricks.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent aefb589 commit 2c3d129

4 files changed

Lines changed: 169 additions & 0 deletions

File tree

NEXT_CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### Added
66
- Added `CallableStatement` support with IN parameters. `Connection.prepareCall()` now returns a working `DatabricksCallableStatement` that supports positional parameter binding and execution via `{call proc(?)}` JDBC escape syntax. OUT/INOUT parameters and named parameters throw `SQLFeatureNotSupportedException`.
7+
- Added AI coding agent detection to the User-Agent header. When the driver is invoked by a known AI coding agent (e.g. Claude Code, Cursor, Gemini CLI), `agent/<product>` is appended to the User-Agent string.
78

89
### Updated
910

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package com.databricks.jdbc.common.util;
2+
3+
import java.util.ArrayList;
4+
import java.util.List;
5+
import java.util.Optional;
6+
import java.util.function.Function;
7+
8+
/**
9+
* Detects whether the JDBC driver is being invoked by an AI coding agent by checking for well-known
10+
* environment variables that agents set in their spawned shell processes.
11+
*
12+
* <p>Detection only succeeds when exactly one agent environment variable is present, to avoid
13+
* ambiguous attribution when multiple agent environments overlap.
14+
*
15+
* <p>Adding a new agent requires only a new constant and a new entry in {@link #KNOWN_AGENTS}.
16+
*
17+
* <p>References for each environment variable:
18+
*
19+
* <ul>
20+
* <li>ANTIGRAVITY_AGENT: Closed source. Google Antigravity sets this variable.
21+
* <li>CLAUDECODE: https://github.com/anthropics/claude-code (sets CLAUDECODE=1)
22+
* <li>CLINE_ACTIVE: https://github.com/cline/cline (shipped in v3.24.0)
23+
* <li>CODEX_CI: https://github.com/openai/codex (part of UNIFIED_EXEC_ENV array in codex-rs)
24+
* <li>CURSOR_AGENT: Closed source. Referenced in a gist by johnlindquist.
25+
* <li>GEMINI_CLI: https://google-gemini.github.io/gemini-cli/docs/tools/shell.html (sets
26+
* GEMINI_CLI=1)
27+
* <li>OPENCODE: https://github.com/opencode-ai/opencode (sets OPENCODE=1)
28+
* </ul>
29+
*/
30+
public class AgentDetector {
31+
32+
public static final String ANTIGRAVITY = "antigravity";
33+
public static final String CLAUDE_CODE = "claude-code";
34+
public static final String CLINE = "cline";
35+
public static final String CODEX = "codex";
36+
public static final String CURSOR = "cursor";
37+
public static final String GEMINI_CLI = "gemini-cli";
38+
public static final String OPEN_CODE = "opencode";
39+
40+
static final String[][] KNOWN_AGENTS = {
41+
{"ANTIGRAVITY_AGENT", ANTIGRAVITY},
42+
{"CLAUDECODE", CLAUDE_CODE},
43+
{"CLINE_ACTIVE", CLINE},
44+
{"CODEX_CI", CODEX},
45+
{"CURSOR_AGENT", CURSOR},
46+
{"GEMINI_CLI", GEMINI_CLI},
47+
{"OPENCODE", OPEN_CODE},
48+
};
49+
50+
/**
51+
* Detects which AI coding agent (if any) is driving the current process.
52+
*
53+
* @return the agent product string if exactly one agent is detected, or empty otherwise
54+
*/
55+
public static Optional<String> detect() {
56+
return detect(System::getenv);
57+
}
58+
59+
/**
60+
* Detects which AI coding agent (if any) is present, using the provided function to look up
61+
* environment variables. This overload exists for testability.
62+
*
63+
* @param envLookup function that returns the value of an environment variable, or null if unset
64+
* @return the agent product string if exactly one agent is detected, or empty otherwise
65+
*/
66+
static Optional<String> detect(Function<String, String> envLookup) {
67+
List<String> detected = new ArrayList<>();
68+
for (String[] entry : KNOWN_AGENTS) {
69+
String value = envLookup.apply(entry[0]);
70+
if (value != null && !value.isEmpty()) {
71+
detected.add(entry[1]);
72+
}
73+
}
74+
if (detected.size() == 1) {
75+
return Optional.of(detected.get(0));
76+
}
77+
return Optional.empty();
78+
}
79+
}

src/main/java/com/databricks/jdbc/common/util/UserAgentManager.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public class UserAgentManager {
1616
public static final String USER_AGENT_SEA_CLIENT = "SQLExecHttpClient";
1717
public static final String USER_AGENT_THRIFT_CLIENT = "THttpClient";
1818
private static final String VERSION_FILLER = "version";
19+
private static final String AGENT_KEY = "agent";
1920

2021
/**
2122
* Parse custom user agent string into name and version components.
@@ -62,6 +63,9 @@ public static void setUserAgent(IDatabricksConnectionContext connectionContext)
6263
}
6364
}
6465
}
66+
67+
// Detect AI coding agent and append to user agent
68+
AgentDetector.detect().ifPresent(product -> UserAgent.withOtherInfo(AGENT_KEY, product));
6569
}
6670

6771
/**
@@ -106,6 +110,10 @@ public static String buildUserAgentForConnectorService(
106110
}
107111
}
108112

113+
// Detect AI coding agent and append to user agent
114+
AgentDetector.detect()
115+
.ifPresent(product -> userAgent.append(" ").append(AGENT_KEY).append("/").append(product));
116+
109117
return userAgent.toString();
110118
}
111119

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package com.databricks.jdbc.common.util;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertTrue;
5+
6+
import java.util.HashMap;
7+
import java.util.Map;
8+
import java.util.Optional;
9+
import java.util.stream.Stream;
10+
import org.junit.jupiter.api.Test;
11+
import org.junit.jupiter.params.ParameterizedTest;
12+
import org.junit.jupiter.params.provider.Arguments;
13+
import org.junit.jupiter.params.provider.MethodSource;
14+
15+
public class AgentDetectorTest {
16+
17+
/** Creates an env lookup function that returns values from the given map. */
18+
private static java.util.function.Function<String, String> envWith(Map<String, String> env) {
19+
return env::get;
20+
}
21+
22+
@ParameterizedTest
23+
@MethodSource("singleAgentCases")
24+
void testDetectsSingleAgent(String envVar, String expectedProduct) {
25+
Map<String, String> env = new HashMap<>();
26+
env.put(envVar, "1");
27+
assertEquals(Optional.of(expectedProduct), AgentDetector.detect(envWith(env)));
28+
}
29+
30+
static Stream<Arguments> singleAgentCases() {
31+
return Stream.of(
32+
Arguments.of("ANTIGRAVITY_AGENT", AgentDetector.ANTIGRAVITY),
33+
Arguments.of("CLAUDECODE", AgentDetector.CLAUDE_CODE),
34+
Arguments.of("CLINE_ACTIVE", AgentDetector.CLINE),
35+
Arguments.of("CODEX_CI", AgentDetector.CODEX),
36+
Arguments.of("CURSOR_AGENT", AgentDetector.CURSOR),
37+
Arguments.of("GEMINI_CLI", AgentDetector.GEMINI_CLI),
38+
Arguments.of("OPENCODE", AgentDetector.OPEN_CODE));
39+
}
40+
41+
@Test
42+
void testReturnsEmptyWhenNoAgentDetected() {
43+
Map<String, String> env = new HashMap<>();
44+
assertTrue(AgentDetector.detect(envWith(env)).isEmpty());
45+
}
46+
47+
@Test
48+
void testReturnsEmptyWhenMultipleAgentsDetected() {
49+
Map<String, String> env = new HashMap<>();
50+
env.put("CLAUDECODE", "1");
51+
env.put("CURSOR_AGENT", "1");
52+
assertTrue(AgentDetector.detect(envWith(env)).isEmpty());
53+
}
54+
55+
@Test
56+
void testIgnoresEmptyEnvVarValues() {
57+
Map<String, String> env = new HashMap<>();
58+
env.put("CLAUDECODE", "");
59+
assertTrue(AgentDetector.detect(envWith(env)).isEmpty());
60+
}
61+
62+
@Test
63+
void testIgnoresNullEnvVarValues() {
64+
Map<String, String> env = new HashMap<>();
65+
env.put("CLAUDECODE", null);
66+
assertTrue(AgentDetector.detect(envWith(env)).isEmpty());
67+
}
68+
69+
@Test
70+
void testAllKnownAgentsAreCovered() {
71+
// Verify every entry in KNOWN_AGENTS can be detected individually
72+
for (String[] entry : AgentDetector.KNOWN_AGENTS) {
73+
Map<String, String> env = new HashMap<>();
74+
env.put(entry[0], "1");
75+
assertEquals(
76+
Optional.of(entry[1]),
77+
AgentDetector.detect(envWith(env)),
78+
"Agent with env var " + entry[0] + " should be detected as " + entry[1]);
79+
}
80+
}
81+
}

0 commit comments

Comments
 (0)