From f3dcae67ac00b4bbf2aabe5957d21aea39b2eae2 Mon Sep 17 00:00:00 2001 From: Michael Sverdlov Date: Thu, 6 Aug 2026 18:20:02 +0300 Subject: [PATCH 01/10] Append ai-client and ai-model tokens to agent User-Agent suffix. Build on ExecutionContext.AIClient/AIModel so agent-driven jf invocations advertise client app and model alongside ai-agent on the wire. --- utils/cliutils/utils.go | 22 +++++++++++++++++---- utils/cliutils/utils_test.go | 38 ++++++++++++++++++++++++++++++++---- 2 files changed, 52 insertions(+), 8 deletions(-) diff --git a/utils/cliutils/utils.go b/utils/cliutils/utils.go index 3dd079863..97cd68076 100644 --- a/utils/cliutils/utils.go +++ b/utils/cliutils/utils.go @@ -104,11 +104,25 @@ const AgentUserAgentSuffixFormat = " ai-agent/%s" // table, or the literal "unknown" for the generic AGENT variable — a raw environment // value is never propagated. func GetCliUserAgentWithAgent() string { - userAgent := coreutils.GetCliUserAgent() - if executionContext := commonCommands.DetectExecutionContext(); executionContext.IsAgent { - userAgent += fmt.Sprintf(AgentUserAgentSuffixFormat, executionContext.Agent) + return coreutils.GetCliUserAgent() + agentUserAgentSuffix(commonCommands.DetectExecutionContext()) +} + +// agentUserAgentSuffix renders the agent-attribution suffix appended to the +// User-Agent for agent-driven invocations: one product token per detected axis +// (ai-agent, and — when the harness advertised them — ai-client and ai-model). +// Empty for human invocations, keeping them byte-identical to a plain CLI. +func agentUserAgentSuffix(executionContext commonCommands.ExecutionContext) string { + if !executionContext.IsAgent { + return "" + } + suffix := fmt.Sprintf(AgentUserAgentSuffixFormat, executionContext.Agent) + if executionContext.AIClient != "" { + suffix += fmt.Sprintf(" ai-client/%s", executionContext.AIClient) + } + if executionContext.AIModel != "" { + suffix += fmt.Sprintf(" ai-model/%s", executionContext.AIModel) } - return userAgent + return suffix } func GetCliError(err error, success, failed int, failNoOp bool) error { diff --git a/utils/cliutils/utils_test.go b/utils/cliutils/utils_test.go index 3215a566f..00331553a 100644 --- a/utils/cliutils/utils_test.go +++ b/utils/cliutils/utils_test.go @@ -395,11 +395,18 @@ var agentDetectorEnvVars = []string{ "GEMINI_CLI", "GOOSE_TERMINAL", "CURSOR_AGENT", "CURSOR_CLI", "CURSOR_TRACE_ID", - "COPILOT_CLI", + "COPILOT_CLI", "COPILOT_AGENT_SESSION_ID", "KILO_IPC_SOCKET_PATH", "KILO_SERVER_PASSWORD", - "ROO_CODE_IPC_SOCKET_PATH", - "CODEX_CI", - "AGENT", + "ROO_CODE_IPC_SOCKET_PATH", "ROO_ACTIVE", + "CODEX_CI", "CODEX_THREAD_ID", "CODEX_SANDBOX", + "WINDSURF_AGENT", "CODEIUM_EDITOR_APP_ROOT", + "AIDER_API_KEY", "CLINE_ACTIVE", "OPENCODE", "OPENCODE_CLIENT", + "AMP_CURRENT_THREAD_ID", "AUGMENT_AGENT", "QWEN_CODE", + "ANTIGRAVITY_AGENT", "CRUSH", "IFLOW_CLI", "TRAE_AI_SHELL_ID", + "AI_AGENT", "AGENT", + // Host editor and model axes — cleared so the wire format is deterministic + // regardless of the shell running `go test`. + "TERM_PROGRAM", "JFROG_CLI_AI_MODEL", } func clearAgentEnvVarsForTest(t *testing.T) { @@ -573,6 +580,29 @@ func TestGetCliUserAgentWithAgentNoVersion(t *testing.T) { assert.Equal(t, "jfrog-cli-go ai-agent/claude", GetCliUserAgentWithAgent()) } +func TestGetCliUserAgentWithAgentAppendsHostAndModel(t *testing.T) { + clearAgentEnvVarsForTest(t) + withCliUserAgent(t, "jfrog-cli-go", "2.117.0") + t.Setenv("CURSOR_AGENT", "1") + t.Setenv("TERM_PROGRAM", "vscode") + t.Setenv("JFROG_CLI_AI_MODEL", "opus-4.7") + corecommands.ResetExecutionContextForTest() + + assert.Equal(t, "jfrog-cli-go/2.117.0 ai-agent/cursor ai-client/vscode ai-model/opus-4.7", + GetCliUserAgentWithAgent()) +} + +func TestGetCliUserAgentWithAgentOmitsAbsentAxes(t *testing.T) { + // Host and model are optional: with neither advertised, the suffix is just + // the agent token — byte-identical to the pre-host/model behaviour. + clearAgentEnvVarsForTest(t) + withCliUserAgent(t, "jfrog-cli-go", "2.117.0") + t.Setenv("CLAUDECODE", "1") + corecommands.ResetExecutionContextForTest() + + assert.Equal(t, "jfrog-cli-go/2.117.0 ai-agent/claude", GetCliUserAgentWithAgent()) +} + func TestGetCliUserAgentWithAgentMarkerIsWellFormed(t *testing.T) { clearAgentEnvVarsForTest(t) withCliUserAgent(t, "jfrog-cli-go", "2.117.0") From d39f73f72b6f6ac7b73eaebb4a4b9562b466a039 Mon Sep 17 00:00:00 2001 From: Michael Sverdlov Date: Thu, 6 Aug 2026 18:22:03 +0300 Subject: [PATCH 02/10] Document that agent User-Agent suffix is silent best-effort attribution. --- utils/cliutils/utils.go | 1 + 1 file changed, 1 insertion(+) diff --git a/utils/cliutils/utils.go b/utils/cliutils/utils.go index 97cd68076..e2604b28b 100644 --- a/utils/cliutils/utils.go +++ b/utils/cliutils/utils.go @@ -111,6 +111,7 @@ func GetCliUserAgentWithAgent() string { // User-Agent for agent-driven invocations: one product token per detected axis // (ai-agent, and — when the harness advertised them — ai-client and ai-model). // Empty for human invocations, keeping them byte-identical to a plain CLI. +// Pure string work — never logs and never fails the calling command. func agentUserAgentSuffix(executionContext commonCommands.ExecutionContext) string { if !executionContext.IsAgent { return "" From 13ed1a817fc6abe3eefc7890900181713e23ebdd Mon Sep 17 00:00:00 2001 From: Michael Sverdlov Date: Thu, 6 Aug 2026 18:22:55 +0300 Subject: [PATCH 03/10] Clarify ai-agent / ai-client / ai-model User-Agent axis comments. --- utils/cliutils/utils.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/utils/cliutils/utils.go b/utils/cliutils/utils.go index e2604b28b..44a2d0b62 100644 --- a/utils/cliutils/utils.go +++ b/utils/cliutils/utils.go @@ -108,10 +108,15 @@ func GetCliUserAgentWithAgent() string { } // agentUserAgentSuffix renders the agent-attribution suffix appended to the -// User-Agent for agent-driven invocations: one product token per detected axis -// (ai-agent, and — when the harness advertised them — ai-client and ai-model). -// Empty for human invocations, keeping them byte-identical to a plain CLI. -// Pure string work — never logs and never fails the calling command. +// User-Agent for agent-driven invocations. One product token per axis: +// +// ai-agent/ — which harness invoked jf (e.g. ai-agent/cursor) +// ai-client/ — which app hosts it (e.g. ai-client/vscode) +// ai-model/ — which model it is running (e.g. ai-model/opus-4.7) +// +// Client/model tokens are omitted when empty. The whole suffix is empty for +// human invocations (byte-identical to a plain CLI). Pure string work — never +// logs and never fails the calling command. func agentUserAgentSuffix(executionContext commonCommands.ExecutionContext) string { if !executionContext.IsAgent { return "" From bc7a75af5b1488b5b0fb8c9434f61a51e2789d84 Mon Sep 17 00:00:00 2001 From: Michael Sverdlov Date: Thu, 6 Aug 2026 18:24:24 +0300 Subject: [PATCH 04/10] Shorten agent User-Agent suffix comment. --- utils/cliutils/utils.go | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/utils/cliutils/utils.go b/utils/cliutils/utils.go index 44a2d0b62..5f67636f5 100644 --- a/utils/cliutils/utils.go +++ b/utils/cliutils/utils.go @@ -107,16 +107,8 @@ func GetCliUserAgentWithAgent() string { return coreutils.GetCliUserAgent() + agentUserAgentSuffix(commonCommands.DetectExecutionContext()) } -// agentUserAgentSuffix renders the agent-attribution suffix appended to the -// User-Agent for agent-driven invocations. One product token per axis: -// -// ai-agent/ — which harness invoked jf (e.g. ai-agent/cursor) -// ai-client/ — which app hosts it (e.g. ai-client/vscode) -// ai-model/ — which model it is running (e.g. ai-model/opus-4.7) -// -// Client/model tokens are omitted when empty. The whole suffix is empty for -// human invocations (byte-identical to a plain CLI). Pure string work — never -// logs and never fails the calling command. +// agentUserAgentSuffix appends ai-agent / ai-client / ai-model tokens for +// agent runs; empty for humans. Never logs or fails the command. func agentUserAgentSuffix(executionContext commonCommands.ExecutionContext) string { if !executionContext.IsAgent { return "" From a7b08b5214307d4148c71f824962b8afe5943617 Mon Sep 17 00:00:00 2001 From: Michael Sverdlov Date: Thu, 6 Aug 2026 18:29:00 +0300 Subject: [PATCH 05/10] Use ExecutionContext.Client for the ai-client User-Agent token. --- utils/cliutils/utils.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/utils/cliutils/utils.go b/utils/cliutils/utils.go index 5f67636f5..4d03fa40b 100644 --- a/utils/cliutils/utils.go +++ b/utils/cliutils/utils.go @@ -114,8 +114,8 @@ func agentUserAgentSuffix(executionContext commonCommands.ExecutionContext) stri return "" } suffix := fmt.Sprintf(AgentUserAgentSuffixFormat, executionContext.Agent) - if executionContext.AIClient != "" { - suffix += fmt.Sprintf(" ai-client/%s", executionContext.AIClient) + if executionContext.Client != "" { + suffix += fmt.Sprintf(" ai-client/%s", executionContext.Client) } if executionContext.AIModel != "" { suffix += fmt.Sprintf(" ai-model/%s", executionContext.AIModel) From a29ad8ca5d070b960cf4dd2e2728f5d1291b2ec6 Mon Sep 17 00:00:00 2001 From: Michael Sverdlov Date: Thu, 6 Aug 2026 18:29:56 +0300 Subject: [PATCH 06/10] Use ExecutionContext.Model for the ai-model User-Agent token. --- utils/cliutils/utils.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/utils/cliutils/utils.go b/utils/cliutils/utils.go index 4d03fa40b..2991ca3dd 100644 --- a/utils/cliutils/utils.go +++ b/utils/cliutils/utils.go @@ -117,8 +117,8 @@ func agentUserAgentSuffix(executionContext commonCommands.ExecutionContext) stri if executionContext.Client != "" { suffix += fmt.Sprintf(" ai-client/%s", executionContext.Client) } - if executionContext.AIModel != "" { - suffix += fmt.Sprintf(" ai-model/%s", executionContext.AIModel) + if executionContext.Model != "" { + suffix += fmt.Sprintf(" ai-model/%s", executionContext.Model) } return suffix } From c905a143d662907712c3b8f5de521d247777ef67 Mon Sep 17 00:00:00 2001 From: Michael Sverdlov Date: Thu, 6 Aug 2026 18:52:42 +0300 Subject: [PATCH 07/10] Align agent-detector test env clears with core session markers. Match the tightened detector table (Cursor host role, Kilo/Roo/Windsurf signals) so survey and User-Agent tests stay deterministic. --- main_test.go | 18 +++++++++------ utils/cliutils/utils_test.go | 43 ++++++++++++++++++++---------------- 2 files changed, 35 insertions(+), 26 deletions(-) diff --git a/main_test.go b/main_test.go index c81673082..ed13b8581 100644 --- a/main_test.go +++ b/main_test.go @@ -456,15 +456,19 @@ func TestDockerScanHelp(t *testing.T) { // survey-visibility assertions are deterministic regardless of the shell running // `go test` (e.g. running inside Claude Code, Cursor, etc.). var agentDetectorEnvVars = []string{ - "CLAUDECODE", "CLAUDE_CODE_ENTRYPOINT", + "CLAUDECODE", "CLAUDE_CODE", "CLAUDE_CODE_ENTRYPOINT", "GEMINI_CLI", "GOOSE_TERMINAL", - "CURSOR_AGENT", "CURSOR_CLI", "CURSOR_TRACE_ID", - "COPILOT_CLI", - "KILO_IPC_SOCKET_PATH", "KILO_SERVER_PASSWORD", - "ROO_CODE_IPC_SOCKET_PATH", - "CODEX_CI", - "AGENT", + "CURSOR_AGENT", "CURSOR_TRACE_ID", "CURSOR_EXTENSION_HOST_ROLE", + "COPILOT_CLI", "COPILOT_AGENT_SESSION_ID", "COPILOT_MODEL", "COPILOT_ALLOW_ALL", + "KILOCODE_FEATURE", "KILO_PID", + "ROO_ACTIVE", "ROO_CLI_RUNTIME", + "CODEX_CI", "CODEX_THREAD_ID", "CODEX_SANDBOX", + "WINDSURF_CASCADE_TERMINAL", + "CLINE_ACTIVE", "OPENCODE", "OPENCODE_CLIENT", + "AMP_CURRENT_THREAD_ID", "AUGMENT_AGENT", "QWEN_CODE", + "ANTIGRAVITY_AGENT", "CRUSH", "IFLOW_CLI", "TRAE_AI_SHELL_ID", + "AI_AGENT", "AGENT", } func clearAgentEnvVarsForTest(t *testing.T) { diff --git a/utils/cliutils/utils_test.go b/utils/cliutils/utils_test.go index 00331553a..a8feed2f6 100644 --- a/utils/cliutils/utils_test.go +++ b/utils/cliutils/utils_test.go @@ -391,16 +391,16 @@ func (t *redirectingTransport) RoundTrip(req *http.Request) (*http.Response, err // ShouldHideSurveyLink's agent check is deterministic regardless of the shell // running `go test` (e.g. running inside Claude Code, Cursor, etc.). var agentDetectorEnvVars = []string{ - "CLAUDECODE", "CLAUDE_CODE_ENTRYPOINT", + "CLAUDECODE", "CLAUDE_CODE", "CLAUDE_CODE_ENTRYPOINT", "GEMINI_CLI", "GOOSE_TERMINAL", - "CURSOR_AGENT", "CURSOR_CLI", "CURSOR_TRACE_ID", - "COPILOT_CLI", "COPILOT_AGENT_SESSION_ID", - "KILO_IPC_SOCKET_PATH", "KILO_SERVER_PASSWORD", - "ROO_CODE_IPC_SOCKET_PATH", "ROO_ACTIVE", + "CURSOR_AGENT", "CURSOR_TRACE_ID", "CURSOR_EXTENSION_HOST_ROLE", + "COPILOT_CLI", "COPILOT_AGENT_SESSION_ID", "COPILOT_MODEL", "COPILOT_ALLOW_ALL", + "KILOCODE_FEATURE", "KILO_PID", + "ROO_ACTIVE", "ROO_CLI_RUNTIME", "CODEX_CI", "CODEX_THREAD_ID", "CODEX_SANDBOX", - "WINDSURF_AGENT", "CODEIUM_EDITOR_APP_ROOT", - "AIDER_API_KEY", "CLINE_ACTIVE", "OPENCODE", "OPENCODE_CLIENT", + "WINDSURF_CASCADE_TERMINAL", + "CLINE_ACTIVE", "OPENCODE", "OPENCODE_CLIENT", "AMP_CURRENT_THREAD_ID", "AUGMENT_AGENT", "QWEN_CODE", "ANTIGRAVITY_AGENT", "CRUSH", "IFLOW_CLI", "TRAE_AI_SHELL_ID", "AI_AGENT", "AGENT", @@ -533,25 +533,30 @@ func TestGetCliUserAgentWithAgentPerDetector(t *testing.T) { testCases := []struct { name string envVar string + envValue string // empty → "1" wantAgent string }{ - {"claude code", "CLAUDECODE", "claude"}, - {"claude code entrypoint", "CLAUDE_CODE_ENTRYPOINT", "claude"}, - {"gemini", "GEMINI_CLI", "gemini"}, - {"goose", "GOOSE_TERMINAL", "goose"}, - {"cursor agent", "CURSOR_AGENT", "cursor"}, - {"cursor cli", "CURSOR_CLI", "cursor"}, - {"copilot", "COPILOT_CLI", "copilot"}, - {"kilocode", "KILO_IPC_SOCKET_PATH", "kilocode"}, - {"roo code", "ROO_CODE_IPC_SOCKET_PATH", "roo_code"}, - {"codex", "CODEX_CI", "codex"}, - {"generic agent collapses to unknown", "AGENT", "unknown"}, + {"claude code", "CLAUDECODE", "", "claude"}, + {"claude code entrypoint", "CLAUDE_CODE_ENTRYPOINT", "", "claude"}, + {"gemini", "GEMINI_CLI", "", "gemini"}, + {"goose", "GOOSE_TERMINAL", "", "goose"}, + {"cursor agent", "CURSOR_AGENT", "", "cursor"}, + {"cursor extension host", "CURSOR_EXTENSION_HOST_ROLE", "agent-exec", "cursor"}, + {"copilot", "COPILOT_CLI", "", "copilot"}, + {"kilocode", "KILO_PID", "", "kilocode"}, + {"roo code", "ROO_ACTIVE", "", "roo_code"}, + {"codex", "CODEX_CI", "", "codex"}, + {"generic agent collapses to unknown", "AGENT", "", "unknown"}, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { clearAgentEnvVarsForTest(t) withCliUserAgent(t, "jfrog-cli-go", "2.117.0") - t.Setenv(testCase.envVar, "1") + val := testCase.envValue + if val == "" { + val = "1" + } + t.Setenv(testCase.envVar, val) corecommands.ResetExecutionContextForTest() assert.Equal(t, "jfrog-cli-go/2.117.0 ai-agent/"+testCase.wantAgent, GetCliUserAgentWithAgent()) From e161f41cc8e561f662d48b8c1629ea42735c61cc Mon Sep 17 00:00:00 2001 From: Michael Sverdlov Date: Thu, 6 Aug 2026 19:02:54 +0300 Subject: [PATCH 08/10] Bump jfrog-cli-core to Client/Agent/Model tip for stacked review. Pin require to jfrog-cli-core PR #1602 tip so UA enrichment compiles; keep replace commented (No-Replace gate). --- go.mod | 6 +++--- go.sum | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 71c3056eb..270e6ea9d 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/jfrog/gofrog v1.7.6 github.com/jfrog/jfrog-cli-application v1.0.2-0.20260723152309-34eeb81e2847 github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260804124646-1a5e6a2d3caf - github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260804120604-edaa34435a80 + github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260806155414-14c84dde00d1 github.com/jfrog/jfrog-cli-evidence v0.9.5 github.com/jfrog/jfrog-cli-platform-services v1.10.1-0.20260618062042-6053ab368cab github.com/jfrog/jfrog-cli-security v1.32.1 @@ -247,8 +247,8 @@ require ( //replace github.com/ktrysmt/go-bitbucket => github.com/ktrysmt/go-bitbucket v0.9.80 -// replace github.com/jfrog/jfrog-cli-core/v2 => github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260729061834-1c527b8abaa6 - +// Temporary pin to jfrog-cli-core PR #1602 tip — drop after that merges and re-bump require. +// replace github.com/jfrog/jfrog-cli-core/v2 => github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260806155414-14c84dde00d1 //replace github.com/jfrog/jfrog-client-go => github.com/jfrog/jfrog-client-go v1.54.2-0.20251007084958-5eeaa42c31a6 // replace github.com/jfrog/jfrog-cli-artifactory => github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260723100012-d9e9c3412cb2 diff --git a/go.sum b/go.sum index bc60a4c69..badcfcd9f 100644 --- a/go.sum +++ b/go.sum @@ -408,8 +408,8 @@ github.com/jfrog/jfrog-cli-application v1.0.2-0.20260723152309-34eeb81e2847 h1:w github.com/jfrog/jfrog-cli-application v1.0.2-0.20260723152309-34eeb81e2847/go.mod h1:p8yLtbmCxxQucIbLZKnWu0F+EDtj6NLXbRQCEK/nb6o= github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260804124646-1a5e6a2d3caf h1:HJob3Bsj6FtQ3nq72GGzBWXJ7ZXvUz6rKSGpYGAXwKI= github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260804124646-1a5e6a2d3caf/go.mod h1:UkVDiTbSgtk+7N2ePOsPvjPsgO8r8rJtUchjcnAk08w= -github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260804120604-edaa34435a80 h1:V8wTPQAO/9MMxYFMM5qD08E8QRCmV3EtS8Gh+7SmJzU= -github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260804120604-edaa34435a80/go.mod h1:MygQx8pekgPCXyXnejIAVG9S4ImGcDFmcfRPUug/0d0= +github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260806155414-14c84dde00d1 h1:02LpskT11FvXtpVefsSmZte2ONRGTSiGLQd44BaQ/yY= +github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260806155414-14c84dde00d1/go.mod h1:MygQx8pekgPCXyXnejIAVG9S4ImGcDFmcfRPUug/0d0= github.com/jfrog/jfrog-cli-evidence v0.9.5 h1:YzkoYZtqChStPOxEj1odF7satpv1YPl1Zb/IZ/wZ9kc= github.com/jfrog/jfrog-cli-evidence v0.9.5/go.mod h1:xTtHBeiVg3gbJ7jcx48sMlcWlCsRnvqlPKpbGJt22k0= github.com/jfrog/jfrog-cli-platform-services v1.10.1-0.20260618062042-6053ab368cab h1:Zn/qB8LYhSu82YDtbqXwErN1RPHTHe/a3gQY6Ti/OBE= From c0a29c5778130d068fee865521b628bf7c64cdfc Mon Sep 17 00:00:00 2001 From: Michael Sverdlov Date: Thu, 6 Aug 2026 19:05:26 +0300 Subject: [PATCH 09/10] Align User-Agent axis format consts and tighten review nits. Use matching private format consts for ai-agent/ai-client/ai-model, refresh wire-safety comments, and clear client/model env in main_test. --- main_test.go | 1 + utils/cliutils/utils.go | 46 +++++++++++++++++++---------------------- 2 files changed, 22 insertions(+), 25 deletions(-) diff --git a/main_test.go b/main_test.go index ed13b8581..2ce2d8b97 100644 --- a/main_test.go +++ b/main_test.go @@ -469,6 +469,7 @@ var agentDetectorEnvVars = []string{ "AMP_CURRENT_THREAD_ID", "AUGMENT_AGENT", "QWEN_CODE", "ANTIGRAVITY_AGENT", "CRUSH", "IFLOW_CLI", "TRAE_AI_SHELL_ID", "AI_AGENT", "AGENT", + "TERM_PROGRAM", "JFROG_CLI_AI_MODEL", } func clearAgentEnvVarsForTest(t *testing.T) { diff --git a/utils/cliutils/utils.go b/utils/cliutils/utils.go index 2991ca3dd..691af712c 100644 --- a/utils/cliutils/utils.go +++ b/utils/cliutils/utils.go @@ -74,35 +74,31 @@ func splitAgentNameAndVersion(fullAgentName string) (string, string) { return agentName, agentVersion } -// AgentUserAgentSuffixFormat renders the detected AI agent as an additional RFC 9110 -// User-Agent product token, e.g. "jfrog-cli-go/2.117.0 ai-agent/claude". +// User-Agent product-token formats for the Client → Agent → Model axes +// (e.g. "jfrog-cli-go/2.117.0 ai-agent/claude ai-client/vscode ai-model/opus-4.7"). // -// A product token rather than a comment, for two reasons. It is the native User-Agent -// shape (compare "Mozilla/5.0 … Chrome/120 Safari/537.36"), so anything that splits on -// whitespace and reads name/version pairs surfaces it as a structured component instead -// of discarding it as comment text — and being parsed is the whole point of a census -// signal. And "ai-agent" is unambiguous, where a bare "agent" would collide with this -// codebase's existing use of the word for the CLI itself (see SetCliUserAgentName and -// build-info's agent name). -// -// The product-version slot deliberately carries the harness NAME, not a version: the -// execution-context detector exposes no harness version. Should one ever be wanted, it -// belongs in its own product token rather than crammed in here. -const AgentUserAgentSuffixFormat = " ai-agent/%s" +// Product tokens rather than comments so UA parsers that split on whitespace and +// read name/version pairs keep the census signal. "ai-agent" avoids colliding with +// this codebase's use of "agent" for the CLI itself (SetCliUserAgentName / build-info). +// The version slot of each token carries the axis value (harness/app/model slug), not +// a software version. +const ( + aiAgentUserAgentFormat = " ai-agent/%s" + aiClientUserAgentFormat = " ai-client/%s" + aiModelUserAgentFormat = " ai-model/%s" +) -// GetCliUserAgentWithAgent returns the CLI user-agent, enriched with the AI agent that -// invoked the CLI when one was detected (AGW-86). Without this the agent identity never +// GetCliUserAgentWithAgent returns the CLI user-agent, enriched with Client → Agent → +// Model tokens when an AI agent is detected (AGW-86). Without this the identity never // leaves the machine on the request itself — it reaches the platform only as a label on // a separate telemetry call — so an agent and a human running the same command are // byte-identical on the wire. // -// The value is attribution metadata, NOT a credential: it derives from harness -// environment variables the client sets and can trivially unset or forge. Consumers must -// treat it as a routing/census hint only. +// Attribution metadata, not a credential: harness env vars can be unset or forged. +// Consumers must treat it as a routing/census hint only. // -// Injection-safe by construction: DetectExecutionContext returns a name from a fixed -// table, or the literal "unknown" for the generic AGENT variable — a raw environment -// value is never propagated. +// Wire-safe by construction: Agent is a fixed table name (or "unknown"); Client and +// Model are sanitizeToken'd ([a-z0-9._-], capped) in DetectExecutionContext before use. func GetCliUserAgentWithAgent() string { return coreutils.GetCliUserAgent() + agentUserAgentSuffix(commonCommands.DetectExecutionContext()) } @@ -113,12 +109,12 @@ func agentUserAgentSuffix(executionContext commonCommands.ExecutionContext) stri if !executionContext.IsAgent { return "" } - suffix := fmt.Sprintf(AgentUserAgentSuffixFormat, executionContext.Agent) + suffix := fmt.Sprintf(aiAgentUserAgentFormat, executionContext.Agent) if executionContext.Client != "" { - suffix += fmt.Sprintf(" ai-client/%s", executionContext.Client) + suffix += fmt.Sprintf(aiClientUserAgentFormat, executionContext.Client) } if executionContext.Model != "" { - suffix += fmt.Sprintf(" ai-model/%s", executionContext.Model) + suffix += fmt.Sprintf(aiModelUserAgentFormat, executionContext.Model) } return suffix } From d807597f53180db147cb1a1e1495d52cc58cbe7e Mon Sep 17 00:00:00 2001 From: Michael Sverdlov Date: Thu, 6 Aug 2026 19:51:09 +0300 Subject: [PATCH 10/10] Sync agent UA tests with strong session-marker detectors. Bump jfrog-cli-core to the tip that drops IDE/config false positives and update clear-lists plus CLAUDE_CODE_CHILD_SESSION fixtures. --- go.mod | 4 ++-- go.sum | 4 ++-- main_test.go | 9 +++++---- metrics_visibility_test.go | 1 + utils/cliutils/utils_test.go | 20 +++++++++++--------- 5 files changed, 21 insertions(+), 17 deletions(-) diff --git a/go.mod b/go.mod index 270e6ea9d..dd717a17d 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/jfrog/gofrog v1.7.6 github.com/jfrog/jfrog-cli-application v1.0.2-0.20260723152309-34eeb81e2847 github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260804124646-1a5e6a2d3caf - github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260806155414-14c84dde00d1 + github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260806165013-a9fb8a640f22 github.com/jfrog/jfrog-cli-evidence v0.9.5 github.com/jfrog/jfrog-cli-platform-services v1.10.1-0.20260618062042-6053ab368cab github.com/jfrog/jfrog-cli-security v1.32.1 @@ -248,7 +248,7 @@ require ( //replace github.com/ktrysmt/go-bitbucket => github.com/ktrysmt/go-bitbucket v0.9.80 // Temporary pin to jfrog-cli-core PR #1602 tip — drop after that merges and re-bump require. -// replace github.com/jfrog/jfrog-cli-core/v2 => github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260806155414-14c84dde00d1 +// replace github.com/jfrog/jfrog-cli-core/v2 => github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260806165013-a9fb8a640f22 //replace github.com/jfrog/jfrog-client-go => github.com/jfrog/jfrog-client-go v1.54.2-0.20251007084958-5eeaa42c31a6 // replace github.com/jfrog/jfrog-cli-artifactory => github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260723100012-d9e9c3412cb2 diff --git a/go.sum b/go.sum index badcfcd9f..ab6cca7b1 100644 --- a/go.sum +++ b/go.sum @@ -408,8 +408,8 @@ github.com/jfrog/jfrog-cli-application v1.0.2-0.20260723152309-34eeb81e2847 h1:w github.com/jfrog/jfrog-cli-application v1.0.2-0.20260723152309-34eeb81e2847/go.mod h1:p8yLtbmCxxQucIbLZKnWu0F+EDtj6NLXbRQCEK/nb6o= github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260804124646-1a5e6a2d3caf h1:HJob3Bsj6FtQ3nq72GGzBWXJ7ZXvUz6rKSGpYGAXwKI= github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260804124646-1a5e6a2d3caf/go.mod h1:UkVDiTbSgtk+7N2ePOsPvjPsgO8r8rJtUchjcnAk08w= -github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260806155414-14c84dde00d1 h1:02LpskT11FvXtpVefsSmZte2ONRGTSiGLQd44BaQ/yY= -github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260806155414-14c84dde00d1/go.mod h1:MygQx8pekgPCXyXnejIAVG9S4ImGcDFmcfRPUug/0d0= +github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260806165013-a9fb8a640f22 h1:uTz6aqLmc8lgQsLOQC7daCiqq2ACSy8g8HmrnuoPato= +github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260806165013-a9fb8a640f22/go.mod h1:MygQx8pekgPCXyXnejIAVG9S4ImGcDFmcfRPUug/0d0= github.com/jfrog/jfrog-cli-evidence v0.9.5 h1:YzkoYZtqChStPOxEj1odF7satpv1YPl1Zb/IZ/wZ9kc= github.com/jfrog/jfrog-cli-evidence v0.9.5/go.mod h1:xTtHBeiVg3gbJ7jcx48sMlcWlCsRnvqlPKpbGJt22k0= github.com/jfrog/jfrog-cli-platform-services v1.10.1-0.20260618062042-6053ab368cab h1:Zn/qB8LYhSu82YDtbqXwErN1RPHTHe/a3gQY6Ti/OBE= diff --git a/main_test.go b/main_test.go index 2ce2d8b97..55c5f30b4 100644 --- a/main_test.go +++ b/main_test.go @@ -456,13 +456,14 @@ func TestDockerScanHelp(t *testing.T) { // survey-visibility assertions are deterministic regardless of the shell running // `go test` (e.g. running inside Claude Code, Cursor, etc.). var agentDetectorEnvVars = []string{ + "CLAUDE_CODE_CHILD_SESSION", "CLAUDECODE", "CLAUDE_CODE", "CLAUDE_CODE_ENTRYPOINT", "GEMINI_CLI", "GOOSE_TERMINAL", - "CURSOR_AGENT", "CURSOR_TRACE_ID", "CURSOR_EXTENSION_HOST_ROLE", + "CURSOR_AGENT", "CURSOR_TRACE_ID", "CURSOR_EXTENSION_HOST_ROLE", "CURSOR_CLI", "COPILOT_CLI", "COPILOT_AGENT_SESSION_ID", "COPILOT_MODEL", "COPILOT_ALLOW_ALL", - "KILOCODE_FEATURE", "KILO_PID", - "ROO_ACTIVE", "ROO_CLI_RUNTIME", + "KILOCODE_FEATURE", "KILO_PID", "KILO_IPC_SOCKET_PATH", "KILO_SERVER_PASSWORD", + "ROO_ACTIVE", "ROO_CLI_RUNTIME", "ROO_CODE_IPC_SOCKET_PATH", "CODEX_CI", "CODEX_THREAD_ID", "CODEX_SANDBOX", "WINDSURF_CASCADE_TERMINAL", "CLINE_ACTIVE", "OPENCODE", "OPENCODE_CLIENT", @@ -501,7 +502,7 @@ func TestSurvey_NotDisplayedOnHelpCI(t *testing.T) { func TestSurvey_NotDisplayedOnHelpAgent(t *testing.T) { t.Setenv("CI", "false") clearAgentEnvVarsForTest(t) - t.Setenv("CLAUDECODE", "true") + t.Setenv("CLAUDE_CODE_CHILD_SESSION", "true") commands.ResetExecutionContextForTest() jfrogCli := coreTests.NewJfrogCli(execMain, "jfrog", "") diff --git a/metrics_visibility_test.go b/metrics_visibility_test.go index 2f0058aa5..3d4d1b6e8 100644 --- a/metrics_visibility_test.go +++ b/metrics_visibility_test.go @@ -166,6 +166,7 @@ func TestVisibility_NoAgent_E2E(t *testing.T) { corecommands.ResetExecutionContextForTest() t.Cleanup(corecommands.ResetExecutionContextForTest) t.Setenv("CURSOR_AGENT", "") + t.Setenv("CLAUDE_CODE_CHILD_SESSION", "") t.Setenv("CLAUDECODE", "") t.Setenv("AGENT", "") diff --git a/utils/cliutils/utils_test.go b/utils/cliutils/utils_test.go index a8feed2f6..1869a1a0d 100644 --- a/utils/cliutils/utils_test.go +++ b/utils/cliutils/utils_test.go @@ -391,13 +391,16 @@ func (t *redirectingTransport) RoundTrip(req *http.Request) (*http.Response, err // ShouldHideSurveyLink's agent check is deterministic regardless of the shell // running `go test` (e.g. running inside Claude Code, Cursor, etc.). var agentDetectorEnvVars = []string{ + "CLAUDE_CODE_CHILD_SESSION", + // Cleared even though no longer detectors — leftover process env must not + // bleed into human / strong-signal assertions. "CLAUDECODE", "CLAUDE_CODE", "CLAUDE_CODE_ENTRYPOINT", "GEMINI_CLI", "GOOSE_TERMINAL", - "CURSOR_AGENT", "CURSOR_TRACE_ID", "CURSOR_EXTENSION_HOST_ROLE", + "CURSOR_AGENT", "CURSOR_TRACE_ID", "CURSOR_EXTENSION_HOST_ROLE", "CURSOR_CLI", "COPILOT_CLI", "COPILOT_AGENT_SESSION_ID", "COPILOT_MODEL", "COPILOT_ALLOW_ALL", - "KILOCODE_FEATURE", "KILO_PID", - "ROO_ACTIVE", "ROO_CLI_RUNTIME", + "KILOCODE_FEATURE", "KILO_PID", "KILO_IPC_SOCKET_PATH", "KILO_SERVER_PASSWORD", + "ROO_ACTIVE", "ROO_CLI_RUNTIME", "ROO_CODE_IPC_SOCKET_PATH", "CODEX_CI", "CODEX_THREAD_ID", "CODEX_SANDBOX", "WINDSURF_CASCADE_TERMINAL", "CLINE_ACTIVE", "OPENCODE", "OPENCODE_CLIENT", @@ -468,7 +471,7 @@ func TestSurveyHiddenForAgent(t *testing.T) { t.Setenv(coreutils.CI, "") t.Setenv(JfrogCliHideSurvey, "") clearAgentEnvVarsForTest(t) - t.Setenv("CLAUDECODE", "true") + t.Setenv("CLAUDE_CODE_CHILD_SESSION", "true") corecommands.ResetExecutionContextForTest() assert.True(t, ShouldHideSurveyLink(), "Expected survey to be hidden when invoked by an agent") @@ -536,8 +539,7 @@ func TestGetCliUserAgentWithAgentPerDetector(t *testing.T) { envValue string // empty → "1" wantAgent string }{ - {"claude code", "CLAUDECODE", "", "claude"}, - {"claude code entrypoint", "CLAUDE_CODE_ENTRYPOINT", "", "claude"}, + {"claude child session", "CLAUDE_CODE_CHILD_SESSION", "", "claude"}, {"gemini", "GEMINI_CLI", "", "gemini"}, {"goose", "GOOSE_TERMINAL", "", "goose"}, {"cursor agent", "CURSOR_AGENT", "", "cursor"}, @@ -569,7 +571,7 @@ func TestGetCliUserAgentWithAgentPreservesCustomUserAgent(t *testing.T) { // marker must be appended to whatever that resolves to, never replace it. clearAgentEnvVarsForTest(t) withCliUserAgent(t, "my-wrapper", "9.9.9") - t.Setenv("CLAUDECODE", "true") + t.Setenv("CLAUDE_CODE_CHILD_SESSION", "true") corecommands.ResetExecutionContextForTest() assert.Equal(t, "my-wrapper/9.9.9 ai-agent/claude", GetCliUserAgentWithAgent()) @@ -579,7 +581,7 @@ func TestGetCliUserAgentWithAgentNoVersion(t *testing.T) { // GetCliUserAgent omits the slash when no version is set; the marker still appends. clearAgentEnvVarsForTest(t) withCliUserAgent(t, "jfrog-cli-go", "") - t.Setenv("CLAUDECODE", "true") + t.Setenv("CLAUDE_CODE_CHILD_SESSION", "true") corecommands.ResetExecutionContextForTest() assert.Equal(t, "jfrog-cli-go ai-agent/claude", GetCliUserAgentWithAgent()) @@ -602,7 +604,7 @@ func TestGetCliUserAgentWithAgentOmitsAbsentAxes(t *testing.T) { // the agent token — byte-identical to the pre-host/model behaviour. clearAgentEnvVarsForTest(t) withCliUserAgent(t, "jfrog-cli-go", "2.117.0") - t.Setenv("CLAUDECODE", "1") + t.Setenv("CLAUDE_CODE_CHILD_SESSION", "1") corecommands.ResetExecutionContextForTest() assert.Equal(t, "jfrog-cli-go/2.117.0 ai-agent/claude", GetCliUserAgentWithAgent())