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-configure-67593.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "enhancement",
"category": "configure",
"description": "Suggest installing the Agent Toolkit for AWS when a supported AI coding agent is detected. A one-line tip pointing at ``aws configure agent-toolkit`` is printed after ``aws configure``, ``aws configure sso``, and a first-time ``aws login`` that creates a new profile; it appears only on a terminal when no AWS skills are installed yet, and can be suppressed by setting the ``AWS_CLI_AGENT_TOOLKIT_HINT_DISABLED`` environment variable to ``true``. The same tip is also printed by the install scripts and after ``aws update``."
}
8 changes: 8 additions & 0 deletions awscli/customizations/agenttoolkit/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -419,3 +419,11 @@ def get_detected_agents(agent_configs=None):
if agent is not None:
detected.append(agent)
return detected


def get_detected_real_agents(agent_configs=None):
return [
agent
for agent in get_detected_agents(agent_configs)
if agent.config.id != UNIVERSAL_ROW_ID
]
72 changes: 72 additions & 0 deletions awscli/customizations/agenttoolkit/hint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
"""End-of-command hint pointing at the Agent Toolkit wizard.

After a successful ``aws configure``, ``aws configure sso``, or first-time
``aws login``, print a one-line tip suggesting ``aws configure agent-toolkit``
when a supported AI coding agent is present and no AWS skills are installed
yet. The tip only shows on a TTY and can be suppressed with an environment
variable.
"""

import logging
import os

from botocore.utils import ensure_boolean

from awscli.customizations.agenttoolkit.agents import (
get_detected_real_agents,
)
from awscli.customizations.utils import uni_print
from awscli.utils import is_a_tty

LOG = logging.getLogger(__name__)

HINT_DISABLED_ENV_VAR = 'AWS_CLI_AGENT_TOOLKIT_HINT_DISABLED'

# The Agent Toolkit skill APIs are only available in us-east-1 today, so the
# tip pins the region explicitly rather than routing the caller's configured
# region through a hidden cross-region call.
HINT_TEXT = (
"\nTip: run 'aws configure agent-toolkit --region us-east-1' to set up "
'AWS skills and the AWS MCP server for your AI coding agent(s).\n'
)


def hint_disabled():
return ensure_boolean(os.environ.get(HINT_DISABLED_ENV_VAR, ''))


def _has_installed_skills(detected_agents):
return any(agent.get_installed_skills() for agent in detected_agents)


def _is_eligible():
if not is_a_tty():
return False
if hint_disabled():
return False
detected_agents = get_detected_real_agents()
if not detected_agents:
return False
if _has_installed_skills(detected_agents):
return False
return True


def maybe_print_agent_toolkit_hint():
try:
if _is_eligible():
uni_print(HINT_TEXT)
except Exception as e:
LOG.debug('Agent toolkit hint failed: %s', e, exc_info=True)
Comment thread
AndrewAsseily marked this conversation as resolved.
4 changes: 4 additions & 0 deletions awscli/customizations/configure/configure.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
from awscli.customizations.agenttoolkit.configure import (
ConfigureAgentToolkitCommand,
)
from awscli.customizations.agenttoolkit.hint import (
maybe_print_agent_toolkit_hint,
)
from awscli.customizations.commands import BasicCommand
from awscli.customizations.configure.addmodel import AddModelCommand
from awscli.customizations.configure.exportcreds import (
Expand Down Expand Up @@ -193,6 +196,7 @@ def _run_main(self, parsed_args, parsed_globals):
section = profile_to_section(profile)
new_values['__section__'] = section
self._config_writer.update_config(new_values, config_filename)
maybe_print_agent_toolkit_hint()
return 0

def _write_out_creds_file_values(self, new_values, profile_name):
Expand Down
4 changes: 4 additions & 0 deletions awscli/customizations/configure/sso_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@
from botocore.exceptions import ProfileNotFound
from botocore.useragent import register_feature_id

from awscli.customizations.agenttoolkit.hint import (
maybe_print_agent_toolkit_hint,
)
from awscli.customizations.configure import (
get_section_header,
profile_to_section,
Expand Down Expand Up @@ -352,6 +355,7 @@ def _run_main(self, parsed_args, parsed_globals):

self._write_new_config(profile_name)
self._print_conclusion(configured_for_aws_credentials, profile_name)
maybe_print_agent_toolkit_hint()
return 0

def _prompt_for_sso_registration_args(self, verify=None):
Expand Down
11 changes: 10 additions & 1 deletion awscli/customizations/login/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
)

from awscli.compat import compat_input
from awscli.customizations.agenttoolkit.hint import (
maybe_print_agent_toolkit_hint,
)
from awscli.customizations.commands import BasicCommand
from awscli.customizations.configure.writer import ConfigFileWriter
from awscli.customizations.exceptions import ConfigurationError
Expand Down Expand Up @@ -92,7 +95,8 @@ def _run_main(self, parsed_args, parsed_globals):
# If the profile specified via --profile doesn't already exist
# add it to the session so the client creation still succeeds.
# If the login is successful we'll save the profile at the end.
if profile_name not in self._session.available_profiles:
is_new_profile = profile_name not in self._session.available_profiles
if is_new_profile:
self._session._profile_map[profile_name] = {}

# Abort if the profile is already configured with a different style
Expand Down Expand Up @@ -153,6 +157,11 @@ def _run_main(self, parsed_args, parsed_globals):
f'such as "aws sts get-caller-identity --profile {profile_name}"\n'
)

