Skip to content

Commit e66fb1d

Browse files
committed
Fixes bug in sandbox_client
1 parent e74a50e commit e66fb1d

File tree

2 files changed

+89
-35
lines changed

2 files changed

+89
-35
lines changed

.gitignore

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,8 @@ bin/
77
.netlify
88
# Added by goreleaser init:
99
dist/
10-
release_assets/
10+
release_assets/
11+
12+
# Ignore temporary folders
13+
tmp/
14+
temp/

clients/python/agentic-sandbox-client/agentic_sandbox/sandbox_client.py

Lines changed: 84 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@
3333
SANDBOX_API_VERSION = "v1alpha1"
3434
SANDBOX_PLURAL_NAME = "sandboxes"
3535

36-
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', stream=sys.stdout)
36+
logging.basicConfig(level=logging.INFO,
37+
format='%(asctime)s - %(levelname)s - %(message)s', stream=sys.stdout)
38+
3739

3840
@dataclass
3941
class ExecutionResult:
@@ -42,15 +44,17 @@ class ExecutionResult:
4244
stderr: str
4345
exit_code: int
4446

47+
4548
class SandboxClient:
4649
"""
4750
The main client for creating and interacting with a stateful Sandbox (now named SandboxClient).
4851
This class is a context manager, designed to be used with a `with` statement.
4952
"""
53+
5054
def __init__(
51-
self,
52-
template_name: str,
53-
namespace: str = "default",
55+
self,
56+
template_name: str,
57+
namespace: str = "default",
5458
server_port: int = 8888,
5559
sandbox_ready_timeout: int = 180,
5660
port_forward_ready_timeout: int = 30
@@ -62,6 +66,7 @@ def __init__(
6266
self.port_forward_ready_timeout = port_forward_ready_timeout
6367
self.claim_name: str | None = None
6468
self.sandbox_name: str | None = None
69+
self.pod_name: str | None = None
6570
self.base_url = f"http://127.0.0.1:{self.server_port}"
6671
self.port_forward_process: subprocess.Popen | None = None
6772

@@ -97,11 +102,39 @@ def _create_claim(self):
97102

98103
def _wait_for_sandbox_ready(self):
99104
"""
100-
Waits for the Sandbox custom resource to have a 'Ready' status condition.
105+
Waits for the SandboxClaim to be populated with the Sandbox name, and then
106+
waits for the Sandbox custom resource to have a 'Ready' status condition.
101107
This indicates that the underlying pod is running and has passed its checks.
102108
"""
103109
if not self.claim_name:
104-
raise RuntimeError("Cannot wait for sandbox, claim has not been created.")
110+
raise RuntimeError(
111+
"Cannot wait for sandbox, claim has not been created.")
112+
113+
# Watch for the SandboxClaim to be updated with the sandbox name
114+
w_claim = watch.Watch()
115+
for event in w_claim.stream(
116+
self.custom_objects_api.list_namespaced_custom_object,
117+
namespace=self.namespace,
118+
group=CLAIM_API_GROUP,
119+
version=CLAIM_API_VERSION,
120+
plural=CLAIM_PLURAL_NAME,
121+
field_selector=f"metadata.name={self.claim_name}",
122+
timeout_seconds=self.sandbox_ready_timeout
123+
):
124+
if event["type"] in ["ADDED", "MODIFIED"]:
125+
claim_obj = event["object"]
126+
status = claim_obj.get("status", {})
127+
sandbox_status = status.get("sandbox", {})
128+
if sandbox_status and sandbox_status.get("Name"):
129+
self.sandbox_name = sandbox_status.get("Name")
130+
w_claim.stop()
131+
break
132+
else:
133+
self.__exit__(None, None, None) # Attempt cleanup
134+
raise TimeoutError(
135+
f"SandboxClaim did not become ready within {self.sandbox_ready_timeout} seconds.")
136+
137+
# Watch for the Sandbox to become ready
105138
w = watch.Watch()
106139
logging.info("Watching for Sandbox to become ready...")
107140
for event in w.stream(
@@ -110,40 +143,52 @@ def _wait_for_sandbox_ready(self):
110143
group=SANDBOX_API_GROUP,
111144
version=SANDBOX_API_VERSION,
112145
plural=SANDBOX_PLURAL_NAME,
113-
field_selector=f"metadata.name={self.claim_name}",
146+
field_selector=f"metadata.name={self.sandbox_name}",
114147
timeout_seconds=self.sandbox_ready_timeout
115148
):
116-
sandbox_object = event['object']
117-
status = sandbox_object.get('status', {})
118-
conditions = status.get('conditions', [])
119-
is_ready = False
120-
for cond in conditions:
121-
if cond.get('type') == 'Ready' and cond.get('status') == 'True':
122-
is_ready = True
123-
break
149+
if event["type"] in ["ADDED", "MODIFIED"]:
150+
sandbox_object = event['object']
151+
status = sandbox_object.get('status', {})
152+
conditions = status.get('conditions', [])
153+
is_ready = False
154+
for cond in conditions:
155+
if cond.get('type') == 'Ready' and cond.get('status') == 'True':
156+
is_ready = True
157+
break
124158

125-
if is_ready:
126-
self.sandbox_name = sandbox_object['metadata']['name']
127-
w.stop()
128-
logging.info(f"Sandbox {self.sandbox_name} is ready.")
129-
break
159+
if is_ready:
160+
annotations = sandbox_object.get(
161+
'metadata', {}).get('annotations', {})
162+
pod_name_annotation = "agents.x-k8s.io/pod-name"
163+
if pod_name_annotation in annotations:
164+
self.pod_name = annotations[pod_name_annotation]
165+
logging.info(
166+
f"Found pod name from annotation: {self.pod_name}")
167+
else:
168+
self.pod_name = self.sandbox_name
169+
w.stop()
170+
logging.info(f"Sandbox {self.sandbox_name} is ready.")
171+
break
130172

131-
if not self.sandbox_name:
173+
if not self.pod_name:
132174
self.__exit__(None, None, None)
133-
raise TimeoutError(f"Sandbox did not become ready within {self.sandbox_ready_timeout} seconds.")
175+
raise TimeoutError(
176+
f"Sandbox did not become ready or pod name could not be determined within {self.sandbox_ready_timeout} seconds.")
134177

135178
def _start_and_wait_for_port_forward(self):
136179
"""
137180
Starts the 'kubectl port-forward' subprocess and waits for the local port
138181
to be open and listening, ensuring the tunnel is ready for traffic.
139182
"""
140-
if not self.sandbox_name:
141-
raise RuntimeError("Cannot start port-forwarding, sandbox name is not known.")
142-
logging.info(f"Starting port-forwarding for sandbox {self.sandbox_name}...")
183+
if not self.pod_name:
184+
raise RuntimeError(
185+
"Cannot start port-forwarding, sandbox pod name is not known.")
186+
logging.info(
187+
f"Starting port-forwarding for sandbox {self.sandbox_name} with sandbox pod {self.pod_name}...")
143188
self.port_forward_process = subprocess.Popen(
144189
[
145190
"kubectl", "port-forward",
146-
f"pod/{self.sandbox_name}",
191+
f"pod/{self.pod_name}",
147192
f"{self.server_port}:{self.server_port}",
148193
"-n", self.namespace
149194
],
@@ -165,14 +210,16 @@ def _start_and_wait_for_port_forward(self):
165210

166211
try:
167212
with socket.create_connection(("127.0.0.1", self.server_port), timeout=0.1):
168-
logging.info(f"Port-forwarding is ready on port {self.server_port}.")
213+
logging.info(
214+
f"Port-forwarding is ready on port {self.server_port}.")
169215
return
170216
except (socket.timeout, ConnectionRefusedError):
171217
time.sleep(0.2) # Wait before retrying
172218

173219
# If the loop finishes, it timed out
174220
self.__exit__(None, None, None)
175-
raise TimeoutError(f"Port-forwarding did not become ready within {self.port_forward_ready_timeout} seconds.")
221+
raise TimeoutError(
222+
f"Port-forwarding did not become ready within {self.port_forward_ready_timeout} seconds.")
176223

177224
def __enter__(self) -> 'SandboxClient':
178225
"""Creates the SandboxClaim resource and waits for the Sandbox to become ready."""
@@ -200,7 +247,8 @@ def __exit__(self, exc_type, exc_val, exc_tb):
200247
)
201248
except client.ApiException as e:
202249
if e.status != 404:
203-
logging.error(f"Error deleting sandbox claim: {e}", exc_info=True)
250+
logging.error(
251+
f"Error deleting sandbox claim: {e}", exc_info=True)
204252

205253
def _request(self, method: str, endpoint: str, **kwargs) -> requests.Response:
206254
"""
@@ -210,22 +258,24 @@ def _request(self, method: str, endpoint: str, **kwargs) -> requests.Response:
210258
if not self.is_ready():
211259
raise RuntimeError("Sandbox is not ready. Cannot send requests.")
212260

213-
url = f"http://127.0.0.1:{self.server_port}/{endpoint}"
261+
url = f"{self.base_url}/{endpoint}"
214262
try:
215263
response = requests.request(method, url, **kwargs)
216264
response.raise_for_status() # Raise an exception for bad status codes (4xx or 5xx)
217265
return response
218266
except requests.exceptions.RequestException as e:
219267
logging.error(f"Request to sandbox failed: {e}")
220-
raise RuntimeError(f"Failed to communicate with the sandbox at {url}.") from e
268+
raise RuntimeError(
269+
f"Failed to communicate with the sandbox at {url}.") from e
221270

222271
def run(self, command: str, timeout: int = 60) -> ExecutionResult:
223272
"""
224273
Executes a shell command inside the running sandbox.
225274
"""
226275
payload = {"command": command}
227-
response = self._request("POST", "execute", json=payload, timeout=timeout)
228-
276+
response = self._request(
277+
"POST", "execute", json=payload, timeout=timeout)
278+
229279
response_data = response.json()
230280
return ExecutionResult(
231281
stdout=response_data['stdout'],
@@ -253,4 +303,4 @@ def read(self, path: str) -> bytes:
253303
The base path for the download is the root of the sandbox's filesystem.
254304
"""
255305
response = self._request("GET", f"download/{path}")
256-
return response.content
306+
return response.content

0 commit comments

Comments
 (0)