Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 182 additions & 0 deletions server/src/agent_control_server/bootstrap/out_of_box_controls.py
Original file line number Diff line number Diff line change
Expand Up @@ -429,9 +429,191 @@ def _leaf_control_payload(
tags=["owasp", "owasp-llm05", "output-handling", "uri", "regex"],
),
),
OutOfBoxControlTemplate.from_payload(
source_id="oob-owasp-llm01-prompt-injection-input-match",
name="owasp-llm01-prompt-injection-input-match",
data=_leaf_control_payload(
description=(
"Block LLM input containing common prompt-injection or jailbreak phrasing."
),
selector_path="input",
evaluator_name="regex",
evaluator_config={
"pattern": (
r"(?:\b(?:ignore|disregard)\s+(?:all\s+|any\s+)?"
r"(?:previous|prior|above)\s+instructions\b|"
r"\b(?:override|bypass)\s+(?:the\s+)?"
r"(?:system|developer|safety|policy)\s*"
r"(?:prompt|instructions|rules|guidelines)?\b|"
r"\breveal\s+(?:the\s+)?(?:system|developer|hidden)\s+"
r"(?:prompt|message|instructions)\b|"
r"\bjailbreak\b)"
),
"flags": ["IGNORECASE"],
},
step_types=["llm"],
stages=["pre"],
decision="deny",
tags=["owasp", "owasp-llm01", "prompt-injection", "jailbreak", "regex"],
),
),
OutOfBoxControlTemplate.from_payload(
source_id="oob-ssrf-metadata-endpoint-match",
name="ssrf-metadata-endpoint-match",
data=_leaf_control_payload(
description=(
"Block tool calls targeting cloud metadata or loopback endpoints "
"commonly abused for SSRF."
),
selector_path="input.url",
evaluator_name="list",
evaluator_config={
"values": [
"169.254.169.254",
"metadata.google.internal",
"100.100.100.200",
"169.254.170.2",
"metadata.azure.com",
"localhost",
"127.0.0.1",
"0.0.0.0",
"[::1]",
],
"logic": "any",
"match_on": "match",
"match_mode": "contains",
"case_sensitive": False,
},
step_types=["tool"],
stages=["pre"],
decision="deny",
tags=["tool", "ssrf", "network", "list"],
),
),
)


_LUNA_EVALUATOR_NAME = "galileo.luna"
_LUNA_OPERATOR = "gte"
_LUNA_THRESHOLD = 0.5

# (settings attribute, source_id, scorer_label, category, stage)
#
# `scorer_label` is the exact Luna scorer name observed via
# `galileo_metric_pull.py` discovery mode against the reference project; it
# is metadata-only (the evaluator invokes by `scorer_id`), kept here so the
# seeded control documents which scorer it maps to.
_LUNA_CONTROL_SPECS: tuple[tuple[str, str, str, str, str], ...] = (
(
"luna_input_toxicity_scorer_id",
"oob-input-toxicity-slm-match",
"input_toxicity_luna",
"toxicity",
"pre",
),
(
"luna_output_toxicity_scorer_id",
"oob-output-toxicity-slm-match",
"output_toxicity_luna",
"toxicity",
"post",
),
("luna_input_tone_scorer_id", "oob-input-tone-slm-match", "input_tone", "tone", "pre"),
("luna_output_tone_scorer_id", "oob-output-tone-slm-match", "output_tone", "tone", "post"),
(
"luna_input_sexism_scorer_id",
"oob-input-sexism-slm-match",
"input_sexist_luna",
"sexism",
"pre",
),
(
"luna_output_sexism_scorer_id",
"oob-output-sexism-slm-match",
"output_sexist_luna",
"sexism",
"post",
),
)


def _luna_control_template(
*,
source_id: str,
scorer_id: str,
scorer_label: str,
category: str,
stage: str,
) -> OutOfBoxControlTemplate:
side = "input" if stage == "pre" else "output"
return OutOfBoxControlTemplate.from_payload(
source_id=source_id,
name=source_id.removeprefix("oob-"),
data=_leaf_control_payload(
description=(
f"Block LLM {side} scored above threshold for {category} by the "
f"Galileo Luna '{scorer_label}' SLM scorer."
),
selector_path=side,
evaluator_name=_LUNA_EVALUATOR_NAME,
evaluator_config={
"scorer_id": scorer_id,
"scorer_label": scorer_label,
"operator": _LUNA_OPERATOR,
"threshold": _LUNA_THRESHOLD,
"payload_field": side,
},
step_types=["llm"],
stages=[stage],
decision="deny",
tags=["slm", "galileo", "luna", category, side],
),
)


def luna_out_of_box_control_templates(
*,
input_toxicity_scorer_id: str | None = None,
output_toxicity_scorer_id: str | None = None,
input_tone_scorer_id: str | None = None,
output_tone_scorer_id: str | None = None,
input_sexism_scorer_id: str | None = None,
output_sexism_scorer_id: str | None = None,
) -> tuple[OutOfBoxControlTemplate, ...]:
"""Build the Luna SLM out-of-box templates that have a configured scorer ID.

`galileo.luna` invokes a scorer instance by ``scorer_id``, and that UUID is
minted per Galileo project/org when the scorer is configured for it — unlike
the rest of the out-of-box catalog, there is no stable literal to embed. Each
of the 6 templates (toxicity/tone/sexism, each for input and output) is only
included when the caller supplies its scorer ID; the rest are omitted so
seeding neither creates a control that can never match nor fails outright.
"""
scorer_ids_by_attr = {
"luna_input_toxicity_scorer_id": input_toxicity_scorer_id,
"luna_output_toxicity_scorer_id": output_toxicity_scorer_id,
"luna_input_tone_scorer_id": input_tone_scorer_id,
"luna_output_tone_scorer_id": output_tone_scorer_id,
"luna_input_sexism_scorer_id": input_sexism_scorer_id,
"luna_output_sexism_scorer_id": output_sexism_scorer_id,
}
templates: list[OutOfBoxControlTemplate] = []
for attr, source_id, scorer_label, category, stage in _LUNA_CONTROL_SPECS:
scorer_id = scorer_ids_by_attr[attr]
if not scorer_id:
continue
templates.append(
_luna_control_template(
source_id=source_id,
scorer_id=scorer_id,
scorer_label=scorer_label,
category=category,
stage=stage,
)
)
return tuple(templates)


