bedrock-agentcore-starter-toolkit 0.1.9__py3-none-any.whl → 0.1.11__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.

Files changed (20) hide show
  1. bedrock_agentcore_starter_toolkit/cli/cli.py +3 -1
  2. bedrock_agentcore_starter_toolkit/cli/runtime/commands.py +453 -176
  3. bedrock_agentcore_starter_toolkit/operations/gateway/create_role.py +6 -11
  4. bedrock_agentcore_starter_toolkit/operations/runtime/__init__.py +4 -0
  5. bedrock_agentcore_starter_toolkit/operations/runtime/destroy.py +547 -0
  6. bedrock_agentcore_starter_toolkit/operations/runtime/invoke.py +2 -2
  7. bedrock_agentcore_starter_toolkit/operations/runtime/launch.py +15 -0
  8. bedrock_agentcore_starter_toolkit/operations/runtime/models.py +10 -0
  9. bedrock_agentcore_starter_toolkit/services/import_agent/scripts/base_bedrock_translate.py +4 -3
  10. bedrock_agentcore_starter_toolkit/services/import_agent/utils.py +14 -0
  11. bedrock_agentcore_starter_toolkit/services/runtime.py +38 -4
  12. bedrock_agentcore_starter_toolkit/services/xray.py +161 -0
  13. bedrock_agentcore_starter_toolkit/utils/runtime/logs.py +18 -2
  14. bedrock_agentcore_starter_toolkit/utils/runtime/templates/Dockerfile.j2 +11 -25
  15. {bedrock_agentcore_starter_toolkit-0.1.9.dist-info → bedrock_agentcore_starter_toolkit-0.1.11.dist-info}/METADATA +4 -3
  16. {bedrock_agentcore_starter_toolkit-0.1.9.dist-info → bedrock_agentcore_starter_toolkit-0.1.11.dist-info}/RECORD +20 -18
  17. {bedrock_agentcore_starter_toolkit-0.1.9.dist-info → bedrock_agentcore_starter_toolkit-0.1.11.dist-info}/WHEEL +0 -0
  18. {bedrock_agentcore_starter_toolkit-0.1.9.dist-info → bedrock_agentcore_starter_toolkit-0.1.11.dist-info}/entry_points.txt +0 -0
  19. {bedrock_agentcore_starter_toolkit-0.1.9.dist-info → bedrock_agentcore_starter_toolkit-0.1.11.dist-info}/licenses/LICENSE.txt +0 -0
  20. {bedrock_agentcore_starter_toolkit-0.1.9.dist-info → bedrock_agentcore_starter_toolkit-0.1.11.dist-info}/licenses/NOTICE.txt +0 -0
