Skip to content

Commit 04fa424

Browse files
committed
Make explicit env vars win over AGENT fallback in agent detection
Previously, agents like amp and goose had dual matchers: their explicit env var (AMP_CURRENT_THREAD_ID, GOOSE_TERMINAL) plus AGENT=<name>. This caused asymmetric ambiguity: AGENT=goose + CLAUDECODE=1 resolved to "" (both matchers fired on different products), while AGENT=cursor + CLAUDECODE=1 resolved to "claude-code" (only claude-code matched, cursor was handled by the AGENT fallback which does not trigger once an explicit matcher has fired). The rule is now uniform: explicit env var matchers always take precedence over the generic AGENT=<name> signal. AGENT is treated purely as a fallback for agents without an explicit matcher, or for products we do not yet specifically recognize. Changes: - Remove per-agent AGENT=<name> matchers from amp and goose entries. Those products still set AGENT=<name>; the central fallback in lookupAgentProvider handles them. - Update the lookupAgentProvider doc comment to reflect the new rule. - Flip the existing AGENT=goose + CLAUDECODE=1 test to expect "claude-code" and rename accordingly. - Add test for GOOSE_TERMINAL=1 + AGENT=cursor -> "goose". - Add test for COPILOT_CLI=1 + COPILOT_MODEL=gpt-4 -> "" (documents the known, intentional ambiguity for Copilot CLI BYOK users). - Update NEXT_CHANGELOG entry to mention precedence rule. Signed-off-by: simon <simon.faltum@databricks.com>
1 parent e9c4eaa commit 04fa424

3 files changed

Lines changed: 51 additions & 11 deletions

File tree

NEXT_CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
### New Features and Improvements
66
* Add support for authentication through Azure Managed Service Identity (MSI) via the new `azure-msi` credential provider.
77
* Support `default_profile` in `[__settings__]` section of `.databrickscfg` for consistent default profile resolution across CLI and SDKs.
8-
* Added automatic detection of AI coding agents (Amp, Antigravity, Augment, Claude Code, Cline, Codex, Copilot CLI, Copilot VS Code, Cursor, Gemini CLI, Goose, Kiro, OpenClaw, OpenCode, Windsurf) in the user-agent string. The SDK now appends `agent/<name>` to HTTP request headers when running inside a known AI agent environment. Also honors the `AGENT=<name>` standard: when `AGENT` is set to a known product name the SDK reports that product, and when set to an unrecognized non-empty value the SDK reports `agent/unknown`. Environment variables set to the empty string (e.g. `CLAUDECODE=""`) now count as "set" for presence-only matchers, matching `databricks-sdk-go` semantics; previously they were treated as unset.
8+
* Added automatic detection of AI coding agents (Amp, Antigravity, Augment, Claude Code, Cline, Codex, Copilot CLI, Copilot VS Code, Cursor, Gemini CLI, Goose, Kiro, OpenClaw, OpenCode, Windsurf) in the user-agent string. The SDK now appends `agent/<name>` to HTTP request headers when running inside a known AI agent environment. Also honors the `AGENT=<name>` standard: when `AGENT` is set to a known product name the SDK reports that product, and when set to an unrecognized non-empty value the SDK reports `agent/unknown`. Environment variables set to the empty string (e.g. `CLAUDECODE=""`) now count as "set" for presence-only matchers, matching `databricks-sdk-go` semantics; previously they were treated as unset. Explicit agent env vars (e.g. `CLAUDECODE`, `GOOSE_TERMINAL`) always take precedence over the generic `AGENT=<name>` signal.
99

1010
### Bug Fixes
1111
* Fixed non-JSON error responses (e.g. plain-text "Invalid Token" with HTTP 403) producing `Unknown` instead of the correct typed exception (`PermissionDenied`, `Unauthenticated`, etc.). The error message no longer contains Jackson deserialization internals.

