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
2 changes: 2 additions & 0 deletions audits/agentic/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ def validate_rules(rules: Any) -> list[RuleError]:
errors.append(RuleError(index, "", "each rule must be an object"))
elif rule.get("type") != "check":
errors.append(RuleError(index, str(rule.get("type", "")), "rules must be custom checks"))
elif set(rule) == {"type", "check", "legacy"} and rule["check"] == "unsupported" and isinstance(rule["legacy"], dict):
continue # kept by migration 0004; the engine reports it as INCONCLUSIVE
else:
errors.extend(RuleError(index, "check", problem) for problem in validate_check(rule))
return errors
Expand Down
19 changes: 19 additions & 0 deletions audits/agentic/tests/test_rule_migration.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,22 @@ def test_all_of_groups_are_flattened_into_separate_rules():
def test_metadata_without_rules_is_unchanged():
metadata = {"agentic": {"trace": {"required": True}}}
assert migration.convert_metadata(metadata) == metadata


def test_unsupported_rules_kept_by_the_migration_pass_validation_and_are_inconclusive():
from audits.agentic.checks.rules import rule_checks
from audits.agentic.expectations import validate_agentic_metadata
from audits.agentic.trajectory import normalize

metadata = migration.convert_metadata({"agentic": {"schema_version": 2, "enforcement": {}, "rules": [
{"type": "precedes", "first": "a", "then": "b"}]}})
converted = metadata["agentic"]["rules"]
assert validate_agentic_metadata(metadata).rules == converted
trace = normalize([{"span_id": "t1", "name": "execute_tool", "attributes": {
"gen_ai.operation.name": "execute_tool", "gen_ai.tool.name": "a"}}])
assert [check.status for check in rule_checks(trace, converted)] == ["INCONCLUSIVE"]


def test_only_the_migration_shape_of_an_unsupported_rule_skips_validation():
junk = {"type": "check", "check": "unsupported", "select": "junk"}
assert validate_rules([junk]) != []
7 changes: 7 additions & 0 deletions infra/tests/test_rules_form.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,10 @@ def test_custom_check_built_in_the_editor_is_saved():
"check": "count", "expect": {"op": "at_least", "value": 1}}
merged = _merge_structured_agentic_fields({}, _post(agentic_rules_json=json.dumps([rule])))
assert merged["agentic"]["rules"] == [rule]


def test_unsupported_rule_from_the_migration_is_saved_unchanged_beside_a_new_rule():
unsupported = {"type": "check", "check": "unsupported", "legacy": {"type": "precedes", "first": "a", "then": "b"}}
rule = {"type": "check", "select": {"kind": "tool", "where": []}, "check": "count", "expect": {"op": "at_least", "value": 1}}
merged = _merge_structured_agentic_fields({}, _post(agentic_rules_json=json.dumps([unsupported, rule])))
assert merged["agentic"]["rules"] == [unsupported, rule]
12 changes: 11 additions & 1 deletion templates/partials/agentic_rules_editor.html
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,15 @@
card.querySelector('[data-remove]').addEventListener('click', () => { rules.splice(index, 1); render(); sync(); });
}

// Mirrors rule_engine.unsupported_text. The rule is shown as stored and kept unchanged on save.
function renderUnsupported(card, rule, index) {
const legacy = rule.legacy && typeof rule.legacy === 'object' ? rule.legacy : {};
const name = legacy.type || rule.check || rule.type || 'rule';
card.querySelector('[data-summary]').textContent = `No longer supported (${name}): edit this rule to a count, exists or none check.`;
card.querySelector('[data-code]').textContent = JSON.stringify(legacy);
card.querySelector('[data-remove]').addEventListener('click', () => { rules.splice(index, 1); render(); sync(); });
}

function render() {
list.innerHTML = '';
rules.forEach((rule, index) => {
Expand All @@ -186,7 +195,8 @@
<button type="button" data-remove class="text-[11px] text-gray-500 hover:text-red-300 shrink-0">Remove</button>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-2" data-fields></div>`;
renderCustom(card, card.querySelector('[data-fields]'), rule, index);
if (rule.check === 'unsupported') renderUnsupported(card, rule, index);
else renderCustom(card, card.querySelector('[data-fields]'), rule, index);
list.appendChild(card);
});
sync();
Expand Down
Loading