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/__init__.py +14 -0
- falsepass/_demo/falsepass_toy_env.py +29 -0
- falsepass/_demo/pyproject.toml +13 -0
- falsepass/adapter.py +61 -0
- falsepass/auto.py +394 -0
- falsepass/cli.py +381 -0
- falsepass/container.py +98 -0
- falsepass/container_probe.py +233 -0
- falsepass/draft.py +229 -0
- falsepass/hub.py +201 -0
- falsepass/hud_support.py +156 -0
- falsepass/hygiene.py +57 -0
- falsepass/inspect_support.py +284 -0
- falsepass/mcp_server.py +122 -0
- falsepass/model.py +82 -0
- falsepass/probes.py +553 -0
- falsepass/propose.py +164 -0
- falsepass/report.py +227 -0
- falsepass/repro.py +286 -0
- falsepass/rewardlint_bridge.py +56 -0
- falsepass/rollout.py +205 -0
- falsepass/runner.py +154 -0
- falsepass/scorers.py +349 -0
- falsepass/search.py +276 -0
- falsepass/static.py +399 -0
- falsepass/upstream.py +225 -0
- falsepass/verify.py +127 -0
- falsepass-0.3.1.dist-info/METADATA +358 -0
- falsepass-0.3.1.dist-info/RECORD +32 -0
- falsepass-0.3.1.dist-info/WHEEL +4 -0
- falsepass-0.3.1.dist-info/entry_points.txt +2 -0
- falsepass-0.3.1.dist-info/licenses/LICENSE +202 -0
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)
|