> For the complete documentation index, see [llms.txt](https://docs.kyvvu.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.kyvvu.com/policy-authoring/compound.md).

# 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).

## 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.

```json
{
  "name": "External content taint: high-impact actions require a fresh gate",
  "rule_type": "not",
  "params": {
    "condition": {
      "rule_type": "all_of",
      "params": {
        "conditions": [
          {
            "rule_type": "any_of",
            "params": {
              "conditions": [
                {"rule_type": "current_is", "params": {"step_type": "step.exec"}},
                {"rule_type": "current_is", "params": {"step_type": "step.message", "verb": "POST"}},
                {"rule_type": "current_is", "params": {"step_type": "step.resource", "verb": "POST"}},
                {"rule_type": "current_is", "params": {"step_type": "step.resource", "verb": "PATCH"}},
                {"rule_type": "current_is", "params": {"step_type": "step.resource", "verb": "DELETE"}}
              ]
            }
          },
          {
            "rule_type": "history_contains",
            "params": {
              "step_type": "step.resource",
              "verb": "GET",
              "property_filter": {"target.trust": "external"}
            }
          },
          {
            "rule_type": "not",
            "params": {
              "condition": {
                "rule_type": "step_directly_preceded_by",
                "params": {"required_step_type": "step.gate"}
              }
            }
          }
        ]
      }
    }
  },
  "severity": "critical",
  "enforcement_point": "step_execution"
}
```

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.

## 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."

```json
{
  "name": "PII + product data + model requires human approval",
  "rule_type": "not",
  "params": {
    "condition": {
      "rule_type": "all_of",
      "params": {
        "conditions": [
          {"rule_type": "current_is",
           "params": {"step_type": "step.message", "verb": "POST"}},
          {"rule_type": "history_contains",
           "params": {"step_type": "step.resource", "verb": "GET",
                      "property_filter": {"target.table": "customer-data"}}},
          {"rule_type": "history_contains",
           "params": {"step_type": "step.resource", "verb": "GET",
                      "property_filter": {"target.table": "product-data"}}},
          {"rule_type": "history_contains",
           "params": {"step_type": "step.model"}},
          {"rule_type": "not",
           "params": {"condition": {"rule_type": "step_requires_gate",
                                    "params": {"target_step_types": ["step.message"],
                                               "target_verb": "POST",
                                               "gate_check_type": "human_approval"}}}}
        ]
      }
    }
  },
  "severity": "critical",
  "enforcement_point": "step_execution"
}
```

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:

```python
print(engine.explain(intended, context))
```

```
Evaluated 1 policy for step.message/POST "send_reply" (task=task-abc step=5):
  ✗ External content taint           (critical) FAILED
    not → FAIL (inner passed)
      all_of → pass (3/3 conditions met)
        any_of → pass (1/5 matched: current_is step.message/POST)
        history_contains → pass (step.resource/GET target.trust=external found)
        not → pass (inner failed)
          step_directly_preceded_by → FAIL (previous was step.model, not step.gate)
→ action=block (risk_score=1.00)
```

Incidents from compound policies carry the full condition tree in `violation_details`.

***

## Next steps

* [OWASP Default Template](/policy-authoring/owasp-default.md) — see compound policies in context
* [Built-in Rules Reference](/policy-authoring/rules-reference.md) — all primitive rules you can compose
* [Creating Policies](/policy-authoring/creating.md) — create and deploy your compound policy
