File size: 4,874 Bytes
de0f30b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
"""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})