Skip to content

Commit 825142a

Browse files
authored
Improve User-Agent attribution for agentic callers (#10485)
1 parent d519fa2 commit 825142a

4 files changed

Lines changed: 110 additions & 1 deletion

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"type": "enhancement",
3+
"category": "User Agent",
4+
"description": "Improve User-Agent attribution for agentic callers."
5+
}

awscli/botocore/useragent.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,29 @@
104104
'FLEXIBLE_CHECKSUMS_REQ_XXHASH3': 'AG',
105105
'FLEXIBLE_CHECKSUMS_REQ_XXHASH64': 'AH',
106106
'FLEXIBLE_CHECKSUMS_REQ_XXHASH128': 'AI',
107+
'AGENTIC_CALLER_CLAUDE_CODE': 'AN',
108+
'AGENTIC_CALLER_GEMINI_CLI': 'AO',
109+
'AGENTIC_CALLER_CODEX': 'AP',
110+
'AGENTIC_CALLER_KIRO': 'AQ',
111+
'AGENTIC_CALLER_OPENCODE': 'AR',
112+
'AGENTIC_CALLER_ANTIGRAVITY': 'AS',
113+
'AGENTIC_CALLER_AMP': 'AT',
114+
'AGENTIC_CALLER_PI': 'AU',
115+
'AGENTIC_CALLER_COPILOT_CLI': 'AV',
116+
'AGENTIC_CALLER_CURSOR': 'AW',
107117
}
118+
_USERAGENT_AGENTIC_CALLER_ENV_VAR_MAPPINGS = (
119+
('CLAUDECODE', 'AGENTIC_CALLER_CLAUDE_CODE', '1'),
120+
('GEMINI_CLI', 'AGENTIC_CALLER_GEMINI_CLI', '1'),
121+
('CODEX_THREAD_ID', 'AGENTIC_CALLER_CODEX', None),
122+
('KIRO_SESSION_ID', 'AGENTIC_CALLER_KIRO', None),
123+
('OPENCODE', 'AGENTIC_CALLER_OPENCODE', None),
124+
('ANTIGRAVITY_AGENT', 'AGENTIC_CALLER_ANTIGRAVITY', None),
125+
('AMP_CURRENT_THREAD_ID', 'AGENTIC_CALLER_AMP', None),
126+
('PI_CODING_AGENT', 'AGENTIC_CALLER_PI', 'true'),
127+
('COPILOT_CLI', 'AGENTIC_CALLER_COPILOT_CLI', '1'),
128+
('CURSOR_AGENT', 'AGENTIC_CALLER_CURSOR', '1'),
129+
)
108130

109131

110132
def register_feature_id(feature_id):
@@ -136,6 +158,27 @@ def register_feature_ids(feature_ids):
136158
register_feature_id(feature_id)
137159

138160

161+
def _agentic_env_var_is_set(env_var, expected_value):
162+
"""
163+
Returns true if an environment variable is set,
164+
optionally with specific value
165+
"""
166+
value = os.environ.get(env_var)
167+
if expected_value is None:
168+
return value not in (None, '')
169+
return value == expected_value
170+
171+
172+
def _register_agentic_caller_env_features():
173+
for (
174+
env_var,
175+
feature_id,
176+
expected_value,
177+
) in _USERAGENT_AGENTIC_CALLER_ENV_VAR_MAPPINGS:
178+
if _agentic_env_var_is_set(env_var, expected_value):
179+
register_feature_id(feature_id)
180+
181+
139182
def sanitize_user_agent_string_component(raw_str, allow_hash):
140183
"""Replaces all not allowed characters in the string with a dash ("-").
141184
@@ -549,6 +592,7 @@ def _build_feature_metadata(self):
549592
Returns a single component with prefix "m" followed by a list of
550593
comma-separated metric values.
551594
"""
595+
_register_agentic_caller_env_features()
552596
ctx = get_context()
553597
context_features = set() if ctx is None else ctx.features
554598
client_features = self._client_features or set()

