falsepass 0.3.1__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.
falsepass/cli.py ADDED
@@ -0,0 +1,381 @@
1
+ """`falsepass audit <env>` — run the probe kit against one audited environment.
2
+
3
+ Hub environments are built for different, mutually incompatible verifiers
4
+ versions, so each audit runs in its own throw-away virtualenv. The pins live in
5
+ `audits/<env>/env.toml`; this command turns them into one `uv run` invocation.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import json
12
+ import shlex
13
+ import subprocess
14
+ import sys
15
+ import tomllib
16
+ from pathlib import Path
17
+
18
+
19
+ def install_args(env: dict, source: str | Path | None = None) -> list[str]:
20
+ """The `uv run --with` arguments that install verifiers and the environment.
21
+
22
+ `env.toml` may name local trees instead of releases: `source` for the environment
23
+ package (a folder with a pyproject.toml) and `verifiers_source` for verifiers itself.
24
+ `source` given here (a patched copy, an upstream checkout) overrides the file's."""
25
+ source = source or env.get("source")
26
+ if env.get("framework") == "hud":
27
+ args = ["--with", f"hud=={env['hud']}" if env.get("hud") else "hud"]
28
+ if source:
29
+ args += ["--with", str(source)]
30
+ elif env.get("framework") == "inspect":
31
+ args = ["--with", f"inspect-ai=={env['inspect_ai']}" if env.get("inspect_ai") else "inspect-ai"]
32
+ if source:
33
+ args += ["--with", str(source)]
34
+ elif env.get("package"):
35
+ args += ["--with", f"{env['package']}=={env['version']}" if env.get("version") else env["package"]]
36
+ else:
37
+ args = ["--with", str(env["verifiers_source"]) if env.get("verifiers_source") else f"verifiers=={env['verifiers']}"]
38
+ args += ["--with", str(source) if source else f"{env['package']}=={env['version']}"]
39
+ for requirement in env.get("extra", []):
40
+ args += ["--with", requirement]
41
+ if env.get("index"):
42
+ args += ["--extra-index-url", env["index"]]
43
+ return args
44
+
45
+
46
+ def self_install_args() -> list[str]:
47
+ """How the throw-away virtualenv gets this same copy of falsepass: the checkout it runs
48
+ from, else the URL it was installed from (a git commit, a local wheel), else the
49
+ release with this version number."""
50
+ root = Path(__file__).resolve().parent.parent
51
+ if (root / "pyproject.toml").is_file() and (root / "falsepass" / "__init__.py").is_file():
52
+ return ["--with-editable", str(root)]
53
+ from importlib import metadata
54
+
55
+ try:
56
+ dist = metadata.distribution("falsepass")
57
+ except metadata.PackageNotFoundError:
58
+ return ["--with", "falsepass"]
59
+ direct = json.loads(dist.read_text("direct_url.json") or "{}")
60
+ if direct.get("url"):
61
+ vcs = direct.get("vcs_info")
62
+ url = f"{vcs['vcs']}+{direct['url']}@{vcs['commit_id']}" if vcs else direct["url"]
63
+ return ["--with", f"falsepass @ {url}"]
64
+ return ["--with", f"falsepass=={dist.version}"]
65
+
66
+
67
+ def uv_command(audit_dir: Path, extra: list[str], module: str = "falsepass.runner", source: str | Path | None = None) -> list[str]:
68
+ env = tomllib.loads((audit_dir / "env.toml").read_text())["env"]
69
+ command = [
70
+ "uv", "run", "--isolated", "--no-project", "--python", env.get("python", "3.12"),
71
+ *self_install_args(), "--with", "rewardlint", *install_args(env, source),
72
+ ] # fmt: skip
73
+ package = ["--package", env["package"]] if env.get("package") else []
74
+ return [*command, "python", "-m", module, str(audit_dir), *package, *extra]
75
+
76
+
77
+ ADAPTER_TEMPLATE = '''"""falsepass adapter for {id}."""
78
+
79
+ import random
80
+
81
+ from falsepass import Adapter, Example
82
+ from falsepass.scorers import V0RubricScorer # or V1TaskScorer / DevTasksetScorer
83
+
84
+
85
+ class Env(Adapter):
86
+ name = "{id}"
87
+
88
+ def load(self, n, seed):
89
+ import verifiers as vf
90
+
91
+ self.env = vf.load_environment("{package}")
92
+ self.rows = self.env.get_dataset()
93
+ picks = random.Random(seed).sample(range(len(self.rows)), n)
94
+ return V0RubricScorer(self.env), [self._example(i) for i in picks]
95
+
96
+ def _example(self, i) -> Example:
97
+ row = self.rows[i]
98
+ return Example(f"row-{{i}}", row["prompt"][-1]["content"], row["answer"], row)
99
+
100
+ def hygiene_examples(self, limit):
101
+ return [self._example(i) for i in range(min(limit, len(self.rows)))]
102
+
103
+ def render(self, answer, example):
104
+ """Write `answer` exactly the way the environment's prompt asks for it."""
105
+ raise NotImplementedError
106
+
107
+ def wrong_answer(self, example):
108
+ """Return an answer that is definitely wrong for this example."""
109
+ raise NotImplementedError
110
+
111
+
112
+ ADAPTER = Env()
113
+ '''
114
+
115
+
116
+ def scaffold(args) -> int:
117
+ from falsepass import hub
118
+
119
+ from falsepass import hud_support, inspect_support
120
+
121
+ local = Path(args.env_id)
122
+ if local.exists() and (args.framework == "hud" or (args.framework is None and hud_support.uses_hud(local))):
123
+ info = hud_support.describe(local, args.answers)
124
+ elif local.is_file() and local.suffix == ".py" and inspect_support.uses_inspect(local):
125
+ info = inspect_support.describe_file(local, args.task)
126
+ elif (local / "pyproject.toml").is_file() and (args.framework == "inspect" or inspect_support.uses_inspect(local)):
127
+ info = inspect_support.describe_folder(local, args.task)
128
+ elif (local / "pyproject.toml").is_file():
129
+ info = hub.describe_local(local)
130
+ elif not local.exists() and (args.framework == "inspect" or args.env_id.startswith("inspect_evals/")):
131
+ info = inspect_support.describe_registry(args.env_id)
132
+ elif local.exists() or args.env_id.count("/") != 1:
133
+ print(f"falsepass: {args.env_id} is neither a Hub slug (owner/environment), an Inspect task (a .py file, or "
134
+ "a registry name with --framework inspect) nor a folder with a pyproject.toml", file=sys.stderr)
135
+ return 1
136
+ else:
137
+ info = hub.describe(args.env_id, args.version)
138
+ if args.verifiers:
139
+ info["verifiers"] = args.verifiers
140
+ if args.extra:
141
+ info["extra"] = [*info.get("extra", []), *args.extra]
142
+ name = args.name or info["id"].split("/")[-1].lower().replace("_", "-")
143
+ audit_dir = Path("audits") / name
144
+ if audit_dir.exists():
145
+ print(f"falsepass: {audit_dir} already exists", file=sys.stderr)
146
+ return 1
147
+ audit_dir.mkdir(parents=True)
148
+ keys = ("id", "framework", "task", "package", "version", "module", "index", "verifiers", "inspect_ai", "hud", "source", "answers", "extra")
149
+ required = ("index",) if info.get("framework", "verifiers") == "verifiers" else ()
150
+ lines = [f"{key} = {json.dumps(info[key])}" for key in keys if info.get(key) or key in required]
151
+ declared = f"# declared by the package: {info['declared']}\n" if info.get("declared") else ""
152
+ (audit_dir / "env.toml").write_text("[env]\n" + "\n".join(lines) + "\n" + declared)
153
+ for source, content in info.get("sources", {}).items(): # the wheel's source, for the static pre-scan
154
+ target = audit_dir / "src" / source
155
+ if target.resolve().is_relative_to((audit_dir / "src").resolve()):
156
+ target.parent.mkdir(parents=True, exist_ok=True)
157
+ target.write_bytes(content)
158
+ if args.manual:
159
+ (audit_dir / "adapter.py").write_text(ADAPTER_TEMPLATE.format(id=info["id"], package=info.get("module") or info.get("task", "")))
160
+ print(f"created {audit_dir}/env.toml and adapter.py — fill in render() and wrong_answer(), then:")
161
+ elif info.get("framework") == "hud":
162
+ how = "with the reference answers given" if info.get("answers") else "answer-free probes only; add --answers for all"
163
+ print(f"created {audit_dir}/env.toml — each reply is graded by its task template ({how}):")
164
+ elif info.get("framework") == "inspect":
165
+ print(f"created {audit_dir}/env.toml — each reply goes through the task's own solver chain and scorers:")
166
+ else:
167
+ print(f"created {audit_dir}/env.toml — no adapter needed for single-turn environments with reference answers:")
168
+ if info.get("source"):
169
+ print(f" (installed from {info['source']} on every run, so edits are picked up)")
170
+ if info.get("others"):
171
+ print(f" (task {info['task']}; the others can be audited with --task and --name: {', '.join(info['others'])})")
172
+ print(f" falsepass audit {name}")
173
+ return 0
174
+
175
+
176
+ def demo_source() -> Path:
177
+ """The toy environment: bundled in the wheel as `falsepass/_demo`, in a checkout under examples/."""
178
+ packaged = Path(__file__).resolve().parent / "_demo"
179
+ return packaged if packaged.is_dir() else Path(__file__).resolve().parent.parent / "examples" / "toy-env"
180
+
181
+
182
+ def demo(target: Path) -> int:
183
+ import shutil
184
+
185
+ if not target.exists():
186
+ shutil.copytree(demo_source(), target, ignore=shutil.ignore_patterns("__pycache__", "*.egg-info"))
187
+ audit_dir = Path("audits") / "falsepass-toy-env"
188
+ if not (audit_dir / "env.toml").is_file():
189
+ code = main(["init", str(target)])
190
+ if code:
191
+ return code
192
+ print(f"auditing the toy environment in {target} (its grader pays `answer in reply`: expect several probes to fail, all from that one line)", flush=True)
193
+ return subprocess.call(uv_command(audit_dir, ["-n", "10"]))
194
+
195
+
196
+ def main(argv: list[str] | None = None) -> int:
197
+ from falsepass import __version__
198
+
199
+ parser = argparse.ArgumentParser(
200
+ prog="falsepass", description="Probe RL environment verifiers for false passes and false fails.",
201
+ epilog="Results are leads to reproduce and read, not verdicts. Docs: https://github.com/Super-Eric/falsepass",
202
+ ) # fmt: skip
203
+ parser.add_argument("--version", action="version", version=f"falsepass {__version__}")
204
+ sub = parser.add_subparsers(dest="command", required=True)
205
+ audit = sub.add_parser("audit", help="probe one environment in an isolated virtualenv")
206
+ audit.add_argument("env", help="name of a folder under audits/, or a path to one")
207
+ audit.add_argument("--print-command", action="store_true", help="show the uv command and exit")
208
+ audit.add_argument("--source", type=Path, help="install the environment from this local folder instead of its release")
209
+ roll = sub.add_parser("rollout", help="score a local model's own replies (cross-check; Ollama by default)")
210
+ roll.add_argument("env")
211
+ roll.add_argument("--print-command", action="store_true", help="show the uv command and exit")
212
+ roll.add_argument("--source", type=Path, help="install the environment from this local folder instead of its release")
213
+ prop = sub.add_parser("propose", help="let a local model write wrong answers and climb on the reward (single-turn)")
214
+ prop.add_argument("env")
215
+ prop.add_argument("--print-command", action="store_true", help="show the uv command and exit")
216
+ prop.add_argument("--source", type=Path, help="install the environment from this local folder instead of its release")
217
+ init = sub.add_parser("init", help="set up audits/<name>/ for a Hub environment or a local environment folder")
218
+ init.add_argument("env_id", help="Hub slug (owner/environment), a local folder with a pyproject.toml, an Inspect task file, "
219
+ "an Inspect registry name such as inspect_evals/gsm8k, or a HUD environment folder")
220
+ init.add_argument("--name", help="folder name under audits/ (default: the environment name)")
221
+ init.add_argument("--version", help="environment version (default: latest on the Hub)")
222
+ init.add_argument("--verifiers", help="verifiers version to pin (default: inferred from the wheel)")
223
+ init.add_argument("--manual", action="store_true", help="also write an adapter.py skeleton to fill in")
224
+ init.add_argument("--framework", choices=["verifiers", "inspect", "hud"], help="force the framework (default: detected)")
225
+ init.add_argument("--answers", help="HUD: a JSON file with reference answers (slug -> answer, or a list in task order)")
226
+ init.add_argument("--task", help="Inspect: which @task to audit when a file or project defines several")
227
+ init.add_argument("--with", dest="extra", action="append", default=[], metavar="REQ",
228
+ help="extra requirement to install for the audit, e.g. 'inspect-evals[math]' (repeatable)")
229
+ disc = sub.add_parser("discover", help="list actively maintained Hub environments and what each needs")
230
+ disc.add_argument("--days", type=int, default=90, help="updated within this many days")
231
+ disc.add_argument("--min-releases", type=int, default=2, help="published versions required to count as maintained")
232
+ disc.add_argument("--min-stars", type=int, default=0)
233
+ disc.add_argument("--limit", type=int, default=40, help="most-starred recent environments to inspect")
234
+ disc.add_argument("-o", "--output", type=Path, help="also write the full list as JSON")
235
+ repro = sub.add_parser("repro", help="write a stand-alone reproduction script for the failing probes")
236
+ repro.add_argument("env")
237
+ repro.add_argument("--probe", action="append", help="only these probe codes, e.g. --probe P6 (repeatable)")
238
+ repro.add_argument("--per-group", type=int, default=2, help="examples per failing probe variant")
239
+ drafted = sub.add_parser("draft", help="write REPORT.draft.md: candidate findings for manual review")
240
+ drafted.add_argument("env")
241
+ over = sub.add_parser("overview", help="one table over every audited environment")
242
+ over.add_argument("-o", "--output", type=Path)
243
+ box = sub.add_parser("container", help="probe a container task by writing its key, and mutations of it, as the output file (Docker)")
244
+ box.add_argument("env")
245
+ box.add_argument("--print-command", action="store_true", help="show the uv command and exit")
246
+ box.add_argument("--source", type=Path, help="install the environment from this local folder instead of its release")
247
+ up = sub.add_parser("upstream", help="find the environment's source repository on GitHub and compare it with the audit")
248
+ up.add_argument("env")
249
+ up.add_argument("--repo", help="check this owner/name instead of searching")
250
+ up.add_argument("--limit", type=int, default=5, help="search results per query")
251
+ fix = sub.add_parser("verify-fix", help="rerun a stored repro against a patched source and show what changed")
252
+ fix.add_argument("env")
253
+ fix.add_argument("--source", type=Path, required=True, help="the patched environment package (a folder with pyproject.toml)")
254
+ fix.add_argument("--script", default="repro.py", help="repro script in the audit folder (default: repro.py)")
255
+ fix.add_argument("--print-command", action="store_true", help="show the uv command and exit")
256
+ res = sub.add_parser("results", help="print the last probe run of an environment (summary, or --json for scripts and agents)")
257
+ res.add_argument("env")
258
+ res.add_argument("--json", action="store_true", help="headline numbers, failing probes and hints as JSON")
259
+ trial = sub.add_parser("demo", help="audit a bundled toy environment with one planted defect (about a minute)")
260
+ trial.add_argument("--dir", type=Path, default=Path("falsepass-demo"), help="where to write the toy environment")
261
+ sub.add_parser("mcp", help="serve falsepass as MCP tools over stdio (needs the `mcp` extra)")
262
+ scan = sub.add_parser("scan", help="static pre-scan of reward source (no environment install needed)")
263
+ scan.add_argument("path", type=Path, help="a Python file or a source directory")
264
+ scan.add_argument("--json", action="store_true", help="print the hints as JSON")
265
+ args, extra = parser.parse_known_args(argv)
266
+
267
+ if args.command in ("repro", "draft"):
268
+ from falsepass import draft, repro
269
+
270
+ audit_dir = Path(args.env) if (Path(args.env) / "probe-results.json").is_file() else Path("audits") / args.env
271
+ if not (audit_dir / "probe-results.json").is_file():
272
+ print(f"falsepass: no probe-results.json in {audit_dir} — `falsepass audit {args.env}` has not completed there "
273
+ "(if it stopped at the control, this environment needs a hand-written script)", file=sys.stderr)
274
+ return 1
275
+ if args.command == "repro":
276
+ path = repro.generate(audit_dir, args.probe, args.per_group)
277
+ print(f"wrote {path} — run it twice from a clean virtualenv (command in its docstring)")
278
+ else:
279
+ print(f"wrote {draft.draft(audit_dir)} — candidates only, review before reporting")
280
+ return 0
281
+
282
+ if args.command == "results":
283
+ from falsepass import report
284
+
285
+ audit_dir = Path(args.env) if (Path(args.env) / "probe-results.json").is_file() else Path("audits") / args.env
286
+ if not (audit_dir / "probe-results.json").is_file():
287
+ print(f"falsepass: no probe-results.json in {audit_dir}; run `falsepass audit {args.env}` first", file=sys.stderr)
288
+ return 1
289
+ payload = json.loads((audit_dir / "probe-results.json").read_text())
290
+ print(json.dumps(report.digest(payload), indent=1) if args.json else report.summary(payload))
291
+ return 0
292
+
293
+ if args.command == "demo":
294
+ return demo(args.dir)
295
+
296
+ if args.command == "mcp":
297
+ from falsepass import mcp_server
298
+
299
+ return mcp_server.main()
300
+
301
+ if args.command == "overview":
302
+ from falsepass import draft
303
+
304
+ table = draft.overview(Path("audits"))
305
+ if args.output:
306
+ args.output.write_text(table)
307
+ print(table)
308
+ return 0
309
+
310
+ if args.command == "discover":
311
+ from falsepass import hub
312
+
313
+ found = hub.discover(args.days, args.min_releases, args.limit, args.min_stars)
314
+ if args.output:
315
+ args.output.write_text(json.dumps(found, indent=1))
316
+ print(f"{'stars':>5} {'updated':10} {'rel':>3} {'gen':3} {'turns':11} {'auto':4} needs / environment")
317
+ for e in found:
318
+ if "error" in e:
319
+ print(f"{e['stars']:>5} {e['updated']:10} ? ? ? no {e['id']} ({e['error']})")
320
+ continue
321
+ mark = "yes" if e["automatic"] and e["active"] else "no"
322
+ needs = ",".join(e["needs"]) or "-"
323
+ print(f"{e['stars']:>5} {e['updated']:10} {e['releases']:>3} {e['generation']:3} {e['turns']:11} {mark:4} {needs:22} {e['id']}")
324
+ ready = sum(1 for e in found if e.get("automatic") and e.get("active"))
325
+ print(f"{len(found)} inspected; {ready} maintained and auditable without an adapter.")
326
+ return 0
327
+
328
+ if args.command == "scan":
329
+ from falsepass import static
330
+
331
+ hints = static.scan(args.path)
332
+ if args.json:
333
+ print(json.dumps([h.to_dict() for h in hints], indent=1))
334
+ return 0
335
+ for h in hints:
336
+ print(f"{h.file}:{h.line}: {h.rule} [{h.where}] {static.RULES[h.rule][1]}\n {h.evidence}")
337
+ print(f"{len(hints)} hint(s). Hints are reasons to look; run `falsepass audit` to confirm them.")
338
+ return 0
339
+
340
+ if args.command == "init":
341
+ return scaffold(args)
342
+
343
+ if args.command == "upstream":
344
+ from falsepass import upstream
345
+
346
+ audit_dir = Path(args.env) if (Path(args.env) / "env.toml").is_file() else Path("audits") / args.env
347
+ print(upstream.describe(upstream.locate(audit_dir, repo=args.repo, limit=args.limit)))
348
+ return 0
349
+
350
+ if args.command == "verify-fix":
351
+ from falsepass import verify
352
+
353
+ audit_dir = Path(args.env) if (Path(args.env) / "env.toml").is_file() else Path("audits") / args.env
354
+ source = args.source.resolve()
355
+ if args.print_command:
356
+ print(shlex.join(verify.command_for(audit_dir, args.script, source)))
357
+ return 0
358
+ found, stderr, code = verify.run(audit_dir, source, args.script)
359
+ if code != 0:
360
+ print(f"falsepass: the patched run exited {code}:\n{stderr}", file=sys.stderr)
361
+ return 1
362
+ print(verify.describe(found))
363
+ return 0
364
+
365
+ audit_dir = Path(args.env)
366
+ if not (audit_dir / "env.toml").is_file():
367
+ audit_dir = Path("audits") / args.env
368
+ module = {"rollout": "falsepass.rollout", "propose": "falsepass.propose", "container": "falsepass.container_probe"}.get(
369
+ args.command, "falsepass.runner"
370
+ )
371
+ source = args.source.resolve() if args.source else None
372
+ command = uv_command(audit_dir, extra, module, source)
373
+ if args.print_command:
374
+ print(shlex.join(command))
375
+ return 0
376
+ sys.stdout.flush()
377
+ return subprocess.call(command)
378
+
379
+
380
+ if __name__ == "__main__":
381
+ sys.exit(main())
falsepass/container.py ADDED
@@ -0,0 +1,98 @@
1
+ """Score `verifiers.v1` tasks that are graded on what is left in a container.
2
+
3
+ Such a task has no text answer: `setup` writes its input files into a box, an agent works
4
+ there, `finalize` reads the artifact back and the rewards compare it with a key held in
5
+ the host process. A probe is therefore a submission, not a completion: files to write
6
+ into the box, a script to run there, or both.
7
+
8
+ with_box(task) one Docker container from the task's own image and workdir
9
+ score_submission(...) setup -> write files -> run script -> finalize -> score
10
+
11
+ The caller decides what a submission does; nothing here limits it. The audits in this
12
+ repository keep to a convention: a submission reads only the input files the task's own
13
+ `setup` wrote and writes only the output the prompt names. Needs a running Docker daemon.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import contextlib
19
+ from dataclasses import dataclass, field
20
+ from typing import Any
21
+
22
+
23
+ @dataclass
24
+ class Submission:
25
+ """What a probe leaves in the box. `script` is Python source run with `python3` in the
26
+ task's workdir after `files` are written."""
27
+
28
+ name: str
29
+ files: dict[str, str] = field(default_factory=dict)
30
+ script: str | None = None
31
+
32
+
33
+ @dataclass
34
+ class BoxScore:
35
+ reward: float | None
36
+ metrics: dict[str, float]
37
+ info: dict[str, Any]
38
+ script_exit: int | None = None
39
+ script_stderr: str = ""
40
+
41
+
42
+ def workdir_of(task: Any) -> str:
43
+ """The task's workdir. A task that leaves `data.workdir` empty may fall back to a
44
+ constant of its own inside `setup`; this cannot see that and uses the runtime default,
45
+ so pass such tasks with `data.workdir` set."""
46
+ return task.data.workdir or "/app"
47
+
48
+
49
+ @contextlib.asynccontextmanager
50
+ async def with_box(task: Any):
51
+ from verifiers.v1.runtimes import provision_runtime
52
+ from verifiers.v1.runtimes.docker import DockerConfig
53
+
54
+ config = DockerConfig(workdir=workdir_of(task))
55
+ if task.data.image:
56
+ config.image = task.data.image
57
+ async with provision_runtime(config) as runtime:
58
+ yield runtime
59
+
60
+
61
+ def empty_trace(task: Any) -> Any:
62
+ import verifiers.v1 as vf
63
+ from verifiers.v1.graph import MessageNode
64
+ from verifiers.v1.trace import AgentInfo, TraceTask
65
+
66
+ trace = vf.Trace(
67
+ task=TraceTask(type=type(task).__name__, data=task.data, key=task.key, hash=task.hash),
68
+ agent=AgentInfo(config=vf.AgentConfig()),
69
+ )
70
+ trace.nodes.append(MessageNode(message=vf.UserMessage(content=task.data.prompt_text)))
71
+ return trace
72
+
73
+
74
+ async def score_submission(task: Any, submission: Submission, runtime: Any = None) -> BoxScore:
75
+ """One task, one submission, one fresh box unless `runtime` is passed in."""
76
+ if runtime is None:
77
+ async with with_box(task) as box:
78
+ return await score_submission(task, submission, box)
79
+ trace = empty_trace(task)
80
+ workdir = workdir_of(task)
81
+ await task.setup(trace, runtime)
82
+ for name, body in submission.files.items():
83
+ await runtime.write(f"{workdir}/{name}", body.encode())
84
+ exit_code, stderr = None, ""
85
+ if submission.script is not None:
86
+ path = f"{workdir}/.falsepass_submission.py"
87
+ await runtime.write(path, submission.script.encode())
88
+ done = await runtime.run(["python3", path], {})
89
+ exit_code, stderr = done.exit_code, (done.stderr or "")[-2000:]
90
+ await runtime.run(["rm", "-f", path], {})
91
+ await task.finalize(trace, runtime)
92
+ await task.score(trace, runtime)
93
+ # Reward entries are the weighted values (a 0.5-weight reward that fired reads 0.5);
94
+ # metrics are added under their own names and win on a name clash.
95
+ metrics = {k: v.value for k, v in trace.rewards.items() if v is not None}
96
+ metrics.update({k: v for k, v in trace.metrics.items() if v is not None})
97
+ reward = float(trace.reward) if trace.reward is not None else None
98
+ return BoxScore(reward, metrics, dict(trace.info), exit_code, stderr)