Spaces:
Sleeping
Sleeping
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())
|