tests/functional/botocore/test_useragent.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,27 @@ def captured_ua_string(self):
5757
return None
5858

5959

60+
_AGENTIC_CALLER_ENV_FEATURE_MAPPINGS = (
61+
('CLAUDECODE', '1', 'AN'),
62+
('GEMINI_CLI', '1', 'AO'),
63+
('CODEX_THREAD_ID', 'thread-id', 'AP'),
64+
('KIRO_SESSION_ID', 'session-id', 'AQ'),
65+
('OPENCODE', '1', 'AR'),
66+
('ANTIGRAVITY_AGENT', '1', 'AS'),
67+
('AMP_CURRENT_THREAD_ID', 'thread-id', 'AT'),
68+
('PI_CODING_AGENT', 'true', 'AU'),
69+
('COPILOT_CLI', '1', 'AV'),
70+
('CURSOR_AGENT', '1', 'AW'),
71+
)
72+
73+
74+
@pytest.fixture(autouse=True)
75+
def clear_agentic_caller_env(monkeypatch):
76+
"""Unset agentic tool env vars that could affect user-agent tests."""
77+
for env_var, _, _ in _AGENTIC_CALLER_ENV_FEATURE_MAPPINGS:
78+
monkeypatch.delenv(env_var, raising=False)
79+
80+
6081
@pytest.mark.parametrize(
6182
'sess_name, sess_version, sess_extra, cfg_extra, cfg_appid',
6283
# Produce every combination of User-Agent related config settings other
@@ -229,6 +250,22 @@ def test_user_agent_has_registered_feature_id(patched_session):
229250
assert 'C' in feature_list
230251

231252

253+
@pytest.mark.parametrize(
254+
'env_var, env_value, expected_feature',
255+
_AGENTIC_CALLER_ENV_FEATURE_MAPPINGS,
256+
)
257+
def test_user_agent_has_agentic_caller_feature_id(
258+
patched_session, monkeypatch, env_var, env_value, expected_feature
259+
):
260+
monkeypatch.setenv(env_var, env_value)
261+
client_s3 = patched_session.create_client('s3')
262+
with UACapHTTPStubber(client_s3) as stub_client:
263+
client_s3.list_buckets()
264+
265+
feature_list = parse_registered_feature_ids(stub_client.captured_ua_string)
266+
assert expected_feature in feature_list
267+
268+
232269
def test_registered_feature_ids_dont_bleed_between_requests(patched_session):
233270
client_s3 = patched_session.create_client('s3')
234271
with UACapHTTPStubber(client_s3) as stub_client:

tests/unit/botocore/test_useragent.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,29 @@
2626
sanitize_user_agent_string_component,
2727
)
2828

29+
_AGENTIC_CALLER_ENV_VARS = (
30+
'CLAUDECODE',
31+
'GEMINI_CLI',
32+
'CODEX_THREAD_ID',
33+
'KIRO_SESSION_ID',
34+
'OPENCODE',
35+
'ANTIGRAVITY_AGENT',
36+
'AMP_CURRENT_THREAD_ID',
37+
'PI_CODING_AGENT',
38+
'COPILOT_CLI',
39+
'CURSOR_AGENT',
40+
)
41+
42+
43+
@pytest.fixture(autouse=True)
44+
def clear_agentic_caller_env(monkeypatch):
45+
"""
46+
Unset any env vars from agentic tools running the user-agent
47+
tests which could influence the user-agent
48+
"""
49+
for env_var in _AGENTIC_CALLER_ENV_VARS:
50+
monkeypatch.delenv(env_var, raising=False)
51+
2952

3053
@pytest.mark.parametrize(
3154
'raw_str, allow_hash, expected_str',
@@ -259,4 +282,4 @@ def test_hash_in_user_agent_appid():
259282
'exec-env/AWS_Lambda_python3.8 '
260283
'app/fooapp#1.0.0'
261284
)
262-
assert actual == expected
285+
assert actual == expected

0 commit comments

Comments
 (0)