verifiers 0.3.2.dev59__py3-none-any.whl → 0.3.2.dev61__py3-none-any.whl

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.
@@ -316,13 +316,30 @@ class Runtime(ABC):
316
316
  """Read `path` into host memory. `max_bytes` caps the transfer, raising past
317
317
  the cap — for a file written by something we don't control, whose size we
318
318
  can't assume. The cap is enforced inside the box rather than after the
319
- transfer, and base64 because `run` returns decoded text. Framework method —
320
- override `_read`, not this."""
319
+ transfer. Framework method — override `_read`, not this."""
321
320
  if max_bytes is None:
322
321
  return await self._read(path)
322
+ if max_bytes < 0:
323
+ raise ValueError("max_bytes must be non-negative")
324
+ # One extra byte distinguishes an exact fit from a truncated file.
325
+ data = await self._read(path, max_bytes=max_bytes + 1)
326
+ if len(data) > max_bytes:
327
+ raise SandboxError(f"read {path!r}: over the {max_bytes} byte limit")
328
+ return data
329
+
330
+ @abstractmethod
331
+ async def _read(self, path: str, max_bytes: int | None = None) -> bytes:
332
+ """Read at most `max_bytes` bytes at the source, or the whole file if None.
333
+
334
+ Overrides must accept `max_bytes` and enforce it before transferring data.
335
+ The public `read` method checks for overflow using one extra byte.
336
+ Runtimes without bounded binary reads delegate capped reads here, using
337
+ base64 because `run` returns decoded text.
338
+ """
339
+ assert max_bytes is not None
323
340
  # Through a temp file, not a pipe: `head | base64` exits with base64's 0
324
341
  # even when the path is missing, and a missing file must raise here just
325
- # as it does from `_read`.
342
+ # as it does from a native binary read.
326
343
  result = await self.run(
327
344
  [
328
345
  "sh",
@@ -333,21 +350,14 @@ class Runtime(ABC):
333
350
  'base64 < "$t"; rc=$?; rm -f "$t"; exit $rc'
334
351
  ),
335
352
  "sh",
336
- str(max_bytes + 1),
353
+ str(max_bytes),
337
354
  path,
338
355
  ],
339
356
  {},
340
357
  )
341
358
  if result.exit_code:
342
359
  raise SandboxError(f"read {path!r}: {result.stderr.strip()[-500:]}")
343
- data = base64.b64decode(result.stdout)
344
- if len(data) > max_bytes:
345
- raise SandboxError(f"read {path!r}: over the {max_bytes} byte limit")
346
- return data
347
-
348
- @abstractmethod
349
- async def _read(self, path: str) -> bytes:
350
- """Read the whole file at `path`; `read` adds the optional transfer cap."""
360
+ return base64.b64decode(result.stdout)
351
361
 
352
362
  @abstractmethod
353
363
  async def write(self, path: str, data: bytes) -> None:
@@ -506,19 +506,28 @@ class DockerRuntime(Runtime):
506
506
  if run.exit_code != 0:
507
507
  raise SandboxError(f"docker exec -d failed: {run.stderr.strip()}")
508
508
 
509
- async def _read(self, path: str) -> bytes:
509
+ async def _read(self, path: str, max_bytes: int | None = None) -> bytes:
510
+ argv = ["cat"] if max_bytes is None else ["head", "-c", str(max_bytes)]
510
511
  proc = await asyncio.create_subprocess_exec(
511
512
  "docker",
512
513
  "exec",
513
514
  "--workdir",
514
515
  self.config.workdir,
515
516
  self._container,
516
- "cat",
517
+ *argv,
518
+ "--",
517
519
  path,
518
520
  stdout=asyncio.subprocess.PIPE,
519
521
  stderr=asyncio.subprocess.PIPE,
520
522
  )
521
- stdout, stderr = await proc.communicate()
523
+ try:
524
+ stdout, stderr = await proc.communicate()
525
+ except BaseException:
526
+ if proc.returncode is None:
527
+ with contextlib.suppress(ProcessLookupError):
528
+ proc.kill()
529
+ await run_shielded(proc.communicate())
530
+ raise
522
531
  if proc.returncode != 0:
523
532
  raise SandboxError(
524
533
  f"read {path!r}: {stderr.decode(errors='replace').strip()}"
@@ -279,7 +279,9 @@ class ModalRuntime(Runtime):
279
279
  return path
280
280
  return f"{self.config.workdir.rstrip('/')}/{path}"
281
281
 
282
- async def _read(self, path: str) -> bytes:
282
+ async def _read(self, path: str, max_bytes: int | None = None) -> bytes:
283
+ if max_bytes is not None:
284
+ return await super()._read(path, max_bytes)
283
285
  try:
284
286
  return await self._sandbox.filesystem.read_bytes.aio(self._abs(path))
285
287
  except Exception as e:
@@ -365,7 +365,9 @@ class PrimeRuntime(Runtime):
365
365
  except Exception as e:
366
366
  raise SandboxError(f"prime background launch failed: {e}") from e
367
367
 
368
- async def _read(self, path: str) -> bytes:
368
+ async def _read(self, path: str, max_bytes: int | None = None) -> bytes:
369
+ if max_bytes is not None:
370
+ return await super()._read(path, max_bytes)
369
371
  # Avoid background-job log limits and base64 overhead by downloading binary data directly.
370
372
  # The temporary file is removed on every exit, and its byte read stays off the event loop.
371
373
  target = (
@@ -382,21 +384,14 @@ class PrimeRuntime(Runtime):
382
384
  raise SandboxError(f"read {path!r}: {e}") from e
383
385
 
384
386
  async def write(self, path: str, data: bytes) -> None:
385
- # Upload via the gateway (multipart) — never inline the bytes on the command line
386
- # (a large file, e.g. a task tarball, overflows the exec command-length limit and
387
- # fails with ENAMETOOLONG). The upload does NOT run in the workdir, so resolve a
388
- # relative path against it (and mkdir its parent) — otherwise the sidecar writes
389
- # it somewhere unwritable ("Operation not permitted").
387
+ # The gateway creates missing parents and uploads binary data without command-line
388
+ # limits. Resolve relative paths here because uploads do not use this runtime's workdir.
390
389
  target = (
391
390
  path
392
391
  if path.startswith("/")
393
392
  else f"{self.config.workdir.rstrip('/')}/{path}"
394
393
  )
395
394
  try:
396
- await self._client.execute_command(
397
- self.info.id,
398
- f"mkdir -p {shlex.quote(str(PurePosixPath(target).parent))}",
399
- )
400
395
  await self._client.upload_bytes(
401
396
  self.info.id, target, data, filename=PurePosixPath(target).name
402
397
  )
@@ -5,12 +5,14 @@ import contextlib
5
5
  import os
6
6
  import shutil
7
7
  import signal
8
+ import stat
8
9
  from collections.abc import AsyncIterator
9
10
  from pathlib import Path
10
11
  from typing import ClassVar, Literal
11
12
 
12
13
  from pydantic_config import BaseConfig
13
14
 
15
+ from verifiers.v1.errors import SandboxError
14
16
  from verifiers.v1.runtimes.base import (
15
17
  BaseRuntimeInfo,
16
18
  ProgramResult,
@@ -161,8 +163,28 @@ class SubprocessRuntime(Runtime):
161
163
  proc
162
164
  ) # killed in stop() — a host process won't die on its own
163
165
 
164
- async def _read(self, path: str) -> bytes:
165
- return await asyncio.to_thread((self.workdir / path).read_bytes)
166
+ async def _read(self, path: str, max_bytes: int | None = None) -> bytes:
167
+ if max_bytes is None:
168
+ return await asyncio.to_thread((self.workdir / path).read_bytes)
169
+
170
+ # Leave special files to the cancellable shell path without opening a
171
+ # FIFO and waking its writer. A nonblocking open and descriptor check
172
+ # also cover a regular path being replaced by a FIFO after the stat.
173
+ def read() -> bytes | None:
174
+ target = self.workdir / path
175
+ if not stat.S_ISREG(target.stat().st_mode):
176
+ return None
177
+ fd = os.open(target, os.O_RDONLY | os.O_NONBLOCK)
178
+ with os.fdopen(fd, "rb") as file:
179
+ if not stat.S_ISREG(os.fstat(file.fileno()).st_mode):
180
+ return None
181
+ return file.read(max_bytes)
182
+
183
+ try:
184
+ data = await asyncio.to_thread(read)
185
+ except OSError as exc:
186
+ raise SandboxError(f"read {path!r}: {exc}") from exc
187
+ return data if data is not None else await super()._read(path, max_bytes)
166
188
 
167
189
  async def write(self, path: str, data: bytes) -> None:
168
190
  target = self.workdir / path
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: verifiers
3
- Version: 0.3.2.dev59
3
+ Version: 0.3.2.dev61
4
4
  Summary: Verifiers: Environments for LLM Reinforcement Learning
5
5
  Project-URL: Homepage, https://github.com/primeintellect-ai/verifiers
6
6
  Project-URL: Documentation, https://github.com/primeintellect-ai/verifiers
@@ -141,12 +141,12 @@ verifiers/v1/mcp/launch.py,sha256=K6M8ZaoOO7wt65s93wK_9Wl7bvjeQ4C8BLkrGE149DI,23
141
141
  verifiers/v1/mcp/server.py,sha256=Jqi8OMT8DnoZegXqM3UNNRzhVnbdrPEwx0DjOJo_K2A,13256
142
142
  verifiers/v1/mcp/toolset.py,sha256=G2wzNdEMGLXoQUcrwF0TartJkqokNPI3o7YUOAaaa6U,1007
143
143
  verifiers/v1/runtimes/__init__.py,sha256=ppc1R_sx-aMcuJSx4t99rTSjloUXI0M6EUj676-1PeY,2862
144
- verifiers/v1/runtimes/base.py,sha256=SRTUNAcdW0xJIWjfS5RcijFXqdKyXmGvp2tvV58eu58,16826
144
+ verifiers/v1/runtimes/base.py,sha256=kPxbK-VPr4j3Ah0SYlHAMefUJpVF6jTCJkZm7TRVeAs,17376
145
145
  verifiers/v1/runtimes/limiters.py,sha256=C6cWDD4pwyFwUY4QFke3oiMXwydSNIfSbJSIglUC2kE,3062
146
- verifiers/v1/runtimes/modal.py,sha256=yyrqI6agpzi0lp5jR_ExqKwAq8tRgLvHVa3Sd3oh6RU,13222
147
- verifiers/v1/runtimes/prime.py,sha256=1IWRLKdGgscs8JmvkBdaS3au4tV0KUqv4W1gHJvOE2Q,18781
148
- verifiers/v1/runtimes/subprocess.py,sha256=IO82fI1wFbeTtV7T1AHSF8iTUyT9yGJFIUmCKKw6CnQ,7763
149
- verifiers/v1/runtimes/docker/__init__.py,sha256=K6xbJAT1F3-PtgP5Gum2UJKQ8Re1Yz90BP8Uc7Npec4,20594
146
+ verifiers/v1/runtimes/modal.py,sha256=wUNik2Wul1YISausdABIMAoSaTA0MNDwKUayKxVLwAA,13342
147
+ verifiers/v1/runtimes/prime.py,sha256=fbyR5s3WqsJNWki3eHJa83j4iobnqDugdJWm8GwEs7w,18494
148
+ verifiers/v1/runtimes/subprocess.py,sha256=QmpQ235_xrX6gowrr8557VgbydOXpLbfFq0KKGfJThk,8774
149
+ verifiers/v1/runtimes/docker/__init__.py,sha256=JN0mI10a_BKQpRRJKX_NqStAoODAEk2dAJF9mrz69ug,20972
150
150
  verifiers/v1/runtimes/docker/egress.py,sha256=iFH_1XotlEM-OCaW7GNu3TWVEq1sqesKYweyVBX8g90,14360
151
151
  verifiers/v1/serve/__init__.py,sha256=doImCSxSYZEjVDXfRafLHkhHXcXQxzA6qUGMBQ8bSko,477
152
152
  verifiers/v1/serve/client.py,sha256=biy46qOOFTjMXSIsY8W1vMEfctn7usCeNuBQsJqc2mc,7789
@@ -189,8 +189,8 @@ verifiers/v1/utils/prime.py,sha256=UTYRjp9cbjNb6CVmBHda-1wWZAIIfxNOmTyNuT7_wL4,9
189
189
  verifiers/v1/utils/retries.py,sha256=Y2ZgrAjn-qNkRKZP_RVNL_7EK0iaRcZeCa404lpGYi4,5417
190
190
  verifiers/v1/utils/score.py,sha256=493yJVMw8teCu9JxapxMFPFzI0hNUqdo0Y2nGW4kckk,6200
191
191
  verifiers/v1/utils/version.py,sha256=-obEo_-l9-D8FLef4hYxncOe-uJpxrM1g2Hig_37Sgs,1607
192
- verifiers-0.3.2.dev59.dist-info/METADATA,sha256=PsSfsNRgqhTgCJsBY8_ODR_rUmPRliclAgnVutS2FyY,4204
193
- verifiers-0.3.2.dev59.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
194
- verifiers-0.3.2.dev59.dist-info/entry_points.txt,sha256=uqQje0TMsr7k6nXlWxlPs13Si0evb3pVCEDF6c-YL80,241
195
- verifiers-0.3.2.dev59.dist-info/licenses/LICENSE,sha256=v0RrUsdV3IDoZhrRce297IXS3xMHNJ-_LdLpFAUWb9k,1072
196
- verifiers-0.3.2.dev59.dist-info/RECORD,,
192
+ verifiers-0.3.2.dev61.dist-info/METADATA,sha256=7o5tiEoE-ol4J9KtYpx1306yqLvQ6cEKtF0inhzekjQ,4204
193
+ verifiers-0.3.2.dev61.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
194
+ verifiers-0.3.2.dev61.dist-info/entry_points.txt,sha256=uqQje0TMsr7k6nXlWxlPs13Si0evb3pVCEDF6c-YL80,241
195
+ verifiers-0.3.2.dev61.dist-info/licenses/LICENSE,sha256=v0RrUsdV3IDoZhrRce297IXS3xMHNJ-_LdLpFAUWb9k,1072
196
+ verifiers-0.3.2.dev61.dist-info/RECORD,,