Skip to content

Commit d223cf8

Browse files
claudeapgiorgi
authored andcommitted
chore: tag User-Agent with mode=agent|interactive
Per PR review, re-add an interface identifier to the User-Agent but in a parseable, analytics-friendly form. The UA now always carries a mode=agent|interactive token (present in both modes, no client prefix) so traffic can be grouped by interface across clients. The token reflects only the interface, never the end user, so the value stays a stable client identifier.
1 parent 7abf8d8 commit d223cf8

3 files changed

Lines changed: 38 additions & 17 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ that is cheap to parse and free of decoration:
9292
- Default `--output` becomes `toon` (compact, token-efficient) instead of `table`
9393
- No color, spinners, or other terminal decoration
9494
- Banners, tips, and status chatter go to **stderr**, leaving **stdout** for data only
95+
- The request `User-Agent` carries `mode=agent` (human invocations carry `mode=interactive`) so API traffic can be segmented by interface
9596

9697
### How agent mode is detected
9798

main.go

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,8 @@ func run() (exitCode int) {
207207
customerContextFlagValue = ""
208208

209209
// Resolve agent mode once up front. Downstream behavior — color, default
210-
// output format, and stderr routing — all key off this.
210+
// output format, stderr routing, and the User-Agent mode token — all key off
211+
// this.
211212
agentEnvDetected = detectedAgentEnv()
212213
if v := strings.TrimSpace(os.Getenv("DCI_AGENT_MODE")); v != "" {
213214
if _, ok := parseBoolish(v); !ok {
@@ -258,10 +259,11 @@ func run() (exitCode int) {
258259
os.Setenv("RSH_PROFILE", "default")
259260
viper.Set("rsh-profile", "default")
260261

261-
// Hardcode user-agent so the DCI API can identify CLI traffic. The value is
262-
// stable across invocations, independent of agent mode or the end user.
263-
// Restish picks this up via rsh-header and skips its own default.
264-
viper.Set("rsh-header", []string{"user-agent:" + buildUserAgent()})
262+
// Hardcode user-agent so the DCI API can identify CLI traffic. It carries a
263+
// mode=agent|interactive token (never the end user) so traffic can be
264+
// segmented by interface. Restish picks this up via rsh-header and skips its
265+
// own default.
266+
viper.Set("rsh-header", []string{"user-agent:" + buildUserAgent(agentMode)})
265267

266268
cli.Load("dci", cli.Root)
267269
applyAPIKeyAuth()
@@ -609,10 +611,16 @@ func parseBoolish(v string) (bool, bool) {
609611
}
610612

611613
// buildUserAgent returns the User-Agent header value identifying CLI traffic to
612-
// the DCI API. It is constant for a given build, independent of agent mode or
613-
// the end user, so the API always sees a stable client identifier.
614-
func buildUserAgent() string {
615-
return fmt.Sprintf("dci-cli/%s (%s; %s/%s)", version, runtime.Version(), runtime.GOOS, runtime.GOARCH)
614+
// the DCI API. It always carries a mode=agent|interactive token so API traffic
615+
// can be segmented by interface (agent- vs human-driven) in analytics. The
616+
// token reflects only the interface, never the end user, so the value stays a
617+
// stable client identifier.
618+
func buildUserAgent(agent bool) string {
619+
mode := "interactive"
620+
if agent {
621+
mode = "agent"
622+
}
623+
return fmt.Sprintf("dci-cli/%s (%s; %s/%s; mode=%s)", version, runtime.Version(), runtime.GOOS, runtime.GOARCH, mode)
616624
}
617625

618626
// defaultOutputFormat is the output format used when --output is not given.

main_test.go

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -460,14 +460,26 @@ func TestAgentFlagOverride(t *testing.T) {
460460
}
461461

462462
func TestBuildUserAgent(t *testing.T) {
463-
ua := buildUserAgent()
464-
if !strings.HasPrefix(ua, "dci-cli/") {
465-
t.Fatalf("unexpected User-Agent prefix: %q", ua)
466-
}
467-
// The User-Agent must be a stable client identifier — it must not vary with
468-
// agent mode or the end user.
469-
if strings.Contains(ua, "agent=1") {
470-
t.Fatalf("User-Agent must not carry agent-mode tag: %q", ua)
463+
human := buildUserAgent(false)
464+
if !strings.HasPrefix(human, "dci-cli/") {
465+
t.Fatalf("unexpected User-Agent prefix: %q", human)
466+
}
467+
// The mode token is always present and self-describing so analytics can
468+
// segment by interface; human mode is explicitly mode=interactive.
469+
if !strings.Contains(human, "mode=interactive") {
470+
t.Fatalf("human User-Agent must carry mode=interactive: %q", human)
471+
}
472+
if strings.Contains(human, "mode=agent") {
473+
t.Fatalf("human User-Agent must not carry mode=agent: %q", human)
474+
}
475+
476+
agent := buildUserAgent(true)
477+
if !strings.Contains(agent, "mode=agent") {
478+
t.Fatalf("agent User-Agent must carry mode=agent: %q", agent)
479+
}
480+
// Guard against the legacy opaque tag regressing.
481+
if strings.Contains(agent, "agent=1") {
482+
t.Fatalf("User-Agent must use mode=agent, not the legacy agent=1 tag: %q", agent)
471483
}
472484
}
473485

0 commit comments

Comments
 (0)