> ## Documentation Index
> Fetch the complete documentation index at: https://ctrlrun.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Protect a function

> Wrap the function that acts with @ctrlrun.protect, name the action and its effect key, say who is acting.

Decorate the function that acts, name the action and the consequence, and put the call inside
a context that names who is acting. From then on every call is decided, reserved, executed and
recorded, and a refusal is an exception raised before the function body runs.

**Prerequisites:** `pip install ctrlrun`, Python 3.11 or later, an empty directory. Every block
below runs offline; the deploy tool is a stand-in that records calls.

<Steps>
  <Step title="Write the policy">
    Three actions across two domains: a rollout restart that is cheap to undo, a namespace
    delete that needs a human, and a role grant that is autonomous for readers and needs a
    human for anything else.

    ```yaml runnable theme={null}
    schema: ctrlrun.policy/v2

    actions:
      k8s.rollout_restart:
        effect: "restart:{cluster}:{deployment}"
        decision: allow
      k8s.delete_namespace:
        effect: "namespace:{cluster}:{name}"
        decision: approve
      iam.grant_role:
        effect: "grant:{principal}:{role}"
        rules:
          - when: { role_in: [reader, viewer] }
            decision: allow
          - decision: approve
    ```
  </Step>

  <Step title="Decorate the functions">
    `effect=` is a template over the function's own parameters. It names the consequence, so the
    same delete proposed twice, by a retry or by a second worker, is one effect.

    ```python runnable file=infra.py theme={null}
    import sys

    import ctrlrun

    calls: list[str] = []


    def kubectl(*args: str) -> str:
        calls.append(" ".join(args))
        return "ok"


    @ctrlrun.protect("k8s.rollout_restart", effect="restart:{cluster}:{deployment}")
    def restart(cluster: str, deployment: str) -> str:
        return kubectl("rollout", "restart", f"deployment/{deployment}", "--context", cluster)


    @ctrlrun.protect("k8s.delete_namespace", effect="namespace:{cluster}:{name}")
    def delete_namespace(cluster: str, name: str) -> str:
        return kubectl("delete", "namespace", name, "--context", cluster)


    @ctrlrun.protect("iam.grant_role", effect="grant:{principal}:{role}")
    def grant_role(principal: str, role: str) -> str:
        return kubectl("create", "rolebinding", f"{principal}-{role}", "--role", role, "--user", principal)


    with ctrlrun.context(agent="deploy-agent"):
        print("restart:", restart(cluster="prod-eu", deployment="checkout"))
        print("grant reader:", grant_role(principal="ana@example.com", role="reader"))

        try:
            delete_namespace(cluster="prod-eu", name="checkout")
        except ctrlrun.ApprovalRequired as pending:
            print("delete namespace: a human decides:", pending.request_id)
        else:
            sys.exit("a namespace delete ran without a human")

        try:
            restart(cluster="prod-eu", deployment="checkout")
        except ctrlrun.DuplicateEffect as refused:
            print("restart again:", type(refused).__name__)
        else:
            sys.exit("the same restart ran twice")

        try:
            grant_role(principal="ana@example.com", role="admin")
        except ctrlrun.ApprovalRequired:
            print("grant admin: a human decides")

    print("kubectl calls:", len(calls))
    ```

    Run it with `python infra.py`:

    ```text theme={null}
    restart: ok
    grant reader: ok
    delete namespace: a human decides: apr_…
    restart again: DuplicateEffect
    grant admin: a human decides
    kubectl calls: 2
    ```

    Two calls reached the stand-in: the restart and the reader grant. The delete and the admin
    grant are waiting for a person, and the second restart was refused because
    `restart:prod-eu:checkout` had already committed.
  </Step>

  <Step title="Read what happened">
    ```bash runnable theme={null}
    ctrlrun receipts
    ctrlrun effects
    ```

    Every call that reaches a decision has a receipt, denied ones included, and every effect
    key has a state. A call waiting on a human has an approval request and no receipt yet. The
    pending approvals are what `ctrlrun approve <request id>` answers; the
    [quickstart](/get-started/quickstart) walks through presenting one.
  </Step>
</Steps>

## The decorator's arguments

| Argument     | What it does                                                                                                               |
| ------------ | -------------------------------------------------------------------------------------------------------------------------- |
| `name`       | the action name the policy decides                                                                                         |
| `effect=`    | a template over the parameters naming the consequence; without it the call gets no reservation, which is right for a read  |
| `resource=`  | a template for the resource, part of the hash and what a grant's `resources:` matches                                      |
| `wait=True`  | block on the approval provider instead of raising `ApprovalRequired`; what an adapter or a webhook provider needs          |
| `lease=`     | how long the reservation is held for a slow call; past it the effect is `AMBIGUOUS`, never released                        |
| `reconcile=` | a function that asks the remote what happened to an effect key; [Reconcile automatically](/guides/reconcile-automatically) |
| `control=`   | a `Control` you built, instead of the one discovered from `ctrlrun.yaml`                                                   |

Template syntax is checked at decoration time, so a typo fails at import rather than mid-run.
A protected function may not take `*args` or `**kwargs`, and may not name a parameter after a
reserved subject.

## If it didn't work

* `ActionDenied: ... no principal is available`: the call is outside `ctrlrun.context(...)`.
* `ActionDenied: ... unknown_action`: the decorator's name is not a key under `actions:`.
* `EffectKeyError: ... {cluster}`: the template names a parameter the function does not have,
  or the argument was `None`.
* `PolicyError: ... could not be read`: no `ctrlrun.yaml` in the working directory and
  `$CTRLRUN_CONFIG` is unset.

## Next

* [Put the gateway in front of MCP](/guides/gateway-in-front-of-mcp): the same guarantees with no code change.
* [Effect keys](/concepts/effect-keys) · [Get started](/get-started/quickstart) · [Why](/why).


## Related topics

- [protect](/reference/api/protect.md)
- [Three ways in](/get-started/three-ways-in.md)
- [Python API](/reference/api/index.md)
- [Try it in your browser](/try-it.md)
- [Install](/get-started/install.md)
