inspect-sandboxes 0.1.1.dev1__tar.gz → 0.1.2__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 (19) hide show
  1. {inspect_sandboxes-0.1.1.dev1 → inspect_sandboxes-0.1.2}/PKG-INFO +1 -1
  2. {inspect_sandboxes-0.1.1.dev1 → inspect_sandboxes-0.1.2}/src/inspect_sandboxes/_version.py +2 -2
  3. {inspect_sandboxes-0.1.1.dev1 → inspect_sandboxes-0.1.2}/src/inspect_sandboxes/daytona/README.md +9 -1
  4. {inspect_sandboxes-0.1.1.dev1 → inspect_sandboxes-0.1.2}/src/inspect_sandboxes/daytona/_compose.py +5 -0
  5. {inspect_sandboxes-0.1.1.dev1 → inspect_sandboxes-0.1.2}/src/inspect_sandboxes/daytona/_daytona.py +11 -6
  6. {inspect_sandboxes-0.1.1.dev1 → inspect_sandboxes-0.1.2}/src/inspect_sandboxes/modal/README.md +8 -0
  7. {inspect_sandboxes-0.1.1.dev1 → inspect_sandboxes-0.1.2}/src/inspect_sandboxes/modal/_compose.py +30 -2
  8. {inspect_sandboxes-0.1.1.dev1 → inspect_sandboxes-0.1.2}/src/inspect_sandboxes/modal/_modal.py +12 -7
  9. {inspect_sandboxes-0.1.1.dev1 → inspect_sandboxes-0.1.2}/.gitignore +0 -0
  10. {inspect_sandboxes-0.1.1.dev1 → inspect_sandboxes-0.1.2}/LICENSE +0 -0
  11. {inspect_sandboxes-0.1.1.dev1 → inspect_sandboxes-0.1.2}/README.md +0 -0
  12. {inspect_sandboxes-0.1.1.dev1 → inspect_sandboxes-0.1.2}/pyproject.toml +0 -0
  13. {inspect_sandboxes-0.1.1.dev1 → inspect_sandboxes-0.1.2}/src/inspect_sandboxes/__init__.py +0 -0
  14. {inspect_sandboxes-0.1.1.dev1 → inspect_sandboxes-0.1.2}/src/inspect_sandboxes/_registry.py +0 -0
  15. {inspect_sandboxes-0.1.1.dev1 → inspect_sandboxes-0.1.2}/src/inspect_sandboxes/_util/__init__.py +0 -0
  16. {inspect_sandboxes-0.1.1.dev1 → inspect_sandboxes-0.1.2}/src/inspect_sandboxes/_util/compose.py +0 -0
  17. {inspect_sandboxes-0.1.1.dev1 → inspect_sandboxes-0.1.2}/src/inspect_sandboxes/daytona/__init__.py +0 -0
  18. {inspect_sandboxes-0.1.1.dev1 → inspect_sandboxes-0.1.2}/src/inspect_sandboxes/modal/__init__.py +0 -0
  19. {inspect_sandboxes-0.1.1.dev1 → inspect_sandboxes-0.1.2}/src/inspect_sandboxes/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: inspect_sandboxes
3
- Version: 0.1.1.dev1
3
+ Version: 0.1.2
4
4
  Summary: Collection of sandboxes for Inspect AI
5
5
  Project-URL: Source Code, https://github.com/meridianlabs-ai/inspect_sandboxes
