From 7fc52b62d2a9a273b39cc01318426aa233fa89b7 Mon Sep 17 00:00:00 2001 From: Vikrant Puppala Date: Tue, 24 Feb 2026 08:51:02 +0000 Subject: [PATCH] Add AI coding agent detection to User-Agent header Detect when the Node.js SQL driver is invoked by an AI coding agent (e.g. Claude Code, Cursor, Gemini CLI) by checking well-known environment variables, and append `agent/` 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 Signed-off-by: Vikrant Puppala --- lib/utils/agentDetector.ts | 38 ++++++++++++++++++++++++++ lib/utils/buildUserAgentString.ts | 10 ++++++- tests/unit/utils/agentDetector.test.ts | 36 ++++++++++++++++++++++++ tests/unit/utils/utils.test.ts | 17 +++++++++++- 4 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 lib/utils/agentDetector.ts create mode 100644 tests/unit/utils/agentDetector.test.ts diff --git a/lib/utils/agentDetector.ts b/lib/utils/agentDetector.ts new file mode 100644 index 00000000..1c4fff04 --- /dev/null +++ b/lib/utils/agentDetector.ts @@ -0,0 +1,38 @@ +/** + * Detects whether the Node.js SQL driver is being invoked by an AI coding agent + * by checking for well-known environment variables that agents set in their + * spawned shell processes. + * + * Detection only succeeds when exactly one agent environment variable is present, + * to avoid ambiguous attribution when multiple agent environments overlap. + * + * Adding a new agent requires only a new entry in `knownAgents`. + * + * References for each environment variable: + * - ANTIGRAVITY_AGENT: Closed source. Google Antigravity sets this variable. + * - CLAUDECODE: https://github.com/anthropics/claude-code (sets CLAUDECODE=1) + * - CLINE_ACTIVE: https://github.com/cline/cline (shipped in v3.24.0) + * - CODEX_CI: https://github.com/openai/codex (part of UNIFIED_EXEC_ENV array in codex-rs) + * - CURSOR_AGENT: Closed source. Referenced in a gist by johnlindquist. + * - GEMINI_CLI: https://google-gemini.github.io/gemini-cli/docs/tools/shell.html (sets GEMINI_CLI=1) + * - OPENCODE: https://github.com/opencode-ai/opencode (sets OPENCODE=1) + */ + +const knownAgents: Array<{ envVar: string; product: string }> = [ + { envVar: 'ANTIGRAVITY_AGENT', product: 'antigravity' }, + { envVar: 'CLAUDECODE', product: 'claude-code' }, + { envVar: 'CLINE_ACTIVE', product: 'cline' }, + { envVar: 'CODEX_CI', product: 'codex' }, + { envVar: 'CURSOR_AGENT', product: 'cursor' }, + { envVar: 'GEMINI_CLI', product: 'gemini-cli' }, + { envVar: 'OPENCODE', product: 'opencode' }, +]; + +export default function detectAgent(env: Record = process.env): string { + const detected = knownAgents.filter((a) => env[a.envVar]).map((a) => a.product); + + if (detected.length === 1) { + return detected[0]; + } + return ''; +} diff --git a/lib/utils/buildUserAgentString.ts b/lib/utils/buildUserAgentString.ts index cd021e93..5d9db66e 100644 --- a/lib/utils/buildUserAgentString.ts +++ b/lib/utils/buildUserAgentString.ts @@ -1,5 +1,6 @@ import os from 'os'; import packageVersion from '../version'; +import detectAgent from './agentDetector'; const productName = 'NodejsDatabricksSqlConnector'; @@ -27,5 +28,12 @@ export default function buildUserAgentString(userAgentEntry?: string): string { } const extra = [userAgentEntry, getNodeVersion(), getOperatingSystemVersion()].filter(Boolean); - return `${productName}/${packageVersion} (${extra.join('; ')})`; + let ua = `${productName}/${packageVersion} (${extra.join('; ')})`; + + const agentProduct = detectAgent(); + if (agentProduct) { + ua += ` agent/${agentProduct}`; + } + + return ua; } diff --git a/tests/unit/utils/agentDetector.test.ts b/tests/unit/utils/agentDetector.test.ts new file mode 100644 index 00000000..66686e13 --- /dev/null +++ b/tests/unit/utils/agentDetector.test.ts @@ -0,0 +1,36 @@ +import { expect } from 'chai'; +import detectAgent from '../../../lib/utils/agentDetector'; + +describe('detectAgent', () => { + const allAgents = [ + { envVar: 'ANTIGRAVITY_AGENT', product: 'antigravity' }, + { envVar: 'CLAUDECODE', product: 'claude-code' }, + { envVar: 'CLINE_ACTIVE', product: 'cline' }, + { envVar: 'CODEX_CI', product: 'codex' }, + { envVar: 'CURSOR_AGENT', product: 'cursor' }, + { envVar: 'GEMINI_CLI', product: 'gemini-cli' }, + { envVar: 'OPENCODE', product: 'opencode' }, + ]; + + for (const { envVar, product } of allAgents) { + it(`detects ${product} when ${envVar} is set`, () => { + expect(detectAgent({ [envVar]: '1' })).to.equal(product); + }); + } + + it('returns empty string when no agent is detected', () => { + expect(detectAgent({})).to.equal(''); + }); + + it('returns empty string when multiple agents are detected', () => { + expect(detectAgent({ CLAUDECODE: '1', CURSOR_AGENT: '1' })).to.equal(''); + }); + + it('ignores empty env var values', () => { + expect(detectAgent({ CLAUDECODE: '' })).to.equal(''); + }); + + it('ignores undefined env var values', () => { + expect(detectAgent({ CLAUDECODE: undefined })).to.equal(''); + }); +}); diff --git a/tests/unit/utils/utils.test.ts b/tests/unit/utils/utils.test.ts index ed96326e..13a535cc 100644 --- a/tests/unit/utils/utils.test.ts +++ b/tests/unit/utils/utils.test.ts @@ -32,7 +32,7 @@ describe('buildUserAgentString', () => { // Prefix: 'NodejsDatabricksSqlConnector/' // Version: three period-separated digits and optional suffix const re = - /^(?NodejsDatabricksSqlConnector)\/(?\d+\.\d+\.\d+(-[^(]+)?)\s*\((?[^)]+)\)$/i; + /^(?NodejsDatabricksSqlConnector)\/(?\d+\.\d+\.\d+(-[^(]+)?)\s*\((?[^)]+)\)(\s+agent\/[a-z-]+)?$/i; const match = re.exec(ua); expect(match).to.not.be.eq(null); @@ -62,6 +62,21 @@ describe('buildUserAgentString', () => { const userAgentString = buildUserAgentString(userAgentEntry); expect(userAgentString).to.include(''); }); + + it('appends agent suffix when agent env var is set', () => { + const orig = process.env.CLAUDECODE; + try { + process.env.CLAUDECODE = '1'; + const ua = buildUserAgentString(); + expect(ua).to.include('agent/claude-code'); + } finally { + if (orig === undefined) { + delete process.env.CLAUDECODE; + } else { + process.env.CLAUDECODE = orig; + } + } + }); }); describe('formatProgress', () => {