# Only nudge on first-time setup (a newly created profile), not on
# routine re-auth of an existing profile.
if is_new_profile:
maybe_print_agent_toolkit_hint()

def accept_change_to_existing_profile_if_needed(
self, profile_name, new_session_id
):
Expand Down
3 changes: 3 additions & 0 deletions awscli/customizations/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
get_distribution_source,
)
from awscli.compat import is_windows
from awscli.customizations.agenttoolkit.hint import HINT_TEXT, hint_disabled
from awscli.customizations.commands import BasicCommand
from awscli.customizations.utils import uni_print

Expand Down Expand Up @@ -95,6 +96,8 @@ def _run_main(self, parsed_args, parsed_globals):
uni_print(f"Updating AWS CLI (source: {source})\n")
self._no_color = parsed_globals.color == 'off'
self._do_update()
if not hint_disabled():
uni_print(HINT_TEXT)
return 0

def _do_update(self):
Expand Down
1 change: 1 addition & 0 deletions exe/assets/install
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ main() {
create_bin_symlinks
write_install_json
echo "You can now run: $BIN_AWS_EXE --version"
echo "Tip: run 'aws configure agent-toolkit --region us-east-1' to set up AWS skills and the AWS MCP server for your AI coding agent(s)."
exit 0
}

Expand Down
2 changes: 2 additions & 0 deletions macpkg/scripts/postinstall
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,5 @@ EOF
EOF
fi
fi

