Compound Policies
What you'll learn: How to combine rules using all_of, any_of, and not, including the critical not(all_of(...)) pattern for blocking triggers.
Compound rules
Three compound rules let you combine conditions:
all_of
All sub-conditions pass
Multiple requirements that must all be met
any_of
Any sub-condition passes
Alternative conditions, any one sufficient
not
The sub-condition fails
Inverting a condition
Compound rules recurse freely: all_of can contain any_of, which can contain not, which can contain a primitive rule.
The critical gotcha: not(all_of(...))
Rule functions return True to pass and False to block. This is the most common source of compound policy authoring mistakes.
If your intent is "block when conditions A, B, and C are all present," the naive approach is:
{"rule_type": "all_of", "params": {"conditions": [A, B, C]}}This is wrong. all_of returns True (passes) when all conditions are met. It would block every step where any condition is not met — the opposite of what you want.
The correct pattern:
{
"rule_type": "not",
"params": {
"condition": {
"rule_type": "all_of",
"params": {"conditions": [A, B, C]}
}
}
}How it works:
all_ofreturnsTruewhen all danger conditions are present (= dangerous situation detected).notinverts toFalse(= violated = blocked).If any condition is absent,
all_ofreturnsFalse,notinverts toTrue(= passes).
A misspelled step type or verb anywhere inside the nested conditions (step.mesage, post) is refused when the policy loads, the same as at the top level of a policy. A misspelled field or property_filter path is not: see Field and property paths.
Worked example: the taint policy
The OWASP default template's taint policy uses this pattern. It blocks high-impact actions when external content has entered the task and no fresh gate precedes the action.
The same policy as a manifest entry, which is how you would normally ship it (kyvvu try --manifest, kyvvu serve --policy-file and the dashboard all take this form):
Reading the logic:
The outer
notmeans: "block if the innerall_ofpasses."The inner
all_ofchecks three conditions:Current step is a high-impact action (exec, outbound message, or mutating resource call).
External content has been fetched earlier in the task.
The current step is NOT directly preceded by a gate.
If all three are true → dangerous →
all_ofpasses →notblocks.If any is false → safe →
all_offails →notpasses.
A step that a policy blocked stays in the task's history (the task is not ended), and the path rules treat it carefully: step_requires_gate and step_requires_predecessor ignore a blocked step, so a blocked gate never satisfies a requirement, and step_directly_preceded_by looks past blocked attempts to the last step that actually happened. A gate the agent approves and then retries therefore counts, and a blocked attempt does not hide the gate in front of it.
Another example: PII + product data + model requires approval
"If the agent has read customer-data AND product-data AND called a model, then sending an outbound message requires a human-approval gate."
The YAML equivalent:
This policy wraps all_of in an outer not, exactly like the previous example. The inner all_of passes (returns True) when the dangerous situation holds — customer-data and product-data were both read, a model was called, the current step is an outbound message, and no human-approval gate exists (the inner not(step_requires_gate) is True precisely when the gate is absent). Because a rule that returns True means compliant, that dangerous combination must be inverted by the outer not so it surfaces as a violation. The explain() output below shows this structure: not → FAIL (inner passed).
Debugging compound policies
Use the engine's explain() method to see per-node pass/fail:
Incidents from compound policies carry the full condition tree in violation_details.
Next steps
OWASP Default Template — see compound policies in context
Built-in Rules Reference — all primitive rules you can compose
Creating Policies — create and deploy your compound policy
Last updated
