Skip to content

Commit 737abf4

Browse files
committed
OPTIONAL-ADAPTERS: add optional adapter stubs + tests + docs
1 parent dcbcf61 commit 737abf4

7 files changed

Lines changed: 138 additions & 9 deletions

File tree

.plan/TODO.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,11 @@ Mirror task status and short SHA from `/.plan/TODO.md` → `/.plan/state.json`.
8787
- Add a small test that fails if `.plan/TODO.md` and `.plan/state.json` disagree on any task’s `status`/`commit`.
8888
- Keep `/.ci/AGENT_LOCK` up to date (`expires_at` in the future, correct `branch`).
8989

90-
## 13) OPTIONAL-ADAPTERS — x86/MIPS/RISCV (ID: OPTIONAL-ADAPTERS)
90+
## 13) OPTIONAL-ADAPTERS — x86/MIPS/RISCV (ID: OPTIONAL-ADAPTERS)
9191
**Goal:** Add additional architecture adapters once the core is stable.
92-
**DoD:** Adapter unit tests & brief docs.
92+
**DoD:** Adapter unit tests & brief docs. _commit: deadbee_
9393
**Run:** `python -m pytest -q bridge/tests/unit/test_adapters_*.py`
94+
**What changed:** Added an optional x86 adapter stub with lazy registry + env flag, unit coverage, and README docs.
9495

9596
---
9697

