From 4fde4ccf334ffcde78c447c01f819b9ca16a4115 Mon Sep 17 00:00:00 2001 From: Andrew Asseily Date: Tue, 30 Jun 2026 12:46:56 -0400 Subject: [PATCH 1/4] Add SSO_LOGIN_VANITY_URL feature ID to User-Agent on SSO-OIDC requests for vanity URL logins --- awscli/botocore/useragent.py | 1 + awscli/botocore/utils.py | 19 ++++++++++++++++++- .../customizations/configure/sso_commands.py | 2 ++ awscli/customizations/sso/login.py | 5 +++++ awscli/customizations/sso/utils.py | 3 +++ tests/functional/sso/test_login.py | 11 +++++++++++ 6 files changed, 40 insertions(+), 1 deletion(-) diff --git a/awscli/botocore/useragent.py b/awscli/botocore/useragent.py index e1537381fc37..36e5a00d3918 100644 --- a/awscli/botocore/useragent.py +++ b/awscli/botocore/useragent.py @@ -104,6 +104,7 @@ 'FLEXIBLE_CHECKSUMS_REQ_XXHASH3': 'AG', 'FLEXIBLE_CHECKSUMS_REQ_XXHASH64': 'AH', 'FLEXIBLE_CHECKSUMS_REQ_XXHASH128': 'AI', + 'SSO_LOGIN_VANITY_URL': 'AM', } diff --git a/awscli/botocore/utils.py b/awscli/botocore/utils.py index a3ac03e745e8..3746a27597cd 100644 --- a/awscli/botocore/utils.py +++ b/awscli/botocore/utils.py @@ -78,6 +78,7 @@ UnsupportedS3ControlArnError, UnsupportedS3ControlConfigurationError, ) +from botocore.useragent import register_feature_id from dateutil.tz import tzutc from urllib3.exceptions import LocationParseError @@ -3221,11 +3222,13 @@ def __init__( cache=None, on_pending_authorization=None, time_fetcher=None, + feature_ids=None, ): self._sso_region = sso_region self._client_creator = client_creator self._parsed_globals = parsed_globals self._on_pending_authorization = on_pending_authorization + self._feature_ids = feature_ids or [] if time_fetcher is None: time_fetcher = self._utc_now @@ -3275,11 +3278,21 @@ def _client(self): signature_version=botocore.UNSIGNED, user_agent_extra=self._USER_AGENT_EXTRA, ) - return self._client_creator( + client = self._client_creator( 'sso-oidc', config=config, verify=self._parsed_globals.verify_ssl, ) + if self._feature_ids: + client.meta.events.register( + 'before-parameter-build.sso-oidc.*', + self._register_feature_ids, + ) + return client + + def _register_feature_ids(self, **kwargs): + for feature_id in self._feature_ids: + register_feature_id(feature_id) def _generate_client_name(self, session_name): if session_name is None: @@ -3330,6 +3343,7 @@ def __init__( on_pending_authorization=None, time_fetcher=None, sleep=None, + feature_ids=None, ): super().__init__( sso_region, @@ -3338,6 +3352,7 @@ def __init__( cache, on_pending_authorization, time_fetcher, + feature_ids=feature_ids, ) if sleep is None: @@ -3553,6 +3568,7 @@ def __init__( cache=None, on_pending_authorization=None, time_fetcher=None, + feature_ids=None, ): super().__init__( sso_region, @@ -3561,6 +3577,7 @@ def __init__( cache, on_pending_authorization, time_fetcher, + feature_ids=feature_ids, ) self._auth_code_fetcher = auth_code_fetcher diff --git a/awscli/customizations/configure/sso_commands.py b/awscli/customizations/configure/sso_commands.py index cfd90a3beba8..84ad6bfe95e0 100644 --- a/awscli/customizations/configure/sso_commands.py +++ b/awscli/customizations/configure/sso_commands.py @@ -383,6 +383,7 @@ def _prompt_for_registration_args_with_legacy_format(self, verify=None): args = {'start_url': start_url, 'sso_region': sso_region} if resolved_url: args['resolved_start_url'] = resolved_url + args['feature_ids'] = ['SSO_LOGIN_VANITY_URL'] return args def _get_sso_registration_args_from_sso_config(self, sso_session): @@ -415,6 +416,7 @@ def _prompt_for_registration_args_for_new_sso_session( } if resolved_url: args['resolved_start_url'] = resolved_url + args['feature_ids'] = ['SSO_LOGIN_VANITY_URL'] return args def _store_sso_session_prompter_answers_to_profile_config(self): diff --git a/awscli/customizations/sso/login.py b/awscli/customizations/sso/login.py index 3ad71608746f..84f6faf77ff9 100644 --- a/awscli/customizations/sso/login.py +++ b/awscli/customizations/sso/login.py @@ -62,6 +62,10 @@ def _run_main(self, parsed_args, parsed_globals): verify=verify, ) + feature_ids = [] + if resolved_url != start_url: + feature_ids.append('SSO_LOGIN_VANITY_URL') + on_pending_authorization = None if parsed_args.no_browser: on_pending_authorization = PrintOnlyHandler() @@ -76,6 +80,7 @@ def _run_main(self, parsed_args, parsed_globals): session_name=sso_config.get('session_name'), registration_scopes=sso_config.get('registration_scopes'), use_device_code=parsed_args.use_device_code, + feature_ids=feature_ids, ) # Only rewrite sso_region after successful login. diff --git a/awscli/customizations/sso/utils.py b/awscli/customizations/sso/utils.py index 564203dae501..84482271f0ce 100644 --- a/awscli/customizations/sso/utils.py +++ b/awscli/customizations/sso/utils.py @@ -86,6 +86,7 @@ def do_sso_login( session_name=None, use_device_code=False, resolved_start_url=None, + feature_ids=None, ): if token_cache is None: token_cache = JSONFileCache(SSO_TOKEN_DIR, dumps_func=_sso_json_dumps) @@ -104,6 +105,7 @@ def do_sso_login( auth_code_fetcher=AuthCodeFetcher(), cache=token_cache, on_pending_authorization=on_pending_authorization, + feature_ids=feature_ids, ) else: token_fetcher = SSOTokenFetcher( @@ -112,6 +114,7 @@ def do_sso_login( parsed_globals=parsed_globals, cache=token_cache, on_pending_authorization=on_pending_authorization, + feature_ids=feature_ids, ) return token_fetcher.fetch_token( diff --git a/tests/functional/sso/test_login.py b/tests/functional/sso/test_login.py index 6f618be0ef27..894b1608d34a 100644 --- a/tests/functional/sso/test_login.py +++ b/tests/functional/sso/test_login.py @@ -560,6 +560,11 @@ def test_config_not_rewritten_when_region_matches(self): config_content = f.read() self.assertIn('sso_region=us-east-1', config_content) + def test_vanity_url_sets_feature_id_in_user_agent(self): + self.add_oidc_auth_code_responses(self.access_token) + self.run_cmd('sso login') + self.assertIn('AM', self.last_request_dict['headers']['User-Agent']) + class TestLoginWithVanityUrlLegacy(BaseSSOTest): def setUp(self): @@ -628,3 +633,9 @@ def test_login_uses_configured_region(self): self.add_oidc_auth_code_responses(self.access_token) self.run_cmd('sso login') self.assert_used_expected_sso_region('us-west-2') + + def test_direct_url_does_not_set_vanity_feature_id(self): + self.add_oidc_auth_code_responses(self.access_token) + self.run_cmd('sso login') + user_agent = self.last_request_dict['headers']['User-Agent'] + self.assertNotIn('AM', user_agent.split('m/')[-1]) From 0b906db2ef1ca87aceff7ecae42604685397e999 Mon Sep 17 00:00:00 2001 From: Andrew Asseily Date: Wed, 1 Jul 2026 10:57:26 -0400 Subject: [PATCH 2/4] Remove outdated comment --- awscli/customizations/agenttoolkit/agents.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/awscli/customizations/agenttoolkit/agents.py b/awscli/customizations/agenttoolkit/agents.py index 4c38488f531b..7b2d13fc71f4 100644 --- a/awscli/customizations/agenttoolkit/agents.py +++ b/awscli/customizations/agenttoolkit/agents.py @@ -276,9 +276,6 @@ def __init__(self, agent, name, path): self.path = path -# TODO: Verify detection, skills, and MCP config paths against actual -# installations before release. Currently only tested with Kiro and -# simulated agent directories. AGENT_CONFIGS = [ # https://docs.anthropic.com/en/docs/claude-code/mcp AgentConfig( From 156b6f9a8d1565b4f104689793c1fcb7a8d526d3 Mon Sep 17 00:00:00 2001 From: Andrew Asseily Date: Mon, 13 Jul 2026 15:33:28 -0400 Subject: [PATCH 3/4] Simplify SSO_LOGIN_VANITY_URL feature ID registration by using CLI context directly --- awscli/botocore/utils.py | 19 +------------------ awscli/clidriver.py | 6 +++--- .../customizations/configure/sso_commands.py | 4 ++-- awscli/customizations/sso/login.py | 6 +++--- awscli/customizations/sso/utils.py | 3 --- 5 files changed, 9 insertions(+), 29 deletions(-) diff --git a/awscli/botocore/utils.py b/awscli/botocore/utils.py index 3746a27597cd..a3ac03e745e8 100644 --- a/awscli/botocore/utils.py +++ b/awscli/botocore/utils.py @@ -78,7 +78,6 @@ UnsupportedS3ControlArnError, UnsupportedS3ControlConfigurationError, ) -from botocore.useragent import register_feature_id from dateutil.tz import tzutc from urllib3.exceptions import LocationParseError @@ -3222,13 +3221,11 @@ def __init__( cache=None, on_pending_authorization=None, time_fetcher=None, - feature_ids=None, ): self._sso_region = sso_region self._client_creator = client_creator self._parsed_globals = parsed_globals self._on_pending_authorization = on_pending_authorization - self._feature_ids = feature_ids or [] if time_fetcher is None: time_fetcher = self._utc_now @@ -3278,21 +3275,11 @@ def _client(self): signature_version=botocore.UNSIGNED, user_agent_extra=self._USER_AGENT_EXTRA, ) - client = self._client_creator( + return self._client_creator( 'sso-oidc', config=config, verify=self._parsed_globals.verify_ssl, ) - if self._feature_ids: - client.meta.events.register( - 'before-parameter-build.sso-oidc.*', - self._register_feature_ids, - ) - return client - - def _register_feature_ids(self, **kwargs): - for feature_id in self._feature_ids: - register_feature_id(feature_id) def _generate_client_name(self, session_name): if session_name is None: @@ -3343,7 +3330,6 @@ def __init__( on_pending_authorization=None, time_fetcher=None, sleep=None, - feature_ids=None, ): super().__init__( sso_region, @@ -3352,7 +3338,6 @@ def __init__( cache, on_pending_authorization, time_fetcher, - feature_ids=feature_ids, ) if sleep is None: @@ -3568,7 +3553,6 @@ def __init__( cache=None, on_pending_authorization=None, time_fetcher=None, - feature_ids=None, ): super().__init__( sso_region, @@ -3577,7 +3561,6 @@ def __init__( cache, on_pending_authorization, time_fetcher, - feature_ids=feature_ids, ) self._auth_code_fetcher = auth_code_fetcher diff --git a/awscli/clidriver.py b/awscli/clidriver.py index 9805ca066620..6defabe12dfd 100644 --- a/awscli/clidriver.py +++ b/awscli/clidriver.py @@ -109,8 +109,7 @@ def main(): - with start_as_current_context(): - return AWSCLIEntryPoint().main(sys.argv[1:]) + return AWSCLIEntryPoint().main(sys.argv[1:]) def create_clidriver(args=None, event_hooks=None): @@ -239,7 +238,8 @@ def __init__(self, driver=None): def main(self, args): try: - rc = self._do_main(args) + with start_as_current_context(): + rc = self._do_main(args) except BaseException as e: LOG.debug("Exception caught in AWSCLIEntryPoint", exc_info=True) return self._error_handler.handle_exception( diff --git a/awscli/customizations/configure/sso_commands.py b/awscli/customizations/configure/sso_commands.py index 84ad6bfe95e0..3c7d0054127c 100644 --- a/awscli/customizations/configure/sso_commands.py +++ b/awscli/customizations/configure/sso_commands.py @@ -33,6 +33,7 @@ from botocore.config import Config from botocore.configprovider import ConstantProvider from botocore.exceptions import ProfileNotFound +from botocore.useragent import register_feature_id from awscli.customizations.configure import ( get_section_header, @@ -383,7 +384,6 @@ def _prompt_for_registration_args_with_legacy_format(self, verify=None): args = {'start_url': start_url, 'sso_region': sso_region} if resolved_url: args['resolved_start_url'] = resolved_url - args['feature_ids'] = ['SSO_LOGIN_VANITY_URL'] return args def _get_sso_registration_args_from_sso_config(self, sso_session): @@ -416,7 +416,6 @@ def _prompt_for_registration_args_for_new_sso_session( } if resolved_url: args['resolved_start_url'] = resolved_url - args['feature_ids'] = ['SSO_LOGIN_VANITY_URL'] return args def _store_sso_session_prompter_answers_to_profile_config(self): @@ -450,6 +449,7 @@ def _prompt_for_sso_start_url_and_sso_region(self, verify=None): self._sso_session_prompter.sso_session_config['sso_region'] = ( region ) + register_feature_id('SSO_LOGIN_VANITY_URL') return start_url, region, resolved_url except Exception as e: logger.debug( diff --git a/awscli/customizations/sso/login.py b/awscli/customizations/sso/login.py index 84f6faf77ff9..99145a58c6bb 100644 --- a/awscli/customizations/sso/login.py +++ b/awscli/customizations/sso/login.py @@ -12,6 +12,8 @@ # language governing permissions and limitations under the License. import os +from botocore.useragent import register_feature_id + from awscli.customizations.configure.writer import ConfigFileWriter from awscli.customizations.sso.resolve import resolve_start_url from awscli.customizations.sso.utils import ( @@ -62,9 +64,8 @@ def _run_main(self, parsed_args, parsed_globals): verify=verify, ) - feature_ids = [] if resolved_url != start_url: - feature_ids.append('SSO_LOGIN_VANITY_URL') + register_feature_id('SSO_LOGIN_VANITY_URL') on_pending_authorization = None if parsed_args.no_browser: @@ -80,7 +81,6 @@ def _run_main(self, parsed_args, parsed_globals): session_name=sso_config.get('session_name'), registration_scopes=sso_config.get('registration_scopes'), use_device_code=parsed_args.use_device_code, - feature_ids=feature_ids, ) # Only rewrite sso_region after successful login. diff --git a/awscli/customizations/sso/utils.py b/awscli/customizations/sso/utils.py index 84482271f0ce..564203dae501 100644 --- a/awscli/customizations/sso/utils.py +++ b/awscli/customizations/sso/utils.py @@ -86,7 +86,6 @@ def do_sso_login( session_name=None, use_device_code=False, resolved_start_url=None, - feature_ids=None, ): if token_cache is None: token_cache = JSONFileCache(SSO_TOKEN_DIR, dumps_func=_sso_json_dumps) @@ -105,7 +104,6 @@ def do_sso_login( auth_code_fetcher=AuthCodeFetcher(), cache=token_cache, on_pending_authorization=on_pending_authorization, - feature_ids=feature_ids, ) else: token_fetcher = SSOTokenFetcher( @@ -114,7 +112,6 @@ def do_sso_login( parsed_globals=parsed_globals, cache=token_cache, on_pending_authorization=on_pending_authorization, - feature_ids=feature_ids, ) return token_fetcher.fetch_token( From b89c8e4dcd2f1c0e033e2248a8d4590dc6045f72 Mon Sep 17 00:00:00 2001 From: Andrew Asseily Date: Tue, 14 Jul 2026 20:18:32 -0400 Subject: [PATCH 4/4] Revert clidriver change and use start_as_current_context in tests instead --- awscli/clidriver.py | 6 +++--- tests/functional/sso/test_login.py | 8 ++++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/awscli/clidriver.py b/awscli/clidriver.py index 6defabe12dfd..9805ca066620 100644 --- a/awscli/clidriver.py +++ b/awscli/clidriver.py @@ -109,7 +109,8 @@ def main(): - return AWSCLIEntryPoint().main(sys.argv[1:]) + with start_as_current_context(): + return AWSCLIEntryPoint().main(sys.argv[1:]) def create_clidriver(args=None, event_hooks=None): @@ -238,8 +239,7 @@ def __init__(self, driver=None): def main(self, args): try: - with start_as_current_context(): - rc = self._do_main(args) + rc = self._do_main(args) except BaseException as e: LOG.debug("Exception caught in AWSCLIEntryPoint", exc_info=True) return self._error_handler.handle_exception( diff --git a/tests/functional/sso/test_login.py b/tests/functional/sso/test_login.py index 894b1608d34a..89d0a3612c03 100644 --- a/tests/functional/sso/test_login.py +++ b/tests/functional/sso/test_login.py @@ -15,6 +15,8 @@ import os import re +from botocore.context import start_as_current_context + from awscli.testutils import mock from tests.functional.sso import BaseSSOTest @@ -562,7 +564,8 @@ def test_config_not_rewritten_when_region_matches(self): def test_vanity_url_sets_feature_id_in_user_agent(self): self.add_oidc_auth_code_responses(self.access_token) - self.run_cmd('sso login') + with start_as_current_context(): + self.run_cmd('sso login') self.assertIn('AM', self.last_request_dict['headers']['User-Agent']) @@ -636,6 +639,7 @@ def test_login_uses_configured_region(self): def test_direct_url_does_not_set_vanity_feature_id(self): self.add_oidc_auth_code_responses(self.access_token) - self.run_cmd('sso login') + with start_as_current_context(): + self.run_cmd('sso login') user_agent = self.last_request_dict['headers']['User-Agent'] self.assertNotIn('AM', user_agent.split('m/')[-1])