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