File size: 7,564 Bytes
2e658e7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
#!/usr/bin/env python3
"""Independent adjacent-gap audit for the Phase 4 execution boundary."""
from __future__ import annotations

import ast
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
OVERLAY = ROOT / "hermes_overlay"
SERVICE = OVERLAY / "trading" / "domain" / "execution_service.py"
PAPER_ADAPTER = OVERLAY / "trading" / "adapters" / "paper_adapter.py"

sys.path.insert(0, str(ROOT / "scripts"))
from audit_execution_client_paths import audit as audit_client_paths  # noqa: E402

_ADAPTER_MUTATIONS = {
    "create_order",
    "cancel_order",
    "configure_account",
    "set_leverage",
    "set_margin_mode",
}
_RAW_HTTP_MUTATIONS = {"post", "put", "patch", "delete"}
_ALLOWED_MUTATION_ROUTES = {
    ("hermes_overlay/tools/futures_dashboard_api.py", "/api/futures/analyze"),
    ("hermes_overlay/tools/futures_dashboard_api.py", "/api/futures/paper/approve"),
    ("hermes_overlay/tools/futures_dashboard_api.py", "/api/futures/paper/execute"),
    ("hermes_overlay/tools/futures_dashboard_api.py", "/api/futures/positions/close"),
    ("hermes_overlay/tools/telegram_bot.py", "/api/telegram/webhook"),
}
_PRIVATE_EXCHANGE_MARKERS = {
    "/fapi/v1/order",
    "/api/v1/order",
    "/api/v2/order",
    "/api/v3/order",
    "X-MBX-APIKEY",
    "KC-API-KEY",
    "KC-API-SIGN",
}


def _production_files(root: Path) -> list[Path]:
    overlay = root / "hermes_overlay"
    return sorted(
        path for path in overlay.rglob("*.py")
        if "tests" not in path.parts and "__pycache__" not in path.parts
    )


def _route_path(decorator: ast.AST) -> tuple[str | None, str | None]:
    if not isinstance(decorator, ast.Call) or not isinstance(decorator.func, ast.Attribute):
        return None, None
    verb = decorator.func.attr.lower()
    if verb not in {"post", "put", "patch", "delete"}:
        return None, None
    if not decorator.args or not isinstance(decorator.args[0], ast.Constant):
        return verb, None
    value = decorator.args[0].value
    return verb, value if isinstance(value, str) else None


def _function_source(text: str, node: ast.AST) -> str:
    return ast.get_source_segment(text, node) or ""


def audit(root: Path = ROOT) -> list[str]:
    findings = list(audit_client_paths(root))
    overlay = root / "hermes_overlay"
    service = overlay / "trading" / "domain" / "execution_service.py"
    paper_adapter = overlay / "trading" / "adapters" / "paper_adapter.py"

    for path in _production_files(root):
        text = path.read_text(encoding="utf-8")
        rel = path.relative_to(root).as_posix()
        tree = ast.parse(text, filename=rel)

        for node in ast.walk(tree):
            if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute):
                attr = node.func.attr
                if attr in _ADAPTER_MUTATIONS and path not in {service, paper_adapter}:
                    findings.append(f"direct adapter mutation call outside service/adapter: {rel}:{node.lineno} {attr}")
                if attr in _RAW_HTTP_MUTATIONS and path.is_relative_to(overlay / "trading"):
                    findings.append(f"raw HTTP mutation verb inside trading package: {rel}:{node.lineno} {attr}")
            elif isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
                if node.func.id == "create_order" and path != service:
                    findings.append(f"direct durable order creation outside service: {rel}:{node.lineno}")

        for node in tree.body:
            if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                continue
            for decorator in node.decorator_list:
                verb, route = _route_path(decorator)
                if verb is None:
                    continue
                if (rel, route or "") not in _ALLOWED_MUTATION_ROUTES:
                    findings.append(f"unreviewed mutation-capable route: {rel}:{node.lineno} {verb.upper()} {route}")
                source = _function_source(text, node)
                for token in ("._submit_entry(", "._submit_close(", ".create_order(", "execute_futures_position("):
                    if token in source:
                        findings.append(f"route contains forbidden execution bypass: {rel}:{node.lineno} {token}")

        if path != service:
            for marker in _PRIVATE_EXCHANGE_MARKERS:
                if marker in text:
                    findings.append(f"private exchange mutation marker outside service: {rel} {marker}")

    # Background/scheduled and debug-adjacent surfaces must remain analysis-only.
    for rel in (
        "hermes_overlay/trading/trade_cycle.py",
        "hermes_overlay/trading/alerts.py",
        "hermes_overlay/trading/provider_reliability.py",
        "hermes_overlay/tools/telegram_bot.py",
    ):
        text = (root / rel).read_text(encoding="utf-8")
        for token in (
            "execute_approved_plan(",
            ".close_position(",
            "._submit_entry(",
            "._submit_close(",
            ".create_order(",
        ):
            if token in text:
                findings.append(f"background/notification surface contains mutation call: {rel} {token}")
        if rel.endswith("trade_cycle.py") and "Direct cycle execution is disabled" not in text:
            findings.append("trade cycle does not fail closed when execute=True")

    service_text = service.read_text(encoding="utf-8")
    for fn_name in ("_submit_entry", "_submit_close"):
        tree = ast.parse(service_text)
        fn = next(
            node for node in ast.walk(tree)
            if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name == fn_name
        )
        source = _function_source(service_text, fn)
        required = ("claim_idempotency(", "begin_execution(", "acquire_lock_lease(", "self._adapter.create_order(")
        missing = [token for token in required if token not in source]
        if missing:
            findings.append(f"{fn_name} missing lifecycle controls: {', '.join(missing)}")
        elif not (
            source.index("claim_idempotency(")
            < source.index("begin_execution(")
            < source.index("acquire_lock_lease(")
            < source.index("self._adapter.create_order(")
        ):
            findings.append(f"{fn_name} lifecycle controls are ordered incorrectly")
        if "release_lock(" not in source or "if lease is not None" not in source:
            findings.append(f"{fn_name} does not conditionally release an acquired lease")

    compat = (overlay / "trading" / "futures_execution.py").read_text(encoding="utf-8")
    leverage_start = compat.index("async def set_leverage_and_margin")
    leverage_end = compat.index("\ndef _estimate_slippage_pct", leverage_start)
    leverage_source = compat[leverage_start:leverage_end]
    if '"simulated": True' not in leverage_source or "non-Paper leverage mutation is disabled" not in leverage_source:
        findings.append("legacy leverage compatibility boundary is not Paper-simulated and fail-closed")
    for token in (".set_leverage(", ".set_margin_mode(", ".configure_account("):
        if token in leverage_source:
            findings.append(f"legacy leverage compatibility boundary calls adapter directly: {token}")

    return sorted(set(findings))


def main() -> int:
    findings = audit()
    for finding in findings:
        print(f"ERROR: {finding}")
    print(f"phase4_adjacent_findings={len(findings)}")
    return 1 if findings else 0


if __name__ == "__main__":
    raise SystemExit(main())