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
4 changes: 4 additions & 0 deletions models/src/agent_control_models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
expand_action_filter,
normalize_action,
normalize_action_list,
validate_action,
validate_action_list,
)
from .agent import (
BUILTIN_STEP_TYPES,
Expand Down Expand Up @@ -116,6 +118,8 @@
"SteeringContext",
"normalize_action",
"normalize_action_list",
"validate_action",
"validate_action_list",
"expand_action_filter",
# Error models
"ProblemDetail",
Expand Down
36 changes: 33 additions & 3 deletions models/src/agent_control_models/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

type ActionDecision = Literal["deny", "steer", "observe"]

_CANONICAL_ACTIONS = frozenset({"deny", "steer", "observe"})
_OBSERVE_ACTION_ALIASES = frozenset({"allow", "observe", "warn", "log"})
_ACTION_QUERY_EXPANSION: dict[ActionDecision, tuple[str, ...]] = {
"deny": ("deny",),
Expand All @@ -15,15 +16,44 @@
}


def validate_action(action: str) -> ActionDecision:
"""Validate that *action* is one of the canonical action values.

Use this on public API boundaries (control create/update, query filters)
where legacy values should be rejected.
"""
if action in _CANONICAL_ACTIONS:
return cast(ActionDecision, action)
raise ValueError(
f"Invalid action {action!r}. Must be one of: deny, steer, observe."
)


def validate_action_list(actions: Sequence[str]) -> list[ActionDecision]:
"""Validate a list of actions, preserving order and removing duplicates."""
validated: list[ActionDecision] = []
seen: set[ActionDecision] = set()
for action in actions:
canonical = validate_action(action)
if canonical in seen:
continue
seen.add(canonical)
validated.append(canonical)
return validated


def normalize_action(action: str) -> ActionDecision:
"""Normalize a public or legacy action name to the canonical action."""
"""Normalize a stored or legacy action name to the canonical action.

Use this on internal read paths (deserializing DB rows, server responses)
where historical data may contain legacy values.
"""
if action in _OBSERVE_ACTION_ALIASES:
return "observe"
if action in ("deny", "steer"):
return cast(ActionDecision, action)
raise ValueError(
"Invalid action. Expected one of: deny, steer, observe "
"(legacy aliases allow/warn/log are also accepted temporarily)."
f"Invalid action {action!r}. Expected one of: deny, steer, observe."
)


Expand Down
6 changes: 3 additions & 3 deletions models/src/agent_control_models/controls.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import re2
from pydantic import ConfigDict, Field, ValidationInfo, field_validator, model_validator

from .actions import ActionDecision, normalize_action
from .actions import ActionDecision, normalize_action, validate_action
from .base import BaseModel


Expand Down Expand Up @@ -280,8 +280,8 @@ class ControlAction(BaseModel):

@field_validator("decision", mode="before")
@classmethod
def normalize_decision(cls, value: str) -> ActionDecision:
return normalize_action(value)
def validate_decision(cls, value: str) -> ActionDecision:
return validate_action(value)


MAX_CONDITION_DEPTH = 6
Expand Down
10 changes: 7 additions & 3 deletions models/src/agent_control_models/observability.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@

from pydantic import Field, field_validator

from .actions import ActionDecision, normalize_action, normalize_action_list
from .actions import (
ActionDecision,
normalize_action,
validate_action_list,
)
from .agent import AGENT_NAME_MIN_LENGTH, AGENT_NAME_PATTERN, normalize_agent_name
from .base import BaseModel

Expand Down Expand Up @@ -343,12 +347,12 @@ def validate_and_normalize_agent_name(

@field_validator("actions", mode="before")
@classmethod
def normalize_actions_filter(
def validate_actions_filter(
cls, value: list[str] | None
) -> list[ActionDecision] | None:
if value is None:
return None
return normalize_action_list(value)
return validate_action_list(value)


class EventQueryResponse(BaseModel):
Expand Down
Loading
Loading