Skip to content

Merge initial work so .github workflow can be dispatched - #1

Merged
tadamcz merged 47 commits into
mainfrom
develop
Jun 1, 2026
Merged

Merge initial work so .github workflow can be dispatched#1
tadamcz merged 47 commits into
mainfrom
develop

Conversation

@tadamcz

@tadamcz tadamcz commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator

No description provided.

tadamcz added 30 commits June 1, 2026 19:50
Add the apn package skeleton and the core ProofSketch text model:
EVOLVE-BLOCK/EVOLVE-VALUE marker parsing, comment-aware sorry detection,
frozen-skeleton extraction for integrity checks, and search_replace edits
restricted to editable regions. Configure strict mypy and pytest.
apn/verifier/base.py defines the LeanVerifier protocol (compile + print_axioms)
with CompileResult/Diagnostic/AxiomResult. apn/safeverify.py implements the
three-part validation: compiles, statement preserved (skeleton equality), and
axiom guard (sorryAx tolerated only while sorry remains). Add an in-process
FakeVerifier double for offline testing. pytest-asyncio in auto mode.
apn/lean/: Docker image (Lean v4.29.1 + Mathlib v4.29.1 olean cache +
PyPantograph), a lake project, and apn_lean.py -- a sandbox-side daemon that
keeps a warm Pantograph server behind a Unix socket plus a thin client the host
calls per compile. Pin v4.29.1 to match PyPantograph's bundled repl (paper used
v4.27; documented).

apn/verifier/pantograph.py: host-side LeanVerifier that shells into the sandbox
via the daemon client and parses compile diagnostics + axiom queries. Pure
parsing helpers in the daemon are unit-tested.
apn/tools.py: the search_replace Inspect tool (compact-diff edits restricted to
EVOLVE regions, recompiles after each edit, keeps failing edits for iteration)
bound to a mutable EpisodeState. apn/prompts.py: basic agent (A) prompt
transcribed verbatim from Figure 5. Fix the Lean image (drop nonexistent build
target; Pantograph only needs Mathlib oleans on LEAN_PATH).
apn/agents/basic.py implements Figure 1's pseudocode: run_episode (multi-turn
search_replace session validated by SafeVerify at the end), run_subagent (Ralph
loop adopting validated sketches, reverting on validation failure, stopping on a
complete proof), and run_basic_agent (N independent subagents in an anyio task
group, cancelling the rest on first success). Exposed as the basic_agent Inspect
solver. Deterministic tests via mockllm + FakeVerifier.
apn/scorer.py independently re-validates the agent's final sketch with
SafeVerify (correct iff complete proof). apn/dataset.py loads .lean sketches
into Samples (target decls inferred or from metadata) with three bundled
smoke-test sketches. apn/task.py wires dataset + basic_agent + scorer + the
Docker Lean sandbox into the apn_basic task.
Each prover subagent is now an Inspect @agent launched via inspect_ai.agent.run,
which copies/isolates input and records a transcript span per subagent, instead
of calling the orchestration helper from a bare task group. The anyio task group
is retained only to cancel the remaining subagents once one finds a proof. Also
fix sorry detection for Lean v4.29.1's backtick-quoted warning.
Run each prover subagent via inspect_ai.agent.run (own span, isolated state)
and implement each episode as a built-in react agent (submit=False, which
terminates exactly when the model stops calling search_replace), bounded by a
message_limit. This replaces the hand-rolled generate/execute_tools loop.

Since react/run require an active sample context, the agent tests now run
through eval_async with a mockllm model and the in-process FakeVerifier. Also
fix sorry detection for Lean v4.29.1's backtick-quoted warning, add the
backtick test, and the README.
The Pantograph server preloads Mathlib, so an 'import Mathlib' line in the sketch
hit 'invalid import command' on every compile, making compiles=False regardless
of the proof body and causing all episodes to fail. The daemon now blanks import
lines (preserving line numbers) before check_compile / axiom queries. Found via a
real end-to-end eval.
…ubagent

Editing now uses Inspect's text_editor tool on a file in the sandbox, with a
lean_check tool to compile (replacing the bespoke search_replace). Each subagent
gets its own Lean sandbox: apn_basic provisions one Docker service per subagent,
and the subagent's task scopes the visible sandbox to its own so text_editor
(which injects into the first sandbox it finds) is correctly pinned.

EVOLVE-region/statement integrity is enforced post-hoc by SafeVerify. Agent
tests become Docker-gated integration tests (text_editor needs a Linux
container). Also incorporates the strip_import_lines daemon fix.
When restricting a subagent's visible environments to its single sandbox, also
set the default-sandbox context var to that name; otherwise the sample-wide
x-default service name leaks in and sandbox() fails to resolve within the scoped
map (num_subagents>1). Validated end to end: claude-sonnet-4-5 proves the bundled
sketches against real Lean/Mathlib at num_subagents=1 and 2 (accuracy 1.000).
README updated for text_editor + per-subagent sandboxes.
Drop ProofSketch.apply_search_replace, the SearchReplaceError exception, and
EvolveRegion.contains (only used by that method) plus their tests; editing is
now done with text_editor and integrity enforced post-hoc by SafeVerify. Remove
the unreferenced static apn/lean/compose.yaml (apn_basic generates compose
dynamically) and refresh the docstrings that mentioned search_replace.
Remove the bespoke framework -- EVOLVE-BLOCK/VALUE markers, the ProofSketch
model, skeleton-based integrity, and the entire episode/Ralph/parallel-subagent
orchestration with its sandbox-scoping. The solver (apn/agent.py) is now a thin
wrapper around Inspect's deepagent given the proof file plus text_editor +
lean_check; it runs deepagent's own loop and reads the result back.

