Skip to content

Set a base User-Agent and append agent/<id> when a coding agent is detected - #509

Draft
ctufts wants to merge 4 commits into
mainfrom
ctufts/AGI-1012-node-user-agent-tag
Draft

Set a base User-Agent and append agent/<id> when a coding agent is detected#509
ctufts wants to merge 4 commits into
mainfrom
ctufts/AGI-1012-node-user-agent-tag

Conversation

@ctufts

@ctufts ctufts commented Jul 17, 2026

Copy link
Copy Markdown

Summary

The Node SDK doesn't send its own User-Agent today; requests just get got's default (got/11.8.5 ...). This PR gives us a real one: mapbox-sdk-js/<version> on every request, with agent/<id> appended when the process env shows we're running under a coding agent (Claude Code, Codex, Cursor, etc.).

This changes the User-Agent on every Node request the SDK makes, which shows up in server-side logs for every consumer. Calling that out explicitly since it needs reviewer sign-off before merge.

Changes

  • lib/helpers/agent-detect.js (new): Node-only detectAgent(). Checks a hardcoded allowlist of about 24 known coding-agent env indicators in priority order, then falls back to AI_AGENT/AGENT if set. Fallback values get validated against a safe charset (/^[\w.-]{1,64}$/) first, since an env var could contain anything and a stray newline shouldn't be able to crash every request. Returns null outside Node (browser bundle) or if reading process.env throws.
  • lib/helpers/sdk-version.js (new): resolves mapbox-sdk-js/<version> from package.json at runtime so the UA can't drift from the published version.
  • lib/classes/mapi-request.js: defaultHeaders now always sets user-agent, appending agent/<id> when detected. A caller-supplied User-Agent still overrides it, no duplicate header.
  • lib/browser/browser-layer.js: filters user-agent out before calling xhr.setRequestHeader, since browsers won't let scripts set User-Agent via XHR. This is what broke while testing the change (see Testing below). Browser agent-tagging itself is out of scope here; that'll need its own mechanism.
  • rollup.config.js / package.json: added rollup-plugin-json so the UMD browser bundle can inline the version for sdk-version.js.

Testing

Full suite passes (490/490), with new coverage for the allowlist, the AI_AGENT/AGENT fallback and its charset validation (including a header-injection attempt with an embedded newline), the Node/browser guard, and the header merge/override path. Lint and bundle both clean. Manually verified against a real MapiRequest: CLAUDECODE=1 produces mapbox-sdk-js/0.16.3 agent/claude-code, a clean env produces just the base UA, and an AI_AGENT value with a newline gets rejected without crashing.

Downstream impact

Every Node consumer's requests to api.mapbox.com will show a new User-Agent going forward. Worth knowing if anyone has UA-based logging, analytics, or allowlisting downstream. No change for browser consumers (the header gets filtered before it reaches the network, same as today). No change to auth, retries, or other request semantics.

Operational considerations

None. Pure client-side header change, no deploy or feature flag needed. Takes effect on the next version bump.

…tected

The Node SDK sends no User-Agent today, so requests fall back to got's
default. Establish mapbox-sdk-js/<version> as the base UA on every request,
and append " agent/<id>" when a coding-agent env indicator is present
(Node only; the allowlist + precedence mirrors mapbox/tilesets-cli's
agent_detect.py). The browser client explicitly filters the user-agent
header back out, since browsers forbid script-set UA via XHR.
@ctufts ctufts added the locationai-team-skills Opened via the location-ai PR creation skill label Jul 17, 2026
Per review feedback, the fallback detector should only check whether
AI_AGENT/AGENT is present, never read and forward its value - an
arbitrary, unvalidated string should not become an "agent id" in
production telemetry. Fold the fallback into the allowlist itself as
its lowest-precedence entry, returning a fixed `custom-agent` id on
presence instead of the variable's value. This also removes the
charset/length validation that existed only to sanitize that value,
since every returned id is now a fixed literal.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@ox-security

ox-security Bot commented Sep 10, 2026

Copy link
Copy Markdown

OX Security Logo

OX Security reviewed this pull request — nothing to fix.

No issues found

Branch ctufts/AGI-1012-node-user-agent-tagmain

View scan in OX Security →

ctufts and others added 2 commits September 10, 2026 14:47
The allowlist's presence-check branch (expectedValue === null) required
a non-empty, non-whitespace value, so an env var explicitly set to ""
or whitespace was treated as unset. Per feedback, existence is all that
should matter - whether the key is present at all, not what it's set
to. Check membership directly instead of trimming and testing
truthiness.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Every allowlist entry now tests presence only, never a value: collapse
the table from (agentId, [[envVar, expectedValueOrNull], ...]) to a
flat (agentId, [envVar, ...]), and drop the equality-check branch in
scanEnv entirely.

The warp entry compared TERM_PROGRAM against "WarpTerminal" - dropped
outright, since TERM_PROGRAM is set by most terminal emulators (iTerm2,
Apple Terminal, VS Code, Hyper, ...), not just Warp, and an
existence-only check on it would misidentify most terminal sessions.
vtcode's VTCODE has no such collision risk and stays as a plain
presence check.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

locationai-team-skills Opened via the location-ai PR creation skill

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant