Open
Conversation
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
Template controls can now be created without providing parameter values. The server validates the template structure but skips rendering, storing the template metadata with enabled=false. Unrendered controls are visible in listings, attachable to agents, but excluded from evaluation. - Add UnrenderedTemplateControl model for GET response union - Create endpoint branches: complete values render, incomplete store unrendered - Update endpoint supports unrendered→rendered transition when values provided - GET endpoints discriminate via condition key presence in stored JSONB - PATCH rejects enabling unrendered templates with 422 - ControlSummary gains template_rendered field - Runtime evaluation query skips unrendered templates - Agent policy validation skips unrendered templates - 10 new server tests covering the full unrendered lifecycle
- Prevent rendered→unrendered downgrade: updating a rendered template control with incomplete values now forces a full render attempt, returning a clear error about missing parameters instead of silently stripping rendered fields - Deepen unrendered structural validation: validate_template_structure now walks definition_template to check $param bindings, reject undefined parameter references, detect unused parameters, and reject hardcoded agent-scoped evaluator names - Fix PATCH enabled=false on unrendered templates: detect unrendered state before attempting ControlDefinition.model_validate, treating disable as a no-op instead of raising CORRUPTED_DATA - Add 4 behavioral tests: rendered rejects incomplete update, PATCH disable no-op, unrendered rejects undefined $param / unused param / agent-scoped evaluator
- 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
- Reject unknown keys and wrong-typed values in partial template_values on unrendered create (fail fast instead of persisting garbage) - Deduplicate structural validation: render_template_control_input now calls validate_template_structure instead of inlining the same checks - Fix description fallback in list endpoint: unrendered templates show template.description when top-level description is absent - Fix _reject_hardcoded_agent_scoped_evaluators to report actual condition path instead of hardcoded "condition.evaluator.name" - Fix PATCH error message indentation - Move UnrenderedTemplateControl import to module level in controls service - Add tests: unknown value key rejection, wrong-type value rejection, description fallback in list
- Wrap UnrenderedTemplateControl.model_validate in _parse_stored_control_data with proper error handling (returns 422 CORRUPTED_DATA instead of 500) - Wrap unrendered parse in list_controls_for_agent with try/except to skip corrupted rows instead of crashing the entire listing - Remove redundant unused-parameter check in render_template_control_input (already caught by validate_template_structure called at the top)
…elper - Skip unrendered template controls in check_evaluation_with_local so they don't trigger the server-call fallback (prevents hot-path latency regression for agents with attached unrendered templates) - Accept UnrenderedTemplateControl in to_template_control_input so callers can round-trip unrendered template data from GET endpoints - Add test: unrendered template does not trigger server fallback - Add test: to_template_control_input accepts unrendered template data
- Make /controls/validate mirror create: incomplete template values validate structure only (returns 200) instead of forcing a full render that rejects missing params. Use the render preview endpoint to check renderability. - Exclude unrendered templates from list filters that reference rendered-only fields (execution, step_type, stage, tag). Unrendered templates still appear in unfiltered listings and template_backed filter. - Update validate test to expect 200 for incomplete values - Add test: validate rejects structurally invalid unrendered templates - Add test: unrendered templates excluded from rendered-field filters but included in unfiltered listings
List elements now inherit their parent's depth instead of incrementing it. A flat array of strings at depth 11 no longer pushes each element to depth 12+, which was incorrectly rejecting real-world templates with nested boolean condition trees containing list-valued parameters.
# Conflicts: # models/src/agent_control_models/__init__.py # models/src/agent_control_models/controls.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this does
Today, creating or updating a control requires hand-editing the full control JSON — condition trees, evaluator configs, selector paths, and all. Templates let callers define a reusable control shape with named, typed parameters, and then create or update controls by filling in just the parameter values.
For example, instead of constructing an entire regex-denial control from scratch, a caller can submit a template with a
patternparameter and astep_nameparameter, provide values for those parameters, and get a fully rendered, validated control back.Template controls can also be created without parameter values (unrendered templates). This supports a "set up now, configure later" workflow — the template is attached to an agent but excluded from evaluation until values are provided.
RFC and implementation plan: https://gist.github.com/lan17/ea9aaca990c9bcbfda6595469f3e76c5
How it works
Templates use a render-before-save design. The caller sends a
TemplateDefinition(parameter schema + adefinition_templatewith{"$param": "..."}placeholders) andtemplate_values. The server substitutes the values, validates the result as an ordinaryControlDefinition, and stores both the rendered control and the template metadata in the samecontrols.dataJSONB column. No schema migration needed.A template control exists in one of two states:
ControlDefinition, and the control is ready for evaluation (once enabled).enabled: falseand excluded from evaluation.The evaluation engine never sees template metadata or unrendered templates. Rendered template controls use
ControlDefinitionRuntimewithextra="ignore"to skip template fields. Unrendered templates are filtered from runtime queries viadata ? 'condition'.Key design decisions:
PUT /controls) and update (PUT /controls/{id}/data) endpoints detect template payloads via aControlDefinition | TemplateControlInputunion and render transparently. One new endpoint (POST /control-templates/render) provides stateless previews.template_valuesstores an unrendered template (enabled: false). The server validates template structure (parameter references, forbidden fields, agent-scoped evaluators) but skips rendering. Partial values are type-checked on create.enabledandnamestay outside the template. Templates cannot set or bind these fields.enabledis managed viaPATCHand preserved across template updates. Enabling an unrendered template is rejected with 422.PUT /datawith a rawControlDefinitionon a template-backed control returns 409.$parambinding.GET /controls/{id}/datareturnsControlDefinitionfor rendered controls orUnrenderedTemplateControlfor unrendered templates.template_backedfilter.check_evaluation_with_localto avoid triggering server-call fallbacks.Reviewer guide
Start here — these tests show the full lifecycle:
test_render_control_template_preview_returns_rendered_control— preview a template without persistingtest_create_template_backed_control_persists_template_metadata— create rendered and verify stored statetest_create_unrendered_template_control_without_values— create without values and verify unrendered statetest_update_unrendered_template_with_complete_values_renders— provide values to render an unrendered templatetest_template_backed_control_evaluates_after_policy_attachment— attach to agent and verify evaluationtest_unrendered_template_excluded_from_evaluation— verify unrendered templates don't affect evaluationThen follow by layer:
models/.../controls.pyUnrenderedTemplateControl,_ConditionBackedControlMixin,ControlDefinitionextension,ControlDefinitionRuntimemodels/.../server.py_parse_control_input— discriminates raw vs template payloads, rejects mixed payloads. Response unions forGetControlResponse,GetControlDataResponseserver/.../services/control_templates.pycan_render_template,validate_template_structure,validate_partial_template_values,render_template_control_input, reverse path map, error remappingserver/.../endpoints/controls.py_materialize_control_input(rendered vs unrendered branching), PATCH handler (enable guard), list filters,_parse_stored_control_dataunionserver/.../services/controls.py,engine/.../core.pyControlDefinitionRuntimewired into evaluation, unrendered templates skipped in runtime and agent-controls queriessdks/python/.../controls.py,.../evaluation.pyto_template_control_input()handles both rendered and unrendered shapes.check_evaluation_with_localskips unrendered templatesV1 limitations
$paramescaping — the$paramkey is reserved in all template JSON values$paramreplaces the entire JSON value, not a substringTemplateControlInputonly (no rendered fields). Useto_template_control_input()SDK helper to reshape.Validation
make check(lint + typecheck + all tests)make sdk-ts-generate+make sdk-ts-name-check+make sdk-ts-typecheck+make sdk-ts-build