bedrock-agentcore-starter-toolkit 0.0.1__py3-none-any.whl → 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of bedrock-agentcore-starter-toolkit might be problematic. Click here for more details.
- bedrock_agentcore_starter_toolkit/__init__.py +5 -0
- bedrock_agentcore_starter_toolkit/cli/cli.py +32 -0
- bedrock_agentcore_starter_toolkit/cli/common.py +44 -0
- bedrock_agentcore_starter_toolkit/cli/gateway/__init__.py +1 -0
- bedrock_agentcore_starter_toolkit/cli/gateway/commands.py +88 -0
- bedrock_agentcore_starter_toolkit/cli/runtime/__init__.py +1 -0
- bedrock_agentcore_starter_toolkit/cli/runtime/commands.py +651 -0
- bedrock_agentcore_starter_toolkit/cli/runtime/configuration_manager.py +133 -0
- bedrock_agentcore_starter_toolkit/notebook/__init__.py +5 -0
- bedrock_agentcore_starter_toolkit/notebook/runtime/__init__.py +1 -0
- bedrock_agentcore_starter_toolkit/notebook/runtime/bedrock_agentcore.py +239 -0
- bedrock_agentcore_starter_toolkit/operations/__init__.py +1 -0
- bedrock_agentcore_starter_toolkit/operations/gateway/README.md +277 -0
- bedrock_agentcore_starter_toolkit/operations/gateway/__init__.py +6 -0
- bedrock_agentcore_starter_toolkit/operations/gateway/client.py +456 -0
- bedrock_agentcore_starter_toolkit/operations/gateway/constants.py +152 -0
- bedrock_agentcore_starter_toolkit/operations/gateway/create_lambda.py +85 -0
- bedrock_agentcore_starter_toolkit/operations/gateway/create_role.py +90 -0
- bedrock_agentcore_starter_toolkit/operations/gateway/exceptions.py +13 -0
- bedrock_agentcore_starter_toolkit/operations/runtime/__init__.py +26 -0
- bedrock_agentcore_starter_toolkit/operations/runtime/configure.py +241 -0
- bedrock_agentcore_starter_toolkit/operations/runtime/create_role.py +404 -0
- bedrock_agentcore_starter_toolkit/operations/runtime/invoke.py +129 -0
- bedrock_agentcore_starter_toolkit/operations/runtime/launch.py +439 -0
- bedrock_agentcore_starter_toolkit/operations/runtime/models.py +79 -0
- bedrock_agentcore_starter_toolkit/operations/runtime/status.py +66 -0
- bedrock_agentcore_starter_toolkit/services/codebuild.py +332 -0
- bedrock_agentcore_starter_toolkit/services/ecr.py +84 -0
- bedrock_agentcore_starter_toolkit/services/runtime.py +473 -0
- bedrock_agentcore_starter_toolkit/utils/endpoints.py +32 -0
- bedrock_agentcore_starter_toolkit/utils/logging_config.py +72 -0
- bedrock_agentcore_starter_toolkit/utils/runtime/config.py +129 -0
- bedrock_agentcore_starter_toolkit/utils/runtime/container.py +310 -0
- bedrock_agentcore_starter_toolkit/utils/runtime/entrypoint.py +197 -0
- bedrock_agentcore_starter_toolkit/utils/runtime/logs.py +33 -0
- bedrock_agentcore_starter_toolkit/utils/runtime/policy_template.py +74 -0
- bedrock_agentcore_starter_toolkit/utils/runtime/schema.py +151 -0
- bedrock_agentcore_starter_toolkit/utils/runtime/templates/Dockerfile.j2 +44 -0
- bedrock_agentcore_starter_toolkit/utils/runtime/templates/dockerignore.template +68 -0
- bedrock_agentcore_starter_toolkit/utils/runtime/templates/execution_role_policy.json.j2 +98 -0
- bedrock_agentcore_starter_toolkit/utils/runtime/templates/execution_role_trust_policy.json.j2 +21 -0
- bedrock_agentcore_starter_toolkit-0.1.1.dist-info/METADATA +137 -0
- bedrock_agentcore_starter_toolkit-0.1.1.dist-info/RECORD +47 -0
- bedrock_agentcore_starter_toolkit-0.1.1.dist-info/entry_points.txt +2 -0
- bedrock_agentcore_starter_toolkit-0.1.1.dist-info/licenses/NOTICE.txt +190 -0
- bedrock_agentcore_starter_toolkit/init.py +0 -3
- bedrock_agentcore_starter_toolkit-0.0.1.dist-info/METADATA +0 -26
- bedrock_agentcore_starter_toolkit-0.0.1.dist-info/RECORD +0 -5
- {bedrock_agentcore_starter_toolkit-0.0.1.dist-info → bedrock_agentcore_starter_toolkit-0.1.1.dist-info}/WHEEL +0 -0
- /bedrock_agentcore_starter_toolkit-0.0.1.dist-info/licenses/LICENSE → /bedrock_agentcore_starter_toolkit-0.1.1.dist-info/licenses/LICENSE.txt +0 -0
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
"""Client for interacting with Bedrock AgentCore Gateway services."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import time
|
|
6
|
+
import urllib.parse
|
|
7
|
+
import uuid
|
|
8
|
+
from typing import Any, Dict, Optional
|
|
9
|
+
|
|
10
|
+
import boto3
|
|
11
|
+
import urllib3
|
|
12
|
+
|
|
13
|
+
from .constants import (
|
|
14
|
+
API_MODEL_BUCKETS,
|
|
15
|
+
CREATE_OPENAPI_TARGET_INVALID_CREDENTIALS_SHAPE_EXCEPTION_MESSAGE,
|
|
16
|
+
LAMBDA_CONFIG,
|
|
17
|
+
)
|
|
18
|
+
from .create_lambda import create_test_lambda
|
|
19
|
+
from .create_role import create_gateway_execution_role
|
|
20
|
+
from .exceptions import GatewaySetupException
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class GatewayClient:
|
|
24
|
+
"""High-level client for Bedrock AgentCore Gateway operations."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, region_name: Optional[str] = None, endpoint_url: Optional[str] = None):
|
|
27
|
+
"""Initialize the Gateway client.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
region_name: AWS region name (defaults to us-west-2)
|
|
31
|
+
endpoint_url: Custom endpoint URL for the Gateway service
|
|
32
|
+
"""
|
|
33
|
+
self.region = region_name or "us-west-2"
|
|
34
|
+
|
|
35
|
+
if endpoint_url:
|
|
36
|
+
self.client = boto3.client(
|
|
37
|
+
"bedrock-agentcore-control",
|
|
38
|
+
region_name=self.region,
|
|
39
|
+
endpoint_url=endpoint_url,
|
|
40
|
+
)
|
|
41
|
+
else:
|
|
42
|
+
self.client = boto3.client("bedrock-agentcore-control", region_name=self.region)
|
|
43
|
+
|
|
44
|
+
self.session = boto3.Session(region_name=self.region)
|
|
45
|
+
|
|
46
|
+
# Initialize the logger
|
|
47
|
+
self.logger = logging.getLogger("bedrock_agentcore.gateway")
|
|
48
|
+
if not self.logger.handlers:
|
|
49
|
+
handler = logging.StreamHandler()
|
|
50
|
+
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
|
51
|
+
handler.setFormatter(formatter)
|
|
52
|
+
self.logger.addHandler(handler)
|
|
53
|
+
self.logger.setLevel(logging.INFO)
|
|
54
|
+
|
|
55
|
+
def create_mcp_gateway(
|
|
56
|
+
self,
|
|
57
|
+
name=None,
|
|
58
|
+
role_arn=None,
|
|
59
|
+
authorizer_config=None,
|
|
60
|
+
enable_semantic_search=True,
|
|
61
|
+
) -> dict:
|
|
62
|
+
"""Creates an MCP Gateway.
|
|
63
|
+
|
|
64
|
+
:param name: optional - the name of the gateway (defaults to TestGateway).
|
|
65
|
+
:param role_arn: optional - the role arn to use (creates one if none provided).
|
|
66
|
+
:param authorizer_config: optional - the authorizer config (will create one if none provided).
|
|
67
|
+
:param enable_semantic_search: optional - whether to enable search tool (defaults to True).
|
|
68
|
+
:return: the created Gateway.
|
|
69
|
+
"""
|
|
70
|
+
if not name:
|
|
71
|
+
name = f"TestGateway{GatewayClient.generate_random_id()}"
|
|
72
|
+
if not role_arn:
|
|
73
|
+
self.logger.info("Role not provided, creating an execution role to use")
|
|
74
|
+
role_arn = create_gateway_execution_role(self.session, self.logger)
|
|
75
|
+
self.logger.info("✓ Successfully created execution role for Gateway")
|
|
76
|
+
if not authorizer_config:
|
|
77
|
+
self.logger.info("Authorizer config not provided, creating an authorizer to use")
|
|
78
|
+
cognito_result = self.create_oauth_authorizer_with_cognito(name)
|
|
79
|
+
self.logger.info("✓ Successfully created authorizer for Gateway")
|
|
80
|
+
authorizer_config = cognito_result["authorizer_config"]
|
|
81
|
+
create_request = {
|
|
82
|
+
"name": name,
|
|
83
|
+
"roleArn": role_arn,
|
|
84
|
+
"protocolType": "MCP",
|
|
85
|
+
"authorizerType": "CUSTOM_JWT",
|
|
86
|
+
"authorizerConfiguration": authorizer_config,
|
|
87
|
+
}
|
|
88
|
+
if enable_semantic_search:
|
|
89
|
+
create_request["protocolConfiguration"] = {"mcp": {"searchType": "SEMANTIC"}}
|
|
90
|
+
self.logger.info("Creating Gateway")
|
|
91
|
+
self.logger.debug("Creating gateway with params: %s", json.dumps(create_request, indent=2))
|
|
92
|
+
gateway = self.client.create_gateway(**create_request)
|
|
93
|
+
self.logger.info("✓ Created Gateway: %s", gateway["gatewayArn"])
|
|
94
|
+
self.logger.info(" Gateway URL: %s", gateway["gatewayUrl"])
|
|
95
|
+
|
|
96
|
+
# Wait for gateway to be ready
|
|
97
|
+
self.logger.info(" Waiting for Gateway to be ready...")
|
|
98
|
+
self.__wait_for_ready(
|
|
99
|
+
method=self.client.get_gateway,
|
|
100
|
+
identifiers={"gatewayIdentifier": gateway["gatewayId"]},
|
|
101
|
+
resource_name="Gateway",
|
|
102
|
+
)
|
|
103
|
+
self.logger.info("\n✅Gateway is ready")
|
|
104
|
+
return gateway
|
|
105
|
+
|
|
106
|
+
def create_mcp_gateway_target(
|
|
107
|
+
self,
|
|
108
|
+
gateway: dict,
|
|
109
|
+
name=None,
|
|
110
|
+
target_type="lambda",
|
|
111
|
+
target_payload=None,
|
|
112
|
+
credentials=None,
|
|
113
|
+
) -> dict:
|
|
114
|
+
"""Creates an MCP Gateway Target.
|
|
115
|
+
|
|
116
|
+
:param gateway: the gateway (output of create_mcp_gateway or calling get_gateway() with boto3 client).
|
|
117
|
+
:param name: optional - the name of the target (defaults to TestGatewayTarget).
|
|
118
|
+
:param target_type: optional - the type of the target e.g. one of "lambda" |
|
|
119
|
+
"openApiSchema" | "smithyModel" (defaults to "lambda").
|
|
120
|
+
:param target_payload: only required for openApiSchema target - the specification of that target.
|
|
121
|
+
:param credentials: only use with openApiSchema target - the credentials for calling this target
|
|
122
|
+
(api key or oauth2).
|
|
123
|
+
:return: the created target.
|
|
124
|
+
"""
|
|
125
|
+
# there is no name, create one
|
|
126
|
+
if not name:
|
|
127
|
+
name = f"TestGatewayTarget{GatewayClient.generate_random_id()}"
|
|
128
|
+
# instantiate base creation request
|
|
129
|
+
create_request = {
|
|
130
|
+
"gatewayIdentifier": gateway["gatewayId"],
|
|
131
|
+
"name": name,
|
|
132
|
+
"targetConfiguration": {"mcp": {target_type: target_payload}},
|
|
133
|
+
}
|
|
134
|
+
# handle cases of missing target payloads across smithy and lambda (default to something)
|
|
135
|
+
if not target_payload and target_type == "lambda":
|
|
136
|
+
create_request |= self.__handle_lambda_target_creation(gateway["roleArn"])
|
|
137
|
+
if not target_payload and target_type == "smithyModel":
|
|
138
|
+
region_bucket = API_MODEL_BUCKETS.get(self.region)
|
|
139
|
+
if not region_bucket:
|
|
140
|
+
raise Exception(
|
|
141
|
+
"Automatic smithyModel creation is not supported in this region. "
|
|
142
|
+
"Please try again by explicitly providing a smithyModel via targetPayload."
|
|
143
|
+
)
|
|
144
|
+
create_request |= {
|
|
145
|
+
"targetConfiguration": {
|
|
146
|
+
"mcp": {"smithyModel": {"s3": {"uri": f"s3://{region_bucket}/dynamodb-smithy.json"}}}
|
|
147
|
+
},
|
|
148
|
+
"credentialProviderConfigurations": [{"credentialProviderType": "GATEWAY_IAM_ROLE"}],
|
|
149
|
+
}
|
|
150
|
+
# open api schemas need a target config with them
|
|
151
|
+
if not target_payload and target_type == "openApiSchema":
|
|
152
|
+
raise Exception("You must provide a target configuration for your OpenAPI specification.")
|
|
153
|
+
# handle open api schema
|
|
154
|
+
if target_type == "openApiSchema":
|
|
155
|
+
create_request |= self.__handle_openapi_target_credential_provider_creation(
|
|
156
|
+
name=name, credentials=credentials
|
|
157
|
+
)
|
|
158
|
+
# create the target
|
|
159
|
+
self.logger.info("Creating Target")
|
|
160
|
+
self.logger.info(create_request)
|
|
161
|
+
self.logger.debug("Creating target with params: %s", json.dumps(create_request, indent=2))
|
|
162
|
+
target = self.client.create_gateway_target(**create_request)
|
|
163
|
+
self.logger.info("✓ Added target successfully (ID: %s)", target["targetId"])
|
|
164
|
+
self.logger.info(" Waiting for target to be ready...")
|
|
165
|
+
# poll till target is in READY state
|
|
166
|
+
self.__wait_for_ready(
|
|
167
|
+
method=self.client.get_gateway_target,
|
|
168
|
+
identifiers={
|
|
169
|
+
"gatewayIdentifier": gateway["gatewayId"],
|
|
170
|
+
"targetId": target["targetId"],
|
|
171
|
+
},
|
|
172
|
+
resource_name="Target",
|
|
173
|
+
)
|
|
174
|
+
self.logger.info("\n✅Target is ready")
|
|
175
|
+
return target
|
|
176
|
+
|
|
177
|
+
def __handle_lambda_target_creation(self, role_arn: str) -> Dict[str, Any]:
|
|
178
|
+
"""Create a test lambda.
|
|
179
|
+
|
|
180
|
+
:return: the targetConfiguration for the Lambda.
|
|
181
|
+
"""
|
|
182
|
+
lambda_arn = create_test_lambda(self.session, logger=self.logger, gateway_role_arn=role_arn)
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
"targetConfiguration": {"mcp": {"lambda": {"lambdaArn": lambda_arn, "toolSchema": LAMBDA_CONFIG}}},
|
|
186
|
+
"credentialProviderConfigurations": [{"credentialProviderType": "GATEWAY_IAM_ROLE"}],
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
def __handle_openapi_target_credential_provider_creation(
|
|
190
|
+
self, name: str, credentials: Dict[str, Any]
|
|
191
|
+
) -> Dict[str, Any]:
|
|
192
|
+
"""Generate the credential provider config for open api target.
|
|
193
|
+
|
|
194
|
+
:param name: the name of the target.
|
|
195
|
+
:param credentials: credentials to use in setting up this target.
|
|
196
|
+
:return: the credential provider config.
|
|
197
|
+
"""
|
|
198
|
+
acps = self.session.client(service_name="bedrock-agentcore-control")
|
|
199
|
+
if "api_key" in credentials:
|
|
200
|
+
self.logger.info("Creating credential provider")
|
|
201
|
+
credential_provider = acps.create_api_key_credential_provider(
|
|
202
|
+
name=f"{name}-ApiKey-{self.generate_random_id()}",
|
|
203
|
+
apiKey=credentials["api_key"],
|
|
204
|
+
)
|
|
205
|
+
self.logger.info(
|
|
206
|
+
"✓ Added credential provider successfully (ARN: %s)",
|
|
207
|
+
credential_provider["credentialProviderArn"],
|
|
208
|
+
)
|
|
209
|
+
target_cred_provider_config = {
|
|
210
|
+
"credentialProviderType": "API_KEY",
|
|
211
|
+
"credentialProvider": {
|
|
212
|
+
"apiKeyCredentialProvider": {
|
|
213
|
+
"providerArn": credential_provider["credentialProviderArn"],
|
|
214
|
+
"credentialLocation": credentials["credential_location"],
|
|
215
|
+
"credentialParameterName": credentials["credential_parameter_name"],
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
}
|
|
219
|
+
elif "oauth2_provider_config" in credentials:
|
|
220
|
+
self.logger.info("Creating credential provider")
|
|
221
|
+
credential_provider = acps.create_oauth2_credential_provider(
|
|
222
|
+
name=f"{name}-OAuth-Credentials-{self.generate_random_id()}",
|
|
223
|
+
credentialProviderVendor="CustomOauth2",
|
|
224
|
+
oauth2ProviderConfigInput=credentials["oauth2_provider_config"],
|
|
225
|
+
)
|
|
226
|
+
self.logger.info(
|
|
227
|
+
"✓ Added credential provider successfully (ARN: %s)",
|
|
228
|
+
credential_provider["credentialProviderArn"],
|
|
229
|
+
)
|
|
230
|
+
target_cred_provider_config = {
|
|
231
|
+
"credentialProviderType": "OAUTH",
|
|
232
|
+
"credentialProvider": {
|
|
233
|
+
"oauthCredentialProvider": {
|
|
234
|
+
"providerArn": credential_provider["credentialProviderArn"],
|
|
235
|
+
"scopes": credentials.get("scopes", []),
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
}
|
|
239
|
+
else:
|
|
240
|
+
raise Exception(CREATE_OPENAPI_TARGET_INVALID_CREDENTIALS_SHAPE_EXCEPTION_MESSAGE)
|
|
241
|
+
return {"credentialProviderConfigurations": [target_cred_provider_config]}
|
|
242
|
+
|
|
243
|
+
@staticmethod
|
|
244
|
+
def __wait_for_ready(resource_name, method, identifiers, max_attempts: int = 30, delay: int = 2) -> None:
|
|
245
|
+
"""Wait for the resource to be ready.
|
|
246
|
+
|
|
247
|
+
:param resource_name: the name of the resource.
|
|
248
|
+
:param method: the method to be invoked.
|
|
249
|
+
:param identifiers: the identifiers to fetch the resource (e.g. gateway id, target id).
|
|
250
|
+
:param max_attempts: the maximum number of times to poll.
|
|
251
|
+
:param delay: time delay in between polls.
|
|
252
|
+
:return:
|
|
253
|
+
"""
|
|
254
|
+
attempts = 0
|
|
255
|
+
while True:
|
|
256
|
+
response = method(**identifiers)
|
|
257
|
+
status = response.get("status", "UNKNOWN")
|
|
258
|
+
if not status == "CREATING":
|
|
259
|
+
break
|
|
260
|
+
time.sleep(delay)
|
|
261
|
+
attempts += 1
|
|
262
|
+
if attempts >= max_attempts:
|
|
263
|
+
raise TimeoutError(f"{resource_name} not ready after {max_attempts} attempts")
|
|
264
|
+
if status == "READY":
|
|
265
|
+
return
|
|
266
|
+
else:
|
|
267
|
+
raise Exception(f"{resource_name} failed: {response}")
|
|
268
|
+
|
|
269
|
+
# Generate unique IDs
|
|
270
|
+
@staticmethod
|
|
271
|
+
def generate_random_id():
|
|
272
|
+
"""Generate a random ID for Cognito resources."""
|
|
273
|
+
return str(uuid.uuid4())[:8]
|
|
274
|
+
|
|
275
|
+
def create_oauth_authorizer_with_cognito(self, gateway_name: str) -> Dict[str, Any]:
|
|
276
|
+
"""Creates Cognito OAuth authorization server.
|
|
277
|
+
|
|
278
|
+
:param gateway_name: the name of the gateway being created for use in naming Cognito resources.
|
|
279
|
+
:return: dictionary with details of the authorization server, client id, and client secret.
|
|
280
|
+
"""
|
|
281
|
+
self.logger.info("Starting EZ Auth setup: Creating Cognito resources...")
|
|
282
|
+
|
|
283
|
+
cognito_client = self.session.client("cognito-idp")
|
|
284
|
+
|
|
285
|
+
try:
|
|
286
|
+
# 1. Create User Pool
|
|
287
|
+
pool_name = f"agentcore-gateway-{GatewayClient.generate_random_id()}"
|
|
288
|
+
user_pool_response = cognito_client.create_user_pool(PoolName=pool_name)
|
|
289
|
+
user_pool_id = user_pool_response["UserPool"]["Id"]
|
|
290
|
+
self.logger.info(" ✓ Created User Pool: %s", user_pool_id)
|
|
291
|
+
|
|
292
|
+
# 2. Create User Pool Domain
|
|
293
|
+
domain_prefix = f"agentcore-{GatewayClient.generate_random_id()}"
|
|
294
|
+
cognito_client.create_user_pool_domain(Domain=domain_prefix, UserPoolId=user_pool_id)
|
|
295
|
+
self.logger.info(" ✓ Created domain: %s", domain_prefix)
|
|
296
|
+
|
|
297
|
+
# Wait for domain to be available
|
|
298
|
+
self.logger.info(" ⏳ Waiting for domain to be available...")
|
|
299
|
+
domain_ready = False
|
|
300
|
+
for _ in range(30): # Wait up to 30 seconds
|
|
301
|
+
try:
|
|
302
|
+
response = cognito_client.describe_user_pool_domain(Domain=domain_prefix)
|
|
303
|
+
if response.get("DomainDescription", {}).get("Status") == "ACTIVE":
|
|
304
|
+
domain_ready = True
|
|
305
|
+
break
|
|
306
|
+
except cognito_client.exceptions.ClientError as e:
|
|
307
|
+
self.logger.debug("Domain not yet active: %s", e)
|
|
308
|
+
pass
|
|
309
|
+
time.sleep(1)
|
|
310
|
+
|
|
311
|
+
if not domain_ready:
|
|
312
|
+
self.logger.warning(" ⚠️ Domain may not be fully available yet")
|
|
313
|
+
else:
|
|
314
|
+
self.logger.info(" ✓ Domain is active")
|
|
315
|
+
|
|
316
|
+
# 3. Create Resource Server
|
|
317
|
+
# Using gateway_name as the resource server identifier
|
|
318
|
+
resource_server_id = gateway_name
|
|
319
|
+
gateway_scopes = [
|
|
320
|
+
{
|
|
321
|
+
"ScopeName": "invoke", # Just 'invoke', will be formatted as resource_server_id/invoke
|
|
322
|
+
"ScopeDescription": "Scope for invoking the agentcore gateway",
|
|
323
|
+
}
|
|
324
|
+
]
|
|
325
|
+
|
|
326
|
+
cognito_client.create_resource_server(
|
|
327
|
+
UserPoolId=user_pool_id,
|
|
328
|
+
Identifier=resource_server_id,
|
|
329
|
+
Name=gateway_name,
|
|
330
|
+
Scopes=gateway_scopes,
|
|
331
|
+
)
|
|
332
|
+
self.logger.info(" ✓ Created resource server: %s", resource_server_id)
|
|
333
|
+
|
|
334
|
+
# 4. Create User Pool Client
|
|
335
|
+
client_name = f"agentcore-client-{GatewayClient.generate_random_id()}"
|
|
336
|
+
|
|
337
|
+
# Format scopes as {resource_server_id}/{scope_name} as per the update
|
|
338
|
+
scope_names = [f"{resource_server_id}/{scope['ScopeName']}" for scope in gateway_scopes]
|
|
339
|
+
# This results in: "gateway_name/invoke"
|
|
340
|
+
|
|
341
|
+
user_pool_client_response = cognito_client.create_user_pool_client(
|
|
342
|
+
UserPoolId=user_pool_id,
|
|
343
|
+
ClientName=client_name,
|
|
344
|
+
GenerateSecret=True,
|
|
345
|
+
AllowedOAuthFlows=["client_credentials"],
|
|
346
|
+
AllowedOAuthScopes=scope_names, # Using the formatted scope names
|
|
347
|
+
AllowedOAuthFlowsUserPoolClient=True,
|
|
348
|
+
SupportedIdentityProviders=["COGNITO"],
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
client_id = user_pool_client_response["UserPoolClient"]["ClientId"]
|
|
352
|
+
client_secret = user_pool_client_response["UserPoolClient"]["ClientSecret"]
|
|
353
|
+
self.logger.info(" ✓ Created client: %s", client_id)
|
|
354
|
+
|
|
355
|
+
# Build the return structure
|
|
356
|
+
discovery_url = (
|
|
357
|
+
f"https://cognito-idp.{self.region}.amazonaws.com/{user_pool_id}/.well-known/openid-configuration"
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
# Format for AgentCore Gateway authorizer config
|
|
361
|
+
custom_jwt_authorizer = {
|
|
362
|
+
"customJWTAuthorizer": {
|
|
363
|
+
"allowedClients": [client_id],
|
|
364
|
+
"discoveryUrl": discovery_url,
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
result = {
|
|
369
|
+
"authorizer_config": custom_jwt_authorizer,
|
|
370
|
+
"client_info": {
|
|
371
|
+
"client_id": client_id,
|
|
372
|
+
"client_secret": client_secret,
|
|
373
|
+
"user_pool_id": user_pool_id,
|
|
374
|
+
"token_endpoint": f"https://{domain_prefix}.auth.{self.region}.amazoncognito.com/oauth2/token",
|
|
375
|
+
"scope": scope_names[0],
|
|
376
|
+
"domain_prefix": domain_prefix,
|
|
377
|
+
},
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if domain_prefix:
|
|
381
|
+
self.logger.info(
|
|
382
|
+
" ⏳ Waiting for DNS propagation of domain: %s.auth.%s.amazoncognito.com",
|
|
383
|
+
domain_prefix,
|
|
384
|
+
self.region,
|
|
385
|
+
)
|
|
386
|
+
# Wait for DNS to propagate (60 seconds)
|
|
387
|
+
time.sleep(60)
|
|
388
|
+
|
|
389
|
+
self.logger.info("✓ EZ Auth setup complete!")
|
|
390
|
+
return result
|
|
391
|
+
|
|
392
|
+
except Exception as e:
|
|
393
|
+
raise GatewaySetupException(f"Failed to create Cognito resources: {e}") from e
|
|
394
|
+
|
|
395
|
+
def get_access_token_for_cognito(self, client_info: Dict[str, Any]) -> str:
|
|
396
|
+
"""Get OAuth token using client credentials flow.
|
|
397
|
+
|
|
398
|
+
:param client_info: credentials and context needed to get the access token
|
|
399
|
+
(output of the create_oauth_authorizer_with_cognito method).
|
|
400
|
+
:return: the access token.
|
|
401
|
+
"""
|
|
402
|
+
self.logger.info("Fetching test token from Cognito...")
|
|
403
|
+
|
|
404
|
+
max_retries = 5
|
|
405
|
+
retry_delay = 10
|
|
406
|
+
|
|
407
|
+
for attempt in range(max_retries):
|
|
408
|
+
try:
|
|
409
|
+
# Make HTTP request to token endpoint
|
|
410
|
+
http = urllib3.PoolManager()
|
|
411
|
+
|
|
412
|
+
# Prepare the form data
|
|
413
|
+
form_data = {
|
|
414
|
+
"grant_type": "client_credentials",
|
|
415
|
+
"client_id": client_info["client_id"],
|
|
416
|
+
"client_secret": client_info["client_secret"],
|
|
417
|
+
"scope": client_info["scope"],
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
# Log token endpoint for debugging
|
|
421
|
+
self.logger.info(
|
|
422
|
+
" Attempting to connect to token endpoint: %s",
|
|
423
|
+
client_info["token_endpoint"],
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
response = http.request(
|
|
427
|
+
"POST",
|
|
428
|
+
client_info["token_endpoint"],
|
|
429
|
+
body=urllib.parse.urlencode(form_data),
|
|
430
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
431
|
+
timeout=10.0, # Add explicit timeout
|
|
432
|
+
retries=False,
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
if response.status != 200:
|
|
436
|
+
raise GatewaySetupException(f"Token request failed: {response.data.decode()}")
|
|
437
|
+
|
|
438
|
+
token_data = json.loads(response.data.decode())
|
|
439
|
+
access_token = token_data["access_token"]
|
|
440
|
+
|
|
441
|
+
self.logger.info("✓ Got test token successfully")
|
|
442
|
+
return access_token
|
|
443
|
+
|
|
444
|
+
except urllib3.exceptions.MaxRetryError as e:
|
|
445
|
+
if "NameResolutionError" in str(e) and attempt < max_retries - 1:
|
|
446
|
+
self.logger.warning(
|
|
447
|
+
" Domain not yet resolvable (attempt %s/%s). Waiting %s seconds...",
|
|
448
|
+
attempt + 1,
|
|
449
|
+
max_retries,
|
|
450
|
+
retry_delay,
|
|
451
|
+
)
|
|
452
|
+
time.sleep(retry_delay)
|
|
453
|
+
continue
|
|
454
|
+
raise GatewaySetupException(f"Failed to get test token: {e}") from e
|
|
455
|
+
except Exception as e:
|
|
456
|
+
raise GatewaySetupException(f"Failed to get test token: {e}") from e
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Constants for use in Bedrock AgentCore Gateway."""
|
|
2
|
+
|
|
3
|
+
API_MODEL_BUCKETS = {
|
|
4
|
+
"ap-southeast-2": "amazonbedrockagentcore-built-sampleschemas455e0815-yigvs4je21kx",
|
|
5
|
+
"us-west-2": "amazonbedrockagentcore-built-sampleschemas455e0815-omxvr7ybq9g8",
|
|
6
|
+
"eu-central-1": "amazonbedrockagentcore-built-sampleschemas455e0815-egpctdjskcrf",
|
|
7
|
+
"us-east-1": "amazonbedrockagentcore-built-sampleschemas455e0815-oj7jujcd8xiu",
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
CREATE_OPENAPI_TARGET_INVALID_CREDENTIALS_SHAPE_EXCEPTION_MESSAGE = """
|
|
11
|
+
Provided credentials object was not formatted correctly. Correct formats below:
|
|
12
|
+
|
|
13
|
+
API Key:
|
|
14
|
+
{
|
|
15
|
+
"api_key": "<key>",
|
|
16
|
+
"credential_location": "HEADER | BODY",
|
|
17
|
+
"credential_parameter_name": "<name of parameter>"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
OAuth:
|
|
21
|
+
{
|
|
22
|
+
"oauth2_provider_config": {
|
|
23
|
+
"customOauth2ProviderConfig": {
|
|
24
|
+
<same as the agentcredentialprovider customOauth2ProviderConfig object>
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
Example for OAuth:
|
|
30
|
+
{
|
|
31
|
+
"oauth2_provider_config": {
|
|
32
|
+
"customOauth2ProviderConfig": {
|
|
33
|
+
"oauthDiscovery" : {
|
|
34
|
+
"authorizationServerMetadata" : {
|
|
35
|
+
"issuer" : "< issuer endpoint >",
|
|
36
|
+
"authorizationEndpoint" : "< authorization endpoint >",
|
|
37
|
+
"tokenEndpoint" : "< token endpoint >"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"clientId" : "< client id >",
|
|
41
|
+
"clientSecret" : "< client secret >"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
BEDROCK_AGENTCORE_TRUST_POLICY = {
|
|
48
|
+
"Version": "2012-10-17",
|
|
49
|
+
"Statement": [
|
|
50
|
+
{
|
|
51
|
+
"Effect": "Allow",
|
|
52
|
+
"Principal": {"Service": "bedrock-agentcore.amazonaws.com"},
|
|
53
|
+
"Action": "sts:AssumeRole",
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
AGENTCORE_FULL_ACCESS = {
|
|
59
|
+
"Version": "2012-10-17",
|
|
60
|
+
"Statement": [
|
|
61
|
+
{
|
|
62
|
+
"Sid": "BedrockAgentCoreFullAccess",
|
|
63
|
+
"Effect": "Allow",
|
|
64
|
+
"Action": ["bedrock-agentcore:*"],
|
|
65
|
+
"Resource": "arn:aws:bedrock-agentcore:*:*:*",
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"Sid": "GetSecretValue",
|
|
69
|
+
"Effect": "Allow",
|
|
70
|
+
"Action": ["secretsmanager:GetSecretValue"],
|
|
71
|
+
"Resource": "*",
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"Sid": "LambdaInvokeAccess",
|
|
75
|
+
"Effect": "Allow",
|
|
76
|
+
"Action": ["lambda:InvokeFunction"],
|
|
77
|
+
"Resource": "arn:aws:lambda:*:*:function:*",
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
POLICIES_TO_CREATE = [("BedrockAgentCoreGatewayStarterFullAccess", AGENTCORE_FULL_ACCESS)]
|
|
83
|
+
|
|
84
|
+
POLICIES = {
|
|
85
|
+
"arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess",
|
|
86
|
+
"arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess",
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
LAMBDA_FUNCTION_CODE = """
|
|
90
|
+
import json
|
|
91
|
+
|
|
92
|
+
def lambda_handler(event, context):
|
|
93
|
+
# Extract tool name from context
|
|
94
|
+
tool_name = context.client_context.custom.get('bedrockAgentCoreToolName', 'unknown')
|
|
95
|
+
|
|
96
|
+
if 'get_weather' in tool_name:
|
|
97
|
+
return {
|
|
98
|
+
'statusCode': 200,
|
|
99
|
+
'body': json.dumps({
|
|
100
|
+
'location': event.get('location', 'Unknown'),
|
|
101
|
+
'temperature': '72°F',
|
|
102
|
+
'conditions': 'Sunny'
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
elif 'get_time' in tool_name:
|
|
106
|
+
return {
|
|
107
|
+
'statusCode': 200,
|
|
108
|
+
'body': json.dumps({
|
|
109
|
+
'timezone': event.get('timezone', 'UTC'),
|
|
110
|
+
'time': '2:30 PM'
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
else:
|
|
114
|
+
return {
|
|
115
|
+
'statusCode': 200,
|
|
116
|
+
'body': json.dumps({'message': 'Unknown tool'})
|
|
117
|
+
}
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
LAMBDA_TRUST_POLICY = {
|
|
121
|
+
"Version": "2012-10-17",
|
|
122
|
+
"Statement": [
|
|
123
|
+
{
|
|
124
|
+
"Effect": "Allow",
|
|
125
|
+
"Principal": {"Service": "lambda.amazonaws.com"},
|
|
126
|
+
"Action": "sts:AssumeRole",
|
|
127
|
+
}
|
|
128
|
+
],
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
LAMBDA_CONFIG = {
|
|
132
|
+
"inlinePayload": [
|
|
133
|
+
{
|
|
134
|
+
"name": "get_weather",
|
|
135
|
+
"description": "Get weather for a location",
|
|
136
|
+
"inputSchema": {
|
|
137
|
+
"type": "object",
|
|
138
|
+
"properties": {"location": {"type": "string"}},
|
|
139
|
+
"required": ["location"],
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"name": "get_time",
|
|
144
|
+
"description": "Get time for a timezone",
|
|
145
|
+
"inputSchema": {
|
|
146
|
+
"type": "object",
|
|
147
|
+
"properties": {"timezone": {"type": "string"}},
|
|
148
|
+
"required": ["timezone"],
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Creates a Lambda function to use as a Bedrock AgentCore Gateway Target."""
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import zipfile
|
|
7
|
+
|
|
8
|
+
from boto3 import Session
|
|
9
|
+
|
|
10
|
+
from ...operations.gateway.constants import (
|
|
11
|
+
LAMBDA_FUNCTION_CODE,
|
|
12
|
+
LAMBDA_TRUST_POLICY,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def create_test_lambda(session: Session, logger: logging.Logger, gateway_role_arn: str) -> str:
|
|
17
|
+
"""Create a test Lambda function.
|
|
18
|
+
|
|
19
|
+
:param region_name: the name of the region to create in.
|
|
20
|
+
:param logger: instance of a logger.
|
|
21
|
+
:param gateway_role_arn: the execution role arn of the gateway this lambda is going to be used with.
|
|
22
|
+
:return: the lambda arn
|
|
23
|
+
"""
|
|
24
|
+
lambda_client = session.client("lambda")
|
|
25
|
+
iam = session.client("iam")
|
|
26
|
+
function_name = "AgentCoreLambdaTestFunction"
|
|
27
|
+
role_name = "AgentCoreTestLambdaRole"
|
|
28
|
+
|
|
29
|
+
# Create zip file
|
|
30
|
+
zip_buffer = io.BytesIO()
|
|
31
|
+
with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file:
|
|
32
|
+
zip_file.writestr("lambda_function.py", LAMBDA_FUNCTION_CODE)
|
|
33
|
+
zip_buffer.seek(0)
|
|
34
|
+
|
|
35
|
+
# Create Lambda execution role
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
role_response = iam.create_role(RoleName=role_name, AssumeRolePolicyDocument=json.dumps(LAMBDA_TRUST_POLICY))
|
|
39
|
+
|
|
40
|
+
iam.attach_role_policy(
|
|
41
|
+
RoleName=role_name,
|
|
42
|
+
PolicyArn="arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
role_arn = role_response["Role"]["Arn"]
|
|
46
|
+
logger.info("✓ Created Lambda execution role: %s", role_arn)
|
|
47
|
+
|
|
48
|
+
# Wait a bit for role to propagate
|
|
49
|
+
import time
|
|
50
|
+
|
|
51
|
+
time.sleep(10)
|
|
52
|
+
|
|
53
|
+
except iam.exceptions.EntityAlreadyExistsException:
|
|
54
|
+
role = iam.get_role(RoleName=role_name)
|
|
55
|
+
role_arn = role["Role"]["Arn"]
|
|
56
|
+
|
|
57
|
+
# Create Lambda function
|
|
58
|
+
try:
|
|
59
|
+
response = lambda_client.create_function(
|
|
60
|
+
FunctionName=function_name,
|
|
61
|
+
Runtime="python3.9",
|
|
62
|
+
Role=role_arn,
|
|
63
|
+
Handler="lambda_function.lambda_handler",
|
|
64
|
+
Code={"ZipFile": zip_buffer.read()},
|
|
65
|
+
Description="Test Lambda for AgentCore Gateway",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
lambda_arn = response["FunctionArn"]
|
|
69
|
+
logger.info("✓ Created Lambda function: %s", lambda_arn)
|
|
70
|
+
logger.info("✓ Attaching access policy to: %s for %s", lambda_arn, gateway_role_arn)
|
|
71
|
+
|
|
72
|
+
lambda_client.add_permission(
|
|
73
|
+
FunctionName=function_name,
|
|
74
|
+
StatementId="AllowAgentCoreInvoke",
|
|
75
|
+
Action="lambda:InvokeFunction",
|
|
76
|
+
Principal=gateway_role_arn,
|
|
77
|
+
)
|
|
78
|
+
logger.info("✓ Attached permissions for role invocation: %s", lambda_arn)
|
|
79
|
+
|
|
80
|
+
except lambda_client.exceptions.ResourceConflictException:
|
|
81
|
+
response = lambda_client.get_function(FunctionName=function_name)
|
|
82
|
+
lambda_arn = response["Configuration"]["FunctionArn"]
|
|
83
|
+
logger.info("✓ Lambda already exists: %s", lambda_arn)
|
|
84
|
+
|
|
85
|
+
return lambda_arn
|