Skip to content

Commit 3b9cf84

Browse files
fluffy314cursoragent
authored andcommitted
fix(autoresearch): migrate legacy OProver certifications
Fail closed on every Lean warning and provide a content-addressed bulk audit that revokes historical hash-only or invalid proof claims without touching mathematical source evidence. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 02ae454 commit 3b9cf84

5 files changed

Lines changed: 600 additions & 1 deletion

File tree

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
"""Strict migration helpers for historical OProver certification metadata."""
2+
from __future__ import annotations
3+
4+
import hashlib
5+
import json
6+
import os
7+
import re
8+
import time
9+
from pathlib import Path
10+
from typing import Mapping
11+
12+
13+
STRICT = "STRICTLY_INDEPENDENTLY_VERIFIED"
14+
SHORTCUT = "INVALID_PROOF_SHORTCUT"
15+
INCOMPLETE = "LEGACY_INCOMPLETE"
16+
RECOMPILE_FAILED = "RECOMPILE_FAILED"
17+
MISMATCH = "HASH/ENV_MISMATCH"
18+
REVOKED = "CERTIFICATION_REVOKED"
19+
20+
_HOLE = re.compile(r"\b(?:sorry|admit|sorryAx)\b|\bby\?", re.IGNORECASE)
21+
_WARNING = re.compile(r"(?mi)^.*\bwarning:")
22+
23+
24+
def _canonical(value: object) -> bytes:
25+
return json.dumps(
26+
value, ensure_ascii=False, sort_keys=True, separators=(",", ":"),
27+
).encode()
28+
29+
30+
def _sha256(value: str | bytes) -> str:
31+
if isinstance(value, str):
32+
value = value.encode()
33+
return hashlib.sha256(value).hexdigest()
34+
35+
36+
def artifact_integrity(payload: Mapping[str, object]) -> bool:
37+
"""Validate all self-contained hashes in a retained proof artifact."""
38+
proof = str(payload.get("proof_body", ""))
39+
identity = {
40+
key: value for key, value in payload.items()
41+
if key not in {"artifact_hash", "created_at"}
42+
}
43+
return (
44+
bool(payload.get("artifact_hash"))
45+
and _sha256(_canonical(identity)) == payload.get("artifact_hash")
46+
and _sha256(proof) == payload.get("proof_body_hash")
47+
and _sha256(proof) == payload.get("candidate_hash")
48+
and _sha256(str(payload.get("lean_output", "")))
49+
== payload.get("lean_output_hash")
50+
and len(proof.encode()) == payload.get("proof_byte_length")
51+
)
52+
53+
54+
def classify_candidate(
55+
candidate_hash: str,
56+
artifacts: Mapping[str, Mapping[str, object]],
57+
recompiles: Mapping[str, bool] | None = None,
58+
) -> str:
59+
"""Classify one historically verified candidate under strict policy."""
60+
artifact = artifacts.get(candidate_hash)
61+
if artifact is None:
62+
return INCOMPLETE
63+
if not artifact_integrity(artifact):
64+
return MISMATCH
65+
proof = str(artifact.get("proof_body", ""))
66+
if _HOLE.search(proof):
67+
return SHORTCUT
68+
if _WARNING.search(str(artifact.get("lean_output", ""))):
69+
return RECOMPILE_FAILED
70+
if recompiles is None or candidate_hash not in recompiles:
71+
return INCOMPLETE
72+
return STRICT if recompiles[candidate_hash] else RECOMPILE_FAILED
73+
74+
75+
def _contains_verified_status(value: object) -> bool:
76+
if isinstance(value, dict):
77+
if (
78+
value.get("status") == "INDEPENDENTLY_VERIFIED"
79+
or value.get("proof_status") == "INDEPENDENTLY_VERIFIED"
80+
):
81+
return True
82+
return any(_contains_verified_status(child) for child in value.values())
83+
if isinstance(value, list):
84+
return any(_contains_verified_status(child) for child in value)
85+
return False
86+
87+
88+
def _candidate_fields(value: object) -> set[str]:
89+
found: set[str] = set()
90+
if isinstance(value, dict):
91+
for key in ("verified_candidate_hash", "selected_candidate_hash"):
92+
if value.get(key):
93+
found.add(str(value[key]))
94+
for child in value.values():
95+
found.update(_candidate_fields(child))
96+
elif isinstance(value, list):
97+
for child in value:
98+
found.update(_candidate_fields(child))
99+
return found
100+
101+
102+
def collect_verified_candidate_hashes(value: object) -> set[str]:
103+
"""Collect hashes in records containing a historical verified status."""
104+
if not _contains_verified_status(value):
105+
return set()
106+
return _candidate_fields(value)
107+
108+
109+
def revoke_document(
110+
value: object,
111+
outcomes: Mapping[str, str],
112+
*,
113+
report_hash: str,
114+
inherited_outcome: str = "",
115+
) -> tuple[object, int]:
116+
"""Revoke every invalid status in one JSON document."""
117+
count = 0
118+
if isinstance(value, dict):
119+
candidate = (
120+
value.get("verified_candidate_hash")
121+
or value.get("selected_candidate_hash")
122+
)
123+
outcome = outcomes.get(str(candidate), "") if candidate else ""
124+
if not outcome:
125+
descendant_candidates = collect_verified_candidate_hashes(value)
126+
descendant_outcomes = {
127+
outcomes[candidate]
128+
for candidate in descendant_candidates
129+
if candidate in outcomes and outcomes[candidate] != STRICT
130+
}
131+
if len(descendant_outcomes) == 1:
132+
outcome = descendant_outcomes.pop()
133+
if not outcome:
134+
outcome = inherited_outcome
135+
if (
136+
value.get("status") == "INDEPENDENTLY_VERIFIED"
137+
and outcome
138+
and outcome != STRICT
139+
):
140+
value["revoked_status"] = value["status"]
141+
value["status"] = REVOKED
142+
value["strict_audit_outcome"] = outcome
143+
value["strict_audit_report_hash"] = report_hash
144+
count += 1
145+
if (
146+
value.get("proof_status") == "INDEPENDENTLY_VERIFIED"
147+
and outcome
148+
and outcome != STRICT
149+
):
150+
value["revoked_proof_status"] = value["proof_status"]
151+
value["proof_status"] = REVOKED
152+
value["strict_audit_outcome"] = outcome
153+
value["strict_audit_report_hash"] = report_hash
154+
count += 1
155+
if outcome and outcome != STRICT:
156+
if value.get("integration_status") == "VERIFIED_PROOF_ARTIFACT":
157+
value["integration_status"] = REVOKED
158+
if value.get("classification") == "VERIFIED_SUPPORTING_LEMMA":
159+
value["classification"] = REVOKED
160+
for key, child in tuple(value.items()):
161+
value[key], child_count = revoke_document(
162+
child, outcomes, report_hash=report_hash,
163+
inherited_outcome=outcome,
164+
)
165+
count += child_count
166+
elif isinstance(value, list):
167+
for index, child in enumerate(value):
168+
value[index], child_count = revoke_document(
169+
child, outcomes, report_hash=report_hash,
170+
inherited_outcome=inherited_outcome,
171+
)
172+
count += child_count
173+
return value, count
174+
175+
176+
def commit_revocations(
177+
documents: Mapping[Path, object],
178+
outcomes: Mapping[str, str],
179+
*,
180+
report_hash: str,
181+
journal_path: Path,
182+
) -> int:
183+
"""Stage, journal, and atomically replace each affected JSON file."""
184+
staged: list[tuple[Path, Path]] = []
185+
revoked = 0
186+
for path, original in documents.items():
187+
updated, count = revoke_document(
188+
original, outcomes, report_hash=report_hash,
189+
)
190+
if not count:
191+
continue
192+
temporary = path.with_name(f".{path.name}.{report_hash}.audit.tmp")
193+
temporary.write_bytes(_canonical(updated) + b"\n")
194+
with temporary.open("rb") as handle:
195+
os.fsync(handle.fileno())
196+
staged.append((path, temporary))
197+
revoked += count
198+
journal = {
199+
"schema_version": 1,
200+
"report_hash": report_hash,
201+
"state": "PREPARED",
202+
"created_at": time.time(),
203+
"files": [str(path) for path, _ in staged],
204+
"revoked_status_count": revoked,
205+
}
206+
journal_path.parent.mkdir(parents=True, exist_ok=True)
207+
journal_path.write_bytes(_canonical(journal) + b"\n")
208+
for path, temporary in staged:
209+
os.replace(temporary, path)
210+
journal["state"] = "COMMITTED"
211+
journal_path.write_bytes(_canonical(journal) + b"\n")
212+
return revoked

