Files
xc_agent_platform_demo/src/gate.py

108 lines
2.9 KiB
Python
Raw Normal View History

"""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,
}