bedrock-agentcore-starter-toolkit 0.1.11__py3-none-any.whl → 0.1.13__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/cli/import_agent/agent_info.py +6 -1
- bedrock_agentcore_starter_toolkit/cli/runtime/commands.py +101 -4
- bedrock_agentcore_starter_toolkit/cli/runtime/configuration_manager.py +68 -1
- bedrock_agentcore_starter_toolkit/operations/gateway/client.py +139 -0
- bedrock_agentcore_starter_toolkit/operations/memory/__init__.py +1 -0
- bedrock_agentcore_starter_toolkit/operations/memory/constants.py +98 -0
- bedrock_agentcore_starter_toolkit/operations/memory/manager.py +890 -0
- bedrock_agentcore_starter_toolkit/operations/memory/models/DictWrapper.py +51 -0
- bedrock_agentcore_starter_toolkit/operations/memory/models/Memory.py +17 -0
- bedrock_agentcore_starter_toolkit/operations/memory/models/MemoryStrategy.py +17 -0
- bedrock_agentcore_starter_toolkit/operations/memory/models/MemorySummary.py +17 -0
- bedrock_agentcore_starter_toolkit/operations/runtime/configure.py +4 -0
- bedrock_agentcore_starter_toolkit/operations/runtime/create_role.py +3 -2
- bedrock_agentcore_starter_toolkit/operations/runtime/invoke.py +8 -2
- bedrock_agentcore_starter_toolkit/operations/runtime/launch.py +1 -0
- bedrock_agentcore_starter_toolkit/services/codebuild.py +17 -6
- bedrock_agentcore_starter_toolkit/services/runtime.py +71 -5
- bedrock_agentcore_starter_toolkit/utils/runtime/schema.py +1 -0
- bedrock_agentcore_starter_toolkit/utils/runtime/templates/Dockerfile.j2 +25 -11
- {bedrock_agentcore_starter_toolkit-0.1.11.dist-info → bedrock_agentcore_starter_toolkit-0.1.13.dist-info}/METADATA +19 -4
- {bedrock_agentcore_starter_toolkit-0.1.11.dist-info → bedrock_agentcore_starter_toolkit-0.1.13.dist-info}/RECORD +25 -18
- {bedrock_agentcore_starter_toolkit-0.1.11.dist-info → bedrock_agentcore_starter_toolkit-0.1.13.dist-info}/WHEEL +0 -0
- {bedrock_agentcore_starter_toolkit-0.1.11.dist-info → bedrock_agentcore_starter_toolkit-0.1.13.dist-info}/entry_points.txt +0 -0
- {bedrock_agentcore_starter_toolkit-0.1.11.dist-info → bedrock_agentcore_starter_toolkit-0.1.13.dist-info}/licenses/LICENSE.txt +0 -0
- {bedrock_agentcore_starter_toolkit-0.1.11.dist-info → bedrock_agentcore_starter_toolkit-0.1.13.dist-info}/licenses/NOTICE.txt +0 -0
|
@@ -148,7 +148,12 @@ def get_agent_info(agent_id: str, agent_alias_id: str, bedrock_client, bedrock_a
|
|
|
148
148
|
s3_object_key = action_group["apiSchema"]["s3"]["s3ObjectKey"]
|
|
149
149
|
|
|
150
150
|
s3_client = boto3.client("s3")
|
|
151
|
-
|
|
151
|
+
# Get account ID for bucket ownership verification
|
|
152
|
+
sts_client = boto3.client("sts")
|
|
153
|
+
account_id = sts_client.get_caller_identity()["Account"]
|
|
154
|
+
response = s3_client.get_object(
|
|
155
|
+
Bucket=s3_bucket_name, Key=s3_object_key, ExpectedBucketOwner=account_id
|
|
156
|
+
)
|
|
152
157
|
yaml_content = response["Body"].read().decode("utf-8")
|
|
153
158
|
yaml = YAML(typ="safe")
|
|
154
159
|
action_group["apiSchema"]["payload"] = yaml.load(yaml_content)
|
|
@@ -68,7 +68,7 @@ def _prompt_for_requirements_file(prompt_text: str, default: str = "") -> Option
|
|
|
68
68
|
return None
|
|
69
69
|
|
|
70
70
|
|
|
71
|
-
def _handle_requirements_file_display(requirements_file: Optional[str]) -> Optional[str]:
|
|
71
|
+
def _handle_requirements_file_display(requirements_file: Optional[str], non_interactive: bool = False) -> Optional[str]:
|
|
72
72
|
"""Handle requirements file with display logic for CLI."""
|
|
73
73
|
from ...utils.runtime.entrypoint import detect_dependencies
|
|
74
74
|
|
|
@@ -76,6 +76,15 @@ def _handle_requirements_file_display(requirements_file: Optional[str]) -> Optio
|
|
|
76
76
|
# User provided file - validate and show confirmation
|
|
77
77
|
return _validate_requirements_file(requirements_file)
|
|
78
78
|
|
|
79
|
+
if non_interactive:
|
|
80
|
+
# Auto-detection for non-interactive mode
|
|
81
|
+
deps = detect_dependencies(Path.cwd())
|
|
82
|
+
if deps.found:
|
|
83
|
+
_print_success(f"Using detected file: [dim]{deps.file}[/dim]")
|
|
84
|
+
return None # Use detected file
|
|
85
|
+
else:
|
|
86
|
+
_handle_error("No requirements file specified and none found automatically")
|
|
87
|
+
|
|
79
88
|
# Auto-detection with interactive prompt
|
|
80
89
|
deps = detect_dependencies(Path.cwd())
|
|
81
90
|
|
|
@@ -167,9 +176,19 @@ def configure(
|
|
|
167
176
|
authorizer_config: Optional[str] = typer.Option(
|
|
168
177
|
None, "--authorizer-config", "-ac", help="OAuth authorizer configuration as JSON string"
|
|
169
178
|
),
|
|
179
|
+
request_header_allowlist: Optional[str] = typer.Option(
|
|
180
|
+
None,
|
|
181
|
+
"--request-header-allowlist",
|
|
182
|
+
"-rha",
|
|
183
|
+
help="Comma-separated list of allowed request headers "
|
|
184
|
+
"(Authorization or X-Amzn-Bedrock-AgentCore-Runtime-Custom-*)",
|
|
185
|
+
),
|
|
170
186
|
verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose output"),
|
|
171
187
|
region: Optional[str] = typer.Option(None, "--region", "-r"),
|
|
172
188
|
protocol: Optional[str] = typer.Option(None, "--protocol", "-p", help="Server protocol (HTTP or MCP)"),
|
|
189
|
+
non_interactive: bool = typer.Option(
|
|
190
|
+
False, "--non-interactive", "-ni", help="Skip prompts; use defaults unless overridden"
|
|
191
|
+
),
|
|
173
192
|
):
|
|
174
193
|
"""Configure a Bedrock AgentCore agent. The agent name defaults to your Python file name."""
|
|
175
194
|
if ctx.invoked_subcommand is not None:
|
|
@@ -196,7 +215,7 @@ def configure(
|
|
|
196
215
|
|
|
197
216
|
# Create configuration manager for clean, elegant prompting
|
|
198
217
|
config_path = Path.cwd() / ".bedrock_agentcore.yaml"
|
|
199
|
-
config_manager = ConfigurationManager(config_path)
|
|
218
|
+
config_manager = ConfigurationManager(config_path, non_interactive)
|
|
200
219
|
|
|
201
220
|
# Interactive prompts for missing values - clean and elegant
|
|
202
221
|
if not execution_role:
|
|
@@ -217,7 +236,7 @@ def configure(
|
|
|
217
236
|
_print_success(f"Using existing ECR repository: [dim]{ecr_repository}[/dim]")
|
|
218
237
|
|
|
219
238
|
# Handle dependency file selection with simplified logic
|
|
220
|
-
final_requirements_file = _handle_requirements_file_display(requirements_file)
|
|
239
|
+
final_requirements_file = _handle_requirements_file_display(requirements_file, non_interactive)
|
|
221
240
|
|
|
222
241
|
# Handle OAuth authorization configuration
|
|
223
242
|
oauth_config = None
|
|
@@ -231,6 +250,19 @@ def configure(
|
|
|
231
250
|
else:
|
|
232
251
|
oauth_config = config_manager.prompt_oauth_config()
|
|
233
252
|
|
|
253
|
+
# Handle request header allowlist configuration
|
|
254
|
+
request_header_config = None
|
|
255
|
+
if request_header_allowlist:
|
|
256
|
+
# Parse comma-separated headers and create configuration
|
|
257
|
+
headers = [header.strip() for header in request_header_allowlist.split(",") if header.strip()]
|
|
258
|
+
if headers:
|
|
259
|
+
request_header_config = {"requestHeaderAllowlist": headers}
|
|
260
|
+
_print_success(f"Configured request header allowlist with {len(headers)} headers")
|
|
261
|
+
else:
|
|
262
|
+
_handle_error("Empty request header allowlist provided")
|
|
263
|
+
else:
|
|
264
|
+
request_header_config = config_manager.prompt_request_header_allowlist()
|
|
265
|
+
|
|
234
266
|
try:
|
|
235
267
|
result = configure_bedrock_agentcore(
|
|
236
268
|
agent_name=agent_name,
|
|
@@ -242,6 +274,7 @@ def configure(
|
|
|
242
274
|
enable_observability=not disable_otel,
|
|
243
275
|
requirements_file=final_requirements_file,
|
|
244
276
|
authorizer_configuration=oauth_config,
|
|
277
|
+
request_header_configuration=request_header_config,
|
|
245
278
|
verbose=verbose,
|
|
246
279
|
region=region,
|
|
247
280
|
protocol=protocol.upper() if protocol else None,
|
|
@@ -252,6 +285,12 @@ def configure(
|
|
|
252
285
|
if oauth_config:
|
|
253
286
|
auth_info = "OAuth (customJWTAuthorizer)"
|
|
254
287
|
|
|
288
|
+
# Prepare request headers info for summary
|
|
289
|
+
headers_info = ""
|
|
290
|
+
if request_header_config:
|
|
291
|
+
headers = request_header_config.get("requestHeaderAllowlist", [])
|
|
292
|
+
headers_info = f"Request Headers Allowlist: [dim]{len(headers)} headers configured[/dim]\n"
|
|
293
|
+
|
|
255
294
|
console.print(
|
|
256
295
|
Panel(
|
|
257
296
|
f"[green]Configuration Complete[/green]\n\n"
|
|
@@ -265,7 +304,8 @@ def configure(
|
|
|
265
304
|
f"ECR Repository: [dim]"
|
|
266
305
|
f"{'Auto-create' if result.auto_create_ecr else result.ecr_repository or 'N/A'}"
|
|
267
306
|
f"[/dim]\n"
|
|
268
|
-
f"Authorization: [dim]{auth_info}[/dim]\n
|
|
307
|
+
f"Authorization: [dim]{auth_info}[/dim]\n"
|
|
308
|
+
f"{headers_info}\n"
|
|
269
309
|
f"📄 Config saved to: [dim]{result.config_path}[/dim]\n\n"
|
|
270
310
|
f"[bold]Next Steps:[/bold]\n"
|
|
271
311
|
f" [cyan]agentcore launch[/cyan]",
|
|
@@ -570,6 +610,45 @@ def _show_error_response(error_msg: str):
|
|
|
570
610
|
console.print(f"\n[red]{error_msg}[/red]")
|
|
571
611
|
|
|
572
612
|
|
|
613
|
+
def _parse_custom_headers(headers_str: str) -> dict:
|
|
614
|
+
"""Parse custom headers string and apply prefix logic.
|
|
615
|
+
|
|
616
|
+
Args:
|
|
617
|
+
headers_str: String in format "Header1:value,Header2:value2"
|
|
618
|
+
|
|
619
|
+
Returns:
|
|
620
|
+
dict: Dictionary of processed headers with proper prefixes
|
|
621
|
+
|
|
622
|
+
Raises:
|
|
623
|
+
ValueError: If header format is invalid
|
|
624
|
+
"""
|
|
625
|
+
if not headers_str or not headers_str.strip():
|
|
626
|
+
return {}
|
|
627
|
+
|
|
628
|
+
headers = {}
|
|
629
|
+
header_pairs = [pair.strip() for pair in headers_str.split(",")]
|
|
630
|
+
|
|
631
|
+
for pair in header_pairs:
|
|
632
|
+
if ":" not in pair:
|
|
633
|
+
raise ValueError(f"Invalid header format: '{pair}'. Expected format: 'Header:value'")
|
|
634
|
+
|
|
635
|
+
header_name, header_value = pair.split(":", 1)
|
|
636
|
+
header_name = header_name.strip()
|
|
637
|
+
header_value = header_value.strip()
|
|
638
|
+
|
|
639
|
+
if not header_name:
|
|
640
|
+
raise ValueError(f"Empty header name in: '{pair}'")
|
|
641
|
+
|
|
642
|
+
# Apply prefix logic: if header doesn't start with the custom prefix, add it
|
|
643
|
+
prefix = "X-Amzn-Bedrock-AgentCore-Runtime-Custom-"
|
|
644
|
+
if not header_name.startswith(prefix):
|
|
645
|
+
header_name = prefix + header_name
|
|
646
|
+
|
|
647
|
+
headers[header_name] = header_value
|
|
648
|
+
|
|
649
|
+
return headers
|
|
650
|
+
|
|
651
|
+
|
|
573
652
|
def invoke(
|
|
574
653
|
payload: str = typer.Argument(..., help="JSON payload to send"),
|
|
575
654
|
agent: Optional[str] = typer.Option(
|
|
@@ -581,6 +660,12 @@ def invoke(
|
|
|
581
660
|
),
|
|
582
661
|
local_mode: Optional[bool] = typer.Option(False, "--local", "-l", help="Send request to a running local container"),
|
|
583
662
|
user_id: Optional[str] = typer.Option(None, "--user-id", "-u", help="User id for authorization flows"),
|
|
663
|
+
headers: Optional[str] = typer.Option(
|
|
664
|
+
None,
|
|
665
|
+
"--headers",
|
|
666
|
+
help="Custom headers (format: 'Header1:value,Header2:value2'). "
|
|
667
|
+
"Headers will be auto-prefixed with 'X-Amzn-Bedrock-AgentCore-Runtime-Custom-' if not already present.",
|
|
668
|
+
),
|
|
584
669
|
):
|
|
585
670
|
"""Invoke Bedrock AgentCore endpoint."""
|
|
586
671
|
config_path = Path.cwd() / ".bedrock_agentcore.yaml"
|
|
@@ -613,6 +698,17 @@ def invoke(
|
|
|
613
698
|
"[yellow]Warning: Bearer token provided but OAuth is not configured in .bedrock_agentcore.yaml[/yellow]"
|
|
614
699
|
)
|
|
615
700
|
|
|
701
|
+
# Process custom headers
|
|
702
|
+
custom_headers = {}
|
|
703
|
+
if headers:
|
|
704
|
+
try:
|
|
705
|
+
custom_headers = _parse_custom_headers(headers)
|
|
706
|
+
if custom_headers:
|
|
707
|
+
header_names = list(custom_headers.keys())
|
|
708
|
+
console.print(f"[dim]Using custom headers: {', '.join(header_names)}[/dim]")
|
|
709
|
+
except ValueError as e:
|
|
710
|
+
_handle_error(f"Invalid headers format: {e}")
|
|
711
|
+
|
|
616
712
|
# Invoke
|
|
617
713
|
result = invoke_bedrock_agentcore(
|
|
618
714
|
config_path=config_path,
|
|
@@ -622,6 +718,7 @@ def invoke(
|
|
|
622
718
|
bearer_token=final_bearer_token,
|
|
623
719
|
user_id=user_id,
|
|
624
720
|
local_mode=local_mode,
|
|
721
|
+
custom_headers=custom_headers,
|
|
625
722
|
)
|
|
626
723
|
agent_display = config.name if config else (agent or "unknown")
|
|
627
724
|
_show_invoke_info_panel(agent_display, result, config)
|
|
@@ -10,19 +10,25 @@ from ..common import _handle_error, _print_success, _prompt_with_default, consol
|
|
|
10
10
|
class ConfigurationManager:
|
|
11
11
|
"""Manages interactive configuration prompts with existing configuration defaults."""
|
|
12
12
|
|
|
13
|
-
def __init__(self, config_path: Path):
|
|
13
|
+
def __init__(self, config_path: Path, non_interactive: bool = False):
|
|
14
14
|
"""Initialize the ConfigPrompt with a configuration path.
|
|
15
15
|
|
|
16
16
|
Args:
|
|
17
17
|
config_path: Path to the configuration file
|
|
18
|
+
non_interactive: If True, use defaults without prompting
|
|
18
19
|
"""
|
|
19
20
|
from ...utils.runtime.config import load_config_if_exists
|
|
20
21
|
|
|
21
22
|
project_config = load_config_if_exists(config_path)
|
|
22
23
|
self.existing_config = project_config.get_agent_config() if project_config else None
|
|
24
|
+
self.non_interactive = non_interactive
|
|
23
25
|
|
|
24
26
|
def prompt_execution_role(self) -> Optional[str]:
|
|
25
27
|
"""Prompt for execution role. Returns role name/ARN or None for auto-creation."""
|
|
28
|
+
if self.non_interactive:
|
|
29
|
+
_print_success("Will auto-create execution role")
|
|
30
|
+
return None
|
|
31
|
+
|
|
26
32
|
console.print("\n🔐 [cyan]Execution Role[/cyan]")
|
|
27
33
|
console.print(
|
|
28
34
|
"[dim]Press Enter to auto-create execution role, or provide execution role ARN/name to use existing[/dim]"
|
|
@@ -43,6 +49,10 @@ class ConfigurationManager:
|
|
|
43
49
|
|
|
44
50
|
def prompt_ecr_repository(self) -> tuple[Optional[str], bool]:
|
|
45
51
|
"""Prompt for ECR repository. Returns (repository, auto_create_flag)."""
|
|
52
|
+
if self.non_interactive:
|
|
53
|
+
_print_success("Will auto-create ECR repository")
|
|
54
|
+
return None, True
|
|
55
|
+
|
|
46
56
|
console.print("\n🏗️ [cyan]ECR Repository[/cyan]")
|
|
47
57
|
console.print(
|
|
48
58
|
"[dim]Press Enter to auto-create ECR repository, or provide ECR Repository URI to use existing[/dim]"
|
|
@@ -63,6 +73,10 @@ class ConfigurationManager:
|
|
|
63
73
|
|
|
64
74
|
def prompt_oauth_config(self) -> Optional[dict]:
|
|
65
75
|
"""Prompt for OAuth configuration. Returns OAuth config dict or None."""
|
|
76
|
+
if self.non_interactive:
|
|
77
|
+
_print_success("Using default IAM authorization")
|
|
78
|
+
return None
|
|
79
|
+
|
|
66
80
|
console.print("\n🔐 [cyan]Authorization Configuration[/cyan]")
|
|
67
81
|
console.print("[dim]By default, Bedrock AgentCore uses IAM authorization.[/dim]")
|
|
68
82
|
|
|
@@ -131,3 +145,56 @@ class ConfigurationManager:
|
|
|
131
145
|
|
|
132
146
|
_print_success("OAuth authorizer configuration created")
|
|
133
147
|
return config
|
|
148
|
+
|
|
149
|
+
def prompt_request_header_allowlist(self) -> Optional[dict]:
|
|
150
|
+
"""Prompt for request header allowlist configuration. Returns allowlist config dict or None."""
|
|
151
|
+
if self.non_interactive:
|
|
152
|
+
_print_success("Using default request header configuration")
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
console.print("\n🔒 [cyan]Request Header Allowlist[/cyan]")
|
|
156
|
+
console.print("[dim]Configure which request headers are allowed to pass through to your agent.[/dim]")
|
|
157
|
+
console.print("[dim]Common headers: Authorization, X-Amzn-Bedrock-AgentCore-Runtime-Custom-*[/dim]")
|
|
158
|
+
|
|
159
|
+
# Get existing allowlist values
|
|
160
|
+
existing_headers = ""
|
|
161
|
+
if (
|
|
162
|
+
self.existing_config
|
|
163
|
+
and self.existing_config.request_header_configuration
|
|
164
|
+
and "requestHeaderAllowlist" in self.existing_config.request_header_configuration
|
|
165
|
+
):
|
|
166
|
+
existing_headers = ",".join(self.existing_config.request_header_configuration["requestHeaderAllowlist"])
|
|
167
|
+
|
|
168
|
+
allowlist_default = "yes" if existing_headers else "no"
|
|
169
|
+
response = _prompt_with_default("Configure request header allowlist? (yes/no)", allowlist_default)
|
|
170
|
+
|
|
171
|
+
if response.lower() in ["yes", "y"]:
|
|
172
|
+
return self._configure_request_header_allowlist(existing_headers)
|
|
173
|
+
else:
|
|
174
|
+
_print_success("Using default request header configuration")
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
def _configure_request_header_allowlist(self, existing_headers: str = "") -> dict:
|
|
178
|
+
"""Configure request header allowlist and return config dict."""
|
|
179
|
+
console.print("\n📋 [cyan]Request Header Allowlist Configuration[/cyan]")
|
|
180
|
+
|
|
181
|
+
# Show existing config if available
|
|
182
|
+
if existing_headers:
|
|
183
|
+
console.print(f"[dim]Previously configured: {existing_headers}[/dim]")
|
|
184
|
+
|
|
185
|
+
# Prompt for headers
|
|
186
|
+
default_headers = existing_headers or "Authorization,X-Amzn-Bedrock-AgentCore-Runtime-Custom-*"
|
|
187
|
+
headers_input = _prompt_with_default("Enter allowed request headers (comma-separated)", default_headers)
|
|
188
|
+
|
|
189
|
+
if not headers_input:
|
|
190
|
+
_handle_error("At least one request header must be specified for allowlist configuration")
|
|
191
|
+
|
|
192
|
+
# Parse and validate headers
|
|
193
|
+
headers = [header.strip() for header in headers_input.split(",") if header.strip()]
|
|
194
|
+
|
|
195
|
+
if not headers:
|
|
196
|
+
_handle_error("Empty request header allowlist provided")
|
|
197
|
+
|
|
198
|
+
_print_success(f"Request header allowlist configured with {len(headers)} headers")
|
|
199
|
+
|
|
200
|
+
return {"requestHeaderAllowlist": headers}
|
|
@@ -176,6 +176,145 @@ class GatewayClient:
|
|
|
176
176
|
self.logger.info("\n✅Target is ready")
|
|
177
177
|
return target
|
|
178
178
|
|
|
179
|
+
def fix_iam_permissions(self, gateway: dict) -> None:
|
|
180
|
+
"""Fix IAM role trust policy for the gateway.
|
|
181
|
+
|
|
182
|
+
:param gateway: the gateway dict containing roleArn
|
|
183
|
+
"""
|
|
184
|
+
# Check for None gateway
|
|
185
|
+
if gateway is None:
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
# Check for missing roleArn
|
|
189
|
+
role_arn = gateway.get("roleArn")
|
|
190
|
+
if not role_arn:
|
|
191
|
+
return
|
|
192
|
+
|
|
193
|
+
sts = boto3.client("sts")
|
|
194
|
+
iam = boto3.client("iam")
|
|
195
|
+
|
|
196
|
+
account_id = sts.get_caller_identity()["Account"]
|
|
197
|
+
role_name = role_arn.split("/")[-1]
|
|
198
|
+
|
|
199
|
+
# Update trust policy
|
|
200
|
+
trust_policy = {
|
|
201
|
+
"Version": "2012-10-17",
|
|
202
|
+
"Statement": [
|
|
203
|
+
{
|
|
204
|
+
"Effect": "Allow",
|
|
205
|
+
"Principal": {"Service": "bedrock-agentcore.amazonaws.com"},
|
|
206
|
+
"Action": "sts:AssumeRole",
|
|
207
|
+
"Condition": {
|
|
208
|
+
"StringEquals": {"aws:SourceAccount": account_id},
|
|
209
|
+
"ArnLike": {"aws:SourceArn": f"arn:aws:bedrock-agentcore:{self.region}:{account_id}:*"},
|
|
210
|
+
},
|
|
211
|
+
}
|
|
212
|
+
],
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
iam.update_assume_role_policy(RoleName=role_name, PolicyDocument=json.dumps(trust_policy))
|
|
217
|
+
|
|
218
|
+
# Add Lambda permissions
|
|
219
|
+
iam.put_role_policy(
|
|
220
|
+
RoleName=role_name,
|
|
221
|
+
PolicyName="LambdaInvokePolicy",
|
|
222
|
+
PolicyDocument=json.dumps(
|
|
223
|
+
{
|
|
224
|
+
"Version": "2012-10-17",
|
|
225
|
+
"Statement": [
|
|
226
|
+
{
|
|
227
|
+
"Effect": "Allow",
|
|
228
|
+
"Action": ["lambda:InvokeFunction"],
|
|
229
|
+
"Resource": (
|
|
230
|
+
f"arn:aws:lambda:{self.region}:{account_id}:function:AgentCoreLambdaTestFunction"
|
|
231
|
+
),
|
|
232
|
+
}
|
|
233
|
+
],
|
|
234
|
+
}
|
|
235
|
+
),
|
|
236
|
+
)
|
|
237
|
+
self.logger.info("✓ Fixed IAM permissions for Gateway")
|
|
238
|
+
except Exception as e:
|
|
239
|
+
self.logger.warning("⚠️ IAM role update failed: %s. Continuing with best effort.", str(e))
|
|
240
|
+
|
|
241
|
+
def cleanup_gateway(self, gateway_id: str, client_info: Optional[Dict] = None) -> None:
|
|
242
|
+
"""Remove all resources associated with a gateway.
|
|
243
|
+
|
|
244
|
+
:param gateway_id: the ID of the gateway to clean up
|
|
245
|
+
:param client_info: optional Cognito client info for cleanup
|
|
246
|
+
"""
|
|
247
|
+
self.logger.info("🧹 Cleaning up Gateway resources...")
|
|
248
|
+
|
|
249
|
+
gateway_client = self.client
|
|
250
|
+
|
|
251
|
+
# Step 1: List and delete all targets
|
|
252
|
+
self.logger.info(" • Finding targets for gateway: %s", gateway_id)
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
response = gateway_client.list_gateway_targets(gatewayIdentifier=gateway_id)
|
|
256
|
+
# API returns targets in 'items' field
|
|
257
|
+
targets = response.get("items", [])
|
|
258
|
+
self.logger.info(" Found %s targets to delete", len(targets))
|
|
259
|
+
|
|
260
|
+
for target in targets:
|
|
261
|
+
target_id = target["targetId"]
|
|
262
|
+
self.logger.info(" • Deleting target: %s", target_id)
|
|
263
|
+
try:
|
|
264
|
+
gateway_client.delete_gateway_target(gatewayIdentifier=gateway_id, targetId=target_id)
|
|
265
|
+
self.logger.info(" ✓ Target deletion initiated: %s", target_id)
|
|
266
|
+
# Wait for deletion to complete
|
|
267
|
+
time.sleep(5)
|
|
268
|
+
except Exception as e:
|
|
269
|
+
self.logger.warning(" ⚠️ Error deleting target %s: %s", target_id, str(e))
|
|
270
|
+
|
|
271
|
+
# Verify all targets are deleted
|
|
272
|
+
self.logger.info(" • Verifying targets deletion...")
|
|
273
|
+
time.sleep(5) # Additional wait
|
|
274
|
+
verify_response = gateway_client.list_gateway_targets(gatewayIdentifier=gateway_id)
|
|
275
|
+
remaining_targets = verify_response.get("items", [])
|
|
276
|
+
if remaining_targets:
|
|
277
|
+
self.logger.warning(" ⚠️ %s targets still remain", len(remaining_targets))
|
|
278
|
+
else:
|
|
279
|
+
self.logger.info(" ✓ All targets deleted")
|
|
280
|
+
|
|
281
|
+
except Exception as e:
|
|
282
|
+
self.logger.warning(" ⚠️ Error managing targets: %s", str(e))
|
|
283
|
+
|
|
284
|
+
# Step 2: Delete the gateway
|
|
285
|
+
try:
|
|
286
|
+
self.logger.info(" • Deleting gateway: %s", gateway_id)
|
|
287
|
+
gateway_client.delete_gateway(gatewayIdentifier=gateway_id)
|
|
288
|
+
self.logger.info(" ✓ Gateway deleted: %s", gateway_id)
|
|
289
|
+
except Exception as e:
|
|
290
|
+
self.logger.warning(" ⚠️ Error deleting gateway: %s", str(e))
|
|
291
|
+
|
|
292
|
+
# Step 3: Delete Cognito resources if provided
|
|
293
|
+
if client_info and "user_pool_id" in client_info:
|
|
294
|
+
cognito = boto3.client("cognito-idp", region_name=self.region)
|
|
295
|
+
user_pool_id = client_info["user_pool_id"]
|
|
296
|
+
|
|
297
|
+
# Delete domain first
|
|
298
|
+
if "domain_prefix" in client_info:
|
|
299
|
+
domain_prefix = client_info["domain_prefix"]
|
|
300
|
+
self.logger.info(" • Deleting Cognito domain: %s", domain_prefix)
|
|
301
|
+
try:
|
|
302
|
+
cognito.delete_user_pool_domain(UserPoolId=user_pool_id, Domain=domain_prefix)
|
|
303
|
+
self.logger.info(" ✓ Cognito domain deleted")
|
|
304
|
+
time.sleep(5) # Wait for domain deletion
|
|
305
|
+
except Exception as e:
|
|
306
|
+
self.logger.warning(" ⚠️ Error deleting Cognito domain: %s", str(e))
|
|
307
|
+
|
|
308
|
+
# Now delete the user pool
|
|
309
|
+
self.logger.info(" • Deleting Cognito user pool: %s", user_pool_id)
|
|
310
|
+
try:
|
|
311
|
+
cognito.delete_user_pool(UserPoolId=user_pool_id)
|
|
312
|
+
self.logger.info(" ✓ Cognito user pool deleted")
|
|
313
|
+
except Exception as e:
|
|
314
|
+
self.logger.warning(" ⚠️ Error deleting Cognito user pool: %s", str(e))
|
|
315
|
+
|
|
316
|
+
self.logger.info("✅ Cleanup complete")
|
|
317
|
+
|
|
179
318
|
def __handle_lambda_target_creation(self, role_arn: str) -> Dict[str, Any]:
|
|
180
319
|
"""Create a test lambda.
|
|
181
320
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""BedrockAgentCore Starter Toolkit cli memory package."""
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Constants for Bedrock AgentCore Memory SDK."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class StrategyType(Enum):
|
|
8
|
+
"""Memory strategy types with integrated wrapper key and type methods."""
|
|
9
|
+
|
|
10
|
+
SEMANTIC = "semanticMemoryStrategy"
|
|
11
|
+
SUMMARY = "summaryMemoryStrategy"
|
|
12
|
+
USER_PREFERENCE = "userPreferenceMemoryStrategy"
|
|
13
|
+
CUSTOM = "customMemoryStrategy"
|
|
14
|
+
|
|
15
|
+
def extraction_wrapper_key(self) -> Optional[str]:
|
|
16
|
+
"""Get the extraction wrapper key for this strategy type."""
|
|
17
|
+
extraction_keys = {
|
|
18
|
+
StrategyType.SEMANTIC: "semanticExtractionConfiguration",
|
|
19
|
+
StrategyType.USER_PREFERENCE: "userPreferenceExtractionConfiguration",
|
|
20
|
+
}
|
|
21
|
+
return extraction_keys.get(self)
|
|
22
|
+
|
|
23
|
+
def consolidation_wrapper_key(self) -> Optional[str]:
|
|
24
|
+
"""Get the consolidation wrapper key for this strategy type."""
|
|
25
|
+
# Only SUMMARY strategy has a consolidation wrapper key
|
|
26
|
+
if self == StrategyType.SUMMARY:
|
|
27
|
+
return "summaryConsolidationConfiguration"
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
def get_memory_strategy(self) -> str:
|
|
31
|
+
"""Get the internal memory strategy type string."""
|
|
32
|
+
strategy_mapping = {
|
|
33
|
+
StrategyType.SEMANTIC: "SEMANTIC",
|
|
34
|
+
StrategyType.SUMMARY: "SUMMARIZATION",
|
|
35
|
+
StrategyType.USER_PREFERENCE: "USER_PREFERENCE",
|
|
36
|
+
StrategyType.CUSTOM: "CUSTOM",
|
|
37
|
+
}
|
|
38
|
+
return strategy_mapping[self]
|
|
39
|
+
|
|
40
|
+
def get_override_type(self) -> Optional[str]:
|
|
41
|
+
"""Get the override type for custom strategies."""
|
|
42
|
+
# This method is primarily for CUSTOM strategy type
|
|
43
|
+
# The actual override type would be determined by context
|
|
44
|
+
if self == StrategyType.CUSTOM:
|
|
45
|
+
return "CUSTOM_OVERRIDE" # Base type, specific override determined by usage
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class OverrideType(Enum):
|
|
50
|
+
"""Custom strategy override types."""
|
|
51
|
+
|
|
52
|
+
SEMANTIC_OVERRIDE = "SEMANTIC_OVERRIDE"
|
|
53
|
+
SUMMARY_OVERRIDE = "SUMMARY_OVERRIDE"
|
|
54
|
+
USER_PREFERENCE_OVERRIDE = "USER_PREFERENCE_OVERRIDE"
|
|
55
|
+
|
|
56
|
+
def extraction_wrapper_key(self) -> Optional[str]:
|
|
57
|
+
"""Get the extraction wrapper key for this override type."""
|
|
58
|
+
extraction_keys = {
|
|
59
|
+
OverrideType.SEMANTIC_OVERRIDE: "semanticExtractionOverride",
|
|
60
|
+
OverrideType.USER_PREFERENCE_OVERRIDE: "userPreferenceExtractionOverride",
|
|
61
|
+
}
|
|
62
|
+
return extraction_keys.get(self)
|
|
63
|
+
|
|
64
|
+
def consolidation_wrapper_key(self) -> Optional[str]:
|
|
65
|
+
"""Get the consolidation wrapper key for this override type."""
|
|
66
|
+
consolidation_keys = {
|
|
67
|
+
OverrideType.SEMANTIC_OVERRIDE: "semanticConsolidationOverride",
|
|
68
|
+
OverrideType.SUMMARY_OVERRIDE: "summaryConsolidationOverride",
|
|
69
|
+
OverrideType.USER_PREFERENCE_OVERRIDE: "userPreferenceConsolidationOverride",
|
|
70
|
+
}
|
|
71
|
+
return consolidation_keys.get(self)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class MemoryStatus(Enum):
|
|
75
|
+
"""Memory resource statuses."""
|
|
76
|
+
|
|
77
|
+
CREATING = "CREATING"
|
|
78
|
+
ACTIVE = "ACTIVE"
|
|
79
|
+
FAILED = "FAILED"
|
|
80
|
+
UPDATING = "UPDATING"
|
|
81
|
+
DELETING = "DELETING"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class MemoryStrategyStatus(Enum):
|
|
85
|
+
"""Memory strategy statuses (new from API update)."""
|
|
86
|
+
|
|
87
|
+
CREATING = "CREATING"
|
|
88
|
+
ACTIVE = "ACTIVE"
|
|
89
|
+
DELETING = "DELETING"
|
|
90
|
+
FAILED = "FAILED"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# Default namespaces for each strategy type
|
|
94
|
+
DEFAULT_NAMESPACES: Dict[StrategyType, List[str]] = {
|
|
95
|
+
StrategyType.SEMANTIC: ["/actor/{actorId}/strategy/{strategyId}/{sessionId}"],
|
|
96
|
+
StrategyType.SUMMARY: ["/actor/{actorId}/strategy/{strategyId}/{sessionId}"],
|
|
97
|
+
StrategyType.USER_PREFERENCE: ["/actor/{actorId}/strategy/{strategyId}"],
|
|
98
|
+
}
|