Skip to content

Commit 817c3db

Browse files
committed
fix: close remaining unrendered template validation gaps
- Reject optional params without defaults in unrendered structural validation (catches templates that can never render at creation time) - Fix PATCH rename-only on unrendered templates: detect unrendered state before ControlDefinition.model_validate to avoid false CORRUPTED_DATA - Export UnrenderedTemplateControl from Python SDK - Strengthen rename test to verify enabled=false in response - Add test for optional-param-without-default rejection on unrendered create
1 parent b2e940d commit 817c3db

4 files changed

Lines changed: 63 additions & 9 deletions

File tree

‎sdks/python/src/agent_control/__init__.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ async def handle_input(user_message: str) -> str:
7070
TemplateControlInput,
7171
TemplateDefinition,
7272
TemplateValue,
73+
UnrenderedTemplateControl,
7374
)
7475

7576
from . import agents, controls, evaluation, evaluators, policies
@@ -1371,6 +1372,7 @@ async def main():
13711372
"ControlDefinition",
13721373
"TemplateControlInput",
13731374
"TemplateDefinition",
1375+
"UnrenderedTemplateControl",
13741376
"ControlSelector",
13751377
"ControlScope",
13761378
"ControlAction",

‎server/src/agent_control_server/endpoints/controls.py‎

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1194,12 +1194,17 @@ async def patch_control(
11941194
)
11951195
elif control.data:
11961196
# Get current enabled status for response
1197-
try:
1198-
ctrl_def = ControlDefinition.model_validate(control.data)
1199-
current_enabled = ctrl_def.enabled
1200-
except ValidationError:
1201-
# Data corrupted, use default enabled=True
1202-
_logger.warning("Control '%s' has invalid data, using default", control.name)
1197+
if _is_unrendered_template(control.data):
1198+
current_enabled = False
1199+
else:
1200+
try:
1201+
ctrl_def = ControlDefinition.model_validate(control.data)
1202+
current_enabled = ctrl_def.enabled
1203+
except ValidationError:
1204+
_logger.warning(
1205+
"Control '%s' has invalid data, using default",
1206+
control.name,
1207+
)
12031208

12041209
# Commit if anything changed
12051210
if updated:

‎server/src/agent_control_server/services/control_templates.py‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,25 @@ def _collect_param_references(
550550
code="undefined_parameter_reference",
551551
message=f"Template references undefined parameter '{parameter_name}'.",
552552
)
553+
# Reject optional params without defaults — they can never render.
554+
param_def = template.parameters[parameter_name]
555+
if (
556+
not param_def.required
557+
and _parameter_default(param_def) is _TEMPLATE_VALUE_MISSING
558+
):
559+
raise _render_error(
560+
detail=(
561+
f"Template parameter '{parameter_name}' is optional "
562+
"but referenced without a default value"
563+
),
564+
field=f"template.parameters.{parameter_name}",
565+
code="optional_referenced_parameter_requires_default",
566+
message=(
567+
f"Optional template parameter '{param_def.label}' is "
568+
"referenced in the template and must define a default "
569+
"value or be marked required."
570+
),
571+
)
553572
referenced.add(parameter_name)
554573
return
555574

‎server/tests/test_control_templates.py‎

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1599,7 +1599,9 @@ def test_patch_enable_on_unrendered_template_is_rejected(client: TestClient) ->
15991599
)
16001600

16011601

1602-
def test_patch_name_on_unrendered_template_works(client: TestClient) -> None:
1602+
def test_patch_name_on_unrendered_template_returns_correct_enabled(
1603+
client: TestClient,
1604+
) -> None:
16031605
# Given: an unrendered template control
16041606
control_id, _ = _create_unrendered_control(client)
16051607
new_name = f"renamed-unrendered-{uuid.uuid4()}"
@@ -1610,9 +1612,35 @@ def test_patch_name_on_unrendered_template_works(client: TestClient) -> None:
16101612
json={"name": new_name},
16111613
)
16121614

1613-
# Then: the rename succeeds
1615+
# Then: the rename succeeds and enabled is correctly reported as False
16141616
assert response.status_code == 200, response.text
1615-
assert response.json()["name"] == new_name
1617+
body = response.json()
1618+
assert body["name"] == new_name
1619+
assert body["enabled"] is False
1620+
1621+
1622+
def test_create_unrendered_template_rejects_optional_param_without_default(
1623+
client: TestClient,
1624+
) -> None:
1625+
# Given: a template with an optional parameter that has no default but is
1626+
# referenced in definition_template
1627+
payload = _unrendered_template_payload()
1628+
payload["template"]["parameters"]["pattern"]["required"] = False # type: ignore[index]
1629+
1630+
# When: creating an unrendered control
1631+
response = client.put(
1632+
"/api/v1/controls",
1633+
json={"name": f"bad-optional-{uuid.uuid4()}", "data": payload},
1634+
)
1635+
1636+
# Then: the server rejects — this template can never render
1637+
assert response.status_code == 422
1638+
body = response.json()
1639+
assert body["error_code"] == "TEMPLATE_RENDER_ERROR"
1640+
assert any(
1641+
err.get("code") == "optional_referenced_parameter_requires_default"
1642+
for err in body.get("errors", [])
1643+
)
16161644

16171645

16181646
def test_unrendered_template_excluded_from_evaluation(client: TestClient) -> None:

0 commit comments

Comments
 (0)