Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

import logging
import os
import ssl
import warnings
from base64 import urlsafe_b64encode
from hashlib import sha256
from hmac import new as hmac_new
Expand All @@ -20,6 +22,11 @@

DEFAULT_TIMEOUT_SECS = 10.0
DEFAULT_INTERNAL_TOKEN_TTL_SECS = 3600
# Keep pooled-connection reuse shorter than typical server keepalive/worker
# recycle windows so requests do not pick up sockets the server already closed.
DEFAULT_KEEPALIVE_EXPIRY_SECS = 1.0
DEFAULT_MAX_CONNECTIONS = 100
DEFAULT_MAX_KEEPALIVE_CONNECTIONS = 20
PUBLIC_SCORER_INVOKE_PATH = "/scorers/invoke"
INTERNAL_SCORER_INVOKE_PATH = "/internal/scorers/invoke"
AuthMode = Literal["public", "internal"]
Expand Down Expand Up @@ -56,6 +63,13 @@ def _env_auth_mode() -> AuthMode | None:
value = os.getenv("GALILEO_LUNA_AUTH_MODE")
if value is None or value.strip() == "":
return None
deprecation_message = (
"GALILEO_LUNA_AUTH_MODE is deprecated. Configure exactly one credential "
"(GALILEO_API_KEY for public auth, GALILEO_API_SECRET_KEY for internal "
"auth) or pass auth_mode to GalileoLunaClient."
)
warnings.warn(deprecation_message, DeprecationWarning, stacklevel=2)
logger.warning(deprecation_message)
normalized = value.strip().lower()
if normalized == "public":
return "public"
Expand Down Expand Up @@ -164,9 +178,12 @@ class GalileoLunaClient:
"""Thin HTTP client for Galileo Luna direct scorer invocation.

Environment Variables:
GALILEO_API_SECRET_KEY or GALILEO_API_SECRET: Galileo API internal JWT signing secret.
GALILEO_API_SECRET_KEY: Deployment-provided Galileo API internal JWT signing secret.
GALILEO_API_KEY: Galileo API key fallback for public scorer invocation.
GALILEO_LUNA_AUTH_MODE: Auth mode, either "public" or "internal".
GALILEO_LUNA_API_URL: Galileo Luna scorer invoke API URL override.
GALILEO_API_URL: Galileo API URL fallback.
GALILEO_LUNA_CA_FILE: CA bundle used to verify the scorer API endpoint, for
deployments whose API serves an internally-issued TLS certificate.
GALILEO_CONSOLE_URL: Galileo Console URL (optional, defaults to production).
"""

Expand All @@ -177,23 +194,27 @@ def __init__(
console_url: str | None = None,
api_url: str | None = None,
auth_mode: AuthMode | None = None,
ca_file: str | None = None,
) -> None:
"""Initialize the Galileo Luna client.

Args:
api_key: Galileo API key. If not provided, reads from GALILEO_API_KEY.
api_secret: Galileo API secret for internal JWT auth. If not provided,
reads from GALILEO_API_SECRET_KEY or GALILEO_API_SECRET.
api_secret: Deployment-provided Galileo API secret for internal JWT auth.
If not provided, reads from GALILEO_API_SECRET_KEY.
console_url: Galileo Console URL. If not provided, reads from
GALILEO_CONSOLE_URL or uses the production console URL.
api_url: Galileo API URL. If not provided, reads from GALILEO_API_URL
before deriving from the console URL.
auth_mode: Auth mode to use. If not provided, reads from
GALILEO_LUNA_AUTH_MODE, or infers from the single available credential.
api_url: Galileo API URL. If not provided, reads from GALILEO_LUNA_API_URL,
then GALILEO_API_URL, before deriving from the console URL.
auth_mode: Auth mode to use. If not provided, inferred from the single
available credential.
ca_file: CA bundle path used to verify the scorer API endpoint. If not
provided, reads from GALILEO_LUNA_CA_FILE. Leave unset for endpoints
with publicly-trusted certificates.

Raises:
ValueError: If credentials are missing, ambiguous, or incompatible with
the selected auth mode.
the selected auth mode, or if the CA bundle cannot be loaded.
"""
resolved_api_secret = (
api_secret or os.getenv("GALILEO_API_SECRET_KEY") or os.getenv("GALILEO_API_SECRET")
Expand All @@ -211,12 +232,32 @@ def __init__(
self.console_url = (
console_url or os.getenv("GALILEO_CONSOLE_URL") or "https://console.galileo.ai"
)
self.api_base = (api_url or os.getenv("GALILEO_API_URL") or "").rstrip(
"/"
) or self._derive_api_url(self.console_url)
self.api_base = self._resolve_api_base(api_url)
self.ca_file = (ca_file or os.getenv("GALILEO_LUNA_CA_FILE") or "").strip() or None
self._ssl_context = self._load_ssl_context(self.ca_file)
self._client: httpx.AsyncClient | None = None
logger.info("[GalileoLunaClient] Auth mode selected: %s", self.auth_mode)

def _resolve_api_base(self, api_url: str | None) -> str:
"""Resolve the scorer invoke API base URL from explicit and environment config."""
candidates = [api_url, os.getenv("GALILEO_LUNA_API_URL")]
candidates.append(os.getenv("GALILEO_API_URL"))

for candidate in candidates:
if candidate and candidate.strip():
return candidate.strip().rstrip("/")
return self._derive_api_url(self.console_url)

@staticmethod
def _load_ssl_context(ca_file: str | None) -> ssl.SSLContext | None:
"""Build a TLS verification context from a CA bundle path, if configured."""
if ca_file is None:
return None
try:
return ssl.create_default_context(cafile=ca_file)
except (OSError, ssl.SSLError) as exc:
raise ValueError(f"Failed to load CA bundle from {ca_file!r}: {exc}") from exc

@staticmethod
def _resolve_auth_mode(
auth_mode: AuthMode | None,
Expand All @@ -226,24 +267,21 @@ def _resolve_auth_mode(
) -> AuthMode:
if auth_mode == "public":
if not api_key:
raise ValueError(
"GALILEO_API_KEY is required when GALILEO_LUNA_AUTH_MODE=public."
)
raise ValueError("GALILEO_API_KEY is required for public Luna auth.")
return "public"

if auth_mode == "internal":
if not api_secret:
raise ValueError(
"GALILEO_API_SECRET_KEY or GALILEO_API_SECRET is required when "
"GALILEO_LUNA_AUTH_MODE=internal."
"GALILEO_API_SECRET_KEY is required for internal Luna auth."
)
return "internal"

if api_key and api_secret:
raise ValueError(
"Both Galileo API key and API secret are configured. Set "
"GALILEO_LUNA_AUTH_MODE to 'public' or 'internal' to choose the "
"runtime auth mode explicitly."
"Both a Galileo API key and a Galileo API secret are configured. "
"Unset one credential so the auth mode can be inferred, or pass "
"auth_mode='public' or auth_mode='internal' explicitly."
)
if api_secret:
return "internal"
Expand Down Expand Up @@ -284,9 +322,18 @@ async def _get_client(self) -> httpx.AsyncClient:
headers = {"Content-Type": "application/json"}
if self.auth_mode == "public" and self.api_key is not None:
headers["Galileo-API-Key"] = self.api_key
verify: ssl.SSLContext | bool = (
self._ssl_context if self._ssl_context is not None else True
)
self._client = httpx.AsyncClient(
headers=headers,
timeout=httpx.Timeout(DEFAULT_TIMEOUT_SECS),
limits=httpx.Limits(
max_connections=DEFAULT_MAX_CONNECTIONS,
max_keepalive_connections=DEFAULT_MAX_KEEPALIVE_CONNECTIONS,
keepalive_expiry=DEFAULT_KEEPALIVE_EXPIRY_SECS,
),
verify=verify,
)
return self._client

Expand Down
20 changes: 13 additions & 7 deletions evaluators/contrib/galileo/tests/test_luna_coverage_gaps.py
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,9 @@ def test_client_requires_explicit_mode_when_both_credentials_are_present(monkeyp
monkeypatch.delenv("GALILEO_LUNA_AUTH_MODE", raising=False)
from agent_control_evaluator_galileo.luna.client import GalileoLunaClient

with pytest.raises(ValueError, match="Both Galileo API key and API secret"):
with pytest.raises(
ValueError, match="Both a Galileo API key and a Galileo API secret are configured"
):
GalileoLunaClient()


Expand All @@ -468,7 +470,8 @@ def test_client_uses_explicit_public_mode_when_both_credentials_are_present(monk
monkeypatch.setenv("GALILEO_LUNA_AUTH_MODE", "public")
from agent_control_evaluator_galileo.luna.client import GalileoLunaClient

client = GalileoLunaClient()
with pytest.warns(DeprecationWarning, match="GALILEO_LUNA_AUTH_MODE is deprecated"):
client = GalileoLunaClient()

assert client.auth_mode == "public"
endpoint, request_headers = client._endpoint_and_headers(None)
Expand All @@ -483,7 +486,8 @@ def test_client_uses_explicit_internal_mode_when_both_credentials_are_present(mo
monkeypatch.setenv("GALILEO_LUNA_AUTH_MODE", "internal")
from agent_control_evaluator_galileo.luna.client import GalileoLunaClient

client = GalileoLunaClient()
with pytest.warns(DeprecationWarning, match="GALILEO_LUNA_AUTH_MODE is deprecated"):
client = GalileoLunaClient()

assert client.auth_mode == "internal"
endpoint, request_headers = client._endpoint_and_headers(None)
Expand All @@ -499,8 +503,9 @@ def test_client_rejects_mode_without_matching_credential(monkeypatch):
monkeypatch.setenv("GALILEO_LUNA_AUTH_MODE", "internal")
from agent_control_evaluator_galileo.luna.client import GalileoLunaClient

with pytest.raises(ValueError, match="GALILEO_API_SECRET_KEY"):
GalileoLunaClient()
with pytest.warns(DeprecationWarning, match="GALILEO_LUNA_AUTH_MODE is deprecated"):
with pytest.raises(ValueError, match="GALILEO_API_SECRET_KEY"):
GalileoLunaClient()


def test_client_rejects_invalid_auth_mode(monkeypatch):
Expand All @@ -509,8 +514,9 @@ def test_client_rejects_invalid_auth_mode(monkeypatch):
monkeypatch.setenv("GALILEO_LUNA_AUTH_MODE", "sideways")
from agent_control_evaluator_galileo.luna.client import GalileoLunaClient

with pytest.raises(ValueError, match="GALILEO_LUNA_AUTH_MODE"):
GalileoLunaClient()
with pytest.warns(DeprecationWarning, match="GALILEO_LUNA_AUTH_MODE is deprecated"):
with pytest.raises(ValueError, match="GALILEO_LUNA_AUTH_MODE"):
GalileoLunaClient()


class TestDeriveApiUrl:
Expand Down
Loading
Loading