Skip to content

Commit 2f707ff

Browse files
committed
fix(manifests): validate surface binding contracts
Signed-off-by: Pouyanpi <13303554+Pouyanpi@users.noreply.github.com>
1 parent d5532ad commit 2f707ff

2 files changed

Lines changed: 45 additions & 1 deletion

File tree

nemoguardrails/manifests/manifest.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from enum import Enum
2727
from typing import Any, Literal, Optional, Tuple, Union
2828

29-
from pydantic import BaseModel, ConfigDict, Field, field_validator
29+
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
3030

3131
from nemoguardrails.actions.rail_outcome import TransformTarget
3232

@@ -174,6 +174,14 @@ class Binding(BaseModel):
174174

175175
model_config = ConfigDict(extra="forbid", frozen=True)
176176

177+
@model_validator(mode="after")
178+
def _source_must_match_kind(self) -> "Binding":
179+
if self.kind == "literal" and self.key is not None:
180+
raise ValueError("Literal bindings cannot declare a source key.")
181+
if self.kind != "literal" and self.key is None:
182+
raise ValueError(f"{self.kind} bindings must declare a source key.")
183+
return self
184+
177185
@classmethod
178186
def surface_param(cls, action_param: str, name: str, *, required: bool = True) -> "Binding":
179187
"""Bind `action_param` to a caller-supplied surface parameter.
@@ -227,6 +235,18 @@ class RailSurface(BaseModel):
227235

228236
model_config = ConfigDict(extra="forbid", frozen=True)
229237

238+
@model_validator(mode="after")
239+
def _validate_execution_contract(self) -> "RailSurface":
240+
seen = set()
241+
duplicates = set()
242+
for binding in self.bindings:
243+
if binding.action_param in seen:
244+
duplicates.add(binding.action_param)
245+
seen.add(binding.action_param)
246+
if duplicates:
247+
raise ValueError(f"Rail surfaces cannot bind action parameters more than once: {sorted(duplicates)}.")
248+
return self
249+
230250

231251
class EnvVar(BaseModel):
232252
"""Environment variable declared by a rail requirement."""

tests/manifests/test_manifest.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
from nemoguardrails.manifests import (
2020
ActionRef,
21+
Binding,
2122
ConfigSpecRef,
2223
RailActions,
2324
RailConfigSchema,
@@ -121,3 +122,26 @@ def test_flat_manifest_is_rejected():
121122

122123
with pytest.raises(ValidationError):
123124
RailManifest.model_validate(flat_manifest)
125+
126+
127+
@pytest.mark.parametrize(
128+
"factory",
129+
(
130+
lambda: Binding(kind="context", action_param="text"),
131+
lambda: Binding(kind="surface_param", action_param="model"),
132+
lambda: Binding(kind="literal", action_param="mode", key="source", value="strict"),
133+
),
134+
)
135+
def test_binding_source_matches_binding_kind(factory):
136+
with pytest.raises(ValidationError, match="source key"):
137+
factory()
138+
139+
140+
def test_surface_rejects_duplicate_action_parameter_bindings():
141+
with pytest.raises(ValidationError, match="more than once"):
142+
RailSurface(
143+
name="duplicate bindings",
144+
direction="input",
145+
action=_action(),
146+
bindings=(Binding.context("text", "user_message"), Binding.literal("text", "value")),
147+
)

0 commit comments

Comments
 (0)