Skip to content

Commit bc95735

Browse files
sundargthbSundar Raghavan
andauthored
feat: Add certificates to create_code_interpreter() (#373)
Co-authored-by: Sundar Raghavan <sdraghav@amazon.com>
1 parent 9341468 commit bc95735

File tree

3 files changed

+234
-1
lines changed

3 files changed

+234
-1
lines changed

src/bedrock_agentcore/tools/code_interpreter_client.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,19 @@
1616
from bedrock_agentcore._utils.endpoints import get_control_plane_endpoint, get_data_plane_endpoint
1717
from bedrock_agentcore._utils.user_agent import build_user_agent_suffix
1818

19+
from .config import Certificate
20+
1921
DEFAULT_IDENTIFIER = "aws.codeinterpreter.v1"
2022
DEFAULT_TIMEOUT = 900
2123

2224

25+
def _to_dict(obj):
26+
"""Convert an object to a dict, calling to_dict() if available."""
27+
if hasattr(obj, "to_dict"):
28+
return obj.to_dict()
29+
return obj
30+
31+
2332
class CodeInterpreter:
2433
"""Client for interacting with the AWS Code Interpreter sandbox service.
2534
@@ -134,6 +143,7 @@ def create_code_interpreter(
134143
execution_role_arn: str,
135144
network_configuration: Optional[Dict] = None,
136145
description: Optional[str] = None,
146+
certificates: Optional[List[Union[Certificate, Dict[str, Any]]]] = None,
137147
tags: Optional[Dict[str, str]] = None,
138148
client_token: Optional[str] = None,
139149
) -> Dict:
@@ -155,6 +165,8 @@ def create_code_interpreter(
155165
}
156166
}
157167
description (Optional[str]): Description of the interpreter (1-4096 chars)
168+
certificates (Optional[List[Union[Certificate, Dict]]]): Root CA certificates
169+
from Secrets Manager for the code interpreter to trust.
158170
tags (Optional[Dict[str, str]]): Tags for the interpreter
159171
client_token (Optional[str]): Idempotency token
160172
@@ -193,6 +205,9 @@ def create_code_interpreter(
193205
if description:
194206
request_params["description"] = description
195207

208+
if certificates:
209+
request_params["certificates"] = [_to_dict(c) for c in certificates]
210+
196211
if tags:
197212
request_params["tags"] = tags
198213

tests/bedrock_agentcore/tools/test_code_interpreter_client.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1500,3 +1500,127 @@ def test_execute_code_auto_starts_session(self, mock_boto3, mock_get_data_endpoi
15001500
# Assert
15011501
client.data_plane_client.start_code_interpreter_session.assert_called_once()
15021502
client.data_plane_client.invoke_code_interpreter.assert_called_once()
1503+
1504+
@patch("bedrock_agentcore.tools.code_interpreter_client.get_control_plane_endpoint")
1505+
@patch("bedrock_agentcore.tools.code_interpreter_client.get_data_plane_endpoint")
1506+
@patch("bedrock_agentcore.tools.code_interpreter_client.boto3")
1507+
def test_create_code_interpreter_with_certificates(
1508+
self, mock_boto3, mock_get_data_endpoint, mock_get_control_endpoint
1509+
):
1510+
# Arrange
1511+
mock_session = MagicMock()
1512+
mock_control_client = MagicMock()
1513+
mock_data_client = MagicMock()
1514+
mock_session.client.side_effect = [mock_control_client, mock_data_client]
1515+
mock_boto3.Session.return_value = mock_session
1516+
client = CodeInterpreter("us-west-2")
1517+
1518+
mock_response = {
1519+
"codeInterpreterArn": "arn:aws:bedrock-agentcore:us-west-2:123456789012:code-interpreter/test-interp",
1520+
"codeInterpreterId": "test-interp-123",
1521+
"createdAt": datetime.datetime.now(),
1522+
"status": "CREATING",
1523+
}
1524+
client.control_plane_client.create_code_interpreter.return_value = mock_response
1525+
1526+
certificates = [
1527+
{
1528+
"location": {
1529+
"secretsManager": {"secretArn": "arn:aws:secretsmanager:us-west-2:123456789012:secret:my-ca"}
1530+
}
1531+
}
1532+
]
1533+
1534+
# Act
1535+
result = client.create_code_interpreter(
1536+
name="test_interpreter_with_certs",
1537+
execution_role_arn="arn:aws:iam::123456789012:role/InterpreterRole",
1538+
certificates=certificates,
1539+
description="Interpreter with custom root CA",
1540+
)
1541+
1542+
# Assert
1543+
client.control_plane_client.create_code_interpreter.assert_called_once_with(
1544+
name="test_interpreter_with_certs",
1545+
executionRoleArn="arn:aws:iam::123456789012:role/InterpreterRole",
1546+
networkConfiguration={"networkMode": "PUBLIC"},
1547+
description="Interpreter with custom root CA",
1548+
certificates=certificates,
1549+
)
1550+
assert result == mock_response
1551+
1552+
@patch("bedrock_agentcore.tools.code_interpreter_client.get_control_plane_endpoint")
1553+
@patch("bedrock_agentcore.tools.code_interpreter_client.get_data_plane_endpoint")
1554+
@patch("bedrock_agentcore.tools.code_interpreter_client.boto3")
1555+
def test_create_code_interpreter_with_certificate_dataclass(
1556+
self, mock_boto3, mock_get_data_endpoint, mock_get_control_endpoint
1557+
):
1558+
# Arrange
1559+
mock_session = MagicMock()
1560+
mock_control_client = MagicMock()
1561+
mock_data_client = MagicMock()
1562+
mock_session.client.side_effect = [mock_control_client, mock_data_client]
1563+
mock_boto3.Session.return_value = mock_session
1564+
client = CodeInterpreter("us-west-2")
1565+
1566+
mock_response = {
1567+
"codeInterpreterArn": "arn:aws:bedrock-agentcore:us-west-2:123456789012:code-interpreter/test-interp",
1568+
"codeInterpreterId": "test-interp-123",
1569+
"createdAt": datetime.datetime.now(),
1570+
"status": "CREATING",
1571+
}
1572+
client.control_plane_client.create_code_interpreter.return_value = mock_response
1573+
1574+
# Use a mock Certificate dataclass with to_dict method
1575+
mock_cert = MagicMock()
1576+
mock_cert.to_dict.return_value = {
1577+
"location": {"secretsManager": {"secretArn": "arn:aws:secretsmanager:us-west-2:123456789012:secret:my-ca"}}
1578+
}
1579+
1580+
# Act
1581+
result = client.create_code_interpreter(
1582+
name="test_interpreter_with_certs",
1583+
execution_role_arn="arn:aws:iam::123456789012:role/InterpreterRole",
1584+
certificates=[mock_cert],
1585+
)
1586+
1587+
# Assert
1588+
mock_cert.to_dict.assert_called_once()
1589+
client.control_plane_client.create_code_interpreter.assert_called_once_with(
1590+
name="test_interpreter_with_certs",
1591+
executionRoleArn="arn:aws:iam::123456789012:role/InterpreterRole",
1592+
networkConfiguration={"networkMode": "PUBLIC"},
1593+
certificates=[mock_cert.to_dict.return_value],
1594+
)
1595+
assert result == mock_response
1596+
1597+
@patch("bedrock_agentcore.tools.code_interpreter_client.get_control_plane_endpoint")
1598+
@patch("bedrock_agentcore.tools.code_interpreter_client.get_data_plane_endpoint")
1599+
@patch("bedrock_agentcore.tools.code_interpreter_client.boto3")
1600+
def test_create_code_interpreter_without_certificates(
1601+
self, mock_boto3, mock_get_data_endpoint, mock_get_control_endpoint
1602+
):
1603+
"""Verify that omitting certificates does not include it in the API call (backward compatibility)."""
1604+
# Arrange
1605+
mock_session = MagicMock()
1606+
mock_control_client = MagicMock()
1607+
mock_data_client = MagicMock()
1608+
mock_session.client.side_effect = [mock_control_client, mock_data_client]
1609+
mock_boto3.Session.return_value = mock_session
1610+
client = CodeInterpreter("us-west-2")
1611+
1612+
mock_response = {
1613+
"codeInterpreterId": "test-interp-123",
1614+
"status": "CREATING",
1615+
}
1616+
client.control_plane_client.create_code_interpreter.return_value = mock_response
1617+
1618+
# Act
1619+
client.create_code_interpreter(
1620+
name="test_interpreter_no_certs",
1621+
execution_role_arn="arn:aws:iam::123456789012:role/InterpreterRole",
1622+
)
1623+
1624+
# Assert — certificates key should NOT be in the call
1625+
call_kwargs = client.control_plane_client.create_code_interpreter.call_args[1]
1626+
assert "certificates" not in call_kwargs

tests_integ/tools/test_code.py

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@
44
To run: pytest tests_integ/tools/test_code.py -v
55
"""
66

7-
from bedrock_agentcore.tools.code_interpreter_client import code_session
7+
import os
8+
import time
9+
10+
import boto3
11+
12+
from bedrock_agentcore._utils.endpoints import get_control_plane_endpoint
13+
from bedrock_agentcore.tools.code_interpreter_client import CodeInterpreter, code_session
814

915
# Test 1: Basic code execution with system interpreter
1016
print("Test 1: Basic code execution using execute_code()")
@@ -223,6 +229,94 @@
223229
print(event["result"]["content"])
224230
print("✅ Test 10 passed\n")
225231

232+
# Test 11: Create code interpreter with custom root CA certificate
233+
print("Test 11: Code interpreter with custom root CA certificate")
234+
235+
REGION = "us-west-2"
236+
secret_arn = os.environ.get("TEST_ROOT_CA_SECRET_ARN")
237+
execution_role_arn = os.environ.get("TEST_EXECUTION_ROLE_ARN")
238+
239+
if secret_arn and execution_role_arn:
240+
cp_client = boto3.client(
241+
"bedrock-agentcore-control",
242+
region_name=REGION,
243+
endpoint_url=get_control_plane_endpoint(REGION),
244+
)
245+
246+
# Create interpreter with root CA
247+
import time
248+
249+
response = cp_client.create_code_interpreter(
250+
name=f"integ_test_rootca_{int(time.time())}",
251+
executionRoleArn=execution_role_arn,
252+
networkConfiguration={"networkMode": "PUBLIC"},
253+
certificates=[{"location": {"secretsManager": {"secretArn": secret_arn}}}],
254+
description="Integration test: code interpreter with custom root CA",
255+
)
256+
interpreter_id = response["codeInterpreterId"]
257+
print(f" Created interpreter: {interpreter_id}")
258+
259+
# Wait for ready
260+
ci_client = CodeInterpreter(REGION)
261+
for _ in range(40):
262+
info = ci_client.get_code_interpreter(interpreter_id)
263+
if info["status"] == "READY":
264+
break
265+
elif info["status"] == "CREATE_FAILED":
266+
raise RuntimeError(f"Interpreter creation failed: {info.get('failureReason')}")
267+
time.sleep(3)
268+
269+
print(f" Interpreter status: {info['status']}")
270+
271+
# Start session and test SSL connection to untrusted-root.badssl.com
272+
ci_client.start(identifier=interpreter_id)
273+
print(f" Session started: {ci_client.session_id}")
274+
275+
result = ci_client.invoke(
276+
"executeCode",
277+
{
278+
"code": (
279+
"import urllib.request\n"
280+
"response = urllib.request.urlopen("
281+
'"https://untrusted-root.badssl.com")\n'
282+
'print(f"Status: {response.status}")'
283+
),
284+
"language": "python",
285+
},
286+
)
287+
288+
success = False
289+
for event in result.get("stream", []):
290+
if "result" in event:
291+
structured = event["result"].get("structuredContent", {})
292+
stdout = structured.get("stdout", "")
293+
if "200" in stdout:
294+
success = True
295+
print(f" SSL connection succeeded: {stdout.strip()}")
296+
297+
ci_client.stop()
298+
299+
# Cleanup - delete interpreter
300+
sessions = ci_client.list_sessions(interpreter_id=interpreter_id, status="READY")
301+
for s in sessions.get("items", []):
302+
try:
303+
ci_client.data_plane_client.stop_code_interpreter_session(
304+
codeInterpreterIdentifier=interpreter_id, sessionId=s["sessionId"]
305+
)
306+
except Exception:
307+
pass
308+
time.sleep(5)
309+
cp_client.delete_code_interpreter(codeInterpreterId=interpreter_id)
310+
print(f" Cleaned up interpreter: {interpreter_id}")
311+
312+
assert success, "SSL connection to untrusted-root.badssl.com should succeed with custom root CA"
313+
print("✅ Test 11 passed\n")
314+
else:
315+
print(" ⏭️ Skipped (set TEST_ROOT_CA_SECRET_ARN and TEST_EXECUTION_ROLE_ARN env vars to enable)")
316+
print(" Example:")
317+
print(" export TEST_ROOT_CA_SECRET_ARN=arn:aws:secretsmanager:us-west-2:123456789012:secret:badssl-root-ca")
318+
print(" export TEST_EXECUTION_ROLE_ARN=arn:aws:iam::123456789012:role/AgentCoreRole")
319+
print("✅ Test 11 skipped\n")
226320

227321
print("=" * 50)
228322
print("All integration tests passed! ✅")

0 commit comments

Comments
 (0)