You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix: enforce the analysis invariant — only re_analyze runs a full analysis (#270)
* fix: enforce the analysis invariant — only re_analyze runs a full analysis
The analyze/decompile split (#256–261) gated the whole-program MCP tools on a
saved analysis at the run_tool seam, but several paths could still trigger a
cold whole-binary analysis on their own — the exact ~2547s-timeout class of
incident the split was meant to end:
* the r2 `decompile_probe` ran a cold `aaa` on a plain focus/list miss (so the
LLM `static_analysis` decompile and enrich_recon cold-analyzed directly,
bypassing run_tool's gate);
* `xrefs_probe` never loaded the warm r2 project, so a radare2-backend xref /
search re-ran `aaa` per call;
* `ghidra_probe` cold-imported on any per-call mode without a warm slot;
* `re_recover_constant` (emulate) and the `static_analysis` taint pass reach
the Ghidra ops outside run_tool's gate and cold-analyzed.
Make re_analyze the SINGLE chokepoint. Every other analysis-needing probe / tool
/ task is now warm-only: on a cold miss it returns a structured `needs_analysis`
payload pointing at re_analyze instead of analyzing. Only two EXPLICIT entry
points cold-analyze — `re_analyze` (`--analyze`, detached, generous budget) and
`re_reanalyze` (`--reanalyze`, the deliberate deeper re-analysis). Targeted
`re_disassemble`/`--range` and store-reading `re_search_decompiled` need no
analysis and stay ungated; a raw byte/immediate SEARCH also needs none and still
scans the loaded image.
Probes:
* decompile_probe: refuse a cold whole-binary decompile/list unless
--analyze/--reanalyze; return the re_analyze lead (rc=0, structured).
* xrefs_probe: reload the warm r2 project (dir.projects + -p) for the index
modes; never `aaa`; cold miss → re_analyze lead. SEARCH proceeds cold.
* ghidra_probe: install-check first (missing toolchain → "rebuild
WITH_GHIDRA=1"), then warm-only for every non-analyze mode. Warm-miss is
rc=0 + a structured payload (not a non-zero exit run_probe would raise on),
matching xrefs_probe/decompile_probe.
Host:
* new engine.re.analysis.analysis_lead() — the single host gate for the paths
that DON'T go through run_tool (recover_constant, the taint task); mirrors
_analysis_gate, best-effort.
* emulation.emulate_constant / taint.analyze_taint gate on it (the
arg-dependent pre-check runs first — a pure graph read, more actionable than
"needs_analysis").
* agent_tools._r2_project_mount wires the warm r2 slot into the xrefs/search
probe calls.
Docs: docs/mcp.md updated (r2 xref verbs are warm-only too; recover_constant +
taint gate the same way).
Tests: real-Ghidra/r2 tests now run the re_analyze step (new conftest
warm_ghidra_slot / warm_r2_slot helpers) before a warm-only op — proving the
enforcement end to end; new offline coverage for analysis_lead, the
emulate/taint gates, and xrefs_probe._warm_r2_flags. `just test` green (1648
passed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: address PR #270 review — remote-executor guard, gate dedup, lead surfacing, CI gate
Follow-up to the analysis-invariant enforcement, per the pr-reviewer findings on #270:
- MEDIUM-LOW (remote executor): `_r2_project_mount` returned a slot path regardless
of executor, but `RemoteDockerExecutor.run_probe` refuses any project_mount — so the
r2 xref family + search_code byte-scan would hard-error on a remote executor where
they previously worked. Add an explicit `supports_project_mount` capability
(True on the local executor, False on the remote one) and skip the mount when it's
unsupported, so those calls degrade (index modes → re_analyze lead; search → raw
scan) instead of erroring.
- LOW (lead swallowed): a cold-miss xrefs/callgraph payload carries the re_analyze lead
in `out["error"]` with empty results; `_run_xrefs_probe` now surfaces a
`needs_analysis` payload as the error so every caller points at re_analyze instead of
formatting an empty graph.
- NIT (duplication): `_analysis_gate` now DELEGATES to `analysis.analysis_lead` (the
same gate the recover_constant / taint paths use), so the two can't drift.
- CI: `scripts/ci_ghidra_decompile_check.py` (the WITH_GHIDRA decompile gate) did a
COLD `ghidra_probe` decompile + xrefs, which the invariant now refuses — so it
builds the warm project once via `--analyze` (bind-mounted slot) and drives the
decompile + xrefs warm, exactly the production shape. Verified locally against the
WITH_GHIDRA image (real C recovered, cgi_handler among strcpy's callers).
Offline tests added: `_r2_project_mount` degrades to None on a remote executor.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test: accept the runner kwarg in list_functions gate monkeypatch
_analysis_gate now delegates to analysis.analysis_lead (PR #270 dedup), which calls
analysis_state(project, target, runner=None). This test stubbed analysis_state with a
strict 2-arg lambda, so the runner kwarg raised TypeError → caught → gate bypassed.
Accept **kw (matching the real signature), as _fake_state already does.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
0 commit comments