verifiers 0.3.2.dev156__py3-none-any.whl → 0.3.2.dev158__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/__init__.py CHANGED
@@ -15,7 +15,6 @@ from verifiers.v1.clients import (
15
15
  )
16
16
  from verifiers.v1.configs.agent import AgentConfig
17
17
  from verifiers.v1.configs.cli.env import (
18
- merge_env_defaults,
19
18
  narrowed_env_annotation,
20
19
  resolve_env_field,
21
20
  )
@@ -153,6 +152,7 @@ from verifiers.v1.utils.decorators import (
153
152
  stop,
154
153
  tool,
155
154
  )
155
+ from verifiers.v1.utils.generic import merge_defaults
156
156
  from verifiers.v1.utils.git import (
157
157
  PATCH_CAP_BYTES as PATCH_CAP_BYTES,
158
158
  )
@@ -321,7 +321,7 @@ __all__ = [ # noqa: RUF022 - grouped by public API area
321
321
  "SharedEnvConfig",
322
322
  "ServeConfig",
323
323
  "resolve_env_field",
324
- "merge_env_defaults",
324
+ "merge_defaults",
325
325
  "narrowed_env_annotation",
326
326
  "SingleAgentEnvConfig",
327
327
  "AgentConfig",
@@ -1,5 +1,6 @@
1
1
  """Run-config plumbing around the `[env]` block: narrowing the `env` field of
2
- every config that owns one, and layering shared env defaults under it.
2
+ every config that owns one. `merge_defaults` (`utils/generic.py`) layers shared env
3
+ defaults under it.
3
4
 
4
5
  A run composes the blocks it needs — `[env]` (what runs, `configs/env.py`),
5
6
  `[serve]` (how it's hosted, `configs/serve.py`) — plus its own fields. Nothing here is a base class: the eval
@@ -10,11 +11,11 @@ single_agent_env_config)`. The `SerializeAsAny` is load-bearing: pydantic serial
10
11
  by declared type, so a plain `EnvConfig` silently drops a narrowed subclass's agents
11
12
  and knobs from `model_dump()` — the env-server wire's payload."""
12
13
 
13
- from pydantic import BaseModel, ValidationError
14
+ from pydantic import ValidationError
14
15
  from pydantic_config import BaseConfig
15
16
 
16
- from verifiers.v1.configs.env import EnvConfig, SharedEnvConfig
17
- from verifiers.v1.utils.generic import deep_merge, prefix_validation_error
17
+ from verifiers.v1.configs.env import EnvConfig
18
+ from verifiers.v1.utils.generic import prefix_validation_error
18
19
 
19
20
 
20
21
  def resolve_env_field(data: dict, narrowed: "type[EnvConfig] | None" = None) -> dict:
@@ -45,32 +46,6 @@ def resolve_env_field(data: dict, narrowed: "type[EnvConfig] | None" = None) ->
45
46
  return data
46
47
 
47
48
 
48
- def merge_env_defaults(defaults: SharedEnvConfig, env: dict | None) -> dict:
49
- """A raw `env` block over `defaults`, the knobs that several envs of one run share
50
- (e.g. the retries of every eval source). Only the fields set in `defaults` apply,
51
- and the env's own values win. A subtree whose `id`/`type` differs from the
52
- default's is the env's alone, so one plugin's knobs never leak into another's."""
53
- return deep_merge(_dump_set(defaults), env or {})
54
-
55
-
56
- def _dump_set(config: BaseModel) -> dict:
57
- """`config`'s set fields, including those set on a sub-config after construction
58
- (which leaves the parent's field unmarked). Each dumped sub-config keeps its
59
- `id`/`type` even at the default: `deep_merge` detects a plugin switch only when
60
- both sides name it."""
61
- dump = {}
62
- for name in type(config).model_fields:
63
- value = getattr(config, name)
64
- if isinstance(value, BaseModel):
65
- nested = _dump_set(value)
66
- if nested or name in config.model_fields_set:
67
- keys = [k for k in ("id", "type") if k in type(value).model_fields]
68
- dump[name] = {**{k: getattr(value, k) for k in keys}, **nested}
69
- elif name in config.model_fields_set:
70
- dump[name] = config.model_dump(include={name})[name]
71
- return dump
72
-
73
-
74
49
  def narrowed_env_annotation(cls) -> "type[EnvConfig] | None":
75
50
  """The env field's annotation when the CLI pre-narrowed it (`narrow_config`
76
51
  swaps in a concrete subclass). The base declaration reads as `EnvConfig` itself
@@ -141,7 +141,8 @@ async def restore(runtime: Runtime, collected: dict[str, bytes | None]) -> None:
141
141
 
142
142
  Archive bytes are untrusted: the agent controls both their source files and the
143
143
  runtime tooling that creates them. Every archive is therefore validated on the host
144
- before the grading runtime is changed. Only regular files and directories travel.
144
+ before the grading runtime is changed. Regular files, directories, and confined links
145
+ can travel; link chains and extraction through symlinks cannot.
145
146
  """
146
147
  if not collected:
147
148
  return
@@ -214,6 +215,9 @@ def _validate_restore(root: str, archive: bytes | None) -> None:
214
215
 
215
216
  try:
216
217
  with tarfile.open(fileobj=io.BytesIO(archive), mode="r:") as tar:
218
+ regular_files: set[PurePosixPath] = set()
219
+ members: set[PurePosixPath] = set()
220
+ symlinks: dict[PurePosixPath, str] = {}
217
221
  for member in tar:
218
222
  member_path = PurePosixPath(member.name)
219
223
  destination = PurePosixPath("/") / member_path
@@ -227,10 +231,65 @@ def _validate_restore(root: str, archive: bytes | None) -> None:
227
231
  f"artifact member {member.name!r} is outside declared root "
228
232
  f"{root!r}"
229
233
  )