autoresearch/prefill/independent_reconstruction.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class ReconstructionStatus(str, Enum):
3737
r"\b(?:sorry|admit|sorryAx)\b|\bby\?",
3838
re.IGNORECASE,
3939
)
40+
_LEAN_WARNING = re.compile(r"(?mi)^.*\bwarning:")
4041

4142

4243
def _sha256(value: str | bytes) -> str:
@@ -246,6 +247,10 @@ def persist(
246247
raise VerifiedProofArtifactError(
247248
"VERIFIED_PROOF_REQUIRES_ACCEPTED_LEAN_RESULT"
248249
)
250+
if _LEAN_WARNING.search(lean_result.output):
251+
raise VerifiedProofArtifactError(
252+
"VERIFIED_PROOF_REQUIRES_WARNING_FREE_LEAN_RESULT"
253+
)
249254
candidate = candidate.strip()
250255
if not candidate.startswith("by"):
251256
raise VerifiedProofArtifactError("VERIFIED_PROOF_BODY_INVALID")
@@ -333,6 +338,10 @@ def load(self, reference: str | Path) -> VerifiedProofArtifact:
333338
raise VerifiedProofArtifactError("VERIFIED_PROOF_BODY_HASH_MISMATCH")
334339
if not body.get("lean_accepted") or body.get("lean_timed_out"):
335340
raise VerifiedProofArtifactError("VERIFIED_PROOF_LEAN_STATUS_INVALID")
341+
if _LEAN_WARNING.search(str(body.get("lean_output", ""))):
342+
raise VerifiedProofArtifactError(
343+
"VERIFIED_PROOF_LEAN_WARNINGS_PRESENT"
344+
)
336345
return VerifiedProofArtifact(
337346
**{
338347
**body,
@@ -497,7 +506,7 @@ def verify_candidate_with_project_lean(
497506
output = result.stdout[-8000:]
498507
accepted = (
499508
result.returncode == 0
500-
and "declaration uses 'sorry'" not in output.lower()
509+
and not _LEAN_WARNING.search(output)
501510
)
502511
return LeanResult(accepted, output, False)
503512

0 commit comments

Comments
 (0)