Skip to content

Commit 3216fc8

Browse files
fix(import-agent): create gateway Lambda before targets and handle string apiSchema (#515)
* fix(import-agent): create gateway Lambda before targets and handle string apiSchema Gateway control plane validates that the Lambda ARN exists when creating targets. The previous code called create_gateway_lambda_target inside the action group loop before create_lambda was invoked, causing rejections. Restructured into collect-then-create phases. Also added isinstance guards for apiSchema which can be returned as a raw string by the Bedrock API, causing AttributeError on .get() calls. Constraint: Bedrock API returns apiSchema as either dict or raw string Rejected: Parsing string schema inline in base_bedrock_translate | only dict payloads expected there, safer to skip Confidence: high Scope-risk: narrow * style: fix ruff lint errors (unused imports) * chore: remove .omc state files and add to gitignore * fix: remove pointless sleep from data-collection loop The sleep was there to throttle Gateway API calls, but after restructuring, this loop only collects data locally. The sleep remains in the deferred targets loop where API calls actually happen.
1 parent 550bddf commit 3216fc8

4 files changed

Lines changed: 179 additions & 5 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,4 @@ mise.toml
3232
!tests/create/fixtures/scenarios/
3333
!tests/create/fixtures/scenarios/**
3434
.kiro/
35+
.omc/

src/bedrock_agentcore_starter_toolkit/cli/create/import_agent/agent_info.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,10 @@ def get_agent_info(agent_id: str, agent_alias_id: str, bedrock_client, bedrock_a
139139
action_group["actionGroupName"] = clean_variable_name(action_group["actionGroupName"])
140140

141141
if action_group.get("apiSchema", False):
142+
api_schema = action_group["apiSchema"]
143+
if isinstance(api_schema, str):
144+
yaml = YAML(typ="safe")
145+
action_group["apiSchema"] = {"payload": yaml.load(api_schema)}
142146
open_api_schema = action_group["apiSchema"].get("payload", False)
143147
if open_api_schema:
144148
yaml = YAML(typ="safe")

src/bedrock_agentcore_starter_toolkit/services/import_agent/scripts/base_bedrock_translate.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -478,7 +478,8 @@ def generate_openapi_ag_code(self, ag: Dict, platform: str) -> Tuple[list, str]:
478478
lambda_arn = ag.get("actionGroupExecutor", {}).get("lambda", "")
479479
lambda_region = lambda_arn.split(":")[3] if lambda_arn else "us-west-2"
480480

481-
openapi_schema = ag.get("apiSchema", {}).get("payload", {})
481+
api_schema = ag.get("apiSchema", {})
482+
openapi_schema = api_schema.get("payload", {}) if isinstance(api_schema, dict) else {}
482483

483484
for func_name, func_spec in openapi_schema.get("paths", {}).items():
484485
# Function metadata
@@ -1172,10 +1173,9 @@ def create_gateway_proxy_and_targets(self):
11721173

11731174
# Aggregate info from the action_groups
11741175
tool_mappings = {}
1176+
deferred_targets = []
11751177

11761178
for ag in action_groups:
1177-
time.sleep(10) # Sleep to avoid throttling issues with the Gateway API
1178-
11791179
if "lambda" not in ag.get("actionGroupExecutor", {}):
11801180
continue
11811181

@@ -1186,7 +1186,8 @@ def create_gateway_proxy_and_targets(self):
11861186
tools = []
11871187

11881188
if ag.get("apiSchema", False):
1189-
openapi_schema = ag.get("apiSchema", {}).get("payload", {})
1189+
api_schema = ag.get("apiSchema", {})
1190+
openapi_schema = api_schema.get("payload", {}) if isinstance(api_schema, dict) else {}
11901191

11911192
for func_name, func_spec in openapi_schema.get("paths", {}).items():
11921193
clean_func_name = clean_variable_name(func_name)
@@ -1358,7 +1359,7 @@ def create_gateway_proxy_and_targets(self):
13581359
)
13591360

13601361
if tools:
1361-
self.create_gateway_lambda_target(tools, lambda_arn, clean_action_group_name)
1362+
deferred_targets.append((tools, clean_action_group_name))
13621363

13631364
agent_metadata = {
13641365
"name": self.agent_info.get("agentName", ""),
@@ -1472,6 +1473,10 @@ def lambda_handler(event, context):
14721473

14731474
self.create_lambda(lambda_code, function_name)
14741475

1476+
for tools, clean_action_group_name in deferred_targets:
1477+
time.sleep(10) # Sleep to avoid throttling issues with the Gateway API
1478+
self.create_gateway_lambda_target(tools, lambda_arn, clean_action_group_name)
1479+
14751480
def _update_gateway_role_with_lambda_permission(self, function_name):
14761481
"""Update the gateway role with lambda invoke permission."""
14771482
if not self.created_gateway or not self.created_gateway.get("roleArn"):
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
"""Tests for import-agent bug fixes (V2200635700)."""
2+
3+
from unittest.mock import MagicMock, patch
4+
5+
6+
class TestLambdaCreatedBeforeGatewayTargets:
7+
"""Bug #1: Gateway Lambda must be created before targets reference it."""
8+
9+
@patch("boto3.client")
10+
def test_create_lambda_called_before_gateway_targets(self, mock_boto_client):
11+
"""Verify create_lambda is called before any create_gateway_lambda_target."""
12+
from bedrock_agentcore_starter_toolkit.services.import_agent.scripts.base_bedrock_translate import (
13+
BaseBedrockTranslator,
14+
)
15+
16+
mock_sts = MagicMock()
17+
mock_sts.get_caller_identity.return_value = {"Account": "123456789012"}
18+
mock_boto_client.return_value = mock_sts
19+
20+
translator = BaseBedrockTranslator.__new__(BaseBedrockTranslator)
21+
translator.agent_region = "us-west-2"
22+
translator.cleaned_agent_name = "test_agent"
23+
translator.created_gateway = {"gatewayId": "gw-123", "roleArn": "arn:aws:iam::123456789012:role/test"}
24+
translator.gateway_client = MagicMock()
25+
translator.agent_info = {"agentName": "test", "agentId": "id", "alias": "a", "version": "1"}
26+
27+
translator.custom_ags = [
28+
{
29+
"actionGroupExecutor": {"lambda": "arn:aws:lambda:us-west-2:123456789012:function:orig"},
30+
"actionGroupName": "TestGroup",
31+
"description": "test",
32+
"apiSchema": {
33+
"payload": {
34+
"paths": {
35+
"/test": {
36+
"get": {
37+
"summary": "Test endpoint",
38+
"parameters": [],
39+
}
40+
}
41+
}
42+
}
43+
},
44+
}
45+
]
46+
47+
call_order = []
48+
translator.create_lambda = MagicMock(side_effect=lambda *a, **kw: call_order.append("create_lambda"))
49+
translator.create_gateway_lambda_target = MagicMock(
50+
side_effect=lambda *a, **kw: call_order.append("create_gateway_lambda_target")
51+
)
52+
53+
with patch("time.sleep"):
54+
translator.create_gateway_proxy_and_targets()
55+
56+
assert call_order[0] == "create_lambda", "create_lambda must be called before create_gateway_lambda_target"
57+
assert "create_gateway_lambda_target" in call_order
58+
59+
60+
class TestApiSchemaAsString:
61+
"""Bug #2: apiSchema can be a raw string instead of a dict."""
62+
63+
def test_agent_info_handles_string_api_schema(self):
64+
"""agent_info.py string apiSchema guard converts it to a dict with parsed payload."""
65+
from ruamel.yaml import YAML
66+
67+
# Simulate what agent_info.py does with string apiSchema
68+
action_group = {
69+
"apiSchema": "openapi: '3.0.0'\npaths: {}",
70+
"actionGroupName": "TestGroup",
71+
}
72+
73+
api_schema = action_group["apiSchema"]
74+
if isinstance(api_schema, str):
75+
yaml = YAML(typ="safe")
76+
action_group["apiSchema"] = {"payload": yaml.load(api_schema)}
77+
78+
# Verify it was converted to a dict with parsed YAML
79+
assert isinstance(action_group["apiSchema"], dict)
80+
assert action_group["apiSchema"]["payload"] == {"openapi": "3.0.0", "paths": {}}
81+
# .get() should now work
82+
assert action_group["apiSchema"].get("payload", False) is not False
83+
84+
@patch("boto3.client")
85+
def test_base_translate_handles_string_api_schema(self, mock_boto_client):
86+
"""base_bedrock_translate.py should skip string apiSchema gracefully."""
87+
from bedrock_agentcore_starter_toolkit.services.import_agent.scripts.base_bedrock_translate import (
88+
BaseBedrockTranslator,
89+
)
90+
91+
mock_sts = MagicMock()
92+
mock_sts.get_caller_identity.return_value = {"Account": "123456789012"}
93+
mock_boto_client.return_value = mock_sts
94+
95+
translator = BaseBedrockTranslator.__new__(BaseBedrockTranslator)
96+
translator.agent_region = "us-west-2"
97+
translator.cleaned_agent_name = "test_agent"
98+
translator.created_gateway = {"gatewayId": "gw-123", "roleArn": "arn:aws:iam::123456789012:role/test"}
99+
translator.gateway_client = MagicMock()
100+
translator.agent_info = {"agentName": "test", "agentId": "id", "alias": "a", "version": "1"}
101+
102+
translator.custom_ags = [
103+
{
104+
"actionGroupExecutor": {"lambda": "arn:aws:lambda:us-west-2:123456789012:function:orig"},
105+
"actionGroupName": "TestGroup",
106+
"description": "test",
107+
"apiSchema": "openapi: '3.0.0'\npaths: {}",
108+
}
109+
]
110+
111+
translator.create_lambda = MagicMock()
112+
translator.create_gateway_lambda_target = MagicMock()
113+
114+
with patch("time.sleep"):
115+
translator.create_gateway_proxy_and_targets()
116+
117+
# Should not crash; no targets created since string schema yields no tools
118+
translator.create_gateway_lambda_target.assert_not_called()
119+
120+
@patch("boto3.client")
121+
def test_dict_api_schema_still_works(self, mock_boto_client):
122+
"""Regression: dict apiSchema with payload should still extract tools."""
123+
from bedrock_agentcore_starter_toolkit.services.import_agent.scripts.base_bedrock_translate import (
124+
BaseBedrockTranslator,
125+
)
126+
127+
mock_sts = MagicMock()
128+
mock_sts.get_caller_identity.return_value = {"Account": "123456789012"}
129+
mock_boto_client.return_value = mock_sts
130+
131+
translator = BaseBedrockTranslator.__new__(BaseBedrockTranslator)
132+
translator.agent_region = "us-west-2"
133+
translator.cleaned_agent_name = "test_agent"
134+
translator.created_gateway = {"gatewayId": "gw-123", "roleArn": "arn:aws:iam::123456789012:role/test"}
135+
translator.gateway_client = MagicMock()
136+
translator.agent_info = {"agentName": "test", "agentId": "id", "alias": "a", "version": "1"}
137+
138+
translator.custom_ags = [
139+
{
140+
"actionGroupExecutor": {"lambda": "arn:aws:lambda:us-west-2:123456789012:function:orig"},
141+
"actionGroupName": "TestGroup",
142+
"description": "test",
143+
"apiSchema": {
144+
"payload": {
145+
"paths": {
146+
"/test": {
147+
"get": {
148+
"summary": "Test endpoint",
149+
"parameters": [],
150+
}
151+
}
152+
}
153+
}
154+
},
155+
}
156+
]
157+
158+
translator.create_lambda = MagicMock()
159+
translator.create_gateway_lambda_target = MagicMock()
160+
161+
with patch("time.sleep"):
162+
translator.create_gateway_proxy_and_targets()
163+
164+
translator.create_gateway_lambda_target.assert_called_once()

0 commit comments

Comments
 (0)