230
- if not (member.isfile() or member.isdir()):
234
+ if member_path in members:
235
+ raise RuntimeError(f"duplicate artifact member {member.name!r}")
236
+ members.add(member_path)
237
+ if member.issym():
238
+ symlinks[member_path] = member.linkname
239
+ elif member.islnk():
240
+ # Tar deduplicates hard-linked build outputs (e.g. Cargo's build
241
+ # scripts). A target must already be a regular member of this
242
+ # archive, never a path supplied by the verifier image.
243
+ target = PurePosixPath(member.linkname)
244
+ if (
245
+ not member.linkname
246
+ or target.is_absolute()
247
+ or ".." in target.parts
248
+ or target not in regular_files
249
+ or target == member_path
250
+ ):
251
+ raise RuntimeError(
252
+ f"artifact hard link {member.name!r} has unsafe target "
253
+ f"{member.linkname!r}"
254
+ )
255
+ elif not (member.isfile() or member.isdir()):
231
256
  raise RuntimeError(
232
257
  f"artifact member {member.name!r} is a link or special file"
233
258
  )
259
+ if member.isfile():
260
+ regular_files.add(member_path)
261
+ # Check after reading the whole archive, so a later symlink cannot
262
+ # turn an earlier member's parent or a link target into a redirection.
263
+ for member_path in members:
264
+ if any(parent in symlinks for parent in member_path.parents):
265
+ raise RuntimeError(
266
+ f"artifact member {str(member_path)!r} traverses a symlink"
267
+ )
268
+ relative_root = root_path.relative_to("/")
269
+ for member_path, linkname in symlinks.items():
270
+ target = PurePosixPath(linkname)
271
+ if not linkname or target.is_absolute():
272
+ raise RuntimeError(
273
+ f"artifact symlink {str(member_path)!r} has unsafe target "
274
+ f"{linkname!r}"
275
+ )
276
+ resolved = member_path.parent
277
+ if not resolved.is_relative_to(relative_root):
278
+ raise RuntimeError(
279
+ f"artifact symlink {str(member_path)!r} has unsafe target "
280
+ f"{linkname!r}"
281
+ )
282
+ # Walk before normalizing '..': a/../b is unsafe if a is itself
283
+ # a symlink. Missing internal targets are safe (e.g. build outputs).
284
+ for part in target.parts:
285
+ resolved = resolved.parent if part == ".." else resolved / part
286
+ if not resolved.is_relative_to(relative_root) or any(
287
+ path in symlinks for path in (resolved, *resolved.parents)
288
+ ):
289
+ raise RuntimeError(
290
+ f"artifact symlink {str(member_path)!r} has unsafe target "
291
+ f"{linkname!r}"
292
+ )
234
293
  except tarfile.TarError as exc:
235
294
  raise RuntimeError(f"unreadable artifact archive for {root!r}: {exc}") from exc
236
295
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  from typing import TypeVar, get_args, get_origin
4
4
 
5
- from pydantic import ValidationError
5
+ from pydantic import BaseModel, ValidationError
6
6
 
7
7
  T = TypeVar("T")
8
8
 
@@ -39,6 +39,58 @@ def deep_merge(base: dict, override: dict) -> dict:
39
39
  return merged
40
40
 
41
41
 
42
+ def merge_defaults(defaults: BaseModel, raw: dict | None) -> dict:
43
+ """Fill in a raw config block with the values that `defaults` sets, e.g. a
44
+ group's shared knobs under each member's own block.
45
+
46
+ - Only fields that were set on `defaults` are filled in; its plain defaults are not.
47
+ - Nested blocks are filled in key by key; a value already in `raw` is kept.
48
+ - If `raw` names a different `id`/`type` for a block (at any depth), that block is
49
+ taken from `raw` alone, so one plugin's fields never leak into another's.
50
+
51
+ For example, with `defaults` set to `algo = {type = "grpo", kl = 0.1}`:
52
+ `{}` becomes `{type = "grpo", kl = 0.1}`, `{kl = 0.5}` becomes
53
+ `{type = "grpo", kl = 0.5}`, and `{type = "max_rl"}` stays `{type = "max_rl"}`.
54
+ """
55
+ # `deep_merge` detects an `id`/`type` switch only on nested blocks, so wrap the
56
+ # block once to detect a switch of the block itself too.
57
+ identity = {
58
+ k: getattr(defaults, k)
59
+ for k in ("id", "type")
60
+ if k in type(defaults).model_fields
61
+ }
62
+ block = {**identity, **_dump_set(defaults)}
63
+ return deep_merge({"block": block}, {"block": raw or {}})["block"]
64
+
65
+
66
+ def _dump_set(config: BaseModel) -> dict:
67
+ """The fields set on `config`, including extra fields, as a nested dict. A
68
+ sub-config the parent did not set still counts when its own fields changed after
69
+ construction (pydantic then leaves the parent's field unmarked), but not when it
70
+ only matches the field's default, which may itself be built with arguments. A
71
+ dumped sub-config always includes its `id`/`type`, because `deep_merge` detects a
72
+ switch only when both sides name it."""
73
+ dump = {}
74
+ for name, field in type(config).model_fields.items():
75
+ value = getattr(config, name)
76
+ if isinstance(value, BaseModel):
77
+ nested = _dump_set(value)
78
+ if name not in config.model_fields_set:
79
+ default = field.get_default(
80
+ call_default_factory=True, validated_data=config.__dict__
81
+ )
82
+ if isinstance(default, BaseModel) and nested == _dump_set(default):
83
+ continue
84
+ if nested or name in config.model_fields_set:
85
+ keys = [k for k in ("id", "type") if k in type(value).model_fields]
86
+ dump[name] = {**{k: getattr(value, k) for k in keys}, **nested}
87
+ elif name in config.model_fields_set:
88
+ dump[name] = config.model_dump(include={name})[name]
89
+ # Extra fields of an `extra="allow"` model exist only because they were set.
90
+ dump.update(config.model_dump(include=set(config.model_extra or {})))
91
+ return dump
92
+
93
+
42
94
  def concrete_type(
43
95
  cls: type, bound: type[T], *, origin: type | None = None
44
96
  ) -> type[T] | None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: verifiers
