"""Server-owned runtime policy for hosted SAGE runs. The local service historically accepted the complete :class:`RunRequest` surface so developers could select providers, stages, artifacts, and tuning parameters from scripts. Hosted callers must not have that authority. This module provides an opt-in boundary that replaces every operational field with server environment configuration while retaining only the cohort text and the internal human-confirmation choice. """ from __future__ import annotations import os from pydantic import BaseModel, Field from .models import RunRequest LOCK_RUNTIME_CONFIG_ENV = "SAGE_LOCK_RUNTIME_CONFIG" HOSTED_MODE_ENV = "SAGE_HOSTED_MODE" _TRUE_VALUES = frozenset({"1", "true", "yes", "on"}) def env_flag(name: str) -> bool: """Return whether an opt-in environment flag is explicitly enabled.""" return str(os.getenv(name) or "").strip().casefold() in _TRUE_VALUES def hosted_mode_enabled() -> bool: """Whether the backend is running without its legacy local HTML surface.""" return env_flag(HOSTED_MODE_ENV) def runtime_config_locked() -> bool: """Whether request-controlled operational configuration is disabled. Hosted mode implies the lock so a deployment cannot hide the legacy UI while accidentally leaving its model/path controls available through the raw API. The dedicated flag also permits locked local proxy testing. """ return hosted_mode_enabled() or env_flag(LOCK_RUNTIME_CONFIG_ENV) class LockedRuntimeConfig(BaseModel): """Validated non-secret policy values read only from the backend env.""" max_llm_calls: int = Field(default=1000, ge=1, le=5000) temperature: float = Field(default=0.0, ge=0.0, le=2.0) llm_max_retries: int = Field(default=2, ge=0, le=10) timeout_seconds: float = Field(default=90.0, ge=1.0, le=600.0) stage2_top_k: int = Field(default=10, ge=1, le=100) stage2_mode: str = "adopt_suggestions" stage2_llm_verify_results: bool = False stage2_llm_filter_noise: bool = False stage2_llm_retry_limit: int = Field(default=1, ge=0, le=5) @classmethod def from_environment(cls) -> "LockedRuntimeConfig": mapping = { "max_llm_calls": "SAGE_MAX_LLM_CALLS", "temperature": "SAGE_LLM_TEMPERATURE", "llm_max_retries": "SAGE_LLM_MAX_RETRIES", "timeout_seconds": "SAGE_LLM_TIMEOUT", "stage2_top_k": "SAGE_STAGE2_TOP_K", "stage2_mode": "SAGE_STAGE2_MODE", "stage2_llm_verify_results": "SAGE_STAGE2_LLM_VERIFY_RESULTS", "stage2_llm_filter_noise": "SAGE_STAGE2_LLM_FILTER_NOISE", "stage2_llm_retry_limit": "SAGE_STAGE2_LLM_RETRY_LIMIT", } values = { field_name: os.environ[environment_name] for field_name, environment_name in mapping.items() if environment_name in os.environ and os.environ[environment_name].strip() } return cls.model_validate(values) def apply_runtime_config_policy(request: RunRequest) -> RunRequest: """Return a server-owned request when the hosted runtime lock is enabled. Provider, model, response-format, endpoint, and credential selection remain in their existing provider-specific environment variables. Setting their request fields to ``None`` makes the provider factory consult only that backend configuration and prevents a raw client from overriding it. """ if not runtime_config_locked(): return request configured = LockedRuntimeConfig.from_environment() updates = { # Hosted runs are always end-to-end from inline text. HITL is the one # safe workflow choice retained for the internal frontend. "input_path": None, "case_id": None, "run_id": None, "start_stage": 1, "end_stage": 2 if request.require_human_confirmation else 3, "ir_json_path": None, "retrieval_context_path": None, # Do not load a repository .env file in a container. Provider/model # values and secrets come from the process environment only. "llm_dotenv_path": None, "llm_provider": None, "llm_model": None, "llm_response_format": None, "max_llm_calls": configured.max_llm_calls, "temperature": configured.temperature, "llm_max_retries": configured.llm_max_retries, "timeout_seconds": configured.timeout_seconds, "stage2_top_k": configured.stage2_top_k, "stage2_mode": configured.stage2_mode, "stage2_llm_verify_results": configured.stage2_llm_verify_results, "stage2_llm_filter_noise": configured.stage2_llm_filter_noise, "stage2_llm_retry_limit": configured.stage2_llm_retry_limit, } return RunRequest.model_validate({**request.model_dump(mode="python"), **updates})