# 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": "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"
}
```

Note: this example uses bare `all_of` (not wrapped in `not`), because the `not` inside the conditions already inverts the gate check. The `all_of` passes when all conditions are met AND no gate exists — which means it returns `True` when the situation is safe (gate present) and `False` when dangerous (gate missing). Wait — actually, let's re-read. The inner `not(step_requires_gate)` returns `True` when no gate exists. So `all_of` returns `True` when all conditions hold AND no gate → this is a blocking trigger being detected as "passing." This actually needs to be wrapped in `not(all_of(...))` as well. Be careful with your logic.

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

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

The question should be specific, self-contained, and written in natural language.
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.
