opensandbox-code-interpreter 0.1.2__tar.gz → 1.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. {opensandbox_code_interpreter-0.1.2 → opensandbox_code_interpreter-1.1.0}/.gitignore +20 -0
  2. {opensandbox_code_interpreter-0.1.2 → opensandbox_code_interpreter-1.1.0}/PKG-INFO +10 -11
  3. {opensandbox_code_interpreter-0.1.2 → opensandbox_code_interpreter-1.1.0}/README.md +5 -6
  4. {opensandbox_code_interpreter-0.1.2 → opensandbox_code_interpreter-1.1.0}/pyproject.toml +7 -7
  5. {opensandbox_code_interpreter-0.1.2 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/__init__.py +1 -1
  6. {opensandbox_code_interpreter-0.1.2 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/adapters/__init__.py +1 -1
  7. {opensandbox_code_interpreter-0.1.2 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/adapters/code_adapter.py +39 -13
  8. {opensandbox_code_interpreter-0.1.2 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/adapters/converter/__init__.py +1 -1
  9. {opensandbox_code_interpreter-0.1.2 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/adapters/converter/code_execution_converter.py +1 -1
  10. {opensandbox_code_interpreter-0.1.2 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/adapters/factory.py +1 -1
  11. {opensandbox_code_interpreter-0.1.2 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/code_interpreter.py +139 -6
  12. {opensandbox_code_interpreter-0.1.2 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/models/__init__.py +1 -1
  13. {opensandbox_code_interpreter-0.1.2 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/models/code.py +1 -1
  14. {opensandbox_code_interpreter-0.1.2 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/models/code_sync.py +1 -1
  15. {opensandbox_code_interpreter-0.1.2 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/services/__init__.py +1 -1
  16. {opensandbox_code_interpreter-0.1.2 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/services/code.py +15 -1
  17. {opensandbox_code_interpreter-0.1.2 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/sync/__init__.py +1 -1
  18. {opensandbox_code_interpreter-0.1.2 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/sync/adapters/__init__.py +1 -1
  19. {opensandbox_code_interpreter-0.1.2 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/sync/adapters/code_adapter.py +28 -6
  20. {opensandbox_code_interpreter-0.1.2 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/sync/adapters/factory.py +1 -1
  21. {opensandbox_code_interpreter-0.1.2 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/sync/code_interpreter.py +131 -6
  22. {opensandbox_code_interpreter-0.1.2 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/sync/services/__init__.py +1 -1
  23. {opensandbox_code_interpreter-0.1.2 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/sync/services/code.py +15 -1
  24. {opensandbox_code_interpreter-0.1.2 → opensandbox_code_interpreter-1.1.0}/LICENSE +0 -0
  25. {opensandbox_code_interpreter-0.1.2 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/py.typed +0 -0
@@ -269,8 +269,28 @@ nbdist/
269
269
  generated/
270
270
  **/generated/**
271
271
 
272
+ # Fast Sandbox FastPath gRPC stubs are committed intentionally (deterministic protoc
273
+ # output; the server package needs them at install time, so they are checked
274
+ # in rather than regenerated).
275
+ !server/opensandbox_server/services/fast_sandbox/generated/
276
+ !server/opensandbox_server/services/fast_sandbox/generated/**
277
+ server/opensandbox_server/services/fast_sandbox/generated/__pycache__/
278
+
279
+ # execd eBPF kernel BTF dumps (bpf2go build-time only, never committed —
280
+ # see components/execd/pkg/ebpf/prog/audit.bpf.c; regenerate with:
281
+ # bpftool btf dump file /sys/kernel/btf/vmlinux format c > .../vmlinux.h)
282
+ components/execd/pkg/ebpf/prog/vmlinux.h
283
+ components/execd/pkg/ebpf/prog/vmlinux_*.h
284
+
272
285
  # gVisor runtime binaries (downloaded dynamically)
273
286
  kubernetes/test/kind/gvisor/runsc
274
287
  kubernetes/test/kind/gvisor/containerd-shim-runsc-v1
275
288
  bin/
276
289
  obj/
290
+
291
+ .qoder/
292
+ /.vs
293
+
294
+ # fast-sandbox pinned source checkout (materialized by
295
+ # manifests/release/build-fast-sandbox.sh from manifests/third-party/fast-sandbox.commit)
296
+ .fast-sandbox/
@@ -1,11 +1,11 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: opensandbox-code-interpreter
3
- Version: 0.1.2
3
+ Version: 1.1.0
4
4
  Summary: OpenSandbox Code Interpreter Python SDK - Advanced code execution with persistent contexts
5
5
  Project-URL: Homepage, https://open-sandbox.ai
6
- Project-URL: Repository, https://github.com/alibaba/OpenSandbox
6
+ Project-URL: Repository, https://github.com/opensandbox-group/OpenSandbox
7
7
  Project-URL: Documentation, https://open-sandbox.ai
8
- Project-URL: Issues, https://github.com/alibaba/OpenSandbox/issues
8
+ Project-URL: Issues, https://github.com/opensandbox-group/OpenSandbox/issues
9
9
  Author-email: OpenSandbox Team <ninan.nn@alibaba-inc.com>
10
10
  License: Apache License
11
11
  Version 2.0, January 2004
@@ -223,13 +223,12 @@ Classifier: Programming Language :: Python :: 3.13
223
223
  Classifier: Topic :: Software Development :: Libraries
224
224
  Classifier: Typing :: Typed
225
225
  Requires-Python: >=3.10
226
- Requires-Dist: opensandbox<0.2.0,>=0.1.6
226
+ Requires-Dist: opensandbox<2.0.0,>=1.1.0
227
227
  Requires-Dist: pydantic<3.0,>=2.4.2
228
228
  Description-Content-Type: text/markdown
229
229
 
230
230
  # OpenSandbox Code Interpreter SDK for Python
231
231
 
232
- English | [中文](README_zh.md)
233
232
 
234
233
  A Python SDK for executing code in secure, isolated sandboxes. It provides a high-level API for running Python, Java,
235
234
  Go, TypeScript, and other languages safely, with support for code execution contexts.
@@ -241,7 +240,7 @@ This SDK requires a Docker image containing the Code Interpreter runtime environ
241
240
  Node.js, etc.
242
241
 
243
242
  For detailed information about supported languages and versions, refer to the
244
- [Environment Documentation](../../../sandboxes/code-interpreter/README.md).
243
+ [Environment Documentation](https://github.com/opensandbox-group/sandbox-images).
245
244
 
246
245
  ## Installation
247
246
 
@@ -281,9 +280,9 @@ async def main() -> None:
281
280
 
282
281
  # 2. Create a Sandbox with the code-interpreter image + runtime versions
283
282
  sandbox = await Sandbox.create(
284
- "opensandbox/code-interpreter:v1.0.2",
283
+ "opensandbox/code-interpreter:v1.1.0",
285
284
  connection_config=config,
286
- entrypoint=["/opt/opensandbox/code-interpreter.sh"],
285
+ entrypoint=["/opt/code-interpreter/code-interpreter.sh"],
287
286
  env={
288
287
  "PYTHON_VERSION": "3.11",
289
288
  "JAVA_VERSION": "17",
@@ -342,9 +341,9 @@ config = ConnectionConfigSync(
342
341
  )
343
342
 
344
343
  sandbox = SandboxSync.create(
345
- "opensandbox/code-interpreter:v1.0.2",
344
+ "opensandbox/code-interpreter:v1.1.0",
346
345
  connection_config=config,
347
- entrypoint=["/opt/opensandbox/code-interpreter.sh"],
346
+ entrypoint=["/opt/code-interpreter/code-interpreter.sh"],
348
347
  env={"PYTHON_VERSION": "3.11"},
349
348
  )
350
349
  with sandbox:
@@ -1,6 +1,5 @@
1
1
  # OpenSandbox Code Interpreter SDK for Python
2
2
 
3
- English | [中文](README_zh.md)
4
3
 
5
4
  A Python SDK for executing code in secure, isolated sandboxes. It provides a high-level API for running Python, Java,
6
5
  Go, TypeScript, and other languages safely, with support for code execution contexts.
@@ -12,7 +11,7 @@ This SDK requires a Docker image containing the Code Interpreter runtime environ
12
11
  Node.js, etc.
13
12
 
14
13
  For detailed information about supported languages and versions, refer to the
15
- [Environment Documentation](../../../sandboxes/code-interpreter/README.md).
14
+ [Environment Documentation](https://github.com/opensandbox-group/sandbox-images).
16
15
 
17
16
  ## Installation
18
17
 
@@ -52,9 +51,9 @@ async def main() -> None:
52
51
 
53
52
  # 2. Create a Sandbox with the code-interpreter image + runtime versions
54
53
  sandbox = await Sandbox.create(
55
- "opensandbox/code-interpreter:v1.0.2",
54
+ "opensandbox/code-interpreter:v1.1.0",
56
55
  connection_config=config,
57
- entrypoint=["/opt/opensandbox/code-interpreter.sh"],
56
+ entrypoint=["/opt/code-interpreter/code-interpreter.sh"],
58
57
  env={
59
58
  "PYTHON_VERSION": "3.11",
60
59
  "JAVA_VERSION": "17",
@@ -113,9 +112,9 @@ config = ConnectionConfigSync(
113
112
  )
114
113
 
115
114
  sandbox = SandboxSync.create(
116
- "opensandbox/code-interpreter:v1.0.2",
115
+ "opensandbox/code-interpreter:v1.1.0",
117
116
  connection_config=config,
118
- entrypoint=["/opt/opensandbox/code-interpreter.sh"],
117
+ entrypoint=["/opt/code-interpreter/code-interpreter.sh"],
119
118
  env={"PYTHON_VERSION": "3.11"},
120
119
  )
121
120
  with sandbox:
@@ -1,4 +1,4 @@
1
- # Copyright 2025 Alibaba Group Holding Ltd.
1
+ # Copyright 2025 The OpenSandbox Authors
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -43,14 +43,14 @@ classifiers = [
43
43
  ]
44
44
  dependencies = [
45
45
  "pydantic>=2.4.2,<3.0",
46
- "opensandbox>=0.1.6,<0.2.0",
46
+ "opensandbox>=1.1.0,<2.0.0",
47
47
  ]
48
48
 
49
49
  [project.urls]
50
50
  Homepage = "https://open-sandbox.ai"
51
- Repository = "https://github.com/alibaba/OpenSandbox"
51
+ Repository = "https://github.com/opensandbox-group/OpenSandbox"
52
52
  Documentation = "https://open-sandbox.ai"
53
- Issues = "https://github.com/alibaba/OpenSandbox/issues"
53
+ Issues = "https://github.com/opensandbox-group/OpenSandbox/issues"
54
54
 
55
55
  [tool.hatch.version]
56
56
  source = "vcs"
@@ -58,9 +58,9 @@ source = "vcs"
58
58
  [tool.hatch.version.raw-options]
59
59
  # This package is in a subdirectory; explicitly point setuptools-scm at the git root.
60
60
  root = "../../.."
61
- tag_regex = "^python/code-interpreter/v(?P<version>\\d+\\.\\d+\\.\\d+(?:[\\.\\w\\+\\-]*)?)$"
62
- git_describe_command = 'git describe --dirty --tags --long --match "python/code-interpreter/v*"'
63
- fallback_version = "0.1.0"
61
+ tag_regex = "^release-(?P<version>\\d+\\.\\d+\\.\\d+(?:[\\.\\w\\+\\-]*)?)$"
62
+ git_describe_command = 'git describe --dirty --tags --long --match "release-*"'
63
+ fallback_version = "0.1.3"
64
64
 
65
65
  [tool.hatch.build]
66
66
  include = [
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright 2025 Alibaba Group Holding Ltd.
2
+ # Copyright 2025 The OpenSandbox Authors
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
5
5
  # you may not use this file except in compliance with the License.
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright 2025 Alibaba Group Holding Ltd.
2
+ # Copyright 2025 The OpenSandbox Authors
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
5
5
  # you may not use this file except in compliance with the License.
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright 2025 Alibaba Group Holding Ltd.
2
+ # Copyright 2025 The OpenSandbox Authors
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
5
5
  # you may not use this file except in compliance with the License.
@@ -41,6 +41,7 @@ from opensandbox.config import ConnectionConfig
41
41
  from opensandbox.exceptions import InvalidArgumentException, SandboxApiException
42
42
  from opensandbox.models.execd import Execution, ExecutionHandlers
43
43
  from opensandbox.models.sandboxes import SandboxEndpoint
44
+ from opensandbox.transport import unwrap_retry_transport
44
45
 
45
46
  from code_interpreter.adapters.converter.code_execution_converter import (
46
47
  CodeExecutionConverter,
@@ -130,6 +131,9 @@ class CodesAdapter(Codes):
130
131
  "Accept": "text/event-stream",
131
132
  "Cache-Control": "no-cache",
132
133
  }
134
+ # SSE bootstraps bypass the retry wrapper: request bodies are
135
+ # not replayable and a non-idempotent status opt-in would cause
136
+ # duplicate execution on a resent SSE POST.
133
137
  self._sse_client = httpx.AsyncClient(
134
138
  headers=sse_headers,
135
139
  timeout=httpx.Timeout(
@@ -138,7 +142,7 @@ class CodesAdapter(Codes):
138
142
  write=timeout_seconds,
139
143
  pool=None,
140
144
  ),
141
- transport=self.connection_config.transport,
145
+ transport=unwrap_retry_transport(self.connection_config.transport),
142
146
  )
143
147
 
144
148
  async def _get_client(self):
@@ -216,7 +220,9 @@ class CodesAdapter(Codes):
216
220
  )
217
221
  handle_api_error(response_obj, "List code contexts")
218
222
  parsed_list = require_parsed(response_obj, list, "List code contexts")
219
- return [CodeExecutionConverter.from_api_code_context(c) for c in parsed_list]
223
+ return [
224
+ CodeExecutionConverter.from_api_code_context(c) for c in parsed_list
225
+ ]
220
226
  except Exception as e:
221
227
  logger.error("Failed to list contexts", exc_info=e)
222
228
  raise ExceptionConverter.to_sandbox_exception(e) from e
@@ -269,7 +275,11 @@ class CodesAdapter(Codes):
269
275
  raise InvalidArgumentException("Code cannot be empty")
270
276
 
271
277
  try:
272
- if context is not None and language is not None and context.language != language:
278
+ if (
279
+ context is not None
280
+ and language is not None
281
+ and context.language != language
282
+ ):
273
283
  raise InvalidArgumentException(
274
284
  f"language '{language}' must match context.language '{context.language}'"
275
285
  )
@@ -299,12 +309,10 @@ class CodesAdapter(Codes):
299
309
  await response.aread()
300
310
  error_body = response.text
301
311
  logger.error(
302
- "Failed to run code. Status: %s, Body: %s",
303
- response.status_code,
304
- error_body,
312
+ f"Failed to run code. Status: {response.status_code}, Body: {error_body}"
305
313
  )
306
314
  raise SandboxApiException(
307
- message=f"Failed to run code. Status code: {response.status_code}",
315
+ message=f"Failed to run code. Status code: {response.status_code}, Body: {error_body}",
308
316
  status_code=response.status_code,
309
317
  request_id=extract_request_id(response.headers),
310
318
  )
@@ -325,18 +333,16 @@ class CodesAdapter(Codes):
325
333
  event_node = EventNode(**event_dict)
326
334
  await dispatcher.dispatch(event_node)
327
335
  except json.JSONDecodeError:
328
- logger.debug("Failed to parse SSE line: %s", line)
336
+ logger.debug(f"Failed to parse SSE line: {line}")
329
337
  continue
330
338
  except Exception as e:
331
- logger.error("Error processing event: %s", data, exc_info=e)
339
+ logger.error(f"Error processing event: {data}", exc_info=e)
332
340
  continue
333
341
 
334
342
  return execution
335
343
 
336
344
  except Exception as e:
337
- logger.error(
338
- "Failed to run code (length: %s)", len(code), exc_info=e
339
- )
345
+ logger.error(f"Failed to run code (length: {len(code)})", exc_info=e)
340
346
  raise ExceptionConverter.to_sandbox_exception(e) from e
341
347
 
342
348
  async def interrupt(self, execution_id: str) -> None:
@@ -359,3 +365,23 @@ class CodesAdapter(Codes):
359
365
  except Exception as e:
360
366
  logger.error("Failed to interrupt code execution", exc_info=e)
361
367
  raise ExceptionConverter.to_sandbox_exception(e) from e
368
+
369
+ async def ping(self) -> bool:
370
+ """
371
+ Check if the execd code execution service is alive.
372
+
373
+ Uses the shared generated API client so the ping exercises the same
374
+ HTTP client and endpoint that serve code execution requests.
375
+ """
376
+ try:
377
+ from opensandbox.api.execd.api.health import ping as ping_api
378
+
379
+ client = await self._get_client()
380
+ response_obj = await ping_api.asyncio_detailed(client=client)
381
+
382
+ handle_api_error(response_obj, "Ping code interpreter")
383
+ return True
384
+
385
+ except Exception as e:
386
+ logger.debug(f"Code interpreter ping failed: {e}")
387
+ return False
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright 2025 Alibaba Group Holding Ltd.
2
+ # Copyright 2025 The OpenSandbox Authors
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
5
5
  # you may not use this file except in compliance with the License.
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright 2025 Alibaba Group Holding Ltd.
2
+ # Copyright 2025 The OpenSandbox Authors
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
5
5
  # you may not use this file except in compliance with the License.
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright 2025 Alibaba Group Holding Ltd.
2
+ # Copyright 2025 The OpenSandbox Authors
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
5
5
  # you may not use this file except in compliance with the License.
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright 2025 Alibaba Group Holding Ltd.
2
+ # Copyright 2025 The OpenSandbox Authors
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
5
5
  # you may not use this file except in compliance with the License.
@@ -21,12 +21,16 @@ functionality with specialized code execution features, including multi-language
21
21
  support, session management, and variable persistence.
22
22
  """
23
23
 
24
+ import asyncio
24
25
  import logging
26
+ import time
27
+ from datetime import timedelta
25
28
 
26
29
  from opensandbox.exceptions import (
27
30
  InvalidArgumentException,
28
31
  SandboxException,
29
32
  SandboxInternalException,
33
+ SandboxReadyTimeoutException,
30
34
  )
31
35
  from opensandbox.sandbox import Sandbox
32
36
 
@@ -35,6 +39,21 @@ from code_interpreter.services.code import Codes
35
39
 
36
40
  logger = logging.getLogger(__name__)
37
41
 
42
+ DEFAULT_READY_TIMEOUT = timedelta(seconds=30)
43
+ DEFAULT_HEALTH_CHECK_POLLING_INTERVAL = timedelta(milliseconds=200)
44
+
45
+ # Strict health check script: verifies the code interpreter runtime (Jupyter
46
+ # kernel gateway) is actually serving inside the sandbox. execd starts serving
47
+ # /ping before the entrypoint launches Jupyter, and the setup stage may run
48
+ # short-lived "jupyter kernelspec" helpers, so a daemon ping or a process-name
49
+ # grep cannot prove the runtime is ready. Probing the Jupyter listen port
50
+ # (127.0.0.1:${JUPYTER_PORT:-44771}, same default as the entrypoint) only
51
+ # passes once the server accepts connections.
52
+ RUNTIME_PROCESS_CHECK_COMMAND = (
53
+ "bash -c 'exec 3<>/dev/tcp/127.0.0.1/${JUPYTER_PORT:-44771}' "
54
+ "&& exit 0 || exit 1"
55
+ )
56
+
38
57
 
39
58
  class CodeInterpreter:
40
59
  """
@@ -169,14 +188,114 @@ class CodeInterpreter:
169
188
  """
170
189
  return self._code_service
171
190
 
191
+ async def ping(self) -> bool:
192
+ """
193
+ Check if the code execution service (execd) is responsive.
194
+
195
+ Returns:
196
+ True if the code execution service is responsive, False otherwise
197
+ """
198
+ return await self._code_service.ping()
199
+
200
+ async def is_healthy(self) -> bool:
201
+ """
202
+ Check if the code interpreter is healthy (strict check).
203
+
204
+ Healthy means both:
205
+
206
+ - the code execution service (execd) answers ``GET /ping``; and
207
+ - the code interpreter runtime (Jupyter kernel gateway) is serving
208
+ inside the sandbox, verified by probing its listen port through the
209
+ execd command API.
210
+
211
+ Exceptions raised by either leg are treated as unhealthy.
212
+
213
+ Returns:
214
+ True if healthy, False otherwise
215
+ """
216
+ try:
217
+ return await self.ping() and await self._is_runtime_process_alive()
218
+ except Exception:
219
+ return False
220
+
221
+ async def _is_runtime_process_alive(self) -> bool:
222
+ """
223
+ Check if the code interpreter runtime (Jupyter) is serving.
224
+
225
+ Probes the runtime listen port through the sandbox command service and
226
+ treats a failed command (non-zero exit surfaced as an execution error)
227
+ as an unhealthy runtime.
228
+ """
229
+ try:
230
+ execution = await self._sandbox.commands.run(
231
+ RUNTIME_PROCESS_CHECK_COMMAND
232
+ )
233
+ return execution.error is None
234
+ except Exception:
235
+ return False
236
+
237
+ async def check_ready(
238
+ self,
239
+ timeout: timedelta,
240
+ polling_interval: timedelta,
241
+ ) -> None:
242
+ """
243
+ Wait for the code interpreter to pass the strict health check with polling.
244
+
245
+ Raises:
246
+ SandboxReadyTimeoutException: if the health check doesn't pass within timeout
247
+ """
248
+ logger.info(
249
+ f"Waiting for code interpreter {self.id} to pass health check "
250
+ f"(timeout: {timeout.total_seconds()}s)"
251
+ )
252
+
253
+ deadline = time.monotonic() + timeout.total_seconds()
254
+ attempt = 0
255
+
256
+ while time.monotonic() < deadline:
257
+ attempt += 1
258
+ if await self.is_healthy():
259
+ logger.info(
260
+ f"Code interpreter {self.id} passed health check "
261
+ f"after {attempt} attempts"
262
+ )
263
+ return
264
+
265
+ remaining = deadline - time.monotonic()
266
+ if remaining <= 0:
267
+ break
268
+ await asyncio.sleep(min(polling_interval.total_seconds(), remaining))
269
+
270
+ raise SandboxReadyTimeoutException(
271
+ f"Code interpreter {self.id} health check timed out after "
272
+ f"{timeout.total_seconds()}s ({attempt} attempts). The code execution "
273
+ f"service (execd) or the interpreter runtime (Jupyter) did not become "
274
+ f"ready. Pass skip_health_check=True to skip this check."
275
+ )
276
+
172
277
  @classmethod
173
- async def create(cls, sandbox: Sandbox) -> "CodeInterpreter":
278
+ async def create(
279
+ cls,
280
+ sandbox: Sandbox,
281
+ *,
282
+ ready_timeout: timedelta = DEFAULT_READY_TIMEOUT,
283
+ health_check_polling_interval: timedelta = DEFAULT_HEALTH_CHECK_POLLING_INTERVAL,
284
+ skip_health_check: bool = False,
285
+ ) -> "CodeInterpreter":
174
286
  """
175
287
  Creates a CodeInterpreter from an existing Sandbox instance.
176
288
 
177
289
  This factory method handles the creation and initialization of CodeInterpreter
178
290
  services, including the code execution service and language configuration.
179
291
 
292
+ By default a strict health check runs before the interpreter is returned:
293
+ the code execution service (execd) must answer ``GET /ping`` AND the
294
+ code interpreter runtime (Jupyter kernel gateway) must be serving,
295
+ both within ``ready_timeout``. execd starts serving before the runtime
296
+ launches, so the daemon ping alone is not enough. Set
297
+ ``skip_health_check=True`` to opt out.
298
+
180
299
  CodeInterpreter must be created by wrapping an existing Sandbox instance with
181
300
  code execution capabilities. This design ensures clear separation of concerns:
182
301
  - Sandbox handles infrastructure (containers, resources, networking)
@@ -184,6 +303,10 @@ class CodeInterpreter:
184
303
 
185
304
  Args:
186
305
  sandbox: Existing sandbox instance to wrap with code execution capabilities
306
+ ready_timeout: Maximum time to wait for the code execution service health check
307
+ health_check_polling_interval: Time between health check attempts
308
+ skip_health_check: If True, do not wait for the code execution service
309
+ to become ready; the returned interpreter may fail on first use
187
310
 
188
311
  Returns:
189
312
  CodeInterpreter instance wrapping the sandbox
@@ -191,24 +314,34 @@ class CodeInterpreter:
191
314
  Raises:
192
315
  InvalidArgumentException: If sandbox is not provided
193
316
  SandboxException: If creation fails
317
+ SandboxReadyTimeoutException: If the code execution service health check
318
+ times out and ``skip_health_check`` is False
194
319
  SandboxInternalException: If internal service initialization fails
195
320
  """
196
321
  if sandbox is None:
197
322
  raise InvalidArgumentException("Sandbox instance must be provided")
198
323
 
199
- logger.info("Creating code interpreter from sandbox: %s", sandbox.id)
324
+ logger.info(f"Creating code interpreter from sandbox: {sandbox.id}")
200
325
 
201
326
  factory = AdapterFactory(sandbox.connection_config)
202
327
 
203
328
  try:
204
329
  # Connect to the execd daemon endpoint for code execution services
205
330
  from opensandbox.constants import DEFAULT_EXECD_PORT
331
+
206
332
  code_interpreter_endpoint = await sandbox.get_endpoint(DEFAULT_EXECD_PORT)
207
- code_execution_service = factory.create_code_execution_service(code_interpreter_endpoint)
333
+ code_execution_service = factory.create_code_execution_service(
334
+ code_interpreter_endpoint
335
+ )
336
+
337
+ interpreter = cls(sandbox, code_execution_service)
338
+
339
+ if not skip_health_check:
340
+ await interpreter.check_ready(ready_timeout, health_check_polling_interval)
208
341
 
209
- logger.info("Code interpreter %s created successfully", sandbox.id)
342
+ logger.info(f"Code interpreter {sandbox.id} created successfully")
210
343
 
211
- return cls(sandbox, code_execution_service)
344
+ return interpreter
212
345
  except Exception as e:
213
346
  if isinstance(e, SandboxException):
214
347
  raise
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright 2025 Alibaba Group Holding Ltd.
2
+ # Copyright 2025 The OpenSandbox Authors
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
5
5
  # you may not use this file except in compliance with the License.
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright 2025 Alibaba Group Holding Ltd.
2
+ # Copyright 2025 The OpenSandbox Authors
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
5
5
  # you may not use this file except in compliance with the License.
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright 2025 Alibaba Group Holding Ltd.
2
+ # Copyright 2025 The OpenSandbox Authors
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
5
5
  # you may not use this file except in compliance with the License.
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright 2025 Alibaba Group Holding Ltd.
2
+ # Copyright 2025 The OpenSandbox Authors
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
5
5
  # you may not use this file except in compliance with the License.
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright 2025 Alibaba Group Holding Ltd.
2
+ # Copyright 2025 The OpenSandbox Authors
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
5
5
  # you may not use this file except in compliance with the License.
@@ -212,3 +212,17 @@ class Codes(Protocol):
212
212
  SandboxException: If interruption fails
213
213
  """
214
214
  ...
215
+
216
+ async def ping(self) -> bool:
217
+ """
218
+ Check if the code execution service (execd) is alive.
219
+
220
+ The ping targets the execd daemon endpoint used by this service
221
+ (``GET /ping``). It does not verify that a specific language runtime
222
+ is ready, only that the daemon serving code execution requests is
223
+ responsive.
224
+
225
+ Returns:
226
+ True if the code execution service is responsive, False otherwise
227
+ """
228
+ ...
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright 2025 Alibaba Group Holding Ltd.
2
+ # Copyright 2025 The OpenSandbox Authors
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
5
5
  # you may not use this file except in compliance with the License.
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright 2025 Alibaba Group Holding Ltd.
2
+ # Copyright 2025 The OpenSandbox Authors
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
5
5
  # you may not use this file except in compliance with the License.
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright 2025 Alibaba Group Holding Ltd.
2
+ # Copyright 2025 The OpenSandbox Authors
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
5
5
  # you may not use this file except in compliance with the License.
@@ -39,6 +39,7 @@ from opensandbox.models.sandboxes import SandboxEndpoint
39
39
  from opensandbox.sync.adapters.converter.execution_event_dispatcher import (
40
40
  ExecutionEventDispatcherSync,
41
41
  )
42
+ from opensandbox.transport import unwrap_retry_transport
42
43
 
43
44
  from code_interpreter.models.code_sync import CodeContextSync, SupportedLanguageSync
44
45
  from code_interpreter.sync.services.code import CodesSync
@@ -118,12 +119,15 @@ class CodesAdapterSync(CodesSync):
118
119
  "Accept": "text/event-stream",
119
120
  "Cache-Control": "no-cache",
120
121
  }
122
+ # SSE bootstraps bypass the retry wrapper: request bodies are
123
+ # not replayable and a non-idempotent status opt-in would cause
124
+ # duplicate execution on a resent SSE POST.
121
125
  self._sse_client = httpx.Client(
122
126
  headers=sse_headers,
123
127
  timeout=httpx.Timeout(
124
128
  connect=timeout_seconds, read=None, write=timeout_seconds, pool=None
125
129
  ),
126
- transport=self.connection_config.transport,
130
+ transport=unwrap_retry_transport(self.connection_config.transport),
127
131
  )
128
132
 
129
133
  def _get_execd_url(self, path: str) -> str:
@@ -287,8 +291,9 @@ class CodesAdapterSync(CodesSync):
287
291
  with self._sse_client.stream("POST", url, json=api_request) as response:
288
292
  if response.status_code != 200:
289
293
  response.read()
294
+ error_body = response.text
290
295
  raise SandboxApiException(
291
- message=f"Failed to run code. Status code: {response.status_code}",
296
+ message=f"Failed to run code. Status code: {response.status_code}, Body: {error_body}",
292
297
  status_code=response.status_code,
293
298
  request_id=extract_request_id(response.headers),
294
299
  )
@@ -304,15 +309,15 @@ class CodesAdapterSync(CodesSync):
304
309
  event_node = EventNode(**event_dict)
305
310
  dispatcher.dispatch(event_node)
306
311
  except json.JSONDecodeError:
307
- logger.debug("Failed to parse SSE line: %s", line)
312
+ logger.debug(f"Failed to parse SSE line: {line}")
308
313
  continue
309
314
  except Exception as e:
310
- logger.error("Error processing event: %s", data, exc_info=e)
315
+ logger.error(f"Error processing event: {data}", exc_info=e)
311
316
  continue
312
317
 
313
318
  return execution
314
319
  except Exception as e:
315
- logger.error("Failed to run code (length: %s)", len(code), exc_info=e)
320
+ logger.error(f"Failed to run code (length: {len(code)})", exc_info=e)
316
321
  raise ExceptionConverter.to_sandbox_exception(e) from e
317
322
 
318
323
  def interrupt(self, execution_id: str) -> None:
@@ -332,3 +337,20 @@ class CodesAdapterSync(CodesSync):
332
337
  except Exception as e:
333
338
  logger.error("Failed to interrupt code execution", exc_info=e)
334
339
  raise ExceptionConverter.to_sandbox_exception(e) from e
340
+
341
+ def ping(self) -> bool:
342
+ """
343
+ Check if the execd code execution service is alive (blocking).
344
+
345
+ Uses the shared generated API client so the ping exercises the same
346
+ HTTP client and endpoint that serve code execution requests.
347
+ """
348
+ try:
349
+ from opensandbox.api.execd.api.health import ping as ping_api
350
+
351
+ response_obj = ping_api.sync_detailed(client=self._client)
352
+ handle_api_error(response_obj, "Ping code interpreter")
353
+ return True
354
+ except Exception as e:
355
+ logger.debug(f"Code interpreter ping failed: {e}")
356
+ return False
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright 2025 Alibaba Group Holding Ltd.
2
+ # Copyright 2025 The OpenSandbox Authors
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
5
5
  # you may not use this file except in compliance with the License.
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright 2025 Alibaba Group Holding Ltd.
2
+ # Copyright 2025 The OpenSandbox Authors
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
5
5
  # you may not use this file except in compliance with the License.
@@ -18,12 +18,15 @@ Synchronous Code Interpreter SDK.
18
18
  """
19
19
 
20
20
  import logging
21
+ import time
22
+ from datetime import timedelta
21
23
 
22
24
  from opensandbox.constants import DEFAULT_EXECD_PORT
23
25
  from opensandbox.exceptions import (
24
26
  InvalidArgumentException,
25
27
  SandboxException,
26
28
  SandboxInternalException,
29
+ SandboxReadyTimeoutException,
27
30
  )
28
31
  from opensandbox.sync.sandbox import SandboxSync
29
32
 
@@ -32,6 +35,21 @@ from code_interpreter.sync.services.code import CodesSync
32
35
 
33
36
  logger = logging.getLogger(__name__)
34
37
 
38
+ DEFAULT_READY_TIMEOUT = timedelta(seconds=30)
39
+ DEFAULT_HEALTH_CHECK_POLLING_INTERVAL = timedelta(milliseconds=200)
40
+
41
+ # Strict health check script: verifies the code interpreter runtime (Jupyter
42
+ # kernel gateway) is actually serving inside the sandbox. execd starts serving
43
+ # /ping before the entrypoint launches Jupyter, and the setup stage may run
44
+ # short-lived "jupyter kernelspec" helpers, so a daemon ping or a process-name
45
+ # grep cannot prove the runtime is ready. Probing the Jupyter listen port
46
+ # (127.0.0.1:${JUPYTER_PORT:-44771}, same default as the entrypoint) only
47
+ # passes once the server accepts connections.
48
+ RUNTIME_PROCESS_CHECK_COMMAND = (
49
+ "bash -c 'exec 3<>/dev/tcp/127.0.0.1/${JUPYTER_PORT:-44771}' "
50
+ "&& exit 0 || exit 1"
51
+ )
52
+
35
53
 
36
54
  class CodeInterpreterSync:
37
55
  """
@@ -146,13 +164,110 @@ class CodeInterpreterSync:
146
164
  """
147
165
  return self._code_service
148
166
 
167
+ def ping(self) -> bool:
168
+ """
169
+ Check if the code execution service (execd) is responsive.
170
+
171
+ Returns:
172
+ True if the code execution service is responsive, False otherwise
173
+ """
174
+ return self._code_service.ping()
175
+
176
+ def is_healthy(self) -> bool:
177
+ """
178
+ Check if the code interpreter is healthy (strict check).
179
+
180
+ Healthy means both:
181
+
182
+ - the code execution service (execd) answers ``GET /ping``; and
183
+ - the code interpreter runtime (Jupyter kernel gateway) is serving
184
+ inside the sandbox, verified by probing its listen port through the
185
+ execd command API.
186
+
187
+ Exceptions raised by either leg are treated as unhealthy.
188
+
189
+ Returns:
190
+ True if healthy, False otherwise
191
+ """
192
+ try:
193
+ return self.ping() and self._is_runtime_process_alive()
194
+ except Exception:
195
+ return False
196
+
197
+ def _is_runtime_process_alive(self) -> bool:
198
+ """
199
+ Check if the code interpreter runtime (Jupyter) is serving.
200
+ """
201
+ try:
202
+ execution = self._sandbox.commands.run(RUNTIME_PROCESS_CHECK_COMMAND)
203
+ return execution.error is None
204
+ except Exception:
205
+ return False
206
+
207
+ def check_ready(
208
+ self,
209
+ timeout: timedelta,
210
+ polling_interval: timedelta,
211
+ ) -> None:
212
+ """
213
+ Wait for the code interpreter to pass the strict health check with polling (blocking).
214
+
215
+ Raises:
216
+ SandboxReadyTimeoutException: if the health check doesn't pass within timeout
217
+ """
218
+ logger.info(
219
+ f"Waiting for code interpreter {self.id} to pass health check "
220
+ f"(timeout: {timeout.total_seconds()}s)"
221
+ )
222
+
223
+ deadline = time.monotonic() + timeout.total_seconds()
224
+ attempt = 0
225
+
226
+ while time.monotonic() < deadline:
227
+ attempt += 1
228
+ if self.is_healthy():
229
+ logger.info(
230
+ f"Code interpreter {self.id} passed health check "
231
+ f"after {attempt} attempts"
232
+ )
233
+ return
234
+
235
+ remaining = deadline - time.monotonic()
236
+ if remaining <= 0:
237
+ break
238
+ time.sleep(min(polling_interval.total_seconds(), remaining))
239
+
240
+ raise SandboxReadyTimeoutException(
241
+ f"Code interpreter {self.id} health check timed out after "
242
+ f"{timeout.total_seconds()}s ({attempt} attempts). The code execution "
243
+ f"service (execd) or the interpreter runtime (Jupyter) did not become "
244
+ f"ready. Pass skip_health_check=True to skip this check."
245
+ )
246
+
149
247
  @classmethod
150
- def create(cls, sandbox: SandboxSync) -> "CodeInterpreterSync":
248
+ def create(
249
+ cls,
250
+ sandbox: SandboxSync,
251
+ *,
252
+ ready_timeout: timedelta = DEFAULT_READY_TIMEOUT,
253
+ health_check_polling_interval: timedelta = DEFAULT_HEALTH_CHECK_POLLING_INTERVAL,
254
+ skip_health_check: bool = False,
255
+ ) -> "CodeInterpreterSync":
151
256
  """
152
257
  Create a CodeInterpreterSync from an existing SandboxSync instance (blocking).
153
258
 
259
+ By default a strict health check runs before the interpreter is returned:
260
+ the code execution service (execd) must answer ``GET /ping`` AND the
261
+ code interpreter runtime process (Jupyter kernel gateway) must be
262
+ running inside the sandbox, both within ``ready_timeout``. Set
263
+ ``skip_health_check=True`` to opt out.
264
+
154
265
  Args:
155
266
  sandbox: Existing sandbox instance to wrap with code execution capabilities
267
+ ready_timeout: Maximum time to wait for the code execution service health check
268
+ health_check_polling_interval: Time between health check attempts
269
+ skip_health_check: If True, do not wait for the code execution service
270
+ to become ready; the returned interpreter may fail on first use
156
271
 
157
272
  Returns:
158
273
  CodeInterpreterSync instance wrapping the sandbox
@@ -160,19 +275,29 @@ class CodeInterpreterSync:
160
275
  Raises:
161
276
  InvalidArgumentException: If sandbox is not provided
162
277
  SandboxException: If creation fails
278
+ SandboxReadyTimeoutException: If the code execution service health check
279
+ times out and ``skip_health_check`` is False
163
280
  SandboxInternalException: If internal service initialization fails
164
281
  """
165
282
  if sandbox is None:
166
283
  raise InvalidArgumentException("Sandbox instance must be provided")
167
284
 
168
- logger.info("Creating code interpreter from sandbox: %s", sandbox.id)
285
+ logger.info(f"Creating code interpreter from sandbox: {sandbox.id}")
169
286
  factory = AdapterFactorySync(sandbox.connection_config)
170
287
  try:
171
288
  endpoint = sandbox.get_endpoint(DEFAULT_EXECD_PORT)
172
289
  code_service = factory.create_code_execution_service(endpoint)
173
- logger.info("Code interpreter %s created successfully", sandbox.id)
174
- return cls(sandbox, code_service)
290
+
291
+ interpreter = cls(sandbox, code_service)
292
+
293
+ if not skip_health_check:
294
+ interpreter.check_ready(ready_timeout, health_check_polling_interval)
295
+
296
+ logger.info(f"Code interpreter {sandbox.id} created successfully")
297
+ return interpreter
175
298
  except Exception as e:
176
299
  if isinstance(e, SandboxException):
177
300
  raise
178
- raise SandboxInternalException(f"Failed to create code interpreter: {e}", cause=e) from e
301
+ raise SandboxInternalException(
302
+ f"Failed to create code interpreter: {e}", cause=e
303
+ ) from e
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright 2025 Alibaba Group Holding Ltd.
2
+ # Copyright 2025 The OpenSandbox Authors
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
5
5
  # you may not use this file except in compliance with the License.
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright 2025 Alibaba Group Holding Ltd.
2
+ # Copyright 2025 The OpenSandbox Authors
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
5
5
  # you may not use this file except in compliance with the License.
@@ -159,3 +159,17 @@ class CodesSync(Protocol):
159
159
  SandboxException: If interruption fails.
160
160
  """
161
161
  ...
162
+
163
+ def ping(self) -> bool:
164
+ """
165
+ Check if the code execution service (execd) is alive (blocking).
166
+
167
+ The ping targets the execd daemon endpoint used by this service
168
+ (``GET /ping``). It does not verify that a specific language runtime
169
+ is ready, only that the daemon serving code execution requests is
170
+ responsive.
171
+
172
+ Returns:
173
+ True if the code execution service is responsive, False otherwise.
174
+ """
175
+ ...