For the complete documentation index, see llms.txt. This page is also available as Markdown.

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:

Rule
Passes when
Use for

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_of returns True when all danger conditions are present (= dangerous situation detected).

  • not inverts to False (= violated = blocked).

  • If any condition is absent, all_of returns False, not inverts to True (= 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:

  1. The outer not means: "block if the inner all_of passes."

  2. The inner all_of checks 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.

  3. If all three are true → dangerous → all_of passes → not blocks.

  4. If any is false → safe → all_of fails → not passes.

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

Last updated