> 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:

```json
{"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:

```json
{
  "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",
  "scope": "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",
  "scope": "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


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.kyvvu.com/policy-authoring/compound.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
