Skip to content

Commit 9685a25

Browse files
fluffy314cursoragent
authored andcommitted
fix(autoresearch): make formal root binding canonical
Derive mutable research-goal caches from the formalized ledger root and fail closed on true binding mismatches without discarding accepted contract provenance. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent bf83630 commit 9685a25

4 files changed

Lines changed: 325 additions & 2 deletions

File tree

autoresearch/prefill/supervisor.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1791,6 +1791,92 @@ def route_contract_to_subgoal_generation(
17911791
return True
17921792

17931793

1794+
def reconcile_canonical_root_binding(
1795+
checkpoint: OrchestrationCheckpoint,
1796+
ledger: dict,
1797+
*,
1798+
state_path: Path,
1799+
) -> bool:
1800+
"""Derive mutable root caches from the formalized ledger root."""
1801+
obligation = next(
1802+
(
1803+
item for item in ledger.get("obligations", ())
1804+
if item.get("obligation_id") == checkpoint.target_obligation_id
1805+
),
1806+
None,
1807+
)
1808+
if obligation is None:
1809+
raise ValueError("CANONICAL_ROOT_OBLIGATION_MISSING")
1810+
proposition_hash = str(obligation.get("proposition_hash", ""))
1811+
signature_hash = str(obligation.get("lean_signature_hash", ""))
1812+
canonical_hash = proposition_hash or signature_hash
1813+
statement = str(obligation.get("statement", ""))
1814+
if (
1815+
obligation.get("parent_id")
1816+
or obligation.get("formal_status") != "FORMALIZED"
1817+
or len(canonical_hash) != 64
1818+
or hashlib.sha256(statement.encode()).hexdigest() != canonical_hash
1819+
):
1820+
raise ValueError("CANONICAL_ROOT_BINDING_INVALID")
1821+
protected = {
1822+
"proposition_hash": checkpoint.proposition_hash,
1823+
"parent_statement_sha256": checkpoint.parent_statement_sha256,
1824+
"parent_signature_sha256": checkpoint.parent_signature_sha256,
1825+
}
1826+
if any(
1827+
value and value != canonical_hash for value in protected.values()
1828+
):
1829+
raise ValueError("CANONICAL_ROOT_PROPOSITION_MISMATCH")
1830+
try:
1831+
cached = json.loads(state_path.read_text(encoding="utf-8"))
1832+
except FileNotFoundError:
1833+
cached = {"schema_version": 1}
1834+
if not isinstance(cached, dict):
1835+
raise ValueError("DERIVED_ROOT_GOAL_CACHE_INVALID")
1836+
changed = any((
1837+
checkpoint.root_goal_sha256 != canonical_hash,
1838+
checkpoint.proposition_hash != canonical_hash,
1839+
checkpoint.parent_statement_sha256 != canonical_hash,
1840+
checkpoint.parent_signature_sha256 != canonical_hash,
1841+
checkpoint.target_statement != statement,
1842+
ledger.get("root_goal_hash") != canonical_hash,
1843+
cached.get("research_goal") != statement,
1844+
))
1845+
if not changed:
1846+
return False
1847+
checkpoint.root_goal_sha256 = canonical_hash
1848+
checkpoint.proposition_hash = canonical_hash
1849+
checkpoint.parent_statement_sha256 = canonical_hash
1850+
checkpoint.parent_signature_sha256 = canonical_hash
1851+
checkpoint.target_statement = statement
1852+
ledger["root_goal_hash"] = canonical_hash
1853+
cached["research_goal"] = statement
1854+
cached.setdefault("schema_version", 1)
1855+
encoded = json.dumps(cached, indent=2, ensure_ascii=False) + "\n"
1856+
temporary = state_path.with_name(
1857+
f".{state_path.name}.{os.getpid()}.canonical-root.tmp",
1858+
)
1859+
temporary.write_text(encoded, encoding="utf-8")
1860+
os.chmod(temporary, 0o600)
1861+
os.replace(temporary, state_path)
1862+
checkpoint.recovery_events.append({
1863+
"event_type": "CANONICAL_ROOT_BINDING_RECONCILED",
1864+
"event_id": hashlib.sha256(
1865+
(
1866+
checkpoint.target_obligation_id
1867+
+ canonical_hash
1868+
+ checkpoint.research_contract_id
1869+
).encode()
1870+
).hexdigest(),
1871+
"target_obligation_id": checkpoint.target_obligation_id,
1872+
"proposition_hash": canonical_hash,
1873+
"research_contract_id": checkpoint.research_contract_id,
1874+
"derived_cache": str(state_path.name),
1875+
"created_at": time.time(),
1876+
})
1877+
return True
1878+
1879+
17941880
def is_contract_bound_subgoal_resume(
17951881
checkpoint: OrchestrationCheckpoint | None,
17961882
) -> bool:
@@ -2277,6 +2363,31 @@ def run_iteration(args, iteration: int) -> dict:
22772363
orchestration_checkpoint = load_orchestration_checkpoint(
22782364
orchestration_state_path,
22792365
)
2366+
normalized_ledger = asdict(ledger_object)
2367+
canonical_target = next(
2368+
(
2369+
item for item in normalized_ledger.get("obligations", ())
2370+
if item.get("obligation_id")
2371+
== (
2372+
orchestration_checkpoint.target_obligation_id
2373+
if orchestration_checkpoint is not None else ""
2374+
)
2375+
),
2376+
{},
2377+
)
2378+
if (
2379+
orchestration_checkpoint is not None
2380+
and canonical_target.get("proposition_hash")
2381+
and reconcile_canonical_root_binding(
2382+
orchestration_checkpoint,
2383+
normalized_ledger,
2384+
state_path=state_path,
2385+
)
2386+
):
2387+
save_orchestration_checkpoint(
2388+
orchestration_state_path,
2389+
orchestration_checkpoint,
2390+
)
22802391
if (
22812392
orchestration_checkpoint is not None
22822393
and ledger_object is not None

scripts/agent_gan_repl.py

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5344,7 +5344,57 @@ def run_certified_decomposition(
53445344
ledger_id=ledger.ledger_id,
53455345
ledger_version=ledger.version,
53465346
)
5347-
if orchestration_checkpoint is None or mismatch:
5347+
if orchestration_checkpoint is not None and mismatch:
5348+
orchestration_checkpoint.adapter_blocked(
5349+
"CANONICAL_BINDING_OWNERSHIP_REQUIRED:" + mismatch,
5350+
status="INTEGRATION_BLOCKED",
5351+
)
5352+
orchestration_checkpoint.recovery_events.append({
5353+
"event_type": "CANONICAL_BINDING_MISMATCH_REJECTED",
5354+
"event_id": (
5355+
"canonical-binding-mismatch:"
5356+
+ hashlib.sha256(
5357+
(
5358+
mismatch
5359+
+ orchestration_checkpoint.target_obligation_id
5360+
+ orchestration_checkpoint.proposition_hash
5361+
).encode()
5362+
).hexdigest()[:20]
5363+
),
5364+
"reason": mismatch,
5365+
"preserved_artifact_hashes": sorted(
5366+
reference.sha256
5367+
for reference
5368+
in orchestration_checkpoint.validated_artifacts.values()
5369+
),
5370+
"preserved_research_contract_id": (
5371+
orchestration_checkpoint.research_contract_id
5372+
),
5373+
"created_at": time.time(),
5374+
})
5375+
save_orchestration_checkpoint(
5376+
checkpoint_path,
5377+
orchestration_checkpoint,
5378+
)
5379+
return DecompositionCertificateResult(
5380+
verified=False,
5381+
errors=[
5382+
"CANONICAL_BINDING_OWNERSHIP_REQUIRED:" + mismatch,
5383+
],
5384+
artifacts={},
5385+
artifact_hashes={},
5386+
transcripts={},
5387+
role_run_ids={},
5388+
validation={
5389+
"host_gates_passed": False,
5390+
"blocked": True,
5391+
"failure_status": "INTEGRATION_BLOCKED",
5392+
"preserved_research_contract_id": (
5393+
orchestration_checkpoint.research_contract_id
5394+
),
5395+
},
5396+
)
5397+
if orchestration_checkpoint is None:
53485398
previous_checkpoint = orchestration_checkpoint
53495399
orchestration_checkpoint = OrchestrationCheckpoint(
53505400
state=ProofState.DEFINITION_AUDITOR.value,
@@ -8845,6 +8895,16 @@ def get_stats():
88458895
decomposition_target = (
88468896
resume_checkpoint.target_obligation_id
88478897
)
8898+
canonical_root_goal = next(
8899+
item.statement for item in proof_ledger.obligations
8900+
if item.obligation_id == decomposition_target
8901+
and not item.parent_id
8902+
and item.formal_status == "FORMALIZED"
8903+
and (
8904+
item.proposition_hash
8905+
or item.lean_signature_hash
8906+
) == resume_checkpoint.proposition_hash
8907+
)
88488908
target_ids = {decomposition_target}
88498909
isolated_role_stages = []
88508910

@@ -8891,7 +8951,7 @@ def direct_review_role(
88918951
certificate = run_certified_decomposition(
88928952
proof_ledger,
88938953
decomposition_target,
8894-
research_goal,
8954+
canonical_root_goal,
88958955
direct_review_role,
88968956
project_root=Path(__file__).resolve().parents[1],
88978957
orchestration_id=orchestration_id,

tests/inference_engine/bench/test_autoresearch_supervisor.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import hashlib
12
import json
23
import os
34
import pytest
5+
from dataclasses import asdict
46
from types import SimpleNamespace
57

68
from autoresearch.prefill.live_status import AtomicLiveStatus
@@ -46,6 +48,7 @@
4648
parse_strategy_candidate_transport,
4749
parse_research_verdict,
4850
read_results,
51+
reconcile_canonical_root_binding,
4952
repair_research_contract_artifact_dependency,
5053
repair_candidate_schema,
5154
recover_contract_subgoal_duplicate_block,
@@ -182,6 +185,92 @@ def test_research_contract_dependency_repairs_to_artifact_hash(tmp_path):
182185
assert not repair_research_contract_artifact_dependency(checkpoint)
183186

184187

188+
def test_stale_root_goal_cache_reconciles_without_losing_contract(tmp_path):
189+
canonical = hashlib.sha256(b"RiemannHypothesis").hexdigest()
190+
state_path = tmp_path / "agent_state.json"
191+
state_path.write_text(json.dumps({
192+
"schema_version": 1,
193+
"research_goal": "stale Hilbert-Polya cache",
194+
}))
195+
ledger = {
196+
"ledger_id": "rh",
197+
"version": 96,
198+
"obligations": [{
199+
"obligation_id": "RH-C0-root",
200+
"statement": "RiemannHypothesis",
201+
"status": "UNRESOLVED",
202+
"parent_id": "",
203+
"formal_status": "FORMALIZED",
204+
"lean_signature_hash": canonical,
205+
"proposition_hash": canonical,
206+
}],
207+
}
208+
checkpoint = OrchestrationCheckpoint(
209+
state=ProofState.DECOMPOSER.value,
210+
current_role="decomposer",
211+
target_obligation_id="RH-C0-root",
212+
target_statement="RiemannHypothesis",
213+
proposition_hash=canonical,
214+
parent_statement_sha256=canonical,
215+
parent_signature_sha256=canonical,
216+
root_goal_sha256=hashlib.sha256(b"stale").hexdigest(),
217+
selected_strategy_plan_id="SP-root",
218+
research_contract_id="RC-root",
219+
research_contract_hash="contract-hash",
220+
)
221+
assert reconcile_canonical_root_binding(
222+
checkpoint,
223+
ledger,
224+
state_path=state_path,
225+
)
226+
assert checkpoint.root_goal_sha256 == canonical
227+
assert checkpoint.research_contract_id == "RC-root"
228+
assert checkpoint.research_contract_hash == "contract-hash"
229+
assert ledger["root_goal_hash"] == canonical
230+
assert json.loads(state_path.read_text())["research_goal"] == (
231+
"RiemannHypothesis"
232+
)
233+
assert not reconcile_canonical_root_binding(
234+
checkpoint,
235+
ledger,
236+
state_path=state_path,
237+
)
238+
239+
240+
def test_true_canonical_proposition_mismatch_preserves_checkpoint(tmp_path):
241+
canonical = hashlib.sha256(b"RiemannHypothesis").hexdigest()
242+
state_path = tmp_path / "agent_state.json"
243+
state_path.write_text(json.dumps({
244+
"schema_version": 1,
245+
"research_goal": "stale cache",
246+
}))
247+
ledger = {"obligations": [{
248+
"obligation_id": "RH-C0-root",
249+
"statement": "RiemannHypothesis",
250+
"parent_id": "",
251+
"formal_status": "FORMALIZED",
252+
"proposition_hash": canonical,
253+
}]}
254+
checkpoint = OrchestrationCheckpoint(
255+
target_obligation_id="RH-C0-root",
256+
proposition_hash="f" * 64,
257+
research_contract_id="RC-root",
258+
research_contract_hash="contract-hash",
259+
)
260+
before = asdict(checkpoint)
261+
with pytest.raises(
262+
ValueError,
263+
match="CANONICAL_ROOT_PROPOSITION_MISMATCH",
264+
):
265+
reconcile_canonical_root_binding(
266+
checkpoint,
267+
ledger,
268+
state_path=state_path,
269+
)
270+
assert asdict(checkpoint) == before
271+
assert json.loads(state_path.read_text())["research_goal"] == "stale cache"
272+
273+
185274
def test_live_status_atomic_transitions_and_permissions(tmp_path):
186275
path = tmp_path / "proof_live_status.json"
187276
status = AtomicLiveStatus(

tests/inference_engine/bridge/test_agent_gan_repl.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3570,6 +3570,69 @@ def always_malformed(role, messages, expected_run_id):
35703570
assert len(calls) == calls_before_restart
35713571

35723572

3573+
def test_true_binding_mismatch_preserves_accepted_contract(tmp_path):
3574+
statement = "RiemannHypothesis"
3575+
root_hash = hashlib.sha256(statement.encode()).hexdigest()
3576+
ledger = ProofObligationLedger(
3577+
"rh",
3578+
[ProofObligation(
3579+
"RH-C0-root",
3580+
statement,
3581+
formal_status="FORMALIZED",
3582+
lean_signature_hash=root_hash,
3583+
proposition_hash=root_hash,
3584+
)],
3585+
version=96,
3586+
)
3587+
state_path = tmp_path / "orchestration.json"
3588+
checkpoint = OrchestrationCheckpoint(
3589+
state=ProofState.DECOMPOSER.value,
3590+
current_role="decomposer",
3591+
target_obligation_id="RH-C0-root",
3592+
candidate_sha256="candidate-hash",
3593+
parent_statement_sha256=root_hash,
3594+
parent_signature_sha256=root_hash,
3595+
root_goal_sha256=root_hash,
3596+
proposition_hash=root_hash,
3597+
research_contract_id="RC-root",
3598+
research_contract_hash="contract-hash",
3599+
ledger_id="rh",
3600+
ledger_version=96,
3601+
)
3602+
artifact = persist_validated_artifact(
3603+
state_path,
3604+
checkpoint,
3605+
role="research_contract",
3606+
payload={"schema_version": 1, "contract_id": "RC-root"},
3607+
dependencies=[],
3608+
source_run_id="host:contract",
3609+
)
3610+
runner, calls = _certificate_runner()
3611+
result = run_certified_decomposition(
3612+
ledger,
3613+
"RH-C0-root",
3614+
"Different proposition",
3615+
runner,
3616+
project_root=tmp_path,
3617+
orchestration_id="orch-mismatch",
3618+
signature_validator=_fake_signature_validator,
3619+
proof_validator=_fake_proof_validator,
3620+
checkpoint_path=state_path,
3621+
candidate_sha256="candidate-hash",
3622+
)
3623+
assert result.validation["blocked"] is True
3624+
assert result.validation["preserved_research_contract_id"] == "RC-root"
3625+
assert calls == []
3626+
preserved = load_orchestration_checkpoint(state_path)
3627+
assert preserved.research_contract_id == "RC-root"
3628+
assert preserved.research_contract_hash == "contract-hash"
3629+
assert preserved.validated_artifacts["research_contract"].sha256 == (
3630+
artifact.sha256
3631+
)
3632+
assert preserved.proof_state == ProofState.DECOMPOSER
3633+
assert preserved.adapter_status == "INTEGRATION_BLOCKED"
3634+
3635+
35733636
@pytest.mark.skip(reason="legacy model-authored artifact execution is read-only")
35743637
def test_resume_binding_mismatch_invalidates_cached_artifacts(tmp_path):
35753638
state_path = tmp_path / "orchestration.json"

0 commit comments

Comments
 (0)