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

# A deploy agent

> An agent operates a Kubernetes cluster: a rollout restart runs on its own, applying to production waits for a human.

An on-call agent operates a cluster from incident tickets. Restarting a deployment is cheap to
undo and should just happen; applying a manifest to production should wait for a person;
deleting a namespace is not something an agent does, whatever the ticket says.

## The policy

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

actions:
  k8s.rollout_restart:
    effect: "restart:{cluster}:{deployment}"
    decision: allow
  k8s.apply:
    effect: "apply:{cluster}:{manifest_hash}"
    rules:
      - when: { cluster_in: [staging, dev] }
        decision: allow
      - decision: approve
  k8s.delete_namespace:
    decision: deny
```

## The code

```python runnable file=main.py theme={null}
from pathlib import Path

import ctrlrun
from ctrlrun import Control, Policy, SQLiteStateStore

HERE = Path(__file__).resolve().parent
STATE = HERE / ".ctrlrun"
STATE.mkdir(exist_ok=True)
for name in ("state.db", "state.db-wal", "state.db-shm"):
    (STATE / name).unlink(missing_ok=True)

calls: list[str] = []


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


store = SQLiteStateStore(STATE / "state.db")
control = Control(Policy.from_file(HERE / "ctrlrun.yaml"), store)


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


@ctrlrun.protect("k8s.apply", effect="apply:{cluster}:{manifest_hash}", control=control)
def apply(cluster: str, manifest_hash: str) -> str:
    return kubectl("apply", "-f", manifest_hash, "--context", cluster)


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


with ctrlrun.context(agent="oncall-agent"):
    print("restart checkout on prod:", restart(cluster="prod-eu", deployment="checkout"))
    print("apply to staging:", apply(cluster="staging", manifest_hash="sha256:9c1f"))

    try:
        apply(cluster="prod-eu", manifest_hash="sha256:9c1f")
    except ctrlrun.ApprovalRequired as pending:
        print("apply to prod: a human decides:", pending.request_id)
    else:
        raise SystemExit("a production apply ran without a human")

    try:
        delete_namespace(cluster="prod-eu", name="checkout")
    except ctrlrun.ActionDenied as refused:
        print("delete namespace: refused,", refused.reason)
    else:
        raise SystemExit("a namespace delete ran")

    # A second worker picks up the same ticket.
    try:
        restart(cluster="prod-eu", deployment="checkout")
    except ctrlrun.DuplicateEffect:
        print("second worker restarts checkout: refused, already done")
    else:
        raise SystemExit("the same restart ran twice")

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

## What the agent sees

```text theme={null}
restart checkout on prod: ok
apply to staging: ok
apply to prod: a human decides: apr_…
delete namespace: refused, decision
second worker restarts checkout: refused, already done
kubectl calls: 2
```

## The receipt

```bash runnable theme={null}
ctrlrun receipts --last 5
```

The `delete_namespace` receipt is `deny/blocked` with no effect key: the action has none,
because it never runs. The duplicate restart is `allow/blocked` on `restart:prod-eu:checkout`.

## When an AMBIGUOUS appears

`kubectl apply` that timed out may have applied. Check the cluster (`kubectl diff`), then
`ctrlrun resolve apply:prod-eu:sha256:… --committed` or `--failed`. The effect key is the
manifest's hash, so a re-apply of the same manifest is the same effect and a changed manifest is
a new one, which is what you want.

## Next

* [A database-migration agent](/cookbook/database-migration-agent).
* [Effect keys](/concepts/effect-keys) · [Get started](/get-started/quickstart) · [Why](/why).


## Related topics

- [A database-migration agent](/cookbook/database-migration-agent.md)
- [Cookbook](/cookbook/index.md)
- [Why CTRLRun](/why.md)
- [CTRLRun and durable workflow engines](/compare/durable-workflows.md)
- [Roadmap](/ROADMAP.md)
