Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
cee4ee4
Build fresh household sets and run the tool pilot on them
MaxGhenis Oct 22, 2026
c62b48d
Judge each household-set batch with the reference adversary
MaxGhenis Oct 22, 2026
078cafc
Let the Claude adversary runner bill a declared API key
MaxGhenis Oct 22, 2026
879fd2b
Let a judge exclude the blocked domains by operator
MaxGhenis Oct 22, 2026
b6d835a
Build test household sets on a named dataset; v2 needs a certified pair
MaxGhenis Oct 23, 2026
6e1578f
Add calculator tool version 2 (the pe2 arm)
MaxGhenis Oct 23, 2026
5bbd571
Judge a batch from any model set
MaxGhenis Oct 23, 2026
e881a6a
Calculator features one at a time: tool v3 and ablation arms
MaxGhenis Oct 23, 2026
b3ec406
Read facts as the prompt labels them (labels feature)
MaxGhenis Oct 23, 2026
fd3f5d0
Labels: read the v2-pilot relabelings; flat: names optional
MaxGhenis Oct 23, 2026
5c0f3ec
Flat: a household of one's person fact goes to that person
MaxGhenis Oct 23, 2026
9c43f4f
Keep tool version 2's request as it was: detail only under concise
MaxGhenis Oct 23, 2026
6478e8f
Tool v3 review r1: frame, resume, labels, flat and text fixes
MaxGhenis Oct 23, 2026
d570b8e
Tool v3 review r2: report malformed repeats, resumable harness errors
MaxGhenis Oct 24, 2026
fb35d82
Format the r2 fix
MaxGhenis Oct 24, 2026
6b00a6f
Tool v3 review r3: check every record's treatment, at resume and at s…
MaxGhenis Oct 24, 2026
5d807bb
Tool v3: calculator features one at a time, for the ablation pilot (#…
MaxGhenis Oct 24, 2026
6e15d71
Confirmatory runner: lanes, one engine pool per model, admission, the…
MaxGhenis Oct 25, 2026
c04ffe5
Confirmatory runner: tests
MaxGhenis Oct 25, 2026
ec31e83
Confirmatory runner: create both logs at the start
MaxGhenis Oct 25, 2026
808ccc8
Confirmatory runner: a clock that never goes back, and three test-ora…
MaxGhenis Oct 25, 2026
fdbd411
Confirmatory runner: the review's eight runner findings, stop causes,…
MaxGhenis Oct 25, 2026
06abd1f
Confirmatory runner: log excerpts from a day before the run's start
MaxGhenis Oct 25, 2026
f5099db
Confirmatory runner tests: the interrupt test without a timer
MaxGhenis Oct 25, 2026
f210b6e
Confirmatory runner: round-2 fixes, part 1 (not yet tested)
MaxGhenis Oct 25, 2026
36d553a
Confirmatory runner: round-2 fixes, part 2
MaxGhenis Oct 26, 2026
00d267a
Confirmatory runner: round-3 fixes
MaxGhenis Oct 26, 2026
019f842
Confirmatory runner: round-4 fixes
MaxGhenis Oct 26, 2026
9b30a77
Confirmatory runner: round-5 fixes
MaxGhenis Oct 27, 2026
4002f8e
Confirmatory runner: round-6 wording and coverage
MaxGhenis Oct 27, 2026
047c868
Run the no-tools stage with recoverable attempts and cumulative spend.
MaxGhenis Oct 27, 2026
060b287
Confirmatory runner: wait for host-log samples in two timing tests
MaxGhenis Oct 27, 2026
08d7b18
Confirmatory runner: coordinate no-tools shutdown and freeze inputs
MaxGhenis Oct 27, 2026
5a9735b
Confirmatory runner: require standing no-tools finals before calculation
MaxGhenis Oct 27, 2026
c60086b
Confirmatory runner: coordinate interruption and worker cleanup
MaxGhenis Oct 27, 2026
248e777
Confirmatory runner: validate every saved final and baseline inventory
MaxGhenis Oct 27, 2026
4ce7f8e
Confirmatory runner: keep exceptional cleanup signal-safe
MaxGhenis Oct 27, 2026
2f795ea
Confirmatory runner: defer signals and track worker completion
MaxGhenis Oct 28, 2026
6597adb
Confirmatory runner: restore signal handlers before closing the wake …
MaxGhenis Oct 28, 2026
9acc920
Confirmatory runner: let the forced-exit probe wait for the handler
MaxGhenis Oct 28, 2026
a1114fe
Confirmatory runner: cover watcher startup and pipe teardown
MaxGhenis Oct 28, 2026
89dd946
Confirmatory runner: qualify signal and cleanup guarantees
MaxGhenis Oct 28, 2026
4dafb83
Confirmatory runner: refuse undecodable files with their paths
MaxGhenis Oct 28, 2026
040d206
Confirmatory runner: name board and cost refusals
MaxGhenis Oct 28, 2026
6928b06
Confirmatory runner: check the CLI with a real board
MaxGhenis Oct 28, 2026
8b6c8f7
Confirmatory runner: guard board rereads and strict saved costs
MaxGhenis Oct 28, 2026
0300257
Confirmatory runner: align saved treatment and link checks
MaxGhenis Oct 28, 2026
4fb80d9
Confirmatory runner tests: build preflight finals the treatment check…
MaxGhenis Oct 29, 2026
5e334d9
Confirmatory runner: reproduce unreadable evidence refusals
MaxGhenis Oct 29, 2026
d31c1d7
Confirmatory runner: bind decoded inputs and name evidence failures
MaxGhenis Oct 29, 2026
cccc381
Confirmatory runner: guard intermediate stage inventories
MaxGhenis Oct 29, 2026
4d93b40
Confirmatory runner: name the new test modules for what they check
MaxGhenis Oct 29, 2026
0ae6df5
Confirmatory runner: refuse listed entries without metadata; one read…
MaxGhenis Oct 29, 2026
c2ca2dc
Confirmatory runner: refuse an archive move before creating its folder
MaxGhenis Oct 30, 2026
274e1c7
Confirmatory runner: refuse unfinished optional histories and special…
MaxGhenis Oct 30, 2026
d3673c3
Confirmatory runner: format to CI's ruff 0.15.2 and wrap long lines
MaxGhenis Oct 30, 2026
0193811
Confirmatory runner: pin the archive move's descriptor guard
MaxGhenis Oct 30, 2026
a8226dd
Confirmatory runner tests: hold the deadline test's second turn for t…
MaxGhenis Oct 30, 2026
4441364
Confirmatory runner: hold the preflight to the report's spend and bas…
MaxGhenis Oct 30, 2026
ceae93f
Confirmatory runner: sum saved spend exactly, as the report does
MaxGhenis Oct 31, 2026
3c80377
Confirmatory no-tools: pin startup's exact spend at the stage cap
MaxGhenis Oct 31, 2026
58ba64b
No-tools startup: test the sum across models at the cap and the lock'…
MaxGhenis Oct 31, 2026
3846666
No-tools startup: test costs whose sum overflows
MaxGhenis Oct 31, 2026
2ee8acf
Stub the host facts in the confirmatory tests' World
MaxGhenis Oct 31, 2026
0ab2b1e
Test the host facts that World stubs
MaxGhenis Oct 31, 2026
4ddb784
Confirmatory runner: lanes, one engine pool per model, admission and …
MaxGhenis Oct 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 95 additions & 5 deletions policybench/reference_adversary.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@
REFERENCE_FREEZES = {"us": REFERENCE_FREEZE, "uk": "2026-10-10"}
UK_FISCAL_YEAR = f"{TAX_YEAR}-{str(TAX_YEAR + 1)[-2:]}"

# How the stage headers say the consensus models answered. A payload's
# ``answerCondition`` replaces it, for clusters of tool-using arms (the tool
# pilot's web arms); the default renders the headers byte for byte as before.
DEFAULT_ANSWER_CONDITION = "answering from memory, without tools"

VERDICTS = (
"reference_holds",
"reference_wrong",
Expand Down Expand Up @@ -457,6 +462,7 @@ class AdversaryCase:
metric_type: str = "amount"
state: str = ""
trigger: tuple[str, ...] = ()
answer_condition: str = DEFAULT_ANSWER_CONDITION

def to_manifest_row(self) -> dict:
"""The case as a ``cases.jsonl`` row, without the derivation."""
Expand All @@ -465,6 +471,8 @@ def to_manifest_row(self) -> dict:
row["consensus"] = [dict(cluster) for cluster in self.consensus]
row["trigger"] = list(self.trigger)
row["derivation_sha256"] = sha256_text(derivation) if derivation else None
if self.answer_condition == DEFAULT_ANSWER_CONDITION:
row.pop("answer_condition")
return row

@classmethod
Expand All @@ -486,6 +494,9 @@ def from_manifest_row(cls, row: dict, derivation: str) -> AdversaryCase:
metric_type=str(row.get("metric_type", "amount")),
state=str(row.get("state", "") or ""),
trigger=tuple(row.get("trigger", ())),
answer_condition=str(
row.get("answer_condition") or DEFAULT_ANSWER_CONDITION
),
)


Expand Down Expand Up @@ -600,6 +611,10 @@ def build_adversary_cases(
country = str(payload.get("country") or "us")
scenarios = payload.get("scenarios") or {}
predictions = payload.get("scenarioPredictions") or {}
# A payload may state the definitions its models saw (the tool pilot's
# v2-pilot wording) and the condition they answered under.
definitions = payload.get("outputDefinitions") or {}
condition = answer_condition_of(payload)
cases: list[AdversaryCase] = []
seen: set[tuple[str, str]] = set()
for flag in flags:
Expand Down Expand Up @@ -673,7 +688,8 @@ def build_adversary_cases(
country=country,
scenario_id=scenario_id,
variable=variable,
definition=get_variable_description(variable, country=country),
definition=_clean(definitions.get(variable))
or get_variable_description(variable, country=country),
household_prompt=household_prompt,
reference_value=reference,
consensus=tuple(clusters),
Expand All @@ -684,11 +700,33 @@ def build_adversary_cases(
metric_type=metric_type_for_output(variable),
state=_clean(flag.get("state") or scenario.get("state")),
trigger=tuple(str(t) for t in flag.get("trigger", ())),
answer_condition=condition,
)
)
return cases


def answer_condition_of(payload: dict) -> str:
"""The payload's ``answerCondition``, checked, or the default."""
condition = payload.get("answerCondition")
if condition is None:
return DEFAULT_ANSWER_CONDITION
if not isinstance(condition, str) or not condition.startswith("answering "):
raise ValueError(
f"answerCondition must be a phrase starting 'answering ': {condition!r}"
)
return condition


def _with_condition(header: str, case: AdversaryCase) -> str:
"""A stage header stating the case's answer condition."""
if case.answer_condition == DEFAULT_ANSWER_CONDITION:
return header
if header.count(DEFAULT_ANSWER_CONDITION) != 1:
raise ValueError(f"{case.country} header does not state the answer condition")
return header.replace(DEFAULT_ANSWER_CONDITION, case.answer_condition)


# --- Prompts -------------------------------------------------------------------

_DOMAINS = ", ".join(BLOCKED_DOMAINS)
Expand Down Expand Up @@ -1022,7 +1060,7 @@ def _case_sections(case: AdversaryCase) -> list[str]:

def render_stage1_prompt(case: AdversaryCase) -> str:
"""The law-first prompt: everything about the case except the derivation."""
header = _country_template(_STAGE1_HEADERS, case.country)
header = _with_condition(_country_template(_STAGE1_HEADERS, case.country), case)
return "\n".join([header, "", *_case_sections(case)])


Expand All @@ -1036,7 +1074,7 @@ def render_stage2_prompt(case: AdversaryCase, stage1: dict, stage1_sha256: str)
derivation = case.derivation or "(no derivation was recorded for this case)"
return "\n".join(
[
_country_template(_STAGE2_HEADERS, case.country),
_with_condition(_country_template(_STAGE2_HEADERS, case.country), case),
"",
*_case_sections(case),
"",
Expand Down Expand Up @@ -1646,6 +1684,24 @@ def _markers_in(text: str) -> list[str]:
return [marker for marker in DERIVATION_MARKERS if marker in lowered]


# An exclusion of a blocked domain in a search query ("-site:policyengine.org"):
# how a judge whose search tool takes no blocked_domains (Codex) follows the
# prompt's rule to exclude those domains on every search. It keeps a domain
# out; it does not search for it.
_EXCLUDED_DOMAIN = re.compile(
r"(?<!\S)-site:[\"']?("
+ "|".join(re.escape(d) for d in BLOCKED_DOMAINS)
+ r")[\"']?(?!\S)",
re.IGNORECASE,
)


def searched_for(query: str) -> str:
"""A search query without its exclusions of blocked domains: what the
judge searched for, for the contamination check."""
return _EXCLUDED_DOMAIN.sub(" ", str(query or ""))


def codex_events_audit(events_text: str) -> tuple[list[str], dict]:
"""Audit a ``codex exec --json`` event log for contamination.

Expand Down Expand Up @@ -1714,7 +1770,7 @@ def codex_events_audit(events_text: str) -> tuple[list[str], dict]:
]
url = str(action.get("url") or "")
for text in [str(item.get("query") or ""), *searched]:
for marker in _markers_in(text):
for marker in _markers_in(searched_for(text)):
problems.append(f"line {number}: a web search names {marker!r}")
for marker in _markers_in(str(action.get("pattern") or "")):
problems.append(f"line {number}: an in-page search names {marker!r}")
Expand Down Expand Up @@ -1819,7 +1875,7 @@ def claude_transcript_audit(
query = str(payload.get("query") or "")
activity["searches"].append(query)
searches.append((str(part.get("id")), query))
if any(n in query.lower() for n in BLOCKED_NAMES):
if any(n in searched_for(query).lower() for n in BLOCKED_NAMES):
problems.append(f"the judge searched for the engine: {query}")
else:
problems.append(f"the judge called a tool it was not given: {name}")
Expand Down Expand Up @@ -2064,6 +2120,7 @@ def check_login(
declared: str,
config_dir: str,
desktop_opt_in: bool,
api_key_secret: str = "",
) -> dict:
"""The Claude login a runner may judge with, or ``ValueError`` saying why not.

Expand All @@ -2073,6 +2130,12 @@ def check_login(
in which case it must be exactly the desktop's own claude.ai login. Unlike
that script, it also refuses a token lane whose declared account
(AUDIT_ACCOUNT) is the desktop login's.

With ``api_key_secret`` (the runner's JUDGE_API_KEY_SECRET, Max
2026-10-22) the judges bill that API key instead: the CLI must report a
first-party ``api_key`` login from ``ANTHROPIC_API_KEY`` in exactly
``config_dir`` (the runner's fresh, empty one), with no lane token and no
desktop opt-in.
"""

def parse(text: str) -> dict:
Expand All @@ -2092,6 +2155,31 @@ def parse(text: str) -> dict:
f"the login is through {status.get('apiProvider')!r}, not a "
"first-party subscription"
)
if api_key_secret:
if token or desktop_opt_in:
raise ValueError(
"an API key judge takes no lane token and no desktop opt-in"
)
if method != "api_key" or status.get("apiKeySource") != "ANTHROPIC_API_KEY":
raise ValueError(
f"JUDGE_API_KEY_SECRET is set but the CLI logs in by {method!r} "
f"from {status.get('apiKeySource')!r}, not ANTHROPIC_API_KEY"
)
if not (
isinstance(reported, str)
and os.path.isdir(reported)
and os.path.samefile(reported, config_dir)
):
raise ValueError(
f"the CLI reports config directory {reported!r}, not the run's "
f"own {config_dir}"
)
return {
"method": "api_key",
"account": None,
"org": None,
"secret": api_key_secret,
}
if method not in ("oauth_token", "claude.ai"):
raise ValueError(f"the login method {method!r} is not a subscription login")
same_account = bool(
Expand Down Expand Up @@ -2188,6 +2276,7 @@ def main(argv: list[str] | None = None) -> int:
login.add_argument("--declared", default="")
login.add_argument("--config-dir", required=True)
login.add_argument("--desktop-opt-in", choices=["0", "1"], default="0")
login.add_argument("--api-key-secret", default="")
args = parser.parse_args(argv)

if args.command == "render-stage2":
Expand Down Expand Up @@ -2226,6 +2315,7 @@ def main(argv: list[str] | None = None) -> int:
declared=args.declared,
config_dir=args.config_dir,
desktop_opt_in=args.desktop_opt_in == "1",
api_key_secret=args.api_key_secret,
)
except ValueError as exc:
print(str(exc), file=sys.stderr)
Expand Down
32 changes: 27 additions & 5 deletions policybench/scenarios.py
Original file line number Diff line number Diff line change
Expand Up @@ -918,19 +918,29 @@ def scenario_from_dict(data: dict[str, Any]) -> Scenario:
)


def load_certified_us_person_frame() -> tuple[pd.DataFrame, int, str]:
def load_certified_us_person_frame(
dataset: str | None = None, dataset_label: str | None = None
) -> tuple[pd.DataFrame, int, str]:
"""Load a person-level frame from the certified US microsimulation dataset.

Returns ``(person_df, dataset_year, dataset_label)``. The label is read
from the live simulation's PolicyEngine bundle (``default_dataset``), so it
reflects the dataset the run actually materialized -- the certified
populace build (``populace_us_2024``) -- rather than a hardcoded name.

``dataset`` (a local H5 path) replaces the certified dataset, for test
household sets only; ``dataset_label`` then names it.
"""
from policybench.policyengine_runtime import make_us_microsimulation

sim = make_us_microsimulation()
if dataset is None:
sim = make_us_microsimulation()
dataset_label = str(sim.policyengine_bundle["default_dataset"])
else:
if not dataset_label:
raise ValueError("an explicit dataset needs a dataset_label")
sim = make_us_microsimulation(dataset=dataset)
dataset_year = sim.default_input_period
dataset_label = str(sim.policyengine_bundle["default_dataset"])
input_specs = get_promptable_input_specs()

values = {}
Expand Down Expand Up @@ -1822,10 +1832,22 @@ def generate_scenarios(
seed: int = SEED,
excluded_household_ids: set[int] | None = None,
country: str = DEFAULT_COUNTRY,
dataset: str | None = None,
dataset_label: str | None = None,
) -> list[Scenario]:
"""Generate benchmark scenarios for a country."""
"""Generate benchmark scenarios for a country.

``dataset`` and ``dataset_label`` replace the certified US dataset (see
:func:`load_certified_us_person_frame`); test household sets only.
"""
if dataset is not None and country != "us":
raise ValueError("an explicit dataset is supported for the US only")
if country == "us":
person_df, dataset_year, dataset_label = load_certified_us_person_frame()
person_df, dataset_year, dataset_label = (
load_certified_us_person_frame()
if dataset is None
else load_certified_us_person_frame(dataset, dataset_label)
)
return scenarios_from_cps_frame(
person_df,
n=n,
Expand Down
Loading
Loading