def default_out_of_box_namespace_key() -> str:
"""Return the standalone namespace used for server startup seeding."""
return DEFAULT_NAMESPACE_KEY
Expand Down
32 changes: 32 additions & 0 deletions server/src/agent_control_server/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,38 @@ class Settings(BaseSettings):
),
)

# Luna out-of-box SLM control scorer IDs.
#
# `galileo.luna` invokes a specific scorer instance by UUID, and that
# UUID is minted per Galileo project/org when the scorer is configured —
# there is no stable, org-independent preset ID to embed as a literal.
# Each out-of-box Luna control is only seeded when its scorer ID is set;
# unset scorers are silently skipped (see `luna_out_of_box_control_templates`).
luna_input_toxicity_scorer_id: str | None = Field(
default=None,
validation_alias=AliasChoices("AGENT_CONTROL_LUNA_INPUT_TOXICITY_SCORER_ID"),
)
luna_output_toxicity_scorer_id: str | None = Field(
default=None,
validation_alias=AliasChoices("AGENT_CONTROL_LUNA_OUTPUT_TOXICITY_SCORER_ID"),
)
luna_input_tone_scorer_id: str | None = Field(
default=None,
validation_alias=AliasChoices("AGENT_CONTROL_LUNA_INPUT_TONE_SCORER_ID"),
)
luna_output_tone_scorer_id: str | None = Field(
default=None,
validation_alias=AliasChoices("AGENT_CONTROL_LUNA_OUTPUT_TONE_SCORER_ID"),
)
luna_input_sexism_scorer_id: str | None = Field(
default=None,
validation_alias=AliasChoices("AGENT_CONTROL_LUNA_INPUT_SEXISM_SCORER_ID"),
)
luna_output_sexism_scorer_id: str | None = Field(
default=None,
validation_alias=AliasChoices("AGENT_CONTROL_LUNA_OUTPUT_SEXISM_SCORER_ID"),
)

# Prometheus metrics settings
prometheus_metrics_prefix: str = _env_alias_field(
"agent_control_server",
Expand Down
18 changes: 17 additions & 1 deletion server/src/agent_control_server/endpoints/controls.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,12 @@
from sqlalchemy.ext.asyncio import AsyncSession

from ..auth_framework import Operation, Principal, get_authorizer, require_operation
from ..bootstrap.out_of_box_controls import seed_out_of_box_controls
from ..bootstrap.out_of_box_controls import (
OUT_OF_BOX_CONTROL_TEMPLATES,
luna_out_of_box_control_templates,
seed_out_of_box_controls,
)
from ..config import settings
from ..db import AsyncSessionLocal, get_async_db
from ..errors import (
APIError,
Expand Down Expand Up @@ -275,6 +280,17 @@ async def _run_out_of_box_controls_reconciliation(
session_factory=AsyncSessionLocal,
namespace_key=namespace_key,
available_evaluators=set(list_evaluators().keys()),
templates=(
*OUT_OF_BOX_CONTROL_TEMPLATES,
*luna_out_of_box_control_templates(
input_toxicity_scorer_id=settings.luna_input_toxicity_scorer_id,
output_toxicity_scorer_id=settings.luna_output_toxicity_scorer_id,
input_tone_scorer_id=settings.luna_input_tone_scorer_id,
output_tone_scorer_id=settings.luna_output_tone_scorer_id,
input_sexism_scorer_id=settings.luna_input_sexism_scorer_id,
output_sexism_scorer_id=settings.luna_output_sexism_scorer_id,
),
),
)
except TimeoutError:
_logger.warning(
Expand Down
13 changes: 13 additions & 0 deletions server/src/agent_control_server/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
from . import __version__ as server_version
from .auth import get_api_key_from_header
from .bootstrap.out_of_box_controls import (
OUT_OF_BOX_CONTROL_TEMPLATES,
default_out_of_box_namespace_key,
luna_out_of_box_control_templates,
seed_out_of_box_controls,
)
from .config import observability_settings, settings
Expand Down Expand Up @@ -158,6 +160,17 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
session_factory=AsyncSessionLocal,
namespace_key=default_out_of_box_namespace_key(),
available_evaluators=set(available),
templates=(
*OUT_OF_BOX_CONTROL_TEMPLATES,
*luna_out_of_box_control_templates(
input_toxicity_scorer_id=settings.luna_input_toxicity_scorer_id,
output_toxicity_scorer_id=settings.luna_output_toxicity_scorer_id,
input_tone_scorer_id=settings.luna_input_tone_scorer_id,
output_tone_scorer_id=settings.luna_output_tone_scorer_id,
input_sexism_scorer_id=settings.luna_input_sexism_scorer_id,
output_sexism_scorer_id=settings.luna_output_sexism_scorer_id,
),
),
)
if seed_result.created_count or seed_result.skipped_count:
logger.info(
Expand Down
Loading
Loading