Skip to content

Commit 9742a05

Browse files
committed
fix: remove double-base64 encoding in upload_file/download_file (#458)
1 parent d69db93 commit 9742a05

3 files changed

Lines changed: 90 additions & 21 deletions

File tree

src/bedrock_agentcore/tools/code_interpreter_client.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
applications to start, stop, and invoke code execution in a managed sandbox environment.
55
"""
66

7-
import base64
87
import logging
98
import re
109
import uuid
@@ -482,7 +481,7 @@ def upload_file(
482481
'scripts/analysis.py'). Must be relative to the working directory.
483482
Absolute paths starting with '/' are not allowed.
484483
content: File content as string (text files) or bytes (binary files).
485-
Binary content will be base64 encoded automatically.
484+
Binary content will be encoded automatically by botocore.
486485
description: Optional semantic description of the file contents.
487486
This is stored as metadata and can help LLMs understand
488487
the data structure (e.g., "CSV with columns: date, revenue, product_id").
@@ -514,7 +513,7 @@ def upload_file(
514513

515514
# Handle binary content
516515
if isinstance(content, bytes):
517-
file_content = {"path": path, "blob": base64.b64encode(content).decode("utf-8")}
516+
file_content = {"path": path, "blob": content}
518517
else:
519518
file_content = {"path": path, "text": content}
520519

@@ -564,7 +563,7 @@ def upload_files(
564563
raise ValueError(f"Path must be relative, not absolute. Got: {path}")
565564

566565
if isinstance(content, bytes):
567-
file_contents.append({"path": path, "blob": base64.b64encode(content).decode("utf-8")})
566+
file_contents.append({"path": path, "blob": content})
568567
else:
569568
file_contents.append({"path": path, "text": content})
570569

@@ -649,7 +648,7 @@ def download_file(
649648
if "text" in resource:
650649
return resource["text"]
651650
elif "blob" in resource:
652-
raw = base64.b64decode(resource["blob"])
651+
raw = resource["blob"]
653652
try:
654653
return raw.decode("utf-8")
655654
except (UnicodeDecodeError, ValueError):
@@ -690,7 +689,7 @@ def download_files(
690689
if "text" in resource:
691690
files[file_path] = resource["text"]
692691
elif "blob" in resource:
693-
raw = base64.b64decode(resource["blob"])
692+
raw = resource["blob"]
694693
try:
695694
files[file_path] = raw.decode("utf-8")
696695
except (UnicodeDecodeError, ValueError):

tests/bedrock_agentcore/tools/test_code_interpreter_client.py

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import base64
21
import datetime
32
from unittest.mock import ANY, MagicMock, patch
43

@@ -657,7 +656,6 @@ def test_upload_file_binary_content(self, mock_boto3):
657656
client.data_plane_client.invoke_code_interpreter.return_value = mock_response
658657

659658
binary_content = b"\x89PNG\r\n\x1a\n" # PNG header bytes
660-
expected_b64 = base64.b64encode(binary_content).decode("utf-8")
661659

662660
# Act
663661
result = client.upload_file(path="image.png", content=binary_content)
@@ -667,7 +665,7 @@ def test_upload_file_binary_content(self, mock_boto3):
667665
codeInterpreterIdentifier="test.identifier",
668666
sessionId="test-session-id",
669667
name="writeFiles",
670-
arguments={"content": [{"path": "image.png", "blob": expected_b64}]},
668+
arguments={"content": [{"path": "image.png", "blob": binary_content}]},
671669
)
672670
assert result == mock_response
673671

@@ -758,7 +756,6 @@ def test_upload_files_mixed_content(self, mock_boto3):
758756
client.data_plane_client.invoke_code_interpreter.return_value = mock_response
759757

760758
binary_content = b"\x00\x01\x02\x03"
761-
expected_b64 = base64.b64encode(binary_content).decode("utf-8")
762759

763760
files = [
764761
{"path": "text.txt", "content": "hello world"},
@@ -776,7 +773,7 @@ def test_upload_files_mixed_content(self, mock_boto3):
776773
arguments={
777774
"content": [
778775
{"path": "text.txt", "text": "hello world"},
779-
{"path": "binary.bin", "blob": expected_b64},
776+
{"path": "binary.bin", "blob": binary_content},
780777
]
781778
},
782779
)
@@ -963,14 +960,13 @@ def test_download_file_binary(self, mock_boto3):
963960
client.session_id = "test-session-id"
964961

965962
binary_content = b"\x89PNG\r\n\x1a\n" # PNG header bytes
966-
encoded_content = base64.b64encode(binary_content).decode("utf-8")
967963

968964
mock_response = {
969965
"stream": [
970966
{
971967
"result": {
972968
"content": [
973-
{"type": "resource", "resource": {"uri": "file://image.png", "blob": encoded_content}}
969+
{"type": "resource", "resource": {"uri": "file://image.png", "blob": binary_content}}
974970
]
975971
}
976972
}
@@ -997,14 +993,16 @@ def test_download_file_blob_utf8_returns_str(self, mock_boto3):
997993
client.session_id = "test-session-id"
998994

999995
text_content = "hello world"
1000-
encoded_content = base64.b64encode(text_content.encode("utf-8")).decode("utf-8")
1001996

1002997
mock_response = {
1003998
"stream": [
1004999
{
10051000
"result": {
10061001
"content": [
1007-
{"type": "resource", "resource": {"uri": "file://data.bin", "blob": encoded_content}}
1002+
{
1003+
"type": "resource",
1004+
"resource": {"uri": "file://data.bin", "blob": text_content.encode("utf-8")},
1005+
}
10081006
]
10091007
}
10101008
}
@@ -1102,7 +1100,6 @@ def test_download_files_binary(self, mock_boto3):
11021100
client.session_id = "test-session-id"
11031101

11041102
binary_content = b"\x89PNG\r\n\x1a\n" # PNG header bytes
1105-
encoded_binary = base64.b64encode(binary_content).decode("utf-8")
11061103

11071104
mock_response = {
11081105
"stream": [
@@ -1120,7 +1117,7 @@ def test_download_files_binary(self, mock_boto3):
11201117
"type": "resource",
11211118
"resource": {
11221119
"uri": "file:///opt/amazon/genesis1p-tools/var/chart.png",
1123-
"blob": encoded_binary,
1120+
"blob": binary_content,
11241121
},
11251122
},
11261123
]
@@ -1150,9 +1147,7 @@ def test_download_files_blob_utf8_returns_str(self, mock_boto3):
11501147
client.session_id = "test-session-id"
11511148

11521149
text_content = "some utf-8 blob content"
1153-
encoded_text = base64.b64encode(text_content.encode("utf-8")).decode("utf-8")
11541150
binary_content = b"\x89PNG\r\n\x1a\n"
1155-
encoded_binary = base64.b64encode(binary_content).decode("utf-8")
11561151

11571152
mock_response = {
11581153
"stream": [
@@ -1163,14 +1158,14 @@ def test_download_files_blob_utf8_returns_str(self, mock_boto3):
11631158
"type": "resource",
11641159
"resource": {
11651160
"uri": "file:///opt/amazon/genesis1p-tools/var/data.bin",
1166-
"blob": encoded_text,
1161+
"blob": text_content.encode("utf-8"),
11671162
},
11681163
},
11691164
{
11701165
"type": "resource",
11711166
"resource": {
11721167
"uri": "file:///opt/amazon/genesis1p-tools/var/chart.png",
1173-
"blob": encoded_binary,
1168+
"blob": binary_content,
11741169
},
11751170
},
11761171
]
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
"""Integration tests for code interpreter binary file upload/download.
2+
3+
Verifies that binary content survives a roundtrip through upload_file/download_file
4+
without double-base64 encoding (issue #458).
5+
6+
Run with:
7+
pytest tests_integ/tools/test_code_interpreter_client.py -xvs
8+
"""
9+
10+
import os
11+
12+
import pytest
13+
14+
from bedrock_agentcore.tools.code_interpreter_client import CodeInterpreter
15+
16+
# 67 bytes of binary data; base64 encoding would produce 92 bytes
17+
PAYLOAD = b"\x89PNG\r\n\x1a\n" + bytes(range(59))
18+
EXPECTED_SIZE = len(PAYLOAD) # 67
19+
20+
21+
def _extract_stdout(stream):
22+
"""Extract stdout content from an execute_code stream response.
23+
24+
Returns the stdout string or raises AssertionError if not found.
25+
"""
26+
for event in stream:
27+
r = event.get("result", {})
28+
stdout = r.get("structuredContent", {}).get("stdout", "")
29+
if stdout:
30+
return stdout
31+
content = r.get("content", "")
32+
if content:
33+
return str(content)
34+
raise AssertionError("stdout not found in stream response")
35+
36+
37+
@pytest.mark.integration
38+
class TestCodeInterpreterClient:
39+
"""Integration tests for CodeInterpreter client."""
40+
41+
@classmethod
42+
def setup_class(cls):
43+
cls.region = os.environ.get("BEDROCK_TEST_REGION", "us-east-1")
44+
cls.client = CodeInterpreter(cls.region)
45+
cls.client.start()
46+
cls.client.upload_file(path="test.bin", content=PAYLOAD)
47+
48+
@classmethod
49+
def teardown_class(cls):
50+
cls.client.stop()
51+
52+
def test_upload_file_writes_correct_size(self):
53+
"""upload_file with binary bytes writes the correct size to disk (not double-base64 encoded)."""
54+
result = self.client.execute_code("import os\nprint(os.path.getsize('test.bin'))")
55+
56+
stdout = _extract_stdout(result["stream"])
57+
disk_size = None
58+
for line in stdout.splitlines():
59+
if line.strip().isdigit():
60+
disk_size = int(line.strip())
61+
62+
assert disk_size is not None, "Could not parse disk size from stdout"
63+
assert disk_size == EXPECTED_SIZE, (
64+
f"Expected {EXPECTED_SIZE} bytes on disk, got {disk_size} (92 would indicate double-base64 encoding)"
65+
)
66+
67+
def test_download_file_returns_original_bytes(self):
68+
"""download_file returns the exact original bytes that were uploaded."""
69+
downloaded = self.client.download_file("test.bin")
70+
71+
assert isinstance(downloaded, bytes), f"Expected bytes, got {type(downloaded).__name__}"
72+
assert downloaded == PAYLOAD, (
73+
f"Downloaded content does not match original payload. "
74+
f"Got {len(downloaded)} bytes, expected {EXPECTED_SIZE} bytes."
75+
)

0 commit comments

Comments
 (0)