Independent attempts per problem are just Inspect epochs passed at eval time
(--epochs N), each a fresh sample run with its own sandbox -- no custom subagent
code or reducer, and one Docker service per sample.

The anti-cheat moves entirely into SafeVerify/the scorer: the final proof must
compile, keep the theorem statement verbatim, be sorry-free, and use only
permitted axioms. Bundled sketches drop the EVOLVE markers (just theorem +
sorry). Validated end to end: claude-sonnet-4-5 proves add_comm against real
Lean/Mathlib via deepagent (text_editor x3 + lean_check + submit), accuracy
1.000. mypy clean; 30 unit tests pass.
The real scorer now uses the vendored Lean SafeVerify exe, making the parallel
Python anti-cheat a production-dead, test-only reimplementation. Remove it:

- delete apn/safeverify.py (safe_verify, check_statement_preserved, check_axioms,
  extract_statement, ValidationVerdict) and its tests
- remove checker.FakeSafeVerify (only consumer of the above)
- drop print_axioms + AxiomResult from the verifier layer (only the deleted
  safe_verify used them); LeanVerifier is now compile-only, matching what the
  agent's lean_check actually needs
- delete apn/verifier/fake.py (FakeVerifier now unused)
- retarget test_checker.py at the scorer wiring via a stub checker

