Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/next-release/enhancement-UserAgent-78213.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "enhancement",
"category": "User Agent",
"description": "Improve User-Agent attribution for agentic callers."
}
44 changes: 44 additions & 0 deletions awscli/botocore/useragent.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,29 @@
'FLEXIBLE_CHECKSUMS_REQ_XXHASH3': 'AG',
'FLEXIBLE_CHECKSUMS_REQ_XXHASH64': 'AH',
'FLEXIBLE_CHECKSUMS_REQ_XXHASH128': 'AI',
'AGENTIC_CALLER_CLAUDE_CODE': 'AN',
'AGENTIC_CALLER_GEMINI_CLI': 'AO',
'AGENTIC_CALLER_CODEX': 'AP',
'AGENTIC_CALLER_KIRO': 'AQ',
'AGENTIC_CALLER_OPENCODE': 'AR',
'AGENTIC_CALLER_ANTIGRAVITY': 'AS',
'AGENTIC_CALLER_AMP': 'AT',
'AGENTIC_CALLER_PI': 'AU',
'AGENTIC_CALLER_COPILOT_CLI': 'AV',
'AGENTIC_CALLER_CURSOR': 'AW',
}
_USERAGENT_AGENTIC_CALLER_ENV_VAR_MAPPINGS = (
('CLAUDECODE', 'AGENTIC_CALLER_CLAUDE_CODE', '1'),
('GEMINI_CLI', 'AGENTIC_CALLER_GEMINI_CLI', '1'),
('CODEX_THREAD_ID', 'AGENTIC_CALLER_CODEX', None),
('KIRO_SESSION_ID', 'AGENTIC_CALLER_KIRO', None),
('OPENCODE', 'AGENTIC_CALLER_OPENCODE', None),
('ANTIGRAVITY_AGENT', 'AGENTIC_CALLER_ANTIGRAVITY', None),
('AMP_CURRENT_THREAD_ID', 'AGENTIC_CALLER_AMP', None),
('PI_CODING_AGENT', 'AGENTIC_CALLER_PI', 'true'),
('COPILOT_CLI', 'AGENTIC_CALLER_COPILOT_CLI', '1'),
('CURSOR_AGENT', 'AGENTIC_CALLER_CURSOR', '1'),
)


def register_feature_id(feature_id):
Expand Down Expand Up @@ -136,6 +158,27 @@ def register_feature_ids(feature_ids):
register_feature_id(feature_id)


def _agentic_env_var_is_set(env_var, expected_value):
"""
Returns true if an environment variable is set,
optionally with specific value
"""
value = os.environ.get(env_var)
if expected_value is None:
return value not in (None, '')
return value == expected_value


def _register_agentic_caller_env_features():
for (
env_var,
feature_id,
expected_value,
) in _USERAGENT_AGENTIC_CALLER_ENV_VAR_MAPPINGS:
if _agentic_env_var_is_set(env_var, expected_value):
register_feature_id(feature_id)


def sanitize_user_agent_string_component(raw_str, allow_hash):
"""Replaces all not allowed characters in the string with a dash ("-").

Expand Down Expand Up @@ -549,6 +592,7 @@ def _build_feature_metadata(self):
Returns a single component with prefix "m" followed by a list of
comma-separated metric values.
"""
_register_agentic_caller_env_features()

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is pretty late, let me know if you think there's a better place (somewhere in session creation?)

ctx = get_context()
context_features = set() if ctx is None else ctx.features
client_features = self._client_features or set()
Expand Down
37 changes: 37 additions & 0 deletions tests/functional/botocore/test_useragent.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,27 @@ def captured_ua_string(self):
return None


_AGENTIC_CALLER_ENV_FEATURE_MAPPINGS = (
('CLAUDECODE', '1', 'AN'),
('GEMINI_CLI', '1', 'AO'),
('CODEX_THREAD_ID', 'thread-id', 'AP'),
('KIRO_SESSION_ID', 'session-id', 'AQ'),
('OPENCODE', '1', 'AR'),
('ANTIGRAVITY_AGENT', '1', 'AS'),
('AMP_CURRENT_THREAD_ID', 'thread-id', 'AT'),
('PI_CODING_AGENT', 'true', 'AU'),
('COPILOT_CLI', '1', 'AV'),
('CURSOR_AGENT', '1', 'AW'),
)


@pytest.fixture(autouse=True)
def clear_agentic_caller_env(monkeypatch):
"""Unset agentic tool env vars that could affect user-agent tests."""
for env_var, _, _ in _AGENTIC_CALLER_ENV_FEATURE_MAPPINGS:
monkeypatch.delenv(env_var, raising=False)


@pytest.mark.parametrize(
'sess_name, sess_version, sess_extra, cfg_extra, cfg_appid',
# Produce every combination of User-Agent related config settings other
Expand Down Expand Up @@ -229,6 +250,22 @@ def test_user_agent_has_registered_feature_id(patched_session):
assert 'C' in feature_list


@pytest.mark.parametrize(
'env_var, env_value, expected_feature',
_AGENTIC_CALLER_ENV_FEATURE_MAPPINGS,
)
def test_user_agent_has_agentic_caller_feature_id(
patched_session, monkeypatch, env_var, env_value, expected_feature
):
monkeypatch.setenv(env_var, env_value)
client_s3 = patched_session.create_client('s3')
with UACapHTTPStubber(client_s3) as stub_client:
client_s3.list_buckets()

feature_list = parse_registered_feature_ids(stub_client.captured_ua_string)
assert expected_feature in feature_list


def test_registered_feature_ids_dont_bleed_between_requests(patched_session):
client_s3 = patched_session.create_client('s3')
with UACapHTTPStubber(client_s3) as stub_client:
Expand Down
25 changes: 24 additions & 1 deletion tests/unit/botocore/test_useragent.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,29 @@
sanitize_user_agent_string_component,
)

_AGENTIC_CALLER_ENV_VARS = (
'CLAUDECODE',
'GEMINI_CLI',
'CODEX_THREAD_ID',
'KIRO_SESSION_ID',
'OPENCODE',
'ANTIGRAVITY_AGENT',
'AMP_CURRENT_THREAD_ID',
'PI_CODING_AGENT',
'COPILOT_CLI',
'CURSOR_AGENT',
)


@pytest.fixture(autouse=True)
def clear_agentic_caller_env(monkeypatch):
"""
Unset any env vars from agentic tools running the user-agent
tests which could influence the user-agent
"""
for env_var in _AGENTIC_CALLER_ENV_VARS:
monkeypatch.delenv(env_var, raising=False)


@pytest.mark.parametrize(
'raw_str, allow_hash, expected_str',
Expand Down Expand Up @@ -259,4 +282,4 @@ def test_hash_in_user_agent_appid():
'exec-env/AWS_Lambda_python3.8 '
'app/fooapp#1.0.0'
)
assert actual == expected
assert actual == expected
Loading