3
- Version: 0.3.2.dev156
3
+ Version: 0.3.2.dev158
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
@@ -1,5 +1,5 @@
1
1
  verifiers/__init__.py,sha256=F5IVMw6mhGaPBMCw7QKk0la_r5mMwydP2hdTTdQucik,352
2
- verifiers/v1/__init__.py,sha256=6t2FsosAjmyfJz6qS8tDQ2gWCvQfyRzqk1mYwv5_-EQ,8738
2
+ verifiers/v1/__init__.py,sha256=_8pSA4XU7zR4V-8sRpnZjh-YkfAYx1a0pikJtoFKzIk,8764
3
3
  verifiers/v1/agent.py,sha256=yBCCBe5tVTeQ8OZDlVSZLw_IDvCmEQSnF3wQ1BZcT14,32342
4
4
  verifiers/v1/env.py,sha256=GqrVxxrvv-9WitXhD8Rre0tKQgnQWeD8mTM2gDHMVBs,18040
5
5
  verifiers/v1/episode.py,sha256=UPtA02aGWA-fY8cJKEuYCMtQVnPN1C0BSN13LggNT8U,5986
@@ -55,7 +55,7 @@ verifiers/v1/configs/task.py,sha256=57OnEGAa6jTiG12Lx_elREU02koYh8UkM0t0Vvxahys,
55
55
  verifiers/v1/configs/taskset.py,sha256=LLbKdV2LW0ZjB9-fsKErIVKjchOAPcDLWGLVjgXKG_0,935
56
56
  verifiers/v1/configs/cli/__init__.py,sha256=YMDUPdbRlDJ77dhj3QqzmyRhqNgUvuQdpj_bdBqzZPM,78
57
57
  verifiers/v1/configs/cli/debug.py,sha256=CcUZ9MEcv0iAL9w2BkTfnZg20yWJ-Vn8P30-e5tuCic,2741
58
- verifiers/v1/configs/cli/env.py,sha256=FcapEAtgeHzRuVaqk8BU36ZxmMOfdJ1XlioPbdwwspo,3990
58
+ verifiers/v1/configs/cli/env.py,sha256=qg2LNOasAU0yg3b0xqXpx3cIj4nHbHSlK3Ldc3NhiT4,2667
59
59
  verifiers/v1/configs/cli/eval.py,sha256=9Zw7mAUipMhoIcwRa_lgo5gII4rMmMEBAgzcPxPWPjw,6895
60
60
  verifiers/v1/configs/cli/init.py,sha256=JvIUOdhfZfNOXg2wm2i06jLLHpb97tNMzZqmcK_9wVw,916
61
61
  verifiers/v1/configs/cli/replay.py,sha256=vUCNs-9j1CXcAZhGozN1EaztcXK_dYVK3iN_fRmbSXw,2929
@@ -174,11 +174,11 @@ verifiers/v1/tasksets/textarena/__init__.py,sha256=Os2OlBY_pSH0B1DP32RitumgSLpq2
174
174
  verifiers/v1/tasksets/textarena/taskset.py,sha256=zGslerxmH2ZT7FJdP-72R6a9p71kr2pNS3r5_xnq1lQ,4406
175
175
  verifiers/v1/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
176
176
  verifiers/v1/utils/aio.py,sha256=0yNFOqB2oO6Qktc6NI_ZSAGuo1aHsIK_wuT1BtmoZ3I,1532