Vendor the ported SafeVerify (Lean 4.29.1) and wire SandboxSafeVerify as the
scorer.
Split the monolithic image into a shared Mathlib base plus two derived images:
apn-agent (Pantograph, the agent's writable workspace, NO SafeVerify) and
apn-scorer (SafeVerify, a trusted container the agent never touches). The sample
now provisions two sandbox services (default=agent, scorer=apn-scorer); the
scorer writes the submitted proof into the clean scorer sandbox and runs
SafeVerify there, so the agent cannot tamper with the checker, target spec, or
oleans. Add apn/lean/build.sh; update task + README.
The proof is the edited file, not a text answer, so the default submit(answer)
is meaningless. Pass AgentSubmit(tool=submit()) where submit takes no arguments
and just signals completion.
Replicates the paper's OEIS evaluation (44/492). New v4.27 toolchain track
alongside the existing v4.29.1 bundled track:

- apn/lean/fc/: shared FC base image (Lean v4.27 + Mathlib + the
  FormalConjectures library), agent image (warm PyPantograph v0.3.13, matched to
  Lean v4.27, preloading FormalConjectures.Util.ProblemImports), and scorer
  image (SafeVerify ported to v4.27). compose.yaml + build.sh.
- apn/data/oeis/: vendored 484 OEIS/Auto files + THEOREM_MAPPING.txt (492
  conjectures), pinned to formal-conjectures auto_oeis @67338a1, with NOTICE.
- apn.dataset.oeis_dataset(): one Sample per conjecture; the whole file is the
  sketch so the agent must discharge the embedded test lemmas (misformalization
  guard) plus the conjecture; SafeVerify re-checks every decl verbatim.
- apn.task.apn_oeis: wires the loader + warm verifier + SafeVerify scorer + FC
  sandbox. Reuses the daemon (APN_LEAN_IMPORTS) and PantographVerifier unchanged.
- Generalized agent instructions for multi-decl files.

Validated: FC base compiles a real OEIS file; v4.27 SafeVerify accepts a real
151-line paper proof (A282779) and the agent smoke run exercises the pipeline.
Previously a token/time limit unwound through the solver before it read the
proof file back, so the scorer saw no submission and reported 'did not produce a
final proof'. Read the file in a finally block so SafeVerify always judges the
actual file the agent left -- including a complete proof finished but never
submitted.
The agent can now run python3 in its sandbox (via the built-in bash tool, not
the python tool) as a numerical scratchpad -- compute sequence terms, test a
bound on small cases, hunt for patterns/counterexamples -- before committing to
a Lean proof. Instructions updated to describe it and to stress it is
exploration only (no formal weight; the proof must still be pure Lean).
The sandbox has no network, so the agent's python3 scratchpad (via bash) only
has what's in the image. Add numpy + sympy -- directly useful for the OEIS
number-theory problems (primes, factorization, totient, exact rationals).
Instructions updated to mention them.
An OOM-kill of safe_verify (SIGKILL under memory pressure) was being reported as
a clean rejection, silently failing a *valid* proof -- this masked a genuine
agent solve of an OEIS conjecture. Now:
- apn_safeverify.py keys the verdict on safe_verify's explicit markers
  ('SafeVerify check passed.' / 'SafeVerify check failed.'); if neither is
  present (crash/OOM/timeout) it emits a system_error. A target-spec compile
  failure (trusted data) is likewise a system_error.
- SandboxSafeVerify.check raises on any infrastructure failure (exec failure,
  unparseable output, or system_error), so the sample errors out and is
  rerun/inspected rather than scored INCORRECT.
With gate enabled, the submit tool runs SafeVerify on the agent's file and only
accepts it if verification passes; a failed submission raises ToolError, which
the react loop does not count as a submission, so the agent is forced to keep
working until it produces a valid proof or hits a limit. The agent is told only
that verification failed -- the verifier's output is withheld so it cannot probe
SafeVerify for gaps. An infrastructure failure of the checker propagates (crashes
the sample) rather than being treated as a rejection.

apn_oeis gains a gated=bool flag; the same checker instance drives both the gate
and the final scorer.
The v4.29.1 image set (apn/lean/*) was vestigial scaffolding from before the
real datasets existed; everything the paper needs is v4.27. Collapse the two
duplicated image sets into one:

- Promote apn/lean/fc/* up to apn/lean/* (single Dockerfile.base/agent/scorer,
  compose.yaml, build.sh, safeverify/). Images retagged apn-lean-base/apn-agent/
  apn-scorer (now v4.27 + FormalConjectures).
- Remove the v4.29.1 base lakefile/lean-toolchain and the v4.29.1 SafeVerify.
- Drop apn_basic, the bundled sketches (apn/data/sketches), and the
  bundled-only dataset helpers; dataset.py is now just the OEIS loader.
- Update task.py (single COMPOSE_FILE), the pantograph docstring, tests, README.

One track, one image set; smoke-testing uses an OEIS subset (-T names=...).
The finally-block read captured the agent's final file, but a broad except fell
back to the original sketch on any failure -- silently turning a real sandbox
read failure into a clean INCORRECT (the same anti-pattern just removed from the
scorer). read_file isn't gated by the token limit and the file is written up
front, so it normally succeeds; let a genuine failure error the sample instead.
state.output was being set to ModelOutput.from_content(content=final_proof),
misattributing the raw Lean file as model-generated text -- and discarding
run()'s actual return value. The scorer reads the proof from the store (and the
Score sets answer=final), so state.output is only for display; set it to the
agent run's real output instead.
tadamcz added 17 commits June 1, 2026 19:50
…ges=True

The misleading 'assistant said the verdict' rendering was react folding the
submit tool's return into the assistant message (keep_in_messages=False). Set
keep_in_messages=True so the submit call + result stay as a normal tool
interaction (verdict correctly attributed to the tool, not the model), and name
the tool 'submit_proof' so the main loop's name-based submission scan can never
collide with the deepagent subagents' 'submit' tool -- making keep_in_messages
=True safe from the early-termination bug. Informative returns restored.
The base image git-clones the whole formal-conjectures repo to build the library
closure, which left the entire conjecture corpus in every agent's sandbox -- all
484 OEIS problems plus the Erdős/Arxiv/etc. sets, which contain real proofs and
axioms. An agent could grep its workspace for related solved problems/techniques.
After building FormalConjectures.Util.ProblemImports, remove every
FormalConjectures/ subdir except the Util library; importing ProblemImports still
resolves via the built oleans (verified: an OEIS file still compiles, workspace
FormalConjectures drops from ~all-problems to 96K).
Gating is now Inspect's AgentAttempts: with max_attempts>1, react re-runs the
task scorer (SafeVerify) on each submission and, if not accepted, replays a
fixed INCORRECT_MESSAGE (no verifier output) until acceptance or a token limit.
This deletes the custom gated_submit tool, the gate parameter, and the
ToolError-based gating.

For this to work mid-loop, proof_scorer now reads the submission *live* from the
agent's proof file in the sandbox instead of a solver-written store entry -- so
it gives the same verdict whether react scores it per-submission or Inspect
scores it at the end. The solver therefore no longer needs the store write or the
finally block.

apn_oeis(gated=True) maps to max_attempts=99_999_999 (token-limit bounded);
gated=False -> max_attempts=1 (submit ends the loop, validated once at the end).
Infra failures still propagate (score() doesn't catch), crashing the sample.
Two fixes:

1) A submission that fails the kernel replay (unsafe/partial constant, kernel
   type-check failure, missing imports) makes safe_verify exit nonzero BEFORE
   printing 'SafeVerify check failed.'. The marker-based classifier mistook this
   for an infra failure and raised, crashing the sample/eval -- e.g. a gated run
   died on a real cheat attempt ('unsafe constant fakeProof detected'), which
   SafeVerify had correctly caught. Classify by exit code instead: <0 (signal,
   e.g. OOM) is infra; 0 is pass; any positive code is a genuine rejection
   (INCORRECT). Extracted as a pure, unit-tested classify_safeverify().

2) Also strip FormalConjecturesTest (the library's own test suite) from the
   workspace image -- not needed at runtime and potentially confusing. The
   proving library (Util + FormalConjecturesForMathlib) is kept.
A negative subprocess return code means 'killed by signal N' (POSIX), which is
reliably infra -- but the signal could be SIGKILL (OOM/timeout), SIGSEGV (crash),
etc. Report the actual signal name/number instead of asserting OOM.
…ipping

A builder stage clones the whole formal-conjectures repo and builds the library;
the final image copies in ONLY the toolchain, the .lake build artifacts (oleans),
and the proving-library source (FormalConjectures/Util + FormalConjecturesForMathlib).
The conjecture corpus (other problems / Erdős/Arxiv proofs), the library test
suite, repo tooling/docs, and .git never reach the agent's sandbox -- nothing to
strip after the fact, and the workspace contains only the library it proves with.
Verified: a real OEIS file compiles from the copied artifacts; workspace is just
.lake + the library; lake resolves in both login and non-login shells.
New host-side arxiv_search / arxiv_source tools (network runs in the
controller; the sandbox stays airgapped). Both gate to papers predating
the benchmark paper (arXiv:2605.22763, May 2026): search bounds
submittedDate, source resolves the newest pre-cutoff *version* so a
post-cutoff revision can't surface a solution to these open conjectures.

Exposed as lean_prover(literature=) / apn_oeis(-T literature=true), off
by default -- a distinct literature-augmented run condition. The tools
and their prompt paragraph are only added when enabled.
The bash scratchpad ships sympy, mpmath, and numpy; the prompt only
named sympy and numpy. List all three with what each is for -- notably
mpmath's pslq/identify for guessing closed forms.
got a "System.IO.IOException: No space left on device"
@tadamcz
tadamcz merged commit 40e442c into main Jun 1, 2026
tadamcz added a commit that referenced this pull request Aug 25, 2026
- test_comparator_primitives.py: the §2.3 primitive/builtin-constants
  invariant -- asserts every name comparator looks up in both environments
  (primitiveTargets + builtinTargets from the pinned Main.lean) resolves in
  the dataset toolchain, a cheap guard for comparator/FC-pin bumps.
- test_comparator_security.py: cross-check properties against one live
  comparator sandbox.
  - cross_attempt_filesystem_poisoning: check #1's build swaps the Mathlib
    packages symlink for a poisoned dir; check #2's honest proof must still be
    accepted, proving reset-workspace.sh restored the pristine tree (§3.1).
  - disproof-shape verdicts (§4 delta): exact type_of% and explicit ¬(∀…)
    accept; the defeq-but-not-syntactic (h : ∀…) : False form rejects under
    Comparator's BEq match -- pinning that the prompt's fill-the-sorry
    guidance is load-bearing.
  Verified locally against the built image (4 passed).

Process-survival: no test shipped. Empirically a setsid-detached solution-build
child does not outlive the check in the Docker backend (tini + landrun teardown;
landrun's inherited Landlock domain also confines any survivor to .lake). The
Inspect #5034 per-service-restart gap remains the documented known limitation.
tadamcz added a commit that referenced this pull request Aug 25, 2026
The DAG-shaped text export + kernel replay have no importModules in the
trusted process and no un-memoized rebuildExpr, retiring both unbounded-memory
sources. mem_limit drops 50g -> 16g pending the §7 peak-RSS measurement.
tadamcz added a commit that referenced this pull request Aug 28, 2026
* initial plan

* fix universe handling in comparator migration plan

* codex plan updates

* Add comparator verifier image: Dockerfile stages, workspace reset, disproof certifier

The comparator stage replaces the compile+scorer pair (plan §3): the
Comparator binary is built at its own pinned toolchain (19e111e =
v4.34.0-rc2), lean4export at the rev comparator's manifest pins (cacf989)
rebuilt under the project toolchain (§2.2), and landrun from pinned source.
The builder registers the Challenge/Solution scratch libs before any lake
build and the runtime image stages /opt/pristine + reset-workspace.sh for
the read-only-rootfs + tmpfs hardening (§3.1).

extract_ranges gains certify_disproof, the vendor-time/CI certifier that
recomputes each appended disproof declaration's type via mkNot and asserts
BEq-identity with universe-parameter canonicalization (§4).

* Switch the scoring harness from SafeVerify to Comparator

- checker.py: SandboxComparator replaces SandboxSafeVerify (protocol renamed
  ProofChecker). Spec.lean is extracted from the agent tar host-side with
  tarfile, the workspace is reset per check, and one comparator invocation
  under lake env replaces the compile/verify exec pair. Reference vs
  submission attribution keys off comparator's own 'Building Solution' phase
  marker: earlier failures raise, later ones score INCORRECT (plan §3.2).
- scorer.py: reads the agent's declared claim from the sample store, passes
  the target's fully-qualified decl name to the checker, and records
  checker-agnostic verifier_output metadata (replacing safeverify_report).
- solver.py: the submit tool gains the required claim argument
  ('proof' | 'disproof') recorded in the sample store; resource stages
  renamed to the comparator stage names.
- prompts.py: the fill-one-sorry contract replaces the negateExpr contract
  (the spec now states both the target and its .disproof; NEGATE_EXPR_SOURCE
  deleted, plan §4).
- task.py: two sandbox configs generated directly, one per backend (docker
  compose.yaml with read-only rootfs + size-capped tmpfs for the comparator
  service; chart-native k8s values.yaml with emptyDirs, networkIsolated and
  the CLUSTER_DEFAULT runtime pin), selected by the new sandbox_backend task
  arg (plan §5). The compile service is gone.
- dataset.py: sample metadata gains decl_name (manifest override for targets
  whose environment name carries a namespace prefix the id does not).

* Append the derived disproof declaration to every Isolated spec

Every committed Isolated/<id>.lean now ends with the mechanically derived

    theorem <target>.disproof : ¬ (type_of% @<target>) := sorry

so one committed file is simultaneously the agent's sketch, the proof
challenge and the disproof challenge; the checker sends it verbatim for both
claims and only the configured theorem name differs (plan §4). The four
Erdos138 specs whose isolation cut dropped a trailing 'end' get it restored
first; the appended declaration always sits at top level under the target's
fully-qualified name.

- scripts/isolation.py: comment/string-aware scope scanner (namespace /
  section / end tracking, «...» identifiers, multi-component ends) and the
  shared append_disproof step, wired into all three generators.
- apn/data/oeis/samples.jsonl: decl_name overrides for the 3 targets whose
  environment name carries a namespace prefix their id does not
  (OeisA230507.*, A271099.*).
- Isolation suites: the structural gates expect the appended declaration, and
  a new certification gate runs certify_disproof over every committed spec
  (target/disproof pairing against the manifest + the independent mkNot type
  check). Validated locally: all 928 specs certify, 0 failures.
- Dataset tests updated for the two-theorem/two-sorry sketch shape and the
  decl_name metadata key.

* Port the checker/scorer/security test suites to Comparator

Re-derived from Comparator's model, not blind-ported (plan §6):

- test_checker.py: unit tests for extract_entry (host-side tar handling) and
  SandboxComparator's reset -> stage -> run-under-lake-env orchestration, the
  Building-Solution phase attribution (raise vs INCORRECT), and the scorer's
  claim plumbing from the store.
- test_singlefile_proof.py: real-image acceptance incl. a single-file disproof
  under the disproof claim; the helper-import guard now fails at the solution
  build.
- test_lean_vuln_e2e.py: 14 soundness cases against the real image. The former
  import_superset_violation and unsafe_constant_in_entry become documented
  ACCEPT cases (extra imports / inert unsafe decls are sound under closure
  replay); root_exec retargets to overwriting the lean4export binary and
  doubles as the landrun canary; forbidden-axiom/weakened/missing/injection
  rejects retained. All 14 verified locally against the built image.
- test_gold_proofs.py: gold proof adapted to the migrated spec shape (renamed
  target + appended inert .disproof), scored via SandboxComparator on the
  comparator sandbox.
- test_agent.py / test_tools.py: resource-stage names and the fill-one-sorry
  prompt contract.
- scripts/summarize/task.py: COMPOSE_FILES_DIR -> SANDBOX_FILES_DIR rename.

* Add comparator invariant + cross-attempt/disproof-shape security tests

- test_comparator_primitives.py: the §2.3 primitive/builtin-constants
  invariant -- asserts every name comparator looks up in both environments
  (primitiveTargets + builtinTargets from the pinned Main.lean) resolves in
  the dataset toolchain, a cheap guard for comparator/FC-pin bumps.
- test_comparator_security.py: cross-check properties against one live
  comparator sandbox.
  - cross_attempt_filesystem_poisoning: check #1's build swaps the Mathlib
    packages symlink for a poisoned dir; check #2's honest proof must still be
    accepted, proving reset-workspace.sh restored the pristine tree (§3.1).
  - disproof-shape verdicts (§4 delta): exact type_of% and explicit ¬(∀…)
    accept; the defeq-but-not-syntactic (h : ∀…) : False form rejects under
    Comparator's BEq match -- pinning that the prompt's fill-the-sorry
    guidance is load-bearing.
  Verified locally against the built image (4 passed).

Process-survival: no test shipped. Empirically a setsid-detached solution-build
child does not outlive the check in the Docker backend (tini + landrun teardown;
landrun's inherited Landlock domain also confines any survivor to .lake). The
Inspect #5034 per-service-restart gap remains the documented known limitation.

* Close TODO bug #1: Comparator removes the SafeVerify memory blowup

The DAG-shaped text export + kernel replay have no importModules in the
trusted process and no un-memoized rebuildExpr, retiring both unbounded-memory
sources. mem_limit drops 50g -> 16g pending the §7 peak-RSS measurement.

* Document module-sensitive drift candidates; add sorry'd-def fixture; split CI

- scripts/comparator_drift.py + tests/test_comparator_drift.py: pin the 23
  module-sensitive drift candidates (§3.3) -- 20 specs with a spec-local
  private declaration, 3 with an anonymous instance -- detected by a
  comment-aware scan of the committed specs. A dataset/Lean/exporter/Comparator
  bump that adds or removes a candidate fails this fast pure-Python guard,
  prompting the §7.5 pre-cutover comparator sweep (confirming the precise
  reject subset is left to that sweep, not run in CI).
- test_comparator_security.py: add the statement-with-sorry'd-def fixture
  (§7.3) -- a faithful proof of a theorem whose statement depends on a sorry'd
  def is correctly rejected (sorryAx in the closure), documenting the defective
  -formalization class.
- test_oeis.py: pin the 3 decl_name overrides (targets inside a namespace).
- checks.yml: split the comparator container tests into their own
  comparator-tests job (each check builds Challenge+Solution against Mathlib,
  tens of minutes) so the fast tests job stays ~quick; the pure-Python drift
  guard stays in tests.

* Remove the orphaned vendored SafeVerify project

The comparator migration dropped the scorer Dockerfile stage that built
safe_verify, so apn/lean/safeverify/ is now dead: nothing in the Dockerfile,
apn/, scripts/, or tests/ references it. The disproof certifier
(CertifyDisproof.lean) replicates negateExpr's mkNot∘cleanupAnnotations
independently, so no live code depends on the vendored source. Removing it
per plan §5 (drop the scorer stage and the vendored-safeverify COPY/build).

* Add comparator smoke eval: build workflow, subset, Hawk config

- build-docker-images.yaml: build the comparator target (was scorer, which the
  migration removed) so the verifier image reaches ECR.
- comparator_smoke subset: the 5 smallest-gold-proof proved_38 OEIS conjectures.
- configs/comparator-smoke.yaml: Hawk eval-set pinned to the apn package
  @comparator (main still ships the SafeVerify apn), sandbox_backend: k8s
  (chart-native values.yaml, since Hawk's compose converter rejects the docker
  hardening keys), claude-fable-5 high-effort, $500/sample cap.

* Share one comparator sandbox across test_singlefile_proof cases

Each case previously brought up its own sandbox, so every test re-ran
docker compose build -- ~6 chances per run to hit a transient buildkit EOF
(observed on 15f6b1a's comparator-tests). All cases here are honest and
SandboxComparator resets the workspace before each check, so a single
module-scoped bring-up is safe; it builds the image once, mirroring
test_gold_proofs / the isolation suites, and shrinks the flake surface.

* Skip the one gold proof hit by module-sensitive drift (§3.3)

The gold sweep confirmed 35/36 published proofs verify under Comparator; the
lone rejection is oeis_A258667_conjecture_0 with 'Const does not match between
challenge and target A258667' -- a spec-local private def in the target's
closure mangles to different names in the Challenge vs Solution modules. This
is the documented fail-closed §3.3 limitation (the id is already in
comparator_drift.CANDIDATE_IDS), not a regression, and the plan ships no source
rewrite for it in v1. Skip it in the gold sweep (MODULE_DRIFT_STEMS, alongside
the resource-bound skips) and record the empirical confirmation in
comparator_drift. Removing it needs an upstream generated-name-drift fix.

* Add second smoke eval config (gpt-5.6-sol)

Same 5-problem comparator_smoke subset, sandbox_backend k8s, and $500/sample
cap as comparator-smoke.yaml, with gpt-5.6-sol (high reasoning effort) -- a
side-by-side end-to-end check of the Hawk/k8s pipeline with a second model.

* Add basic soundness floor: disproof-claim + sorry must reject

sorry_in_entry already covers proof-claim + sorry -> reject, but there was no
disproof-claim counterpart. sorry_in_disproof submits under claim=disproof with
the .disproof theorem left as sorry; Comparator targets tgt.disproof, whose
sorry pulls sorryAx into its axiom closure -> reject. Basic check worth having
explicitly given the change to the claim-based security model.

* Add disproof-path smoke evals (fable + gpt-sol)

The proof smokes only exercise Comparator's accept path for claim=proof. These
add the disproof counterpart: comparator_smoke_disproof is 5 OEIS conjectures
whose correct answer is a disproof -- each disproved and accepted in all 15
historical result runs (every model, from the LeanOpenProblems-results repo),
with the shortest published disproofs, so a strong agent can reproduce the
disproof and exercise the claim=disproof accept path end to end on Hawk/k8s.
None are §3.3 module-drift candidates. Two configs mirror the proof smokes
(claude-fable-5 and gpt-5.6-sol, high effort, $500/sample, sandbox_backend k8s).

* Add reject-path smoke evals (fable + gpt-sol, $50)

Third smoke pair, exercising Comparator's reject path with real agent attempts:
comparator_smoke_hard is 5 OEIS conjectures no model ever settled (attempted +
INCORRECT in all 15 historical result runs, every model/budget), i.e. extremely
hard/open. Paired with a low $50/sample cap so the agent burns budget on
attempts Comparator rejects. Two configs mirror the other smokes (claude-fable-5
and gpt-5.6-sol, high effort, sandbox_backend k8s); non-drift ids.

* Add red-team soundness Task: cheat to get Collatz accepted

apn_redteam_collatz hands the agent the apn codebase and explicitly encourages
it to cheat, then scores its Submission/Spec.lean with the real
SandboxComparator against a Collatz challenge (an open problem, so any accepted
proof/disproof is a genuine soundness break). Reuses the production scorer and
two-service sandbox, so accuracy == break rate; desired result 0.0.

The codebase handed to the agent is built dynamically at solve() time -- an
in-memory tar of the installed apn package's source (apn.__file__ walk,
excluding the heavy dataset and vendored docs), so it reflects the running
code, not a committed artifact. Prompt/framing loosely follows MirrorCode's
mc/red. Registered for Hawk and guarded by test_registry.

* Red-team: split into separate system + user prompts (react-only)

Like mc/red: a standing authorized-exploit SYSTEM prompt (the agent's role --
sanctioned sandbox, cheat/exploit/tamper encouraged, never fabricate success)
and a per-attempt USER prompt (the Collatz objective, the staged codebase, the
rules of engagement). Wired via react's AgentPrompt(instructions=...); the
red-team builds a basic react agent directly rather than touching the shared
build_agent.

* Red-team: trim the creative/persistent/fabricate lines from the system prompt

* Red-team: drop the single-channel assumption from the user prompt

The bullet asserted the agent's only channel is Spec.lean and the verifier
sandbox is untouchable -- an unnecessary (and self-referential) constraint on a
soundness red-team. Leave the attack surface open-ended.

* Red-team: drop per-file codebase descriptions from the user prompt

The specific descriptions of checker.py / Dockerfile / reset-workspace.sh etc.
can drift from the actual code; point the agent at the unpacked codebase and
let it read the current source instead of trusting a prose summary.

* Add red-team eval config (apn_redteam_collatz, fable, $1000)

* Add red-team eval config (gpt-5.6-sol variant)

* Strip private modifiers from OEIS isolated specs (comparator#58 drift)

A corpus census (scripts/comparator_drift.py, empirically confirmed by
compiling each candidate as module Challenge in the comparator image and
walking the target/.disproof closure) found 20/928 specs whose faithful
submissions Comparator falsely rejects: 19 from `private` name mangling
(_private.<Module>.0.*) and 1 from an anonymous-instance name collision.

Fix the OEIS cases at the source: scripts.isolation.strip_private removes
the `private` modifier (comment/string-aware, loud on unhandled shapes),
generate_oeis_isolated.py applies it before append_disproof, and the same
function rewrote the 14 committed OEIS specs (21 modifier sites; privacy
has no semantic effect beyond name visibility/mangling). The formerly
rejected oeis_A258667_conjecture_0 gold proof now passes end-to-end, so
MODULE_DRIFT_STEMS empties and the stem becomes the fix's regression
guard; _gold_submission mirrors the strip since the vendored gold files
stay verbatim.

Deliberately scoped to OEIS: the 5 erdos/fc100open private rejects and
the OEIS instance-collision case (oeis_341685_conjecture_0) remain
documented fail-closed limitations, pinned with the widened static net
(binder-form anonymous instances, deriving) in CANDIDATE_IDS /
CONFIRMED_REJECT_IDS and guarded by tests/test_comparator_drift.py.
Amend plan §3.3 accordingly.

* Compare stripped-private OEIS statements modulo the name mangle

The structural gate failed on 21702e5: it compares the isolated target's
raw-Expr type string byte-for-byte against the source's, and for the
samples whose statement references a formerly-private helper the constant
name now differs (`_private.0.products` in the still-private source vs
`products` in the stripped spec; the extractor elaborates under an
anonymous main module, which is also why both sides matched pre-strip).
The strings are otherwise identical, and the compile/certifier/oracle
gates and the gold sweep all passed.

Hardcode the exception: for exactly the 14 stripped samples
(STRIPPED_PRIVATE_IDS, pinned pure-python against the committed files so
a regenerate can't widen it silently), the structural and oracle
comparisons apply the test-local unmangle_private_names to both sides;
every other sample stays byte-strict. Verified by replicating the gate
in the generate image over all 14 + 2 controls: all pass.

* Replace migration smoke configs/subsets with one smoke_easy subset

The comparator-smoke eval configs and their three OEIS subsets (proof,
disproof, hard) were one-off migration-validation artifacts. Keep the
reusable piece as a single subset: smoke_easy, the 5 shortest-gold-proof
tsoukalas_proved_38 conjectures plus the 5 robustly false ones disproved
in every historical result run -- a quick end-to-end pipeline shakeout
covering both answer paths.

* Remove fable/gpt-sol red-team eval configs

The comparator-redteam.yaml (fable) and comparator-redteam-sol.yaml
(gpt-5.6-sol) launch configs were one-off runs pinned to the
giles-hawkbench-newstuff models branch; drop them from the tree.

* Give the red-team agent container internet access

The red-team adversary benefits from reaching the internet (fetch tools,
references, payloads) from its workspace. Grant it entirely in red-team
code: _sandbox_with_agent_internet post-processes the shared
get_sandbox_config output -- dropping network_mode:none (docker) /
networkIsolated:False (k8s) on the agent `default` service only -- and
writes a distinct redteam-internet.* file so the base config other tasks
read is untouched. The `comparator` verifier service stays
network-isolated, so soundness is still tested against the hardened
verifier; only the adversary's own box gets a network.

* Red-team k8s internet: grant world egress, not just un-isolate

The agent still had no network on k8s: the agent-env chart's egress is a
namespace-wide Cilium allow driven by the top-level
allowDomains/allowEntities/allowCIDR (empty == offline). Setting the
agent's networkIsolated:False only dropped its per-service deny; nothing
granted egress. Add top-level allowEntities:["world"] (opens the internet
and enables `*` DNS). The comparator keeps networkIsolated:True, whose
per-service egressDeny/ingressDeny wins over the namespace allow in
Cilium, so the verifier stays fully offline. Docker path unchanged.

* Simplify the comparator sandbox to the threat-model minimum

The security obligation reduces to one invariant: every check starts
with .lake bit-identical to pristine (landrun confines the untrusted
solution build to writes in .lake + /dev, and /dev is a sink no trusted
step reads). Mechanism now:

- Drop the read-only rootfs and size-capped tmpfs/emptyDir mounts from
  both backend configs (they defended against out-of-model events;
  disk exhaustion is fail-closed).
- reset-workspace.sh -> reset-dotlake.sh: rm -rf .lake and recreate it
  from /opt/pristine (skeleton copy + packages symlink); the /tmp and
  run/ wipes were outside the write grant. The checker recreates run/
  itself before staging each check's inputs.
- Run the comparator image as a non-privileged user (comparator README
  assumption 6, a stated precondition of its soundness guarantee).
  /root becomes traverse-only 711 for the toolchain paths; a system
  git safe.directory setting keeps lake from misreading the root-owned
  pristine packages as "URL has changed" (and trying to re-clone them).
- Delete comparator-migration-plan.md; existing section references
  resolve in git history.

Validated: non-root container smoke (reset, staging, lake env, both
binaries link, /root sealed, pristine unwritable) and an end-to-end
comparator check via test_comparator_security. Version bumped to
0.1.7rc1 so the image tags roll.

* Document why the sandbox configs stay backend-native (comparator#83)

k8s_sandbox can auto-convert compose files to Helm values, and with the
hardening keys gone almost everything we set is expressible through its
x-inspect_k8s_sandbox extensions. Record the one reason we still write
the values file directly: the runtimeClassName pin is silently unsound
to lose while comparator#83 is open (gvisor has no Landlock; landrun's
--best-effort disables itself without error), so it must not ride
through a translation layer. Revisit collapsing to compose-only once
the upstream fix makes a missing runtime fail loudly.

* Collapse the four pytest CI jobs into a matrix

The suites differed only in their pytest args; one matrix job keeps the
same check-run names (tests, comparator-tests, isolation, gold-proofs),
fail-fast off preserves their independence, and adding a suite is now a
matrix entry. Trim the multi-line job comments to one line each.

* Keep the stub Challenge/Solution artifacts in the pristine tree

Drop the find that stripped them: the reset copies them into each
check's .lake, where lake's content-hash traces make them inert --
staged files hash differently and rebuild, and reuse on a byte-identical
file is the same compilation. Also note on the stub build itself that it
exists to fail plumbing errors at image build rather than at the first
check.

* Resolve lean4export/landrun via comparator's PATH defaults

Install lean4export in /usr/local/bin beside landrun and drop the
COMPARATOR_* env indirection everywhere: the Dockerfile ENV block and
the checker's per-exec env dict (comparator's own defaults are PATH
lookups, its README-preferred setup). The vuln test keeps the attack
target's baked path as a local constant, and its comment now states the
current write-denial story (landrun + root-owned binary vs the
non-privileged user) instead of the removed read-only rootfs.

Validated by a rebuilt-image smoke (no COMPARATOR_* in env, PATH
resolution) and an end-to-end comparator check.

* Set only a memory limit on the k8s services

k8s defaults the request to the limit, so scheduling still reserves the
memory and the pod keeps Guaranteed QoS on it; CPU is compressible, so
the CPU request/limit knobs bought nothing. Also drop the extensions
aside from the backend-native rationale comment.

* Bump version to 0.1.7rc2

* Keep Kubernetes agent under gVisor

* Bump version to 0.1.7

* Remove resolved TODO

* Treat Comparator execution as one phase

* Score an exec output overflow INCORRECT instead of erroring the sample

Inspect's sandbox exec raises OutputLimitExceededError past its 10 MiB
output cap, and SandboxComparator.check caught only TimeoutError and
UnicodeDecodeError -- so a submission whose build printed enough (an
elaboration-time #eval loop, fully agent-controlled) escaped its
INCORRECT into an uncaught scoring error. Output volume is
submission-controlled for the same reason non-UTF-8 bytes are (the
trusted phases print a handful of progress lines), so map it to a
comparator_output_limit rejection like the other submission-attributed
exec failures.

* Build the gold-proof disproof line via disproof_declaration

The staging transform hand-formatted the appended disproof declaration
as an f-string literal that had to stay byte-identical to what
scripts.isolation.disproof_declaration produces (the generators use it
for every committed spec). Call the shared helper instead, so a change
to the disproof shape cannot leave the gold-proof staging appending a
stale spelling and rejecting every case for the wrong reason.

* Derive the redteam/primitives FC util import from the pin's profile

COLLATZ_SPEC and the primitive-constants probe hardcoded
'import FormalConjectures.Util.ProblemImports' while building against
fc_commit(OEIS_DIR)'s image -- the two remaining sites bypassing the
fc_profile registry that exists exactly to absorb upstream's util-module
rename. Both now render the import from
fc_profile(pin).util_module, so an OEIS pin bump past the rename can no
longer silently break the red-team task's challenge phase or fail the
primitives probe at compile time.

* Render the prompt's axiom list from checker.PERMITTED_AXIOMS

The prompt duplicated the permitted-axiom list as a local literal, with
only the checker comment 'Mirrored in the prompt' holding the two in
sync -- a change to one without the other would tell agents they may
build on an axiom the verifier rejects. user_prompt now renders from
the same tuple comparator_config enforces, and the checker comment
states the new relationship.

* Strip private modifiers from erdos isolated specs (comparator#58 drift)

Erdos101.erdos_101 -- a live bloom_selection member -- was a confirmed
false reject: its private linesWithPointsFor sits in the target's
closure, so the byte-identical Challenge/Solution builds elaborate to
different module-mangled names (_private.Challenge.0... vs
_private.Solution.0...) and Comparator rejects every faithful
submission. Adopt the OEIS generator's strip_private for the erdos
generator and apply it to the one affected committed spec (privacy has
no semantic effect beyond name mangling, and no downstream generation
step touches the line, so the committed edit is byte-equivalent to a
regenerate -- verified by running the strip over the whole corpus:
only this file changes). Erdos101 accordingly leaves
CANDIDATE_IDS/CONFIRMED_REJECT_IDS; the fc100open private case remains
the documented fail-closed limitation. The container isolation suite
re-certifies the spec (statement certificate, disproof certification,
compile) in CI.

* Test Comparator quoted declaration names

* Require explicit proof claims in postprocessing

* Treat Comparator output overflow as resource failure

* Drop comparator privileges per exec instead of a USER directive

Inspect does not play nice with images whose default user is non-root
(write_file's tee/mkdir run as that user, and k8s_sandbox implements
exec's user= switch with runuser, which only root may invoke), so the
comparator image now ends as root and the checker passes
user=COMPARATOR_USER on every exec against that sandbox instead. All
scoring work still runs as the non-privileged user; staged inputs
written as root land 0644 inside the comparator-owned run/.

* Bump version to 0.1.8 for rebuilt sandbox images

* Run the .lake reset as root to clear permission traps

The untrusted build owns what it creates under .lake and Landlock does
not govern chmod, so it can leave a nonempty mode-000 directory that a
comparator-user rm -rf cannot traverse -- turning the next check's
trusted reset into an infrastructure error instead of a verdict. The
checker now execs reset-dotlake.sh as root (rm is then immune to
permission traps) and the script drops back to the comparator user for
the pristine copy, so .lake stays comparator-owned. Regression-tested
end to end: a submission plants the trap via IO.setAccessRights and the
following honest check must still score.

* Use in-process IO for landrun-confined tamper snippets

The submission build runs under landrun, which blocks spawning a
subprocess, so the IO.Process.output lines in these #eval attacks were
silent no-ops. Drop the dead `rm` fallback in the packages-poisoning
snippet (removeFile already unlinks the symlink) and replace the
subprocess chmod in the exporter-tamper snippet with in-process
IO.setAccessRights. Both e2e cases still reject.

* Bump version to 0.1.9 for rebuilt sandbox images
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant