From 3398874f7330bf37c46dbf2cf193e06cc32e6a20 Mon Sep 17 00:00:00 2001 From: samuel <13449963+samuelGrahame@users.noreply.github.com> Date: Thu, 10 Sep 2026 07:07:47 +1000 Subject: [PATCH 1/2] Avoid speculative type requirements in reconstruction prompts --- src/re_agent/agents/prompts/checker_system.md | 2 +- src/re_agent/agents/prompts/reverser_task.md | 4 ++-- tests/test_agents/test_prompt_contract.py | 18 ++++++++++++++++++ 3 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 tests/test_agents/test_prompt_contract.py diff --git a/src/re_agent/agents/prompts/checker_system.md b/src/re_agent/agents/prompts/checker_system.md index 8936d453..3287894c 100644 --- a/src/re_agent/agents/prompts/checker_system.md +++ b/src/re_agent/agents/prompts/checker_system.md @@ -2,7 +2,7 @@ You are a reverse engineering quality checker. Your job is to verify that revers Verification standards: - Every line of Ghidra logic must have corresponding source code -- Every struct offset must map to a named member +- Use supplied type evidence to verify named members. When no layout is available, accept explicit byte offsets that preserve access widths and semantics; do not fail code solely for lacking member names or demand invented structs. - Every function call must be identified and matched - Expression order must match exactly (floating point is order-sensitive) - No missing branches, conditions, or edge cases diff --git a/src/re_agent/agents/prompts/reverser_task.md b/src/re_agent/agents/prompts/reverser_task.md index 891fcb86..c0e714d1 100644 --- a/src/re_agent/agents/prompts/reverser_task.md +++ b/src/re_agent/agents/prompts/reverser_task.md @@ -24,8 +24,8 @@ ${project_rules} Requirements: 1. Match every branch and call from the decompile -2. Map all offsets (e.g. param_1 + 0x88) to real member names +2. Use member names only when supported by supplied type evidence. Otherwise preserve the exact byte offset and access width, and document the unresolved layout; do not invent structs or member names. 3. Preserve exact expression/operand order 4. Use existing project patterns and naming conventions -5. Output the complete function implementation in a ```cpp block +5. Output exactly one complete function implementation in a ```cpp block. Do not wrap it in namespace/class definitions or add helper type/function definitions; report missing declarations outside the code block. 6. End with: REVERSED_FUNCTION: ${class_name}::${function_name} (${address}) diff --git a/tests/test_agents/test_prompt_contract.py b/tests/test_agents/test_prompt_contract.py new file mode 100644 index 00000000..03c06ed3 --- /dev/null +++ b/tests/test_agents/test_prompt_contract.py @@ -0,0 +1,18 @@ +"""Keep reconstruction/review instructions consistent with missing type evidence.""" +from re_agent.agents.checker import PROMPTS_DIR +from re_agent.utils.templates import render_template + + +def test_missing_layout_does_not_require_invented_members(): + reverser = render_template(PROMPTS_DIR / "reverser_task.md") + checker = render_template(PROMPTS_DIR / "checker_system.md") + assert "preserve the exact byte offset and access width" in reverser + assert "do not invent structs or member names" in reverser + assert "accept explicit byte offsets" in checker + assert "do not fail code solely for lacking member names" in checker + + +def test_requested_candidate_matches_single_body_overlay_contract(): + prompt = render_template(PROMPTS_DIR / "reverser_task.md") + assert "exactly one complete function implementation" in prompt + assert "report missing declarations outside the code block" in prompt From 13abe167613542eaeb913b4051a55e578101face Mon Sep 17 00:00:00 2001 From: samuel <13449963+samuelGrahame@users.noreply.github.com> Date: Thu, 10 Sep 2026 07:07:47 +1000 Subject: [PATCH 2/2] Skip model reviews for candidates that fail local preflight --- src/re_agent/agents/loop.py | 23 +++++++++++++++-- tests/test_agents/test_preflight.py | 39 +++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 tests/test_agents/test_preflight.py diff --git a/src/re_agent/agents/loop.py b/src/re_agent/agents/loop.py index 36a028b0..fc0d4487 100644 --- a/src/re_agent/agents/loop.py +++ b/src/re_agent/agents/loop.py @@ -25,6 +25,7 @@ from re_agent.llm.observed import CallBudget, ObservedProvider from re_agent.llm.protocol import LLMProvider from re_agent.parity.source_indexer import SourceIndexer +from re_agent.verification.candidate import extract_candidate_body from re_agent.verification.objective import verify_candidate @@ -128,8 +129,25 @@ def run_fix_loop( log_path = log_dir / f"round{round_num}-{timestamp}-reverser.json" log_path.write_text(json.dumps(log_entry, indent=2), encoding="utf-8") - # Check - verdict = checker.check(code, target) + # Avoid spending a reviewer call on output the configured candidate gate + # cannot consume. Keep the same repair/checkpoint path as other failures. + preflight_error = None + if candidate_gate is not None: + try: + extract_candidate_body(code) + except ValueError as exc: + preflight_error = str(exc) + if preflight_error: + checker.last_prompt = "" + checker.last_response = "" + verdict = CheckerVerdict( + verdict=Verdict.FAIL, + summary="Local candidate preflight failed; model review skipped", + issues=[preflight_error], + fix_instructions=["Return exactly one complete function definition without helper definitions."], + ) + else: + verdict = checker.check(code, target) last_verdict = verdict objective_verdict: ObjectiveVerdict | None = None @@ -148,6 +166,7 @@ def run_fix_loop( "round": round_num, "timestamp": timestamp, "phase": "check", + "checker_skipped": preflight_error is not None, "prompt": checker.last_prompt, "response": checker.last_response, "verdict": verdict.verdict.value, diff --git a/tests/test_agents/test_preflight.py b/tests/test_agents/test_preflight.py new file mode 100644 index 00000000..5dd0347d --- /dev/null +++ b/tests/test_agents/test_preflight.py @@ -0,0 +1,39 @@ +"""Reject unusable candidate shapes before spending reviewer calls.""" +import json + +import pytest + +from re_agent.agents.loop import run_fix_loop +from re_agent.backend.stub import StubBackend +from re_agent.core.models import FunctionTarget +from tests.test_agents.test_loop import MockLLM + + +@pytest.mark.parametrize("invalid", [ + "struct Layout { int field; }; int f(Layout* p) { return p->field; }", + "int f() { return 0;", + "int f() { return 0; } int g() { return 1; }", +]) +def test_invalid_candidate_skips_reviewer_then_repairs_within_budget(tmp_path, invalid): + reverser = MockLLM([f"```cpp\n{invalid}\n```", "```cpp\nint f() { return 0; }\n```"]) + checker = MockLLM(['{"verdict":"PASS","summary":"Matches","issues":[],"fix_instructions":[]}']) + result = run_fix_loop(FunctionTarget("100", "", "f"), StubBackend(), reverser, checker, + candidate_gate=lambda result: result, objective_verifier_enabled=False, + max_rounds=2, max_llm_calls=3, log_dir=tmp_path) + assert result.success + assert result.rounds_used == 2 + assert reverser._idx == 2 + assert checker._idx == 1 + log = next(tmp_path.glob("*/round1-*-checker.json")) + assert json.loads(log.read_text())["checker_skipped"] is True + + +def test_repeated_invalid_candidates_never_call_reviewer(tmp_path): + reverser = MockLLM(["```cpp\nint f() {\n```"]) + checker = MockLLM(["VERDICT: PASS"]) + result = run_fix_loop(FunctionTarget("100", "", "f"), StubBackend(), reverser, checker, + candidate_gate=lambda result: result, objective_verifier_enabled=False, + max_rounds=3, log_dir=tmp_path) + assert not result.success + assert checker._idx == 0 + assert result.error and "without progress" in result.error