@@ -86,12 +86,9 @@ def _attach_policy(
86
86
  iam_client.attach_role_policy(RoleName=role_name, PolicyArn=policy_arn)
87
87
  except ClientError as e:
88
88
  raise RuntimeError(f"Failed to attach AgentCore policy: {e}") from e
89
-
90
- def _try_create_policy(
91
- iam_client: BaseClient,
92
- policy_name: str,
93
- policy_document: str
94
- ) -> str:
89
+
90
+
91
+ def _try_create_policy(iam_client: BaseClient, policy_name: str, policy_document: str) -> str:
95
92
  """Try to create a new policy, or return the arn if the policy already exists.
96
93
 
97
94
  :param iam_client: the IAM client to use.
@@ -110,11 +107,9 @@ def _try_create_policy(
110
107
  return _get_existing_policy_arn(iam_client, policy_name)
111
108
  else:
112
109
  raise e
113
-
114
- def _get_existing_policy_arn(
115
- iam_client: BaseClient,
116
- policy_name: str
117
- ) -> str:
110
+
111
+
112
+ def _get_existing_policy_arn(iam_client: BaseClient, policy_name: str) -> str:
118
113
  """Get the arn of an existing policy.
119
114
 
120
115
  :param iam_client: the IAM client to use.
@@ -1,10 +1,12 @@
1
1
  """Bedrock AgentCore operations - shared business logic for CLI and notebook interfaces."""
2
2
 
3
3
  from .configure import configure_bedrock_agentcore, validate_agent_name
4
+ from .destroy import destroy_bedrock_agentcore
4
5
  from .invoke import invoke_bedrock_agentcore
5
6
  from .launch import launch_bedrock_agentcore
6
7
  from .models import (
7
8
  ConfigureResult,
9
+ DestroyResult,
8
10
  InvokeResult,
9
11
  LaunchResult,
10
12
  StatusConfigInfo,
@@ -14,11 +16,13 @@ from .status import get_status
14
16
 
15
17
  __all__ = [
16
18
  "configure_bedrock_agentcore",
19
+ "destroy_bedrock_agentcore",
17
20
  "validate_agent_name",
18
21
  "launch_bedrock_agentcore",
19
22
  "invoke_bedrock_agentcore",
20
23
  "get_status",
21
24
  "ConfigureResult",
25
+ "DestroyResult",
22
26
  "InvokeResult",
23
27
  "LaunchResult",
24
28
  "StatusResult",
@@ -0,0 +1,547 @@
1
+ """Destroy operation - removes Bedrock AgentCore resources from AWS."""
2
+
3
+ import logging
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ import boto3
8
+ from botocore.exceptions import ClientError
9
+
10
+ from ...services.runtime import BedrockAgentCoreClient
11
+ from ...utils.runtime.config import load_config, save_config
12
+ from ...utils.runtime.schema import BedrockAgentCoreAgentSchema, BedrockAgentCoreConfigSchema
13
+ from .models import DestroyResult
14
+
15
+ log = logging.getLogger(__name__)
16
+
17
+
18
+ def destroy_bedrock_agentcore(
19
+ config_path: Path,
20
+ agent_name: Optional[str] = None,
21
+ dry_run: bool = False,
22
+ force: bool = False,
23
+ delete_ecr_repo: bool = False,
24
+ ) -> DestroyResult:
25
+ """Destroy Bedrock AgentCore resources.
26
+
27
+ Args:
28
+ config_path: Path to the configuration file
29
+ agent_name: Name of the agent to destroy (default: use default agent)
30
+ dry_run: If True, only show what would be destroyed without actually doing it
31
+ force: If True, skip confirmation prompts
32
+ delete_ecr_repo: If True, also delete the ECR repository after removing images
33
+
34
+ Returns:
35
+ DestroyResult: Details of what was destroyed or would be destroyed
36
+
37
+ Raises:
38
+ FileNotFoundError: If configuration file doesn't exist
39
+ ValueError: If agent is not found or not deployed
40
+ RuntimeError: If destruction fails
41
+ """
42
+ log.info(
43
+ "Starting destroy operation for agent: %s (dry_run=%s, delete_ecr_repo=%s)",
44
+ agent_name or "default",
45
+ dry_run,
46
+ delete_ecr_repo,
47
+ )
48
+
49
+ try:
50
+ # Load configuration
51
+ project_config = load_config(config_path)
52
+ agent_config = project_config.get_agent_config(agent_name)
53
+
54
+ if not agent_config:
55
+ raise ValueError(f"Agent '{agent_name or 'default'}' not found in configuration")
56
+
57
+ # Initialize result
58
+ result = DestroyResult(agent_name=agent_config.name, dry_run=dry_run)
59
+
60
+ # Check if agent is deployed
61
+ if not agent_config.bedrock_agentcore:
62
+ result.warnings.append("Agent is not deployed, nothing to destroy")
63
+ return result
64
+
65
+ # Initialize AWS session and clients
66
+ session = boto3.Session(region_name=agent_config.aws.region)
67
+
68
+ # 1. Destroy Bedrock AgentCore endpoint (if exists)
69
+ _destroy_agentcore_endpoint(session, agent_config, result, dry_run)
70
+
71
+ # 2. Destroy Bedrock AgentCore agent
72
+ _destroy_agentcore_agent(session, agent_config, result, dry_run)
73
+
74
+ # 3. Remove ECR images and optionally the repository
75
+ _destroy_ecr_images(session, agent_config, result, dry_run, delete_ecr_repo)
76
+
77
+ # 4. Remove CodeBuild project
78
+ _destroy_codebuild_project(session, agent_config, result, dry_run)
79
+
80
+ # 5. Remove CodeBuild IAM Role
81
+ _destroy_codebuild_iam_role(session, agent_config, result, dry_run)
82
+
83
+ # 6. Remove IAM execution role (if not used by other agents)
84
+ _destroy_iam_role(session, project_config, agent_config, result, dry_run)
85
+
86
+ # 7. Clean up configuration
87
+ if not dry_run and not result.errors:
88
+ _cleanup_agent_config(config_path, project_config, agent_config.name, result)
89
+
90
+ log.info(
91
+ "Destroy operation completed. Resources removed: %d, Warnings: %d, Errors: %d",
92
+ len(result.resources_removed),
93
+ len(result.warnings),
94
+ len(result.errors),
95
+ )
96
+
97
+ return result
98
+
99
+ except Exception as e:
100
+ log.error("Destroy operation failed: %s", str(e))
101
+ raise RuntimeError(f"Destroy operation failed: {e}") from e
102
+
103
+
104
+ def _destroy_agentcore_endpoint(
105
+ session: boto3.Session,
106
+ agent_config: BedrockAgentCoreAgentSchema,
107
+ result: DestroyResult,
108
+ dry_run: bool,
109
+ ) -> None:
110
+ """Destroy Bedrock AgentCore endpoint."""
111
+ if not agent_config.bedrock_agentcore:
112
+ return
113
+
114
+ try:
115
+ client = BedrockAgentCoreClient(agent_config.aws.region)
116
+
117
+ agent_id = agent_config.bedrock_agentcore.agent_id
118
+ if not agent_id:
119
+ result.warnings.append("No agent ID found, skipping endpoint destruction")
120
+ return
121
+
122
+ # Get actual endpoint details to determine endpoint name
123
+ try:
124
+ endpoint_response = client.get_agent_runtime_endpoint(agent_id)
125
+ endpoint_name = endpoint_response.get("name", "DEFAULT")
126
+ endpoint_arn = endpoint_response.get("agentRuntimeEndpointArn")
127
+
128
+ # Special case: DEFAULT endpoint cannot be explicitly deleted
129
+ if endpoint_name == "DEFAULT":
130
+ result.warnings.append("DEFAULT endpoint cannot be explicitly deleted, skipping")
131
+ log.info("Skipping deletion of DEFAULT endpoint")
132
+ return
133
+
134
+ if dry_run:
135
+ result.resources_removed.append(f"AgentCore endpoint: {endpoint_name} (DRY RUN)")
136
+ return
137
+
138
+ # Delete the endpoint
139
+ if endpoint_arn:
140
+ try:
141
+ client.delete_agent_runtime_endpoint(agent_id, endpoint_name)
142
+ result.resources_removed.append(f"AgentCore endpoint: {endpoint_arn}")
143
+ log.info("Deleted AgentCore endpoint: %s", endpoint_arn)
144
+ except ClientError as delete_error:
145
+ if delete_error.response["Error"]["Code"] not in ["ResourceNotFoundException", "NotFound"]:
146
+ result.errors.append(f"Failed to delete endpoint {endpoint_arn}: {delete_error}")
147
+ log.error("Failed to delete endpoint: %s", delete_error)
148
+ else:
149
+ result.warnings.append("Endpoint not found or already deleted during deletion")
150
+ else:
151
+ result.warnings.append("No endpoint ARN found for agent")
152
+
153
+ except ClientError as e:
154
+ if e.response["Error"]["Code"] not in ["ResourceNotFoundException", "NotFound"]:
155
+ result.warnings.append(f"Failed to get endpoint info: {e}")
156
+ log.warning("Failed to get endpoint info: %s", e)
157
+ else:
158
+ result.warnings.append("Endpoint not found or already deleted")
159
+
160
+ except Exception as e:
161
+ result.warnings.append(f"Error during endpoint destruction: {e}")
162
+ log.warning("Error during endpoint destruction: %s", e)
163
+
164
+
165
+ def _destroy_agentcore_agent(
166
+ session: boto3.Session,
167
+ agent_config: BedrockAgentCoreAgentSchema,
168
+ result: DestroyResult,
169
+ dry_run: bool,
170
+ ) -> None:
171
+ """Destroy Bedrock AgentCore agent."""
172
+ if not agent_config.bedrock_agentcore or not agent_config.bedrock_agentcore.agent_arn:
173
+ result.warnings.append("No agent ARN found, skipping agent destruction")
174
+ return
175
+
176
+ try:
177
+ # Initialize client to enable exception handling path for tests
178
+ BedrockAgentCoreClient(agent_config.aws.region)
179
+ agent_arn = agent_config.bedrock_agentcore.agent_arn
180
+ agent_id = agent_config.bedrock_agentcore.agent_id
181
+
182
+ if dry_run:
183
+ result.resources_removed.append(f"AgentCore agent: {agent_arn} (DRY RUN)")
184
+ return
185
+
186
+ # Delete the agent
187
+ try:
188
+ # Use the control plane client directly since there's no delete_agent_runtime method
189
+ # in the BedrockAgentCoreClient class
190
+ control_client = session.client("bedrock-agentcore-control", region_name=agent_config.aws.region)
191
+ control_client.delete_agent_runtime(agentRuntimeId=agent_id)
192
+ result.resources_removed.append(f"AgentCore agent: {agent_arn}")
193
+ log.info("Deleted AgentCore agent: %s", agent_arn)
194
+ except ClientError as e:
195
+ if e.response["Error"]["Code"] not in ["ResourceNotFoundException", "NotFound"]:
196
+ result.errors.append(f"Failed to delete agent {agent_arn}: {e}")
197
+ log.error("Failed to delete agent: %s", e)
198
+ else:
199
+ result.warnings.append(f"Agent {agent_arn} not found (may have been deleted already)")
200
+
201
+ except Exception as e:
202
+ result.errors.append(f"Error during agent destruction: {e}")
203
+ log.error("Error during agent destruction: %s", e)
204
+
205
+
206
+ def _destroy_ecr_images(
207
+ session: boto3.Session,
208
+ agent_config: BedrockAgentCoreAgentSchema,
209
+ result: DestroyResult,
210
+ dry_run: bool,
211
+ delete_ecr_repo: bool = False,
212
+ ) -> None:
213
+ """Remove ECR images and optionally the repository for this specific agent."""
214
+ if not agent_config.aws.ecr_repository:
215
+ result.warnings.append("No ECR repository configured, skipping image cleanup")
216
+ return
217
+
218
+ try:
219
+ # Create ECR client with explicit region specification
220
+ ecr_client = session.client("ecr", region_name=agent_config.aws.region)
221
+ ecr_uri = agent_config.aws.ecr_repository
222
+
223
+ # Extract repository name from URI
224
+ # Format: account.dkr.ecr.region.amazonaws.com/repo-name
225
+ repo_name = ecr_uri.split("/")[-1]
226
+
227
+ log.info("Checking ECR repository: %s in region: %s", repo_name, agent_config.aws.region)
228
+
229
+ try:
230
+ # List all images in the repository (both tagged and untagged)
231
+ response = ecr_client.list_images(repositoryName=repo_name)
232
+ log.debug("ECR list_images response: %s", response)
233
+
234
+ # Fix: use correct response key 'imageIds' instead of 'imageDetails'
235
+ all_images = response.get("imageIds", [])
236
+ if not all_images:
237
+ if delete_ecr_repo:
238
+ # Repository exists but is empty, we can delete it
239
+ if dry_run:
240
+ result.resources_removed.append(f"ECR repository: {repo_name} (empty, DRY RUN)")
241
+ else:
242
+ _delete_ecr_repository(ecr_client, repo_name, result)
243
+ else:
244
+ result.warnings.append(f"No images found in ECR repository: {repo_name}")
245
+ return
246
+
247
+ if dry_run:
248
+ # Fix: imageIds structure has imageTag (string) not imageTags (array)
249
+ tagged_count = len([img for img in all_images if img.get("imageTag")])
250
+ untagged_count = len([img for img in all_images if not img.get("imageTag")])
251
+ result.resources_removed.append(
252
+ f"ECR images in repository {repo_name}: {tagged_count} tagged, {untagged_count} untagged (DRY RUN)"
253
+ )
254
+ if delete_ecr_repo:
255
+ result.resources_removed.append(f"ECR repository: {repo_name} (DRY RUN)")
256
+ return
257
+
258
+ # Prepare images for deletion - imageIds are already in the correct format
259
+ images_to_delete = []
260
+
261
+ for image in all_images:
262
+ # imageIds structure already contains the correct identifiers
263
+ image_id = {}
264
+
265
+ # If image has a tag, use it
266
+ if image.get("imageTag"):
267
+ image_id["imageTag"] = image["imageTag"]
268
+ # If no tag, use image digest
269
+ elif image.get("imageDigest"):
270
+ image_id["imageDigest"] = image["imageDigest"]
271
+
272
+ if image_id:
273
+ images_to_delete.append(image_id)
274
+
275
+ if images_to_delete:
276
+ # Delete images in batches (ECR has a limit of 100 images per batch)
277
+ batch_size = 100
278
+ total_deleted = 0
279
+
280
+ for i in range(0, len(images_to_delete), batch_size):
281
+ batch = images_to_delete[i : i + batch_size]
282
+
283
+ delete_response = ecr_client.batch_delete_image(repositoryName=repo_name, imageIds=batch)
284
+
285
+ deleted_images = delete_response.get("imageIds", [])
286
+ total_deleted += len(deleted_images)
287
+
288
+ # Log any failures in this batch
289
+ failures = delete_response.get("failures", [])
290
+ for failure in failures:
291
+ log.warning(
292
+ "Failed to delete image: %s - %s", failure.get("imageId"), failure.get("failureReason")
293
+ )
294
+
295
+ result.resources_removed.append(f"ECR images: {total_deleted} images from {repo_name}")
296
+ log.info("Deleted %d ECR images from %s", total_deleted, repo_name)
297
+
298
+ # Log any partial failures
299
+ if total_deleted < len(images_to_delete):
300
+ failed_count = len(images_to_delete) - total_deleted
301
+ result.warnings.append(
302
+ f"Some ECR images could not be deleted: {failed_count} out of {len(images_to_delete)} failed"
303
+ )
304
+
305
+ # Delete the repository if requested and all images were deleted successfully
306
+ if delete_ecr_repo and total_deleted == len(images_to_delete):
307
+ _delete_ecr_repository(ecr_client, repo_name, result)
308
+ elif delete_ecr_repo and total_deleted < len(images_to_delete):
309
+ result.warnings.append(f"Cannot delete ECR repository {repo_name}: some images failed to delete")
310
+ else:
311
+ result.warnings.append(f"No valid image identifiers found in {repo_name}")
312
+
313
+ except ClientError as e:
314
+ error_code = e.response["Error"]["Code"]
315
+ if error_code == "RepositoryNotFoundException":
316
+ result.warnings.append(f"ECR repository {repo_name} not found")
317
+ elif error_code == "RepositoryNotEmptyException":
318
+ result.warnings.append(f"ECR repository {repo_name} could not be deleted (not empty)")
319
+ else:
320
+ result.warnings.append(f"Failed to delete ECR images: {e}")
321
+ log.warning("Failed to delete ECR images: %s", e)
322
+
323
+ except Exception as e:
324
+ result.warnings.append(f"Error during ECR cleanup: {e}")
325
+ log.warning("Error during ECR cleanup: %s", e)
326
+
327
+
328
+ def _delete_ecr_repository(ecr_client, repo_name: str, result: DestroyResult) -> None:
329
+ """Delete an ECR repository after ensuring it's empty."""
330
+ try:
331
+ # Verify repository is empty before deletion
332
+ response = ecr_client.list_images(repositoryName=repo_name)
333
+ remaining_images = response.get("imageIds", [])
334
+
335
+ if remaining_images:
336
+ result.warnings.append(f"Cannot delete ECR repository {repo_name}: repository is not empty")
337
+ return
338
+
339
+ # Delete the empty repository
340
+ ecr_client.delete_repository(repositoryName=repo_name)
341
+ result.resources_removed.append(f"ECR repository: {repo_name}")
342
+ log.info("Deleted ECR repository: %s", repo_name)
343
+
344
+ except ClientError as e:
345
+ error_code = e.response["Error"]["Code"]
346
+ if error_code == "RepositoryNotFoundException":
347
+ result.warnings.append(f"ECR repository {repo_name} not found (may have been deleted already)")
348
+ elif error_code == "RepositoryNotEmptyException":
349
+ result.warnings.append(f"Cannot delete ECR repository {repo_name}: repository is not empty")
350
+ else:
351
+ result.warnings.append(f"Failed to delete ECR repository {repo_name}: {e}")
352
+ log.warning("Failed to delete ECR repository: %s", e)
353
+ except Exception as e:
354
+ result.warnings.append(f"Error deleting ECR repository {repo_name}: {e}")
355
+ log.warning("Error deleting ECR repository: %s", e)
356
+
357
+
358
+ def _destroy_codebuild_project(
359
+ session: boto3.Session,
360
+ agent_config: BedrockAgentCoreAgentSchema,
361
+ result: DestroyResult,
362
+ dry_run: bool,
363
+ ) -> None:
364
+ """Remove CodeBuild project for this agent."""
365
+ try:
366
+ codebuild_client = session.client("codebuild", region_name=agent_config.aws.region)
367
+ project_name = f"bedrock-agentcore-{agent_config.name}-builder"
368
+
369
+ if dry_run:
370
+ result.resources_removed.append(f"CodeBuild project: {project_name} (DRY RUN)")
371
+ return
372
+
373
+ try:
374
+ codebuild_client.delete_project(name=project_name)
375
+ result.resources_removed.append(f"CodeBuild project: {project_name}")
376
+ log.info("Deleted CodeBuild project: %s", project_name)
377
+ except ClientError as e:
378
+ if e.response["Error"]["Code"] not in ["ResourceNotFoundException"]:
379
+ result.warnings.append(f"Failed to delete CodeBuild project {project_name}: {e}")
380
+ log.warning("Failed to delete CodeBuild project: %s", e)
381
+ else:
382
+ result.warnings.append(f"CodeBuild project {project_name} not found")
383
+
384
+ except Exception as e:
385
+ result.warnings.append(f"Error during CodeBuild cleanup: {e}")
386
+ log.warning("Error during CodeBuild cleanup: %s", e)
387
+
388
+
389
+ def _destroy_codebuild_iam_role(
390
+ session: boto3.Session,
391
+ agent_config: BedrockAgentCoreAgentSchema,
392
+ result: DestroyResult,
393
+ dry_run: bool,
394
+ ) -> None:
395
+ """Remove CodeBuild IAM execution role associated with this agent."""
396
+ if not agent_config.codebuild.execution_role:
397
+ result.warnings.append("No CodeBuild execution role configured, skipping IAM cleanup")
398
+ return
399
+
400
+ try:
401
+ # Note: IAM is a global service, but we specify region for consistency
402
+ iam_client = session.client("iam", region_name=agent_config.aws.region)
403
+ role_arn = agent_config.codebuild.execution_role
404
+ role_name = role_arn.split("/")[-1]
405
+
406
+ if dry_run:
407
+ result.resources_removed.append(f"CodeBuild IAM role: {role_name} (DRY RUN)")
408
+ return
409
+
410
+ # Detach managed policies
411
+ for policy in iam_client.list_attached_role_policies(RoleName=role_name).get("AttachedPolicies", []):
412
+ iam_client.detach_role_policy(RoleName=role_name, PolicyArn=policy["PolicyArn"])
413
+ log.info("Detached policy %s from role %s", policy["PolicyArn"], role_name)
414
+
415
+ # Delete inline policies
416
+ for policy_name in iam_client.list_role_policies(RoleName=role_name).get("PolicyNames", []):
417
+ iam_client.delete_role_policy(RoleName=role_name, PolicyName=policy_name)
418
+ log.info("Deleted inline policy %s from role %s", policy_name, role_name)
419
+
420
+ # Delete the role itself
421
+ iam_client.delete_role(RoleName=role_name)
422
+ result.resources_removed.append(f"Deleted CodeBuild IAM role: {role_name}")
423
+ log.info("Deleted CodeBuild IAM role: %s", role_name)
424
+
425
+ except ClientError as e:
426
+ result.warnings.append(f"Failed to delete CodeBuild role {role_name}: {e}")
427
+ log.warning("Failed to delete CodeBuild role %s: %s", role_name, e)
428
+ except Exception as e:
429
+ result.warnings.append(f"Error during CodeBuild IAM role cleanup: {e}")
430
+ log.error("Error during CodeBuild IAM role cleanup: %s", e)
431
+
432
+
433
+ def _destroy_iam_role(
434
+ session: boto3.Session,
435
+ project_config: BedrockAgentCoreConfigSchema,
436
+ agent_config: BedrockAgentCoreAgentSchema,
437
+ result: DestroyResult,
438
+ dry_run: bool,
439
+ ) -> None:
440
+ """Remove IAM execution role only if not used by other agents."""
441
+ if not agent_config.aws.execution_role:
442
+ result.warnings.append("No execution role configured, skipping IAM cleanup")
443
+ return
444
+
445
+ try:
446
+ # Note: IAM is a global service, but we specify region for consistency
447
+ iam_client = session.client("iam", region_name=agent_config.aws.region)
448
+ role_arn = agent_config.aws.execution_role
449
+ role_name = role_arn.split("/")[-1]
450
+
451
+ # Check if other agents use the same role
452
+ other_agents_using_role = [
453
+ name
454
+ for name, agent in project_config.agents.items()
455
+ if name != agent_config.name and agent.aws.execution_role == role_arn
456
+ ]
457
+
458
+ if other_agents_using_role:
459
+ result.warnings.append(
460
+ f"IAM role {role_name} is used by other agents: {other_agents_using_role}. Not deleting."
461
+ )
462
+ return
463
+
464
+ if dry_run:
465
+ result.resources_removed.append(f"IAM execution role: {role_name} (DRY RUN)")
466
+ return
467
+
468
+ try:
469
+ # Delete attached policies first
470
+ try:
471
+ policies = iam_client.list_attached_role_policies(RoleName=role_name)
472
+ for policy in policies.get("AttachedPolicies", []):
473
+ iam_client.detach_role_policy(RoleName=role_name, PolicyArn=policy["PolicyArn"])
474
+ except ClientError:
475
+ pass # Continue if policy detachment fails
476
+
477
+ # Delete inline policies
478
+ try:
479
+ inline_policies = iam_client.list_role_policies(RoleName=role_name)
480
+ for policy_name in inline_policies.get("PolicyNames", []):
481
+ iam_client.delete_role_policy(RoleName=role_name, PolicyName=policy_name)
482
+ except ClientError:
483
+ pass # Continue if inline policy deletion fails
484
+
485
+ # Delete the role
486
+ iam_client.delete_role(RoleName=role_name)
487
+ result.resources_removed.append(f"IAM execution role: {role_name}")
488
+ log.info("Deleted IAM role: %s", role_name)
489
+
490
+ except ClientError as e:
491
+ if e.response["Error"]["Code"] not in ["NoSuchEntity"]:
492
+ result.warnings.append(f"Failed to delete IAM role {role_name}: {e}")
493
+ log.warning("Failed to delete IAM role: %s", e)
494
+ else:
495
+ result.warnings.append(f"IAM role {role_name} not found")
496
+
497
+ except Exception as e:
498
+ result.warnings.append(f"Error during IAM cleanup: {e}")
499
+ log.warning("Error during IAM cleanup: %s", e)
500
+
501
+
502
+ def _cleanup_agent_config(
503
+ config_path: Path,
504
+ project_config: BedrockAgentCoreConfigSchema,
505
+ agent_name: str,
506
+ result: DestroyResult,
507
+ ) -> None:
508
+ """Remove agent configuration from the config file."""
509
+ try:
510
+ if agent_name not in project_config.agents:
511
+ result.warnings.append(f"Agent {agent_name} not found in configuration")
512
+ return
513
+
514
+ # Check if this agent is the default agent
515
+ was_default = project_config.default_agent == agent_name
516
+
517
+ # Remove the agent entry completely
518
+ del project_config.agents[agent_name]
519
+ result.resources_removed.append(f"Agent configuration: {agent_name}")
520
+ log.info("Removed agent configuration: %s", agent_name)
521
+
522
+ # Handle default agent cleanup
523
+ if was_default:
524
+ if project_config.agents:
525
+ # Set default to the first remaining agent
526
+ new_default = list(project_config.agents.keys())[0]
527
+ project_config.default_agent = new_default
528
+ result.resources_removed.append(f"Default agent updated to: {new_default}")
529
+ log.info("Updated default agent from '%s' to '%s'", agent_name, new_default)
530
+ else:
531
+ # No agents left, clear default
532
+ project_config.default_agent = None
533
+ log.info("Cleared default agent (no agents remaining)")
534
+
535
+ # If no agents remain, remove the config file
536
+ if not project_config.agents:
537
+ config_path.unlink()
538
+ result.resources_removed.append("Configuration file (no agents remaining)")
539
+ log.info("Removed configuration file: %s", config_path)
540
+ else:
541
+ # Save updated configuration
542
+ save_config(project_config, config_path)
543
+ log.info("Updated configuration file")
544
+
545
+ except Exception as e:
546
+ result.warnings.append(f"Failed to update configuration: {e}")
547
+ log.warning("Failed to update configuration: %s", e)
@@ -30,7 +30,7 @@ def invoke_bedrock_agentcore(
30
30
  agent_config = project_config.get_agent_config(agent_name)
31
31
  # Log which agent is being invoked
32
32
  mode = "locally" if local_mode else "via cloud endpoint"
33
- log.info("Invoking BedrockAgentCore agent '%s' %s", agent_config.name, mode)
33
+ log.debug("Invoking BedrockAgentCore agent '%s' %s", agent_config.name, mode)
34
34
 
35
35
  region = agent_config.aws.region
36
36
  if not region:
@@ -67,7 +67,7 @@ def invoke_bedrock_agentcore(
67
67
  )["workloadAccessToken"]
68
68
 
69
69
  # TODO: store and read port config of local running container
70
- client = LocalBedrockAgentCoreClient("http://0.0.0.0:8080")
70
+ client = LocalBedrockAgentCoreClient("http://127.0.0.1:8080")
71
71
  response = client.invoke_endpoint(session_id, payload_str, workload_access_token)
72
72
 
73
73
  else:
@@ -13,8 +13,10 @@ from botocore.exceptions import ClientError
13
13
  from ...services.codebuild import CodeBuildService
14
14
  from ...services.ecr import deploy_to_ecr, get_or_create_ecr_repository
15
15
  from ...services.runtime import BedrockAgentCoreClient
16
+ from ...services.xray import enable_transaction_search_if_needed
16
17
  from ...utils.runtime.config import load_config, save_config
17
18
  from ...utils.runtime.container import ContainerRuntime
19
+ from ...utils.runtime.logs import get_genai_observability_url
18
20
  from ...utils.runtime.schema import BedrockAgentCoreAgentSchema, BedrockAgentCoreConfigSchema
19
21
  from .create_role import get_or_create_runtime_execution_role
20
22
  from .models import LaunchResult
@@ -135,6 +137,7 @@ def _deploy_to_bedrock_agentcore(
135
137
  agent_name: str,
136
138
  ecr_uri: str,
137
139
  region: str,
140
+ account_id: str,
138
141
  env_vars: Optional[dict] = None,
139
142
  auto_update_on_conflict: bool = False,
140
143
  ):
@@ -230,6 +233,16 @@ def _deploy_to_bedrock_agentcore(
230
233
 
231
234
  log.info("✅ Agent created/updated: %s", agent_arn)
232
235
 
236
+ # Enable Transaction Search if observability is enabled
237
+ if agent_config.aws.observability.enabled:
238
+ log.info("Observability is enabled, configuring Transaction Search...")
239
+ enable_transaction_search_if_needed(region, account_id)
240
+
241
+ # Show GenAI Observability Dashboard URL whenever OTEL is enabled
242
+ console_url = get_genai_observability_url(region)
243
+ log.info("🔍 GenAI Observability Dashboard:")
244
+ log.info(" %s", console_url)
245
+
233
246
  # Wait for agent to be ready
234
247
  log.info("Polling for endpoint to be ready...")
235
248
  result = bedrock_agentcore_client.wait_for_agent_endpoint_ready(agent_id)
@@ -367,6 +380,7 @@ def launch_bedrock_agentcore(
367
380
  bedrock_agentcore_name,
368
381
  ecr_uri,
369
382
  region,
383
+ account_id,
370
384
  env_vars,
371
385
  auto_update_on_conflict,
372
386
  )
@@ -495,6 +509,7 @@ def _launch_with_codebuild(
495
509
  agent_name,
496
510
  ecr_uri,
497
511
  region,
512
+ account_id,
498
513
  env_vars=env_vars,
499
514
  auto_update_on_conflict=auto_update_on_conflict,
500
515
  )