177
- verifiers/v1/utils/artifacts.py,sha256=i9DRhxViMAf-aBPjs3_9Q8RWVRC5pD-_v97VbNPPNcY,9638
177
+ verifiers/v1/utils/artifacts.py,sha256=3QL2ELwqXuek9OLwF4hJdzZb_hwfVjjenw8VjepdLlo,12855
178
178
  verifiers/v1/utils/compile.py,sha256=5pxy31wx2Q-3pQjOO5B9SH6OT-wwb8r4F1LAl6HMeQA,5835
179
179
  verifiers/v1/utils/decorators.py,sha256=vhoN6YoKb15-Fkqmc9RaNlUIbt9WRvNK4zAn2bAxfS4,5367
180
180
  verifiers/v1/utils/format.py,sha256=yX160sGewMMZn9iJqTHRbYYt4Z9pHasjMlFT1dXrKD8,2446
181
- verifiers/v1/utils/generic.py,sha256=2VIN9RapMO9pVkFHB1UKUHck1vlTJ1pjfad2NzxuuXA,2055
181
+ verifiers/v1/utils/generic.py,sha256=Iy_mgApIEBqm0DPpjBDLLkDFN0_NJ-UQmY9bZpdv5cM,4742
182
182
  verifiers/v1/utils/git.py,sha256=MTRinOvG4EddfjdahHkFZUihHmhJ37n5ROtgjzjE1NA,8366
183
183
  verifiers/v1/utils/image.py,sha256=OFw_wdwVtbdwa8uJ3X3vYfhpUAi0CTH5HMBupjVsRRQ,282
184
184
  verifiers/v1/utils/interrupt.py,sha256=F-KKhc5ndPJJfhd3SuMqyqhXhA32FhCRy5KWFJpEoM4,1179
@@ -193,8 +193,8 @@ verifiers/v1/utils/scope.py,sha256=bzUEiWDOVdbj7dXOwGOlzsQ_-Xu0j9NR5TalQB-8TS8,8
193
193
  verifiers/v1/utils/score.py,sha256=493yJVMw8teCu9JxapxMFPFzI0hNUqdo0Y2nGW4kckk,6200
194
194
  verifiers/v1/utils/trace_store.py,sha256=RsCDonGR-bs-tvEstcppiJm3jFmAdIiG83fA5KtCbAw,2801
195
195
  verifiers/v1/utils/version.py,sha256=-obEo_-l9-D8FLef4hYxncOe-uJpxrM1g2Hig_37Sgs,1607
196
- verifiers-0.3.2.dev156.dist-info/METADATA,sha256=siLTRPsZNEdrKeTcWP3JzhmhLsMG3Qmb7PgN4Cv-J34,4161
197
- verifiers-0.3.2.dev156.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
198
- verifiers-0.3.2.dev156.dist-info/entry_points.txt,sha256=iugElcdWPKbQM7uFF0lZ8iUpHsNr17-BwEAAjJWxV3U,259
199
- verifiers-0.3.2.dev156.dist-info/licenses/LICENSE,sha256=v0RrUsdV3IDoZhrRce297IXS3xMHNJ-_LdLpFAUWb9k,1072
200
- verifiers-0.3.2.dev156.dist-info/RECORD,,
196
+ verifiers-0.3.2.dev158.dist-info/METADATA,sha256=v6hCapMsuYV66Fvoz8QpYw8ZjlIG-AAXPIEoef8oSDM,4161
197
+ verifiers-0.3.2.dev158.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
198
+ verifiers-0.3.2.dev158.dist-info/entry_points.txt,sha256=iugElcdWPKbQM7uFF0lZ8iUpHsNr17-BwEAAjJWxV3U,259
199
+ verifiers-0.3.2.dev158.dist-info/licenses/LICENSE,sha256=v0RrUsdV3IDoZhrRce297IXS3xMHNJ-_LdLpFAUWb9k,1072
200
+ verifiers-0.3.2.dev158.dist-info/RECORD,,