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

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