opensandbox-code-interpreter 0.1.2.dev0__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.dev0 → opensandbox_code_interpreter-1.1.0}/.gitignore +31 -0
  2. {opensandbox_code_interpreter-0.1.2.dev0 → opensandbox_code_interpreter-1.1.0}/PKG-INFO +21 -14
  3. {opensandbox_code_interpreter-0.1.2.dev0 → opensandbox_code_interpreter-1.1.0}/README.md +13 -6
  4. {opensandbox_code_interpreter-0.1.2.dev0 → opensandbox_code_interpreter-1.1.0}/pyproject.toml +10 -10
  5. {opensandbox_code_interpreter-0.1.2.dev0 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/__init__.py +1 -1
  6. {opensandbox_code_interpreter-0.1.2.dev0 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/adapters/__init__.py +1 -1
  7. {opensandbox_code_interpreter-0.1.2.dev0 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/adapters/code_adapter.py +60 -14
  8. {opensandbox_code_interpreter-0.1.2.dev0 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/adapters/converter/__init__.py +1 -1
  9. {opensandbox_code_interpreter-0.1.2.dev0 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/adapters/converter/code_execution_converter.py +1 -1
  10. {opensandbox_code_interpreter-0.1.2.dev0 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/adapters/factory.py +1 -1
  11. {opensandbox_code_interpreter-0.1.2.dev0 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/code_interpreter.py +139 -6
  12. {opensandbox_code_interpreter-0.1.2.dev0 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/models/__init__.py +1 -1
  13. {opensandbox_code_interpreter-0.1.2.dev0 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/models/code.py +1 -1
  14. {opensandbox_code_interpreter-0.1.2.dev0 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/models/code_sync.py +1 -1
  15. {opensandbox_code_interpreter-0.1.2.dev0 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/services/__init__.py +1 -1
  16. {opensandbox_code_interpreter-0.1.2.dev0 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/services/code.py +15 -1
  17. {opensandbox_code_interpreter-0.1.2.dev0 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/sync/__init__.py +1 -1
  18. {opensandbox_code_interpreter-0.1.2.dev0 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/sync/adapters/__init__.py +1 -1
  19. {opensandbox_code_interpreter-0.1.2.dev0 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/sync/adapters/code_adapter.py +78 -15
  20. {opensandbox_code_interpreter-0.1.2.dev0 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/sync/adapters/factory.py +1 -1
  21. {opensandbox_code_interpreter-0.1.2.dev0 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/sync/code_interpreter.py +131 -6
  22. {opensandbox_code_interpreter-0.1.2.dev0 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/sync/services/__init__.py +1 -1
  23. {opensandbox_code_interpreter-0.1.2.dev0 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/sync/services/code.py +15 -1
  24. {opensandbox_code_interpreter-0.1.2.dev0 → opensandbox_code_interpreter-1.1.0}/LICENSE +0 -0
  25. {opensandbox_code_interpreter-0.1.2.dev0 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/py.typed +0 -0
@@ -133,6 +133,8 @@ dist
133
133
  # Gatsby files
134
134
  .cache/
135
135
  public
136
+ !docs/public/
137
+ !docs/public/CNAME
136
138
 
137
139
  # Serverless directories
138
140
  .serverless/
@@ -222,6 +224,9 @@ secrets/
222
224
 
223
225
  # Generated API documentation
224
226
  docs/generated/
227
+ docs/.vitepress/generated/
228
+ docs/.vitepress/dist/
229
+ docs/.vitepress/cache/
225
230
  apidocs/
226
231
 
227
232
  # Test results
@@ -263,3 +268,29 @@ nbdist/
263
268
  # Generated files
264
269
  generated/
265
270
  **/generated/**
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
+
285
+ # gVisor runtime binaries (downloaded dynamically)
286
+ kubernetes/test/kind/gvisor/runsc
287
+ kubernetes/test/kind/gvisor/containerd-shim-runsc-v1
288
+ bin/
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.dev0
3
+ Version: 1.1.0
4
4
  Summary: OpenSandbox Code Interpreter Python SDK - Advanced code execution with persistent contexts
5
- Project-URL: Homepage, https://github.com/alibaba/OpenSandbox
6
- Project-URL: Repository, https://github.com/alibaba/OpenSandbox
7
- Project-URL: Documentation, https://docs.opensandbox.io
8
- Project-URL: Issues, https://github.com/alibaba/OpenSandbox/issues
5
+ Project-URL: Homepage, https://open-sandbox.ai
6
+ Project-URL: Repository, https://github.com/opensandbox-group/OpenSandbox
7
+ Project-URL: Documentation, https://open-sandbox.ai
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.1
227
- Requires-Dist: pydantic<3.0,>=2.0.0
226
+ Requires-Dist: opensandbox<2.0.0,>=1.1.0
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:latest",
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:latest",
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:
@@ -355,6 +354,14 @@ with sandbox:
355
354
  sandbox.kill()
356
355
  ```
357
356
 
357
+ ### Installing Python packages at runtime
358
+
359
+ You can install packages directly via `sandbox.commands.run(...)`:
360
+
361
+ ```python
362
+ execution = await sandbox.commands.run("pip install pandas numpy")
363
+ ```
364
+
358
365
  ## Runtime Configuration
359
366
 
360
367
  ### Docker Image
@@ -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:latest",
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:latest",
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:
@@ -126,6 +125,14 @@ with sandbox:
126
125
  sandbox.kill()
127
126
  ```
128
127
 
128
+ ### Installing Python packages at runtime
129
+
130
+ You can install packages directly via `sandbox.commands.run(...)`:
131
+
132
+ ```python
133
+ execution = await sandbox.commands.run("pip install pandas numpy")
134
+ ```
135
+
129
136
  ## Runtime Configuration
130
137
 
131
138
  ### Docker Image
@@ -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.
@@ -42,15 +42,15 @@ classifiers = [
42
42
  "Typing :: Typed",
43
43
  ]
44
44
  dependencies = [
45
- "pydantic>=2.0.0,<3.0",
46
- "opensandbox>=0.1.1,<0.2.0",
45
+ "pydantic>=2.4.2,<3.0",
46
+ "opensandbox>=1.1.0,<2.0.0",
47
47
  ]
48
48
 
49
49
  [project.urls]
50
- Homepage = "https://github.com/alibaba/OpenSandbox"
51
- Repository = "https://github.com/alibaba/OpenSandbox"
52
- Documentation = "https://docs.opensandbox.io"
53
- Issues = "https://github.com/alibaba/OpenSandbox/issues"
50
+ Homepage = "https://open-sandbox.ai"
51
+ Repository = "https://github.com/opensandbox-group/OpenSandbox"
52
+ Documentation = "https://open-sandbox.ai"
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.
@@ -22,6 +22,7 @@ API clients and handling SSE streaming for real-time code execution.
22
22
 
23
23
  import json
24
24
  import logging
25
+ import time
25
26
 
26
27
  import httpx
27
28
  from opensandbox.adapters.converter.event_node import EventNode
@@ -32,6 +33,7 @@ from opensandbox.adapters.converter.execution_event_dispatcher import (
32
33
  ExecutionEventDispatcher,
33
34
  )
34
35
  from opensandbox.adapters.converter.response_handler import (
36
+ extract_request_id,
35
37
  handle_api_error,
36
38
  require_parsed,
37
39
  )
@@ -39,6 +41,7 @@ from opensandbox.config import ConnectionConfig
39
41
  from opensandbox.exceptions import InvalidArgumentException, SandboxApiException
40
42
  from opensandbox.models.execd import Execution, ExecutionHandlers
41
43
  from opensandbox.models.sandboxes import SandboxEndpoint
44
+ from opensandbox.transport import unwrap_retry_transport
42
45
 
43
46
  from code_interpreter.adapters.converter.code_execution_converter import (
44
47
  CodeExecutionConverter,
@@ -49,6 +52,22 @@ from code_interpreter.services.code import Codes
49
52
  logger = logging.getLogger(__name__)
50
53
 
51
54
 
55
+ def _normalize_sse_event(event_dict: dict) -> dict:
56
+ if "type" in event_dict and "timestamp" in event_dict:
57
+ return event_dict
58
+ if "code" in event_dict and "message" in event_dict:
59
+ return {
60
+ "type": "error",
61
+ "timestamp": int(time.time() * 1000),
62
+ "error": {
63
+ "ename": str(event_dict["code"]),
64
+ "evalue": str(event_dict["message"]),
65
+ "traceback": [],
66
+ },
67
+ }
68
+ return event_dict
69
+
70
+
52
71
  class CodesAdapter(Codes):
53
72
  """
54
73
  Adapter implementation for code execution service.
@@ -88,6 +107,7 @@ class CodesAdapter(Codes):
88
107
  headers = {
89
108
  "User-Agent": self.connection_config.user_agent,
90
109
  **self.connection_config.headers,
110
+ **self.execd_endpoint.headers,
91
111
  }
92
112
 
93
113
  # Execd API does not require authentication
@@ -111,6 +131,9 @@ class CodesAdapter(Codes):
111
131
  "Accept": "text/event-stream",
112
132
  "Cache-Control": "no-cache",
113
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.
114
137
  self._sse_client = httpx.AsyncClient(
115
138
  headers=sse_headers,
116
139
  timeout=httpx.Timeout(
@@ -119,7 +142,7 @@ class CodesAdapter(Codes):
119
142
  write=timeout_seconds,
120
143
  pool=None,
121
144
  ),
122
- transport=self.connection_config.transport,
145
+ transport=unwrap_retry_transport(self.connection_config.transport),
123
146
  )
124
147
 
125
148
  async def _get_client(self):
@@ -197,7 +220,9 @@ class CodesAdapter(Codes):
197
220
  )
198
221
  handle_api_error(response_obj, "List code contexts")
199
222
  parsed_list = require_parsed(response_obj, list, "List code contexts")
200
- 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
+ ]
201
226
  except Exception as e:
202
227
  logger.error("Failed to list contexts", exc_info=e)
203
228
  raise ExceptionConverter.to_sandbox_exception(e) from e
@@ -250,7 +275,11 @@ class CodesAdapter(Codes):
250
275
  raise InvalidArgumentException("Code cannot be empty")
251
276
 
252
277
  try:
253
- 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
+ ):
254
283
  raise InvalidArgumentException(
255
284
  f"language '{language}' must match context.language '{context.language}'"
256
285
  )
@@ -280,13 +309,12 @@ class CodesAdapter(Codes):
280
309
  await response.aread()
281
310
  error_body = response.text
282
311
  logger.error(
283
- "Failed to run code. Status: %s, Body: %s",
284
- response.status_code,
285
- error_body,
312
+ f"Failed to run code. Status: {response.status_code}, Body: {error_body}"
286
313
  )
287
314
  raise SandboxApiException(
288
- 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}",
289
316
  status_code=response.status_code,
317
+ request_id=extract_request_id(response.headers),
290
318
  )
291
319
 
292
320
  dispatcher = ExecutionEventDispatcher(execution, handlers)
@@ -301,22 +329,20 @@ class CodesAdapter(Codes):
301
329
  data = data[5:].strip()
302
330
 
303
331
  try:
304
- event_dict = json.loads(data)
332
+ event_dict = _normalize_sse_event(json.loads(data))
305
333
  event_node = EventNode(**event_dict)
306
334
  await dispatcher.dispatch(event_node)
307
335
  except json.JSONDecodeError:
308
- logger.debug("Failed to parse SSE line: %s", line)
336
+ logger.debug(f"Failed to parse SSE line: {line}")
309
337
  continue
310
338
  except Exception as e:
311
- logger.error("Error processing event: %s", data, exc_info=e)
339
+ logger.error(f"Error processing event: {data}", exc_info=e)
312
340
  continue
313
341
 
314
342
  return execution
315
343
 
316
344
  except Exception as e:
317
- logger.error(
318
- "Failed to run code (length: %s)", len(code), exc_info=e
319
- )
345
+ logger.error(f"Failed to run code (length: {len(code)})", exc_info=e)
320
346
  raise ExceptionConverter.to_sandbox_exception(e) from e
321
347
 
322
348
  async def interrupt(self, execution_id: str) -> None:
@@ -339,3 +365,23 @@ class CodesAdapter(Codes):
339
365
  except Exception as e:
340
366
  logger.error("Failed to interrupt code execution", exc_info=e)
341
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.
@@ -19,6 +19,7 @@ Synchronous adapter for code execution service (including SSE streaming).
19
19
 
20
20
  import json
21
21
  import logging
22
+ import time
22
23
 
23
24
  import httpx
24
25
  from opensandbox.adapters.converter.event_node import EventNode
@@ -26,6 +27,7 @@ from opensandbox.adapters.converter.exception_converter import (
26
27
  ExceptionConverter,
27
28
  )
28
29
  from opensandbox.adapters.converter.response_handler import (
30
+ extract_request_id,
29
31
  handle_api_error,
30
32
  require_parsed,
31
33
  )
@@ -37,6 +39,7 @@ from opensandbox.models.sandboxes import SandboxEndpoint
37
39
  from opensandbox.sync.adapters.converter.execution_event_dispatcher import (
38
40
  ExecutionEventDispatcherSync,
39
41
  )
42
+ from opensandbox.transport import unwrap_retry_transport
40
43
 
41
44
  from code_interpreter.models.code_sync import CodeContextSync, SupportedLanguageSync
42
45
  from code_interpreter.sync.services.code import CodesSync
@@ -44,6 +47,22 @@ from code_interpreter.sync.services.code import CodesSync
44
47
  logger = logging.getLogger(__name__)
45
48
 
46
49
 
50
+ def _normalize_sse_event(event_dict: dict) -> dict:
51
+ if "type" in event_dict and "timestamp" in event_dict:
52
+ return event_dict
53
+ if "code" in event_dict and "message" in event_dict:
54
+ return {
55
+ "type": "error",
56
+ "timestamp": int(time.time() * 1000),
57
+ "error": {
58
+ "ename": str(event_dict["code"]),
59
+ "evalue": str(event_dict["message"]),
60
+ "traceback": [],
61
+ },
62
+ }
63
+ return event_dict
64
+
65
+
47
66
  class CodesAdapterSync(CodesSync):
48
67
  """
49
68
  Synchronous adapter for code execution service.
@@ -63,7 +82,9 @@ class CodesAdapterSync(CodesSync):
63
82
  RUN_CODE_PATH = "/code"
64
83
  CREATE_CONTEXT_PATH = "/code/context"
65
84
 
66
- def __init__(self, execd_endpoint: SandboxEndpoint, connection_config: ConnectionConfigSync) -> None:
85
+ def __init__(
86
+ self, execd_endpoint: SandboxEndpoint, connection_config: ConnectionConfigSync
87
+ ) -> None:
67
88
  """
68
89
  Initialize the code service adapter (sync).
69
90
 
@@ -78,7 +99,11 @@ class CodesAdapterSync(CodesSync):
78
99
  base_url = f"{self.connection_config.protocol}://{self.execd_endpoint.endpoint}"
79
100
  timeout_seconds = self.connection_config.request_timeout.total_seconds()
80
101
  timeout = httpx.Timeout(timeout_seconds)
81
- headers = {"User-Agent": self.connection_config.user_agent, **self.connection_config.headers}
102
+ headers = {
103
+ "User-Agent": self.connection_config.user_agent,
104
+ **self.connection_config.headers,
105
+ **(self.execd_endpoint.headers or {}),
106
+ }
82
107
 
83
108
  self._client = Client(base_url=base_url, timeout=timeout)
84
109
  self._httpx_client = httpx.Client(
@@ -89,16 +114,27 @@ class CodesAdapterSync(CodesSync):
89
114
  )
90
115
  self._client.set_httpx_client(self._httpx_client)
91
116
 
92
- sse_headers = {**headers, "Accept": "text/event-stream", "Cache-Control": "no-cache"}
117
+ sse_headers = {
118
+ **headers,
119
+ "Accept": "text/event-stream",
120
+ "Cache-Control": "no-cache",
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.
93
125
  self._sse_client = httpx.Client(
94
126
  headers=sse_headers,
95
- timeout=httpx.Timeout(connect=timeout_seconds, read=None, write=timeout_seconds, pool=None),
96
- transport=self.connection_config.transport,
127
+ timeout=httpx.Timeout(
128
+ connect=timeout_seconds, read=None, write=timeout_seconds, pool=None
129
+ ),
130
+ transport=unwrap_retry_transport(self.connection_config.transport),
97
131
  )
98
132
 
99
133
  def _get_execd_url(self, path: str) -> str:
100
134
  """Build URL for execd endpoint."""
101
- return f"{self.connection_config.protocol}://{self.execd_endpoint.endpoint}{path}"
135
+ return (
136
+ f"{self.connection_config.protocol}://{self.execd_endpoint.endpoint}{path}"
137
+ )
102
138
 
103
139
  def create_context(self, language: str) -> CodeContextSync:
104
140
  """
@@ -225,7 +261,11 @@ class CodesAdapterSync(CodesSync):
225
261
  raise InvalidArgumentException("Code cannot be empty")
226
262
 
227
263
  try:
228
- if context is not None and language is not None and context.language != language:
264
+ if (
265
+ context is not None
266
+ and language is not None
267
+ and context.language != language
268
+ ):
229
269
  raise InvalidArgumentException(
230
270
  f"language '{language}' must match context.language '{context.language}'"
231
271
  )
@@ -233,7 +273,9 @@ class CodesAdapterSync(CodesSync):
233
273
  if context is None:
234
274
  # Default context: language default context (server-side behavior).
235
275
  # When context.id is omitted, execd will create/reuse a default session per language.
236
- context = CodeContextSync(language=language or SupportedLanguageSync.PYTHON)
276
+ context = CodeContextSync(
277
+ language=language or SupportedLanguageSync.PYTHON
278
+ )
237
279
  api_request = {
238
280
  "code": code,
239
281
  "context": {
@@ -249,9 +291,11 @@ class CodesAdapterSync(CodesSync):
249
291
  with self._sse_client.stream("POST", url, json=api_request) as response:
250
292
  if response.status_code != 200:
251
293
  response.read()
294
+ error_body = response.text
252
295
  raise SandboxApiException(
253
- 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}",
254
297
  status_code=response.status_code,
298
+ request_id=extract_request_id(response.headers),
255
299
  )
256
300
 
257
301
  for line in response.iter_lines():
@@ -261,19 +305,19 @@ class CodesAdapterSync(CodesSync):
261
305
  if data.startswith("data:"):
262
306
  data = data[5:].strip()
263
307
  try:
264
- event_dict = json.loads(data)
308
+ event_dict = _normalize_sse_event(json.loads(data))
265
309
  event_node = EventNode(**event_dict)
266
310
  dispatcher.dispatch(event_node)
267
311
  except json.JSONDecodeError:
268
- logger.debug("Failed to parse SSE line: %s", line)
312
+ logger.debug(f"Failed to parse SSE line: {line}")
269
313
  continue
270
314
  except Exception as e:
271
- logger.error("Error processing event: %s", data, exc_info=e)
315
+ logger.error(f"Error processing event: {data}", exc_info=e)
272
316
  continue
273
317
 
274
318
  return execution
275
319
  except Exception as e:
276
- 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)
277
321
  raise ExceptionConverter.to_sandbox_exception(e) from e
278
322
 
279
323
  def interrupt(self, execution_id: str) -> None:
@@ -286,8 +330,27 @@ class CodesAdapterSync(CodesSync):
286
330
  try:
287
331
  from opensandbox.api.execd.api.code_interpreting import interrupt_code
288
332
 
289
- response_obj = interrupt_code.sync_detailed(client=self._client, id=execution_id)
333
+ response_obj = interrupt_code.sync_detailed(
334
+ client=self._client, id=execution_id
335
+ )
290
336
  handle_api_error(response_obj, "Interrupt code execution")
291
337
  except Exception as e:
292
338
  logger.error("Failed to interrupt code execution", exc_info=e)
293
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
+ ...