Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
23 changes: 21 additions & 2 deletions src/re_agent/agents/loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/re_agent/agents/prompts/checker_system.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/re_agent/agents/prompts/reverser_task.md
Original file line number Diff line number Diff line change
Expand Up @@ -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})
39 changes: 39 additions & 0 deletions tests/test_agents/test_preflight.py
Original file line number Diff line number Diff line change
@@ -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
18 changes: 18 additions & 0 deletions tests/test_agents/test_prompt_contract.py
Original file line number Diff line number Diff line change
@@ -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
Loading