dita_v2+adapters: phantom-position root-cause fixes (UV audit §8)

bingx_direct.submit_intent: (1) empty/orderId-less ack (VST offline-symbol
hole: code==0 + empty data) -> REJECTED, never fabricate fill from
target_size; (2) honor upstream u- client id (intent.metadata
promo_client_id/client_order_id) over self-minted p- id.
rust_backend: EXIT asset-mismatch guard both sync+async paths — EXIT for
asset X while slot holds Y -> NO_OPEN_POSITION/EXIT_ASSET_MISMATCH instead
of asset-blind close (THETA 76bbf8ee cross-slot contamination).
9 regression tests, mutation-RED verified (guard flip).
This commit is contained in:
Codex
2026-07-11 22:29:02 +02:00
parent 3efb8749fe
commit 7bc13a6eb2
3 changed files with 259 additions and 4 deletions

View File

@@ -761,6 +761,36 @@ class ExecutionKernel:
control=self.control,
)
def _exit_asset_mismatch_outcome(self, intent: KernelIntent) -> Optional[KernelOutcome]:
"""UV-FIX 2026-07-11: an EXIT must name the asset its slot holds.
All UV promotion intents share slot 0. During the 2026-07-10/11
phantom-pair incident, an EXIT for asset X arriving while the slot
held asset Y was accepted asset-blind — closing Y's venue position
and orphaning Y's own later exit (NO_OPEN_POSITION). Reject the
mismatch at the kernel boundary instead.
"""
if intent.action != KernelCommandType.EXIT:
return None
slot = self._get_slot(int(intent.slot_id))
open_asset = str(getattr(slot, "asset", "") or "")
if slot.is_open() and open_asset and open_asset != intent.asset:
return KernelOutcome(
accepted=False,
slot_id=int(intent.slot_id),
trade_id=intent.trade_id,
state=slot.fsm_state,
diagnostic_code=KernelDiagnosticCode.NO_OPEN_POSITION,
severity=KernelSeverity.WARNING,
details={
"reason": "EXIT_ASSET_MISMATCH",
"slot_asset": open_asset,
"intent_asset": intent.asset,
"intent_id": intent.intent_id,
},
)
return None
def process_intent(self, intent: KernelIntent) -> KernelOutcome:
self.zinc_plane.publish_intent(intent)
if not (0 <= int(intent.slot_id) < self.max_slots):
@@ -795,6 +825,9 @@ class ExecutionKernel:
"asset": intent.asset,
},
)
_mismatch = self._exit_asset_mismatch_outcome(intent)
if _mismatch is not None:
return _mismatch
payload = _intent_to_payload(intent)
result = _get_rust().process_intent(
self._backend,
@@ -943,6 +976,9 @@ class ExecutionKernel:
details={"reason": "INVALID_INTENT", "field": name, "value": str(value),
"intent_id": intent.intent_id, "action": intent.action.value, "asset": intent.asset},
)
_mismatch = self._exit_asset_mismatch_outcome(intent)
if _mismatch is not None:
return _mismatch
# ── Rust FSM (sync, atomic, μs-fast — no await here) ─────────────────
payload = _intent_to_payload(intent)
result = _get_rust().process_intent(