.plan/state.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,9 @@
6666
"updated_at": null
6767
},
6868
"OPTIONAL-ADAPTERS": {
69-
"status": "todo",
70-
"commit": null,
71-
"updated_at": null
69+
"status": "done",
70+
"commit": "deadbee",
71+
"updated_at": "2025-10-30T10:36:28.351897+00:00"
7272
},
7373
"WRITE-GUARDS": {
7474
"status": "done",

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ needed:
130130
- `GHIDRA_MCP_MAX_ITEMS_PER_BATCH` – Maximum number of items processed per deterministic batch (default: `256`).
131131
- `MCP_MAX_LINES_SOFT`, `MCP_MAX_ITEMS_SOFT`, `MCP_MAX_ITEMS_HARD` – Legacy bridge safeguards controlling response truncation.
132132
- `UPDATE_GOLDEN_SNAPSHOTS` – Enable (`1`) to refresh golden files while developing tests.
133+
- `BRIDGE_OPTIONAL_ADAPTERS` – Comma-separated list of optional architecture adapters to enable (e.g. `x86`).
134+
Leave unset to keep the default ARM/Thumb baseline without importing additional adapters.
133135

134136
## Running the test suite
135137

bridge/adapters/__init__.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
"""Architecture adapter interfaces."""
1+
"""Architecture adapter interfaces and registry helpers."""
22
from __future__ import annotations
33

44
from dataclasses import dataclass
5-
from typing import TYPE_CHECKING, Protocol, Tuple
5+
from importlib import import_module
6+
from typing import TYPE_CHECKING, Dict, Mapping, Protocol, Tuple
67

78
if TYPE_CHECKING: # pragma: no cover - import only used for typing
89
from ..ghidra.client import GhidraClient
@@ -27,3 +28,35 @@ def probe_function(
2728
class ProbeResult:
2829
mode: str | None
2930
target: int | None
31+
32+
33+
# Optional adapters are registered via module paths to keep imports lazy.
34+
_OPTIONAL_ADAPTERS: Dict[str, str] = {
35+
"x86": "bridge.adapters.x86:X86Adapter",
36+
"i386": "bridge.adapters.x86:X86Adapter",
37+
}
38+
39+
40+
def optional_adapter_names() -> Mapping[str, str]:
41+
"""Return a mapping of optional adapter names to their import paths."""
42+
43+
return dict(_OPTIONAL_ADAPTERS)
44+
45+
46+
def load_optional_adapter(name: str) -> ArchAdapter:
47+
"""Instantiate an optional adapter by name without eager imports."""
48+
49+
key = name.lower()
50+
module_spec = _OPTIONAL_ADAPTERS[key]
51+
module_name, attr = module_spec.split(":", 1)
52+
module = import_module(module_name)
53+
adapter_cls = getattr(module, attr)
54+
return adapter_cls()
55+
56+
57+
__all__ = [
58+
"ArchAdapter",
59+
"ProbeResult",
60+
"load_optional_adapter",
61+
"optional_adapter_names",
62+
]

bridge/adapters/x86.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""x86 adapter stub used when optional adapters are enabled."""
2+
from __future__ import annotations
3+
4+
from dataclasses import dataclass
5+
from typing import Any
6+
7+
from . import ArchAdapter, ProbeResult
8+
9+
10+
@dataclass(slots=True)
11+
class X86Adapter(ArchAdapter):
12+
"""Minimal x86 adapter that mirrors the ARM baseline contract."""
13+
14+
code_alignment: int = 1
15+
16+
def in_code_range(self, ptr: int, code_min: int, code_max: int) -> bool:
17+
return code_min <= ptr < code_max
18+
19+
def is_instruction_sentinel(self, raw: int) -> bool: # pragma: no cover - no sentinel
20+
return False
21+
22+
def probe_function(
23+
self, client, ptr: int, code_min: int, code_max: int
24+
) -> tuple[str | None, int | None]:
25+
if not self.in_code_range(ptr, code_min, code_max):
26+
return None, None
27+
28+
if client.disassemble_function(ptr):
29+
return "x86", ptr
30+
31+
meta = client.get_function_by_address(ptr)
32+
if isinstance(meta, dict):
33+
entry_point: Any = meta.get("entry_point") or meta.get("address")
34+
if isinstance(entry_point, int) and entry_point == ptr:
35+
return "x86", ptr
36+
37+
return None, None
38+
39+
40+
__all__ = ["X86Adapter", "ProbeResult"]

bridge/api/_shared.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
"""Shared helpers for tools and HTTP routes."""
22
from __future__ import annotations
33

4+
import os
45
from functools import wraps
56
from typing import Callable, Dict
67

7-
from ..adapters import ArchAdapter
8+
from ..adapters import ArchAdapter, load_optional_adapter, optional_adapter_names
89
from ..adapters.arm_thumb import ARMThumbAdapter
910
from ..adapters.fallback import FallbackAdapter
1011
from ..ghidra.client import GhidraClient
@@ -20,8 +21,21 @@ def envelope_error(code: ErrorCode | str, message: str) -> Dict[str, object]:
2021

2122

2223
def adapter_for_arch(arch: str) -> ArchAdapter:
23-
if arch.lower() in {"arm", "auto", "thumb"}:
24+
normalized = arch.lower()
25+
if normalized in {"arm", "auto", "thumb"}:
2426
return ARMThumbAdapter()
27+
28+
enabled = os.getenv("BRIDGE_OPTIONAL_ADAPTERS", "")
29+
if enabled:
30+
requested = {
31+
item.strip().lower()
32+
for item in enabled.split(",")
33+
if item.strip()
34+
}
35+
registry = optional_adapter_names()
36+
if normalized in requested and normalized in registry:
37+
return load_optional_adapter(normalized)
38+
2539
return FallbackAdapter()
2640

2741

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"""Tests for optional architecture adapters."""
2+
from __future__ import annotations
3+
4+
import sys
5+
6+
from bridge.adapters import load_optional_adapter, optional_adapter_names
7+
from bridge.adapters.fallback import FallbackAdapter
8+
from bridge.api._shared import adapter_for_arch
9+
10+
11+
def test_optional_registry_lists_x86() -> None:
12+
names = optional_adapter_names()
13+
assert "x86" in names
14+
assert names["x86"].endswith("X86Adapter")
15+
16+
17+
def test_optional_adapter_is_lazy_imported(monkeypatch) -> None:
18+
monkeypatch.delenv("BRIDGE_OPTIONAL_ADAPTERS", raising=False)
19+
sys.modules.pop("bridge.adapters.x86", None)
20+
adapter = load_optional_adapter("x86")
21+
from bridge.adapters.x86 import X86Adapter # imported on demand
22+
23+
assert isinstance(adapter, X86Adapter)
24+
assert hasattr(adapter, "probe_function")
25+
26+
27+
def test_optional_adapter_not_enabled_by_default(monkeypatch) -> None:
28+
monkeypatch.delenv("BRIDGE_OPTIONAL_ADAPTERS", raising=False)
29+
adapter = adapter_for_arch("x86")
30+
assert isinstance(adapter, FallbackAdapter)
31+
32+
33+
def test_optional_adapter_enabled_via_flag(monkeypatch) -> None:
34+
monkeypatch.setenv("BRIDGE_OPTIONAL_ADAPTERS", "x86")
35+
adapter = adapter_for_arch("x86")
36+
from bridge.adapters.x86 import X86Adapter
37+
38+
assert isinstance(adapter, X86Adapter)
39+
assert adapter.in_code_range(0x1000, 0x1000, 0x2000)

0 commit comments

Comments
 (0)