databricks-sdk-java/src/main/java/com/databricks/sdk/core/UserAgent.java

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -295,11 +295,11 @@ boolean fires(Environment env) {
295295
// Agents are listed alphabetically by product name.
296296
private static List<KnownAgent> listKnownAgents() {
297297
return Arrays.asList(
298+
// Amp also sets AGENT=amp; handled by the central AGENT fallback.
298299
new KnownAgent(
299300
"amp",
300-
Arrays.asList(
301-
new EnvMatcher("AMP_CURRENT_THREAD_ID"),
302-
new EnvMatcher(AGENT_ENV_VAR, "amp"))), // https://ampcode.com/
301+
Collections.singletonList(
302+
new EnvMatcher("AMP_CURRENT_THREAD_ID"))), // https://ampcode.com/
303303
new KnownAgent(
304304
"antigravity",
305305
Collections.singletonList(
@@ -333,11 +333,11 @@ private static List<KnownAgent> listKnownAgents() {
333333
"gemini-cli",
334334
Collections.singletonList(
335335
new EnvMatcher("GEMINI_CLI"))), // https://google-gemini.github.io/gemini-cli
336+
// Goose also sets AGENT=goose; handled by the central AGENT fallback.
336337
new KnownAgent(
337338
"goose",
338-
Arrays.asList(
339-
new EnvMatcher("GOOSE_TERMINAL"),
340-
new EnvMatcher(AGENT_ENV_VAR, "goose"))), // https://block.github.io/goose/
339+
Collections.singletonList(
340+
new EnvMatcher("GOOSE_TERMINAL"))), // https://block.github.io/goose/
341341
new KnownAgent(
342342
"kiro",
343343
Collections.singletonList(new EnvMatcher("KIRO"))), // https://kiro.dev/ (Amazon)
@@ -357,6 +357,11 @@ private static List<KnownAgent> listKnownAgents() {
357357

358358
// Looks up the active agent provider based on environment variables.
359359
//
360+
// Explicit env var matchers (e.g. CLAUDECODE, GOOSE_TERMINAL) always take
361+
// precedence over the generic AGENT=<name> signal. The AGENT env var is
362+
// treated purely as a fallback for agents that have no explicit matcher, or
363+
// for agents we do not yet specifically recognize.
364+
//
360365
// For each agent, it fires if ANY of its matchers fires. The function counts
361366
// how many distinct agents matched:
362367
// - Exactly one agent matched: return its product name.
@@ -367,8 +372,9 @@ private static List<KnownAgent> listKnownAgents() {
367372
//
368373
// Unlike CI/CD detection (which returns the first match), agent detection
369374
// uses an ambiguity guard because agent env vars can be stacked (e.g.,
370-
// running Cline inside Cursor). Known matchers always win over the AGENT
371-
// fallback, so e.g. AGENT=cursor + CLAUDECODE=1 yields "claude-code".
375+
// running Cline inside Cursor). Because explicit matchers win over AGENT,
376+
// e.g. AGENT=cursor + CLAUDECODE=1 yields "claude-code", and
377+
// AGENT=goose + CLAUDECODE=1 also yields "claude-code".
372378
private static String lookupAgentProvider(Environment env) {
373379
List<KnownAgent> knownAgents = listKnownAgents();
374380
String detected = "";

databricks-sdk-java/src/test/java/com/databricks/sdk/core/UserAgentTest.java

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -385,15 +385,49 @@ public void testAgentProviderAgentEnvEmpty() {
385385
}
386386

387387
@Test
388-
public void testAgentProviderAgentEnvAmbiguity() {
389-
// AGENT=goose fires goose, CLAUDECODE=1 fires claude-code. Ambiguity.
388+
public void testAgentProviderExplicitEnvWinsOverAgentEnv() {
389+
// CLAUDECODE=1 is an explicit matcher and wins over AGENT=goose (which
390+
// is only consulted as a fallback when no explicit matcher fires).
390391
setupAgentEnv(
391392
new HashMap<String, String>() {
392393
{
393394
put("AGENT", "goose");
394395
put("CLAUDECODE", "1");
395396
}
396397
});
398+
Assertions.assertTrue(UserAgent.asString().contains("agent/claude-code"));
399+
}
400+
401+
@Test
402+
public void testAgentProviderExplicitEnvWinsOverKnownAgentEnv() {
403+
// GOOSE_TERMINAL=1 is an explicit matcher; AGENT=cursor (even though
404+
// "cursor" is a known product name) is ignored because an explicit
405+
// matcher already fired.
406+
setupAgentEnv(
407+
new HashMap<String, String>() {
408+
{
409+
put("GOOSE_TERMINAL", "1");
410+
put("AGENT", "cursor");
411+
}
412+
});
413+
Assertions.assertTrue(UserAgent.asString().contains("agent/goose"));
414+
Assertions.assertFalse(UserAgent.asString().contains("agent/cursor"));
415+
}
416+
417+
@Test
418+
public void testAgentProviderCopilotCliAndCopilotVscodeAmbiguous() {
419+
// Copilot CLI can be invoked with BYOK models, which may also set
420+
// COPILOT_MODEL. In that case both copilot-cli and copilot-vscode
421+
// matchers fire on different products, so detection is ambiguous.
422+
// This is intentional: ambiguity is preferred over silently picking
423+
// one product.
424+
setupAgentEnv(
425+
new HashMap<String, String>() {
426+
{
427+
put("COPILOT_CLI", "1");
428+
put("COPILOT_MODEL", "gpt-4");
429+
}
430+
});
397431
Assertions.assertFalse(UserAgent.asString().contains("agent/"));
398432
}
399433

0 commit comments

Comments
 (0)