Skip to content

Commit 0d89f7b

Browse files
art-dsitclaude
andcommitted
Translate AMI/region mismatch into a clear error
Now that region is resolved from the boto3 chain rather than a required config field, an eval that hardcodes ami_id (the README's recommended "portable eval" shape) but is silent on region will pick up the runner's AWS_DEFAULT_REGION. If that differs from the AMI's region, run_instances fails with a raw InvalidAMIID.NotFound — opaque to someone running an eval they didn't write. Translate the AMI-not-found ClientError (and the empty describe_images result on the volume_size path) into a ValueError naming the AMI, the resolved region, and the fix (set INSPECT_EC2_SANDBOX_REGION, or omit ami_id to auto-resolve for the region). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent bf3bb4e commit 0d89f7b

2 files changed

Lines changed: 64 additions & 2 deletions

File tree

src/ec2sandbox/_instance_provider.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,23 @@
3434
# default provider's ``find_sandbox_instances`` can discover them.
3535
MARKER_TAG_KEY = "inspect_sandbox"
3636

37+
# EC2 error codes meaning "this AMI ID isn't in this region". Turned into a
38+
# clear ValueError so the common footgun — running an eval that hardcodes an
39+
# AMI in a different region from the one the session resolved — is legible.
40+
_AMI_NOT_FOUND_CODES = frozenset(
41+
{"InvalidAMIID.NotFound", "InvalidAMIID.Malformed", "InvalidAMIID.Unavailable"}
42+
)
43+
44+
45+
def _ami_region_mismatch_error(ami_id: str, region: str) -> ValueError:
46+
return ValueError(
47+
f"AMI '{ami_id}' was not found in region '{region}'. AMI IDs are "
48+
"region-scoped, so an eval that hardcodes ami_id only runs in that "
49+
"AMI's region. Set INSPECT_EC2_SANDBOX_REGION (or the config's region) "
50+
"to the AMI's region, or omit ami_id to auto-resolve the Ubuntu 24.04 "
51+
"image for the resolved region."
52+
)
53+
3754

3855
@dataclass(frozen=True)
3956
class ProvisionedInstance:
@@ -166,7 +183,9 @@ def _root_device_name(ec2_client: Any, ami_id: str) -> str:
166183
resp = ec2_client.describe_images(ImageIds=[ami_id])
167184
images = resp.get("Images", [])
168185
if not images:
169-
raise ValueError(f"AMI {ami_id} not found when resolving root device name")
186+
# Almost always an AMI/region mismatch (describe_images returns an
187+
# empty list rather than erroring for a foreign-region AMI).
188+
raise _ami_region_mismatch_error(ami_id, ec2_client.meta.region_name)
170189
return images[0]["RootDeviceName"]
171190

172191

@@ -290,7 +309,15 @@ async def create_instance(
290309
"Ebs": {"VolumeSize": volume_size},
291310
}
292311
]
293-
response = ec2_client.run_instances(**instance_params, MinCount=1, MaxCount=1)
312+
try:
313+
response = ec2_client.run_instances(
314+
**instance_params, MinCount=1, MaxCount=1
315+
)
316+
except ClientError as e:
317+
code = e.response.get("Error", {}).get("Code")
318+
if code in _AMI_NOT_FOUND_CODES:
319+
raise _ami_region_mismatch_error(ami_id, region) from e
320+
raise
294321
instance = response["Instances"][0]
295322
instance_id = instance["InstanceId"]
296323

tests/ec2sandboxtest/test_instance_provider.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from unittest import mock
22

33
import pytest
4+
from botocore.exceptions import ClientError
45

56
from ec2sandbox._instance_provider import DefaultEc2InstanceProvider
67
from ec2sandbox.schema import Ec2SandboxEnvironmentConfig
@@ -128,6 +129,40 @@ async def test_create_instance_stamps_session_resolved_region():
128129
assert ec2_call.kwargs["region_name"] is None
129130

130131

132+
@pytest.mark.asyncio
133+
async def test_run_instances_ami_not_found_raises_region_hint():
134+
"""A region-scoped AMI missing in the resolved region gets a clear error."""
135+
provider, ec2_client, _ = _make_provider_with_mocks(_make_config())
136+
ec2_client.meta.region_name = "us-east-1"
137+
ec2_client.run_instances.side_effect = ClientError(
138+
{"Error": {"Code": "InvalidAMIID.NotFound", "Message": "nope"}},
139+
"RunInstances",
140+
)
141+
142+
with pytest.raises(ValueError, match="region-scoped"):
143+
await provider.create_instance(
144+
instance_type="t3a.micro",
145+
ami_id="ami-123",
146+
tags=[("Name", "x")],
147+
)
148+
149+
150+
@pytest.mark.asyncio
151+
async def test_volume_size_ami_not_found_raises_region_hint():
152+
"""describe_images returning no images (foreign-region AMI) is translated too."""
153+
provider, ec2_client, _ = _make_provider_with_mocks(_make_config(volume_size=100))
154+
ec2_client.meta.region_name = "us-east-1"
155+
ec2_client.describe_images.return_value = {"Images": []}
156+
157+
with pytest.raises(ValueError, match="region-scoped"):
158+
await provider.create_instance(
159+
instance_type="t3a.micro",
160+
ami_id="ami-123",
161+
tags=[("Name", "x")],
162+
volume_size=100,
163+
)
164+
165+
131166
@pytest.mark.asyncio
132167
async def test_create_instance_with_volume_size_sets_block_device_mappings():
133168
provider, ec2_client, _ = _make_provider_with_mocks(_make_config(volume_size=100))

0 commit comments

Comments
 (0)