echo "Tip: run 'aws configure agent-toolkit --region us-east-1' to set up AWS skills and the AWS MCP server for your AI coding agent(s)."
58 changes: 58 additions & 0 deletions tests/functional/login/test_login.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,3 +296,61 @@ def test_abort_if_profile_has_existing_credentials(
else:
mock_login_command._run_main(DEFAULT_ARGS, DEFAULT_GLOBAL_ARGS)
mock_token_fetcher.assert_called_once()


@mock.patch('awscli.customizations.login.utils.get_base_sign_in_uri')
@mock.patch(
'awscli.customizations.login.utils.SameDeviceLoginTokenFetcher.fetch_token'
)
@mock.patch('awscli.customizations.login.login.maybe_print_agent_toolkit_hint')
def test_hints_agent_toolkit_for_new_profile(
mock_hint,
mock_token_fetcher,
mock_base_sign_in_uri,
mock_login_command,
mock_session,
):
mock_base_sign_in_uri.return_value = 'https://foo'
mock_token_fetcher.return_value = (
{
'accessToken': 'access_token',
'idToken': SAMPLE_ID_TOKEN,
'expiresIn': 3600,
},
'arn:aws:iam::0123456789012:user/Admin',
)
# Profile does not exist yet — this is a new-profile setup.
mock_session.available_profiles = []
mock_session.full_config = {'profiles': {}}

mock_login_command._run_main(DEFAULT_ARGS, DEFAULT_GLOBAL_ARGS)
mock_hint.assert_called_once()


@mock.patch('awscli.customizations.login.utils.get_base_sign_in_uri')
@mock.patch(
'awscli.customizations.login.utils.SameDeviceLoginTokenFetcher.fetch_token'
)
@mock.patch('awscli.customizations.login.login.maybe_print_agent_toolkit_hint')
def test_no_agent_toolkit_hint_for_existing_profile(
mock_hint,
mock_token_fetcher,
mock_base_sign_in_uri,
mock_login_command,
mock_session,
):
mock_base_sign_in_uri.return_value = 'https://foo'
mock_token_fetcher.return_value = (
{
'accessToken': 'access_token',
'idToken': SAMPLE_ID_TOKEN,
'expiresIn': 3600,
},
'arn:aws:iam::0123456789012:user/Admin',
)
# Profile already exists — this is re-auth, not setup.
mock_session.available_profiles = ['profile-name']
mock_session.full_config = {'profiles': {'profile-name': {}}}

mock_login_command._run_main(DEFAULT_ARGS, DEFAULT_GLOBAL_ARGS)
mock_hint.assert_not_called()
23 changes: 23 additions & 0 deletions tests/unit/customizations/agenttoolkit/test_agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@

from awscli.customizations.agenttoolkit.agents import (
AGENT_CONFIGS,
UNIVERSAL_ROW_ID,
AgentConfig,
DetectedAgent,
McpConfigureAction,
get_detected_agents,
get_detected_real_agents,
)
from awscli.testutils import skip_if_windows
from tests.unit.customizations.agenttoolkit.utils import (
Expand Down Expand Up @@ -136,6 +138,27 @@ def test_get_detected_agents(tmp_path):
assert detected[0].display_name == 'Kiro'


def test_get_detected_real_agents_excludes_universal_row(tmp_path):
# Only the universal row detects (``~/.agents`` exists) but no real
# per-agent directory does. The wizard would find nothing here, so the
# hint must not treat this as an eligible detection.
(tmp_path / '.agents').mkdir()
test_configs = [
AgentConfig(
id='cursor',
display_name='Cursor',
detection_path=str(tmp_path / '.cursor'),
),
AgentConfig(
id=UNIVERSAL_ROW_ID,
display_name='Universal',
detection_path=str(tmp_path / '.agents'),
),
]
assert get_detected_agents(agent_configs=test_configs)
assert get_detected_real_agents(agent_configs=test_configs) == []


def test_mcp_config_path_honors_detection_env_override(tmp_path, monkeypatch):
(tmp_path / '.test-agent').mkdir()
override_dir = tmp_path / '.custom-location'
Expand Down
74 changes: 74 additions & 0 deletions tests/unit/customizations/agenttoolkit/test_hint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
from unittest.mock import MagicMock, patch

from awscli.customizations.agenttoolkit import hint


def _agent(installed_skills=None):
agent = MagicMock()
agent.get_installed_skills.return_value = installed_skills or []
return agent


def _run(agents=None, tty=True):
if agents is None:
agents = [_agent()]
with (
patch.object(hint, 'is_a_tty', return_value=tty),
patch.object(hint, 'get_detected_real_agents', return_value=agents),
):
hint.maybe_print_agent_toolkit_hint()


def test_prints_tip_when_eligible(capsys):
_run()
assert '--region us-east-1' in capsys.readouterr().out


def test_no_tip_when_not_a_tty(capsys):
_run(tty=False)
assert capsys.readouterr().out == ''


def test_no_tip_when_env_var_true(capsys, monkeypatch):
monkeypatch.setenv(hint.HINT_DISABLED_ENV_VAR, 'true')
_run()
assert capsys.readouterr().out == ''


def test_tip_shown_when_env_var_false(capsys, monkeypatch):
monkeypatch.setenv(hint.HINT_DISABLED_ENV_VAR, 'false')
_run()
assert '--region us-east-1' in capsys.readouterr().out


def test_no_tip_when_no_agents(capsys):
_run(agents=[])
assert capsys.readouterr().out == ''


def test_no_tip_when_skills_already_installed(capsys):
_run(agents=[_agent(installed_skills=['s'])])
assert capsys.readouterr().out == ''


def test_detection_failure_does_not_raise(capsys):
with (
patch.object(hint, 'is_a_tty', return_value=True),
patch.object(
hint, 'get_detected_real_agents', side_effect=OSError('boom')
),
):
hint.maybe_print_agent_toolkit_hint()
assert capsys.readouterr().out == ''
24 changes: 24 additions & 0 deletions tests/unit/customizations/configure/test_sso.py
Original file line number Diff line number Diff line change
Expand Up @@ -1035,6 +1035,30 @@ def test_single_account_single_role_flow(
],
)

def test_hints_agent_toolkit_after_configuring_profile(
self,
sso_cmd,
ptk_stubber,
aws_config,
stub_simple_single_item_sso_responses,
args,
parsed_globals,
configure_sso_legacy_inputs,
account_id,
role_name,
):
inputs = configure_sso_legacy_inputs
inputs.skip_account_and_role_selection()
ptk_stubber.user_inputs = inputs
stub_simple_single_item_sso_responses(account_id, role_name)

with mock.patch(
'awscli.customizations.configure.sso_commands.'
'maybe_print_agent_toolkit_hint'
) as hint:
sso_cmd(args, parsed_globals)
hint.assert_called_once()

def test_no_accounts_flow_raises_error(
self,
sso_cmd,
Expand Down
Loading
Loading