forked from luopingyi/xc_agent_platform_demo
108 lines
2.9 KiB
Python
108 lines
2.9 KiB
Python
|
|
"""Dual live-submit gate. Default: dry-run / blocked.
|
||
|
|
|
||
|
|
Live is allowed only when ALL of:
|
||
|
|
- ALLOW_LIVE_SUBMIT=1
|
||
|
|
- LIVE_CONFIRM=I_CONFIRM_LIVE_SUBMIT
|
||
|
|
- STRATEGY_ID present
|
||
|
|
|
||
|
|
Phase1: no real HTTP client for /api/adapt/task/add. Live path accepts an
|
||
|
|
injected fake client for unit tests only.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import os
|
||
|
|
from dataclasses import dataclass
|
||
|
|
from typing import Any, Callable, Dict, Optional, Protocol
|
||
|
|
|
||
|
|
from .strategy import strategy_id_present
|
||
|
|
|
||
|
|
LIVE_CONFIRM_VALUE = "I_CONFIRM_LIVE_SUBMIT"
|
||
|
|
|
||
|
|
|
||
|
|
class SubmitClient(Protocol):
|
||
|
|
"""Minimal interface for a submit client (injected fake in tests)."""
|
||
|
|
|
||
|
|
def submit(self, payload: Dict[str, Any]) -> Dict[str, Any]:
|
||
|
|
...
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass(frozen=True)
|
||
|
|
class GateDecision:
|
||
|
|
mode: str # "dry-run" | "live" | "blocked"
|
||
|
|
allowed: bool
|
||
|
|
reason: str
|
||
|
|
|
||
|
|
|
||
|
|
def _allow_live_flag() -> bool:
|
||
|
|
return os.environ.get("ALLOW_LIVE_SUBMIT", "0").strip() == "1"
|
||
|
|
|
||
|
|
|
||
|
|
def _live_confirm_ok() -> bool:
|
||
|
|
return os.environ.get("LIVE_CONFIRM", "").strip() == LIVE_CONFIRM_VALUE
|
||
|
|
|
||
|
|
|
||
|
|
def evaluate_gate() -> GateDecision:
|
||
|
|
"""Evaluate whether live submit is permitted."""
|
||
|
|
if not strategy_id_present():
|
||
|
|
return GateDecision(
|
||
|
|
mode="blocked",
|
||
|
|
allowed=False,
|
||
|
|
reason="STRATEGY_ID missing or empty",
|
||
|
|
)
|
||
|
|
if not _allow_live_flag():
|
||
|
|
return GateDecision(
|
||
|
|
mode="dry-run",
|
||
|
|
allowed=False,
|
||
|
|
reason="ALLOW_LIVE_SUBMIT is not 1 (default dry-run)",
|
||
|
|
)
|
||
|
|
if not _live_confirm_ok():
|
||
|
|
return GateDecision(
|
||
|
|
mode="blocked",
|
||
|
|
allowed=False,
|
||
|
|
reason=f"LIVE_CONFIRM must be exactly {LIVE_CONFIRM_VALUE!r}",
|
||
|
|
)
|
||
|
|
return GateDecision(
|
||
|
|
mode="live",
|
||
|
|
allowed=True,
|
||
|
|
reason="dual gate open (ALLOW_LIVE_SUBMIT=1 and LIVE_CONFIRM set)",
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def attempt_live_submit(
|
||
|
|
payload: Dict[str, Any],
|
||
|
|
client: Optional[SubmitClient] = None,
|
||
|
|
) -> Dict[str, Any]:
|
||
|
|
"""Attempt a live submit only if the gate opens and a client is injected.
|
||
|
|
|
||
|
|
Phase1: without an injected client, even an open gate returns blocked with
|
||
|
|
a message that real HTTP submit is not implemented in this skeleton.
|
||
|
|
Never calls platform write APIs from this module.
|
||
|
|
"""
|
||
|
|
decision = evaluate_gate()
|
||
|
|
if not decision.allowed:
|
||
|
|
return {
|
||
|
|
"ok": False,
|
||
|
|
"mode": decision.mode,
|
||
|
|
"reason": decision.reason,
|
||
|
|
"submitted": False,
|
||
|
|
}
|
||
|
|
if client is None:
|
||
|
|
return {
|
||
|
|
"ok": False,
|
||
|
|
"mode": "blocked",
|
||
|
|
"reason": (
|
||
|
|
"Phase1 skeleton: no real task/add client; "
|
||
|
|
"inject a fake client in tests only"
|
||
|
|
),
|
||
|
|
"submitted": False,
|
||
|
|
}
|
||
|
|
result = client.submit(payload)
|
||
|
|
return {
|
||
|
|
"ok": True,
|
||
|
|
"mode": "live",
|
||
|
|
"reason": decision.reason,
|
||
|
|
"submitted": True,
|
||
|
|
"result": result,
|
||
|
|
}
|