6
6
  Project-URL: Issue Tracker, https://github.com/meridianlabs-ai/inspect_sandboxes/issues
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.1.1.dev1'
32
- __version_tuple__ = version_tuple = (0, 1, 1, 'dev1')
31
+ __version__ = version = '0.1.2'
32
+ __version_tuple__ = version_tuple = (0, 1, 2)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -88,5 +88,13 @@ task = Task(
88
88
  ## Notes
89
89
 
90
90
  - **stderr**: The Daytona API returns a single combined output; `stderr` is always empty and all output appears in `stdout`.
91
- - **stdin**: The `input` parameter to `exec()` is not supported and will be ignored with a warning.
91
+ - **stdin**: The `input` parameter to `exec()` is supported by uploading a temp file and piping it into the command.
92
92
  - **user**: The `user` parameter to `exec()` is not supported; use `os_user` in `x-daytona` to set the OS user.
93
+ - **network_mode**: Docker Compose `network_mode` is automatically translated to Daytona's `network_block_all` parameter. `network_mode: none` blocks all network access; any other value (e.g. `bridge`) allows it. The `x-daytona` extension `network_block_all` takes precedence if set.
94
+
95
+ ## Known Limitations
96
+
97
+ - **Architecture**: Daytona runners use `linux/amd64`. Tasks with arm64-only base images cannot run on Daytona. The image publisher must add amd64 support.
98
+ - **Resource limits**: Daytona enforces per-sandbox limits. Tasks requesting more will fail with `CPU request N exceeds maximum` or `Memory request exceeds maximum`. Use `override_cpus` / `override_memory_mb` task args to cap resources, or contact Daytona support for higher limits.
99
+ - **Single service**: Only one service is supported per sandbox. Multi-service compose files are not supported.
100
+ - **Internet access**: Outbound internet access from sandboxes depends on your Daytona billing tier. Lower tiers (Tier 1–2) have restricted network access that cannot be overridden by the `network_block_all` parameter. If tasks fail with `curl: (35) Recv failure: Connection reset by peer` or similar network errors despite `network_block_all: false`, upgrade to a higher tier (Tier 3+) for full outbound internet access.
@@ -57,6 +57,11 @@ def convert_compose_to_daytona_params(
57
57
  if service.user:
58
58
  sandbox_params["os_user"] = service.user
59
59
 
60
+ # Translate Docker network_mode to Daytona network_block_all.
61
+ # Only set as a default; x-daytona extensions can override below.
62
+ if service.network_mode is not None and "network_block_all" not in sandbox_params:
63
+ sandbox_params["network_block_all"] = service.network_mode == "none"
64
+
60
65
  _apply_daytona_extensions(sandbox_params, config.extensions)
61
66
 
62
67
  return image, resources, sandbox_params
@@ -4,6 +4,7 @@ import errno
4
4
  import os
5
5
  import shlex
6
6
  import sys
7
+ import uuid
7
8
  from contextvars import ContextVar
8
9
  from logging import getLogger
9
10
  from pathlib import PurePosixPath
@@ -306,14 +307,18 @@ class DaytonaSandboxEnvironment(SandboxEnvironment):
306
307
  "Commands will run as the container's default user.",
307
308
  )
308
309
 
310
+ # Daytona's process.exec() doesn't support stdin natively.
311
+ # When input is provided, write it to a temp file and pipe it into the command.
309
312
  if input is not None:
310
- warn_once(
311
- logger,
312
- "The 'input' (stdin) parameter is not supported in DaytonaSandboxEnvironment "
313
- "and will be ignored.",
313
+ data = input.encode("utf-8") if isinstance(input, str) else input
314
+ stdin_file = f"/tmp/.inspect-stdin-{uuid.uuid4().hex}"
315
+ await self.sandbox.fs.upload_file(data, stdin_file)
316
+ command = (
317
+ f"set -o pipefail; cat {shlex.quote(stdin_file)} | {shlex.join(cmd)}"
318
+ f"; _ec=$?; rm -f {shlex.quote(stdin_file)}; exit $_ec"
314
319
  )
315
-
316
- command = shlex.join(cmd)
320
+ else:
321
+ command = shlex.join(cmd)
317
322
 
318
323
  async def _run(t: int | None) -> ExecResult[str]:
319
324
  response = await self.sandbox.process.exec(
@@ -81,6 +81,14 @@ task = Task(
81
81
  )
82
82
  ```
83
83
 
84
+ ## Notes
85
+
86
+ - **network_mode**: Docker Compose `network_mode` is automatically translated to Modal's `block_network` parameter. `network_mode: none` blocks all network access; any other value (e.g. `bridge`) allows it. The `x-modal` extension `block_network` takes precedence if set.
87
+
88
+ ## Known Limitations
89
+
90
+ - **Single service**: Only one compose service is supported per sandbox. Multi-service compose files will raise a `ValueError`.
91
+
84
92
  ## Credits
85
93
 
86
94
  Based on [@anthonyduong9](https://github.com/anthonyduong9)'s work.
@@ -1,3 +1,5 @@
1
+ import shlex
2
+ from dataclasses import dataclass, field
1
3
  from pathlib import Path
2
4
  from typing import Any
3
5
 
@@ -11,9 +13,22 @@ from inspect_sandboxes._util.compose import (
11
13
  )
12
14
 
13
15
 
16
+ @dataclass
17
+ class ModalSandboxParams:
18
+ """Parameters for modal.Sandbox.create().
19
+
20
+ Attributes:
21
+ command: Positional command args passed before **kwargs to Sandbox.create().
22
+ kwargs: Keyword arguments passed to Sandbox.create().
23
+ """
24
+
25
+ command: list[str] = field(default_factory=list)
26
+ kwargs: dict[str, Any] = field(default_factory=dict)
27
+
28
+
14
29
  def convert_compose_to_modal_params(
15
30
  config: ComposeConfig, compose_path: str | None
16
- ) -> dict[str, Any]:
31
+ ) -> ModalSandboxParams:
17
32
  """Convert a ComposeConfig to Modal Sandbox.create() parameters.
18
33
 
19
34
  Args:
@@ -27,6 +42,7 @@ def convert_compose_to_modal_params(
27
42
  service = config.services.get("default") or next(iter(config.services.values()))
28
43
 
29
44
  params: dict[str, Any] = {}
45
+ command: list[str] = []
30
46
  compose_dir = Path(compose_path).parent if compose_path else Path.cwd()
31
47
 
32
48
  if service.build:
@@ -40,6 +56,13 @@ def convert_compose_to_modal_params(
40
56
  elif service.image:
41
57
  params["image"] = modal.Image.from_registry(service.image)
42
58
 
59
+ if service.command:
60
+ command = (
61
+ service.command
62
+ if isinstance(service.command, list)
63
+ else shlex.split(service.command)
64
+ )
65
+
43
66
  if service.working_dir:
44
67
  params["workdir"] = service.working_dir
45
68
 
@@ -58,9 +81,14 @@ def convert_compose_to_modal_params(
58
81
  if gpu is not None:
59
82
  params["gpu"] = gpu
60
83
 
84
+ # Translate Docker network_mode to Modal block_network.
85
+ # Only set as a default; x-modal extensions can override below.
86
+ if service.network_mode is not None:
87
+ params["block_network"] = service.network_mode == "none"
88
+
61
89
  _apply_modal_extensions(params, config.extensions)
62
90
 
63
- return params
91
+ return ModalSandboxParams(command=command, kwargs=params)
64
92
 
65
93
 
66
94
  def _apply_modal_extensions(params: dict[str, Any], extensions: dict[str, Any]) -> None:
@@ -102,24 +102,27 @@ class ModalSandboxEnvironment(SandboxEnvironment):
102
102
  ) -> dict[str, SandboxEnvironment]:
103
103
  app = await cls._lookup_app(MODAL_APP_NAME)
104
104
 
105
- sandbox_params: dict[str, Any] = {
105
+ sandbox_kwargs: dict[str, Any] = {
106
106
  "app": app,
107
107
  "timeout": 60 * 60 * 24,
108
108
  }
109
+ command: list[str] = []
109
110
 
110
111
  if config is None:
111
112
  trace_message(
112
113
  logger, "modal", f"Using default Modal image for task '{task_name}'"
113
114
  )
114
115
  elif is_dockerfile(config):
115
- sandbox_params["image"] = modal.Image.from_dockerfile(config)
116
+ sandbox_kwargs["image"] = modal.Image.from_dockerfile(config)
116
117
  elif is_compose_yaml(config):
117
118
  compose_config = parse_compose_yaml(config, multiple_services=False)
118
119
  modal_params = convert_compose_to_modal_params(compose_config, config)
119
- sandbox_params.update(modal_params)
120
+ command = modal_params.command
121
+ sandbox_kwargs.update(modal_params.kwargs)
120
122
  elif isinstance(config, ComposeConfig):
121
123
  modal_params = convert_compose_to_modal_params(config, None)
122
- sandbox_params.update(modal_params)
124
+ command = modal_params.command
125
+ sandbox_kwargs.update(modal_params.kwargs)
123
126
  else:
124
127
  raise ValueError(
125
128
  f"Unrecognized config: {config}. "
@@ -127,7 +130,7 @@ class ModalSandboxEnvironment(SandboxEnvironment):
127
130
  "ComposeConfig object, or None."
128
131
  )
129
132
 
130
- sandbox = await cls._create_sandbox(sandbox_params)
133
+ sandbox = await cls._create_sandbox(command, sandbox_kwargs)
131
134
  await sandbox.set_tags.aio(INSPECT_SANDBOX_TAG)
132
135
  running_sandboxes().append(sandbox.object_id)
133
136
 
@@ -421,8 +424,10 @@ class ModalSandboxEnvironment(SandboxEnvironment):
421
424
 
422
425
  @staticmethod
423
426
  @_standard_retry
424
- async def _create_sandbox(sandbox_params: dict[str, Any]) -> modal.Sandbox:
425
- return await modal.Sandbox.create.aio(**sandbox_params)
427
+ async def _create_sandbox(
428
+ command: list[str], kwargs: dict[str, Any]
429
+ ) -> modal.Sandbox:
430
+ return await modal.Sandbox.create.aio(*command, **kwargs)
426
431
 
427
432
  @staticmethod
428
433
  @_standard_retry