Skip to content

Tag clickhouse.com requests with detected AI agent#134

Merged
sdairs merged 9 commits into
issue-126-markdown-tablesfrom
issue-133-detect-ai-agent
May 5, 2026
Merged

Tag clickhouse.com requests with detected AI agent#134
sdairs merged 9 commits into
issue-126-markdown-tablesfrom
issue-133-detect-ai-agent

Conversation

@sdairs
Copy link
Copy Markdown
Collaborator

@sdairs sdairs commented May 4, 2026

Summary

Closes #133. Detects when clickhousectl is invoked under an AI coding agent (Claude Code, Cursor, Gemini CLI, OpenAI Codex, Goose, Devin, etc.) and surfaces that signal in the User-Agent header on every outbound HTTP request, so server-side analytics can attribute usage.

When an agent is detected, requests carry:

User-Agent: clickhousectl/0.1.18 (agent=claude-code)

Human-driven invocations send the bare clickhousectl/0.1.18. Detection is via the is-ai-agent crate (AgentId::as_str returns the canonical kebab-case id). RFC 7231 allows parenthesised comments in User-Agent — this matches conventional shapes (Mozilla/5.0 (Windows NT 10.0; ...)).

The change folds entirely into crates/clickhousectl/src/user_agent.rs. Every outbound reqwest::Client already routes through user_agent::user_agent(), so the new attribution flows through cloud, builds, packages, GitHub, and OAuth requests with zero per-call-site wiring.

Drive-by cleanups in cloud/client.rs

While auditing the call sites I noticed CloudClient::new had ~100 lines of duplicated credential branches. The PR also includes:

  • Collapses the four-branch credential ladder in CloudClient::new into a single resolve_auth helper returning ResolvedAuth { creds, source, base_url }. CloudClient::new shrinks to ~25 lines, the lib client is constructed once in a 2-arm match, and the "no credentials" error message lives in one place.
  • Drops dead api_key/api_secret parameters from resolve_active_auth_source. The only production caller (cloud auth status) always passed (None, None); the lenient half-set CLI-flag branch and its test were protecting a contract no caller exercised.

Stacked on #128 (issue-126-markdown-tables).

Test plan

  • cargo build workspace
  • cargo test --workspace — 442 pass (3 new user_agent tests, 1 trimmed test that was protecting dead code)
  • cargo clippy -p clickhousectl --all-targets — clean
  • cargo clippy -p clickhouse-cloud-api — clean

Closes #133. Uses the is-ai-agent crate to detect when the CLI is invoked under a known agent (Claude Code, Cursor, Gemini CLI, Codex, Goose, Devin, etc.) and appends an agent=<id> query param to outbound requests to ClickHouse-owned hosts (builds.clickhouse.com, packages.clickhouse.com, api.clickhouse.cloud). GitHub and other third-party hosts are not annotated. The cloud library gains a generic Client::with_extra_query_params builder so the CLI can attach the tag to every request.
@sdairs sdairs requested a review from iskakaushik as a code owner May 4, 2026 16:56
@sdairs sdairs temporarily deployed to cloud-integration May 4, 2026 16:57 — with GitHub Actions Inactive
Three of the four credential branches in CloudClient::new differed only in which (key, secret) pair they pulled and which AuthSource label to attach. Introduce ResolvedAuth + resolve_auth() to walk the precedence ladder once, then build the lib client and tag it with the agent param at a single site. resolve_active_auth_source becomes a thin wrapper that preserves its lenient half-set CLI-flag behavior for cloud auth status.
@sdairs sdairs temporarily deployed to cloud-integration May 4, 2026 18:20 — with GitHub Actions Inactive
The helper is only called from cloud auth status, which never has --api-key/--api-secret to pass (the subcommand doesn't accept them). The half-set lenient branch and its test were protecting a contract no production caller exercises. Inlining the only sensible call removes the dead parameters and the dead branch, leaving a one-line wrapper that documents its actual purpose: peek at credential precedence without erroring on the empty case (which auth status needs to render no-creds-configured correctly).
@sdairs sdairs temporarily deployed to cloud-integration May 4, 2026 18:31 — with GitHub Actions Inactive
The local AgentId -> kebab-case match arm was duplicating a mapping the crate already maintains for its AGENT= env var parser. is-ai-agent 0.2.1 exposes AgentId::as_str returning the canonical kebab-case id (claude-code, gemini-cli, etc.) — the inverse of the parser. Switch to it. Replaces our 12-arm match plus exhaustive variant test with a single delegation and a contract test against the upstream lookup. New agents added upstream automatically flow through without code changes here.
@sdairs sdairs temporarily deployed to cloud-integration May 4, 2026 18:53 — with GitHub Actions Inactive
…arams

Rather than thread an agent search param through every clickhouse.com request (with a URL-domain gate, two helpers, and a generic extra-query-params feature on the cloud library), fold the signal into the User-Agent header that every outbound request already carries: clickhousectl/0.1.18 (agent=claude-code). RFC 7231 allows parenthesised comments in User-Agent, and this matches conventional shapes (Mozilla/5.0 etc). Detection still uses is-ai-agent. The change deletes the agent_signal module, the cloud library extra_query_params API + tests, the URL-host parser, and the per-call-site wrappers in version_manager — net -146 lines vs the previous implementation. Every reqwest::Client::builder() in the codebase already calls user_agent::user_agent(), so the new attribution flows through with zero per-call-site wiring.
@sdairs sdairs temporarily deployed to cloud-integration May 4, 2026 19:07 — with GitHub Actions Inactive
It's now a one-line implementation detail of an analytics signal — not user-facing functionality, not configurable, not surprising for a future reader to understand from the user_agent.rs source. Doesn't earn a documentation entry.
@sdairs sdairs temporarily deployed to cloud-integration May 4, 2026 19:18 — with GitHub Actions Inactive
Pull in upstream security fixes flagged by Dependabot. Both are transitive dependencies; lockfile-only update, no API or behaviour changes. Build and full workspace tests pass.
@sdairs sdairs temporarily deployed to cloud-integration May 4, 2026 19:33 — with GitHub Actions Inactive
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@sdairs sdairs mentioned this pull request May 4, 2026
1 task
@sdairs sdairs temporarily deployed to cloud-integration May 5, 2026 17:03 — with GitHub Actions Inactive
@sdairs sdairs merged commit 661a4f4 into issue-126-markdown-tables May 5, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants