verifiers 0.3.2.dev168__py3-none-any.whl → 0.3.2.dev170__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.
verifiers/v1/rollout.py CHANGED
@@ -14,6 +14,7 @@ from verifiers.v1.configs.runtime import NetworkPolicyConfig
14
14
  from verifiers.v1.errors import (
15
15
  HarnessError,
16
16
  RolloutError,
17
+ SandboxError,
17
18
  TaskError,
18
19
  ToolsetError,
19
20
  boundary,
@@ -52,6 +53,39 @@ class RolloutTimeouts:
52
53
  """Timeout (in seconds) for the task + harness metrics + scoring hooks."""
53
54
 
54
55
 
56
+ _HOME_PROBE = (
57
+ "home=$(awk -F: -v u=\"$1\" '$1 == u { print $6 }' /etc/passwd); "
58
+ 'echo "$home"; test -n "$home" && test -d "$home"'
59
+ )
60
+
61
+
62
+ async def agent_runtime(runtime: Runtime, user: str | None) -> Runtime:
63
+ """The runtime the harness runs on: `runtime` itself, or a view running as `user`
64
+ with `HOME` set to its home. `user` must exist with a home directory it can write
65
+ (task authors ship it in the image), and the view must really run as it, so a
66
+ runtime ignoring the user never runs the agent as root."""
67
+ if user is None:
68
+ return runtime
69
+ lookup = await runtime.run(["sh", "-c", _HOME_PROBE, "sh", user], {})
70
+ home = lookup.stdout.strip()
71
+ if not home:
72
+ raise TaskError(f"agent_user {user!r} does not exist in the image")
73
+ if lookup.exit_code != 0:
74
+ raise TaskError(f"agent_user {user!r} has no home directory ({home})")
75
+ view = runtime.with_user(user)
76
+ view = view.with_env({**view.env, "HOME": home})
77
+ probe = await view.run(["sh", "-c", 'id -un; test -w "$HOME" && echo writable'], {})
78
+ effective, *rest = probe.stdout.split() or [""]
79
+ if effective != user:
80
+ raise SandboxError(
81
+ f"{type(runtime).__name__} ran as {effective!r} instead of agent_user "
82
+ f"{user!r}; refusing to run the agent"
83
+ )
84
+ if rest != ["writable"]:
85
+ raise TaskError(f"agent_user {user!r} cannot write its home directory ({home})")
86
+ return view
87
+
88
+
55
89
  class Rollout:
56
90
  """Manages one rollout's lifecycle (open, step, close)."""
57
91
 
@@ -341,7 +375,7 @@ class Rollout:
341
375
  self._harness_session = await self.harness.session(
342
376
  self.ctx,
343
377
  self.trace,
344
- runtime,
378
+ await agent_runtime(runtime, harness_data.agent_user),
345
379
  self._endpoint,
346
380
  self._secret,
347
381
  self._urls,
@@ -136,13 +136,16 @@ class BaseRuntimeInfo(BaseConfig):
136
136
 
137
137
 
138
138
  class Runtime(ABC):
139
- __slots__ = ("env",)
139
+ __slots__ = ("env", "user")
140
140
 
141
141
  is_local: bool = True
142
142
  """Whether this runtime exchanges host-local URLs without a public tunnel. True for
143
143
  subprocess and the local container runtimes; remote runtimes override to False and
144
144
  use a host `Tunnel` inward plus `expose` outward."""
145
145
 
146
+ supports_user: ClassVar[bool] = False
147
+ """Whether `with_user` can run this runtime's commands as another existing user."""
148
+
146
149
  scripts_dir: ClassVar[str] = "/tmp/vf-scripts"
147
150
  """Digest-keyed PEP 723 scripts inside the runtime. Sandboxes own their `/tmp`;
148
151
  the host subprocess runtime overrides this with its per-user cache."""
@@ -159,6 +162,8 @@ class Runtime(ABC):
159
162
  # Per-run task values live on the runtime rather than its serializable config/info.
160
163
  # Explicit process values (model credentials, proxy settings, etc.) override these.
161
164
  self.env: dict[str, str] = {}
165
+ self.user: str | None = None
166
+ """Existing user this view runs commands as; None uses the runtime's default."""
162
167
  self._uv_interpreters: dict[str, str] = {}
163
168
  self._uv_script_locks: dict[str, asyncio.Lock] = {}
164
169
  self._mcp_sources: set[str] = set()
@@ -215,6 +220,19 @@ class Runtime(ABC):
215
220
  runtime.env = dict(env)
216
221
  return runtime
217
222
 
223
+ def with_user(self, user: str | None) -> "Runtime":
224
+ """Share this physical runtime through a view that runs commands as `user`, an
225
+ existing user in the box (None keeps the default). Only the view runs as `user`:
226
+ setup, collection, and grading keep using the original runtime."""
227
+ if user is not None and not self.supports_user:
228
+ raise ValueError(
229
+ f"{type(self).__name__} cannot run commands as another user"
230
+ )
231
+ runtime = copy.copy(self)
232
+ runtime.__dict__ = self.__dict__
233
+ runtime.user = user
234
+ return runtime
235
+
218
236
  async def alive(self) -> bool:
219
237
  """Whether the box still executes anything. Not every runtime raises when
220
238
  the box is gone — some surface it as `exec`'s own non-zero result,
@@ -70,6 +70,7 @@ control.sendmsg([b"listener"], [(socket.SOL_SOCKET, socket.SCM_RIGHTS, array.arr
70
70
 
71
71
 
72
72
  class DockerRuntime(ContainerRuntime):
73
+ supports_user: ClassVar[bool] = True
73
74
  engine: ClassVar[str] = "docker"
74
75
  """The CLI binary for the shared OCI container operations."""
75
76
  info_cls: ClassVar[type[BaseRuntimeInfo]] = DockerRuntimeInfo
@@ -529,6 +530,7 @@ class DockerRuntime(ContainerRuntime):
529
530
  "exec",
530
531
  *(("-i",) if stdin else ()),
531
532
  *(arg for key, value in env.items() for arg in ("--env", f"{key}={value}")),
533
+ *(("--user", self.user) if self.user is not None else ()),
532
534
  "--workdir",
533
535
  self.config.workdir,
534
536
  self._container,
@@ -144,6 +144,7 @@ class PrimeProcess(RuntimeProcess):
144
144
 
145
145
 
146
146
  class PrimeRuntime(Runtime):
147
+ supports_user: ClassVar[bool] = True
147
148
  is_local: ClassVar[bool] = False
148
149
 
149
150
  def __init__(self, config: PrimeConfig, name: str | None = None) -> None:
@@ -292,6 +293,7 @@ class PrimeRuntime(Runtime):
292
293
  shlex.join(argv),
293
294
  working_dir=self.config.workdir,
294
295
  env=self.process_env(env),
296
+ user=self.user,
295
297
  )
296
298
  delay = 0.1
297
299
  output_retries = 0
@@ -335,6 +337,7 @@ class PrimeRuntime(Runtime):
335
337
  shlex.join(argv),
336
338
  working_dir=self.config.workdir,
337
339
  env=self.process_env(env),
340
+ user=self.user,
338
341
  )
339
342
  except Exception as e:
340
343
  raise SandboxError(f"prime live process failed to start: {e}") from e
@@ -356,6 +359,7 @@ class PrimeRuntime(Runtime):
356
359
  command,
357
360
  working_dir=self.config.workdir,
358
361
  env=self.process_env(env),
362
+ user=self.user,
359
363
  )
360
364
  except Exception as e:
361
365
  raise SandboxError(f"prime background launch failed: {e}") from e
@@ -370,6 +374,7 @@ class PrimeRuntime(Runtime):
370
374
  f"head -c {max_bytes} -- {shlex.quote(path)}",
371
375
  working_dir=self.config.workdir,
372
376
  env=self.process_env({}),
377
+ user=self.user,
373
378
  )
374
379
  async with contextlib.aclosing(process):
375
380
  with io.BytesIO() as data:
verifiers/v1/task.py CHANGED
@@ -99,6 +99,10 @@ class TaskData(BaseModel):
99
99
  """Optional Docker image to use for the task. Only relevant for tasks that run in a container."""
100
100
  workdir: str | None = None
101
101
  """Optional working directory to use for the task. Only relevant for tasks that run in a container."""
102
+ agent_user: str | None = None
103
+ """Existing user in the task's box that the harness, and so every command the agent
104
+ runs, executes as. None uses the runtime's default user (usually root). Task setup,
105
+ harness setup, artifact collection, and grading keep running as the default user."""
102
106
 
103
107
  skills: list[SkillSource] = Field(default_factory=list)
104
108
  """Skill sources installed before the harness's configured skills for this task."""
@@ -676,6 +676,7 @@ def parse_task(task_dir: Path, idx: int, harbor_config: HarborConfig) -> HarborD
676
676
  prompt=harbor_task.instruction.strip(),
677
677
  image=image,
678
678
  workdir=environment.workdir,
679
+ agent_user=None if parsed.agent.user is None else str(parsed.agent.user),
679
680
  network_allow=(
680
681
  ["*"]
681
682
  if network.network_mode == NetworkMode.PUBLIC
@@ -190,7 +190,17 @@ async def _tar_out(runtime: Runtime, artifact: Artifact, budget: int) -> bytes:
190
190
  )
191
191
  # The runtime enforces the remaining collection budget while transferring the
192
192
  # bytes, so replacing or growing the archive cannot race a separate size probe.
193
- return await runtime.read(path, max_bytes=budget)
193
+ archive, dropped = _drop_special_files(
194
+ await runtime.read(path, max_bytes=budget)
195
+ )
196
+ if dropped:
197
+ logger.warning(
198
+ "artifact %r: dropped %d FIFO or device node(s), e.g. %s",
199
+ artifact.source,
200
+ len(dropped),
201
+ dropped[:3],
202
+ )
203
+ return archive
194
204
  finally:
195
205
  # Best-effort: the box is about to be destroyed and the name is unique per call.
196
206
  try:
@@ -199,6 +209,32 @@ async def _tar_out(runtime: Runtime, artifact: Artifact, budget: int) -> bytes:
199
209
  logger.debug("failed to remove %s", path, exc_info=True)
200
210
 
201
211
 
212
+ def _drop_special_files(archive: bytes) -> tuple[bytes, list[str]]:
213
+ """Remove FIFOs and device nodes, plus hard links to them, from a collected archive.
214
+
215
+ They carry no content, and `restore` refuses them, so one an agent leaves in its
216
+ workspace (e.g. a named pipe it made to test streaming input) would otherwise fail
217
+ the whole restore. Returns the archive and the dropped member names."""
218
+ with tarfile.open(fileobj=io.BytesIO(archive), mode="r:") as source:
219
+ members = source.getmembers()
220
+ special = {m.name for m in members if m.isfifo() or m.ischr() or m.isblk()}
221
+ if not special:
222
+ return archive, []
223
+ dropped = [
224
+ m.name
225
+ for m in members
226
+ if m.name in special or (m.islnk() and m.linkname in special)
227
+ ]
228
+ out = io.BytesIO()
229
+ with tarfile.open(fileobj=out, mode="w:", format=source.format) as target:
230
+ for member in members:
231
+ if member.name in dropped:
232
+ continue
233
+ content = source.extractfile(member) if member.isfile() else None
234
+ target.addfile(member, content)
235
+ return out.getvalue(), dropped
236
+
237
+
202
238
  def _validate_restore(root: str, archive: bytes | None) -> None:
203
239
  """Reject archive content that could restore outside its declared root."""
204
240
  root_path = PurePosixPath(root)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: verifiers
3
- Version: 0.3.2.dev168
3
+ Version: 0.3.2.dev170
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
@@ -36,7 +36,7 @@ Requires-Dist: numpy>=2.1.0
36
36
  Requires-Dist: openai<3.0.0,>=2.54.0
37
37
  Requires-Dist: prime-pydantic-config[toml]>=0.4.3
38
38
  Requires-Dist: prime-runs>=0.1.3
39
- Requires-Dist: prime-sandboxes>=0.3.0
39
+ Requires-Dist: prime-sandboxes>=0.4.1
40
40
  Requires-Dist: prime-tunnel>=0.1.8
41
41
  Requires-Dist: pydantic>=2.12.3
42
42
  Requires-Dist: pyzmq>=27.1.0
@@ -7,11 +7,11 @@ verifiers/v1/errors.py,sha256=nNabzi55J5aHmAmW9UHZVpI7kcBfy4Xcyt4cQthZbM8,5875
7
7
  verifiers/v1/graph.py,sha256=LW0pJRxCCOXZa2KwB0edHIIqmRrzLFBDNyEeOFVskl8,41502
8
8
  verifiers/v1/harness.py,sha256=AcsCmNHB1UQ75nD7RUawsb65q63NdkcMykD0E6oRDOA,15957
9
9
  verifiers/v1/judge.py,sha256=uqn-djkK2_7Q3kOjkDo6ipOvV_HOvuLa9y6AoDjRux8,9618
10
- verifiers/v1/rollout.py,sha256=WiMCJyAypsR1OaXvzJQx7G-T67v__AnnDNoXotuI_VE,25444
10
+ verifiers/v1/rollout.py,sha256=sx9ilprIu-5zrYH9G4yBWZdqzDdb_ctwLJVItLdTo88,26960
11
11
  verifiers/v1/semantic.py,sha256=tX8FYEceuMYauuB3Bc8jLDMS2eR06JjLjTo66R190Fg,4053
12
12
  verifiers/v1/session.py,sha256=qVMgKmqkItemR3uYUDAFuDz7huP593kHQbge8FlRba8,28420
13
13
  verifiers/v1/state.py,sha256=EckF2bWp-vV4b1jYJ9sLI5xrfGuI5spIgYYwW926toI,595
14
- verifiers/v1/task.py,sha256=6-Tw0iBLsc4hinvVPl_b2EUX8unAAnxtHMXNSIHXsnU,12886
14
+ verifiers/v1/task.py,sha256=YN_P3912NYbK4VPTPHu6kyYCpeyoyhDVxBbhpjg8AA0,13185
15
15
  verifiers/v1/taskset.py,sha256=LE_3ogL7itU2Amk-N20cHkZ7Ev6MznJ0HpC8B1xb4w0,9482
16
16
  verifiers/v1/trace.py,sha256=Npc-TrBh-B3H0OvtfA5X31VAlOhfgLDKGdXohqC2uIk,31156
17
17
  verifiers/v1/types.py,sha256=-In5plgB3mQLqtRKHC3QDCPDvuruAqVd8WIjKrsGB_I,10578
@@ -142,14 +142,14 @@ verifiers/v1/mcp/server.py,sha256=Jqi8OMT8DnoZegXqM3UNNRzhVnbdrPEwx0DjOJo_K2A,13
142
142
  verifiers/v1/mcp/toolset.py,sha256=G2wzNdEMGLXoQUcrwF0TartJkqokNPI3o7YUOAaaa6U,1007
143
143
  verifiers/v1/runtimes/__init__.py,sha256=zOG9BYsK19VRNqvggqh3f6g1_UGIq7VjIBcK8kDlmnQ,3515
144
144
  verifiers/v1/runtimes/apptainer.py,sha256=Ru9QJna_l0goIzg71G-4RU_D-7wZUNz139S5TEvbzog,8211
145
- verifiers/v1/runtimes/base.py,sha256=GN2vTW3oSYpRcop_DRQYi7X31BG4NrzMrnkNiWSDp64,18160
145
+ verifiers/v1/runtimes/base.py,sha256=2ShKZypScklVl9r0ZgN9LjdZB1e471plTxLyHg7-Tyo,19035
146
146
  verifiers/v1/runtimes/container.py,sha256=_hNhGOo1Zvorj7THSj2UrZTkgewySUVJe4Kd777-QnI,12563
147
147
  verifiers/v1/runtimes/e2b.py,sha256=yBeocAutsv2l5x-C5IMqrlgBz6Cb-t6vWbX4qTjlztE,30306
148
148
  verifiers/v1/runtimes/limiters.py,sha256=rOfQJcMdYcLSKq8AL9O09BfGOLqpYmNgrCxXXNcHLmU,2849
149
149
  verifiers/v1/runtimes/modal.py,sha256=ZHA4vtNIZwTV-bnVUlambgc8l34j_ZulvtrwvvkE2Kg,17945
150
- verifiers/v1/runtimes/prime.py,sha256=PCakstDt-gdEUI0d5xM5dEFgn3yQie0RmVsXnUPkjNQ,19230
150
+ verifiers/v1/runtimes/prime.py,sha256=pkrHMWzBdmkNRBBelnu_fV5LO2kdEQmVlm0G2p7uZEk,19403
151
151
  verifiers/v1/runtimes/subprocess.py,sha256=vnGVAI3J7pYmRei4MQ7MN4nb8SglLi70NtVTINMPVvQ,8855
152
- verifiers/v1/runtimes/docker/__init__.py,sha256=1QJF08StAXanR9XdJfLfjN4Hzy8K-Ge9_prVOv_RT0A,21716
152
+ verifiers/v1/runtimes/docker/__init__.py,sha256=QoFOTMgRjoWD--O25Jsft9eD2vjcrMpJQyhfQXqWraI,21828
153
153
  verifiers/v1/runtimes/docker/egress.py,sha256=wtKWTL2_zFidxMR7lDRmfevQlOsl2Iyk88ebdO4Kqqw,24566
154
154
  verifiers/v1/serve/__init__.py,sha256=wOKuwfugzmDT4yIniwzCXlf4G6tv6Cg8Kc--WMa38Bs,553
155
155
  verifiers/v1/serve/client.py,sha256=E2y2x4qqYveWRWC9VZYQEu19aMEryPWTdXnYzbq5uTA,9829
@@ -162,7 +162,7 @@ verifiers/v1/tasksets/__init__.py,sha256=2ijE3qIlLoUA02g6iysOwWQylyBBQL1GhSpjv8_
162
162
  verifiers/v1/tasksets/harbor/__init__.py,sha256=JxThzHFYEkuSfQbVf3pEnjordgdfe4MU-QOsSSbyovU,326
163
163
  verifiers/v1/tasksets/harbor/compose.py,sha256=Ab-vpMPFXy7Y7C_82-IRznrlaN3FLEuFDz8w3LDsE-I,14094
164
164
  verifiers/v1/tasksets/harbor/env.py,sha256=-U-CeyeuWysXCNHi3cew0XtakUa7TqJVnVzElKtG41w,6356
165
- verifiers/v1/tasksets/harbor/taskset.py,sha256=IaGeCoH012wcnPmCauk4kgGRdXsDt1qpMnOBKuUkV3Q,36421
165
+ verifiers/v1/tasksets/harbor/taskset.py,sha256=ggz26hWv9XFZAfkzp72vsrQjaXKShO4AOCzrSg2K4ds,36503
166
166
  verifiers/v1/tasksets/harbor/toolset.py,sha256=_7HXu-uuvqzhmJ1EX6c3KWLg0g0eRrLjCq1kBsZhIwo,3159
167
167
  verifiers/v1/tasksets/nemo_gym/__init__.py,sha256=654gAvG3Wd_Z_nh6mypeKn4s_IIQ0Ge6vcZvxTHC2_E,306
168
168
  verifiers/v1/tasksets/nemo_gym/response.py,sha256=EhrsquEP8g0bjzAThGfE6Ypgy86_nVX235NiFYtVDVM,4463
@@ -175,7 +175,7 @@ verifiers/v1/tasksets/textarena/__init__.py,sha256=Os2OlBY_pSH0B1DP32RitumgSLpq2
175
175
  verifiers/v1/tasksets/textarena/taskset.py,sha256=zGslerxmH2ZT7FJdP-72R6a9p71kr2pNS3r5_xnq1lQ,4406
176
176
  verifiers/v1/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
177
177
  verifiers/v1/utils/aio.py,sha256=0yNFOqB2oO6Qktc6NI_ZSAGuo1aHsIK_wuT1BtmoZ3I,1532
178
- verifiers/v1/utils/artifacts.py,sha256=3QL2ELwqXuek9OLwF4hJdzZb_hwfVjjenw8VjepdLlo,12855
178
+ verifiers/v1/utils/artifacts.py,sha256=1FHD0BQPB2NOSBphOGAODWbFQQlPKLmUIBPhFnfBtP8,14354
179
179
  verifiers/v1/utils/compile.py,sha256=5pxy31wx2Q-3pQjOO5B9SH6OT-wwb8r4F1LAl6HMeQA,5835
180
180
  verifiers/v1/utils/decorators.py,sha256=vhoN6YoKb15-Fkqmc9RaNlUIbt9WRvNK4zAn2bAxfS4,5367
181
181
  verifiers/v1/utils/format.py,sha256=yX160sGewMMZn9iJqTHRbYYt4Z9pHasjMlFT1dXrKD8,2446
@@ -194,8 +194,8 @@ verifiers/v1/utils/scope.py,sha256=bzUEiWDOVdbj7dXOwGOlzsQ_-Xu0j9NR5TalQB-8TS8,8
194
194
  verifiers/v1/utils/score.py,sha256=493yJVMw8teCu9JxapxMFPFzI0hNUqdo0Y2nGW4kckk,6200
195
195
  verifiers/v1/utils/trace_store.py,sha256=RsCDonGR-bs-tvEstcppiJm3jFmAdIiG83fA5KtCbAw,2801
196
196
  verifiers/v1/utils/version.py,sha256=-obEo_-l9-D8FLef4hYxncOe-uJpxrM1g2Hig_37Sgs,1607
197
- verifiers-0.3.2.dev168.dist-info/METADATA,sha256=gb78e_KMSGlVaIhDBXpMS56UnLRgcLe5rtTP4aah5Do,4227
198
- verifiers-0.3.2.dev168.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
199
- verifiers-0.3.2.dev168.dist-info/entry_points.txt,sha256=iugElcdWPKbQM7uFF0lZ8iUpHsNr17-BwEAAjJWxV3U,259
200
- verifiers-0.3.2.dev168.dist-info/licenses/LICENSE,sha256=v0RrUsdV3IDoZhrRce297IXS3xMHNJ-_LdLpFAUWb9k,1072
201
- verifiers-0.3.2.dev168.dist-info/RECORD,,
197
+ verifiers-0.3.2.dev170.dist-info/METADATA,sha256=g1Gd_EgCwA-0xc4QtXPfFLMaFdLZ8ge7KxRGidGFbGI,4227
198
+ verifiers-0.3.2.dev170.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
199
+ verifiers-0.3.2.dev170.dist-info/entry_points.txt,sha256=iugElcdWPKbQM7uFF0lZ8iUpHsNr17-BwEAAjJWxV3U,259
200
+ verifiers-0.3.2.dev170.dist-info/licenses/LICENSE,sha256=v0RrUsdV3IDoZhrRce297IXS3xMHNJ-_LdLpFAUWb9k,1072
201
+ verifiers-0.3.2.dev170.dist-info/RECORD,,