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

# Ownership model

> Dorgu understands your whole cluster. It only changes the workloads you have handed it.

Dorgu understands your whole cluster. It only *changes* what you have explicitly handed it.

For a Deployment that Helm, ArgoCD, or Flux reconciles, Dorgu is **understanding and recommendation only**. It still detects the failure, still diagnoses the root cause, and still tells you exactly which value to change. It will **not** patch the Deployment. Instead it names the owner and shapes the fix as an instruction for that owner's source of truth.

Those three are named deliberately. All three run a controller and stamp what they reconcile, so their ownership is a fact Dorgu can read off the object. kustomize is not in that list, and the reason is worth reading before you rely on any of this: see [the kustomize limitation](#the-kustomize-limitation).

<Note>
  **Version requirement.** The ownership record ships in **operator v0.9.0**; the refusal is enforced by **CLI v0.10.0**. Both halves are needed for the guard to hold, and neither has been superseded. Which versions to actually be on, and what every mismatch does: [version coupling](#version-coupling).
</Note>

<Note>
  **Two corrections land in operator v0.10.0 and CLI v0.11.0.** Clean-room run #3 disproved two claims this page used to make, and both fixes now ship:

  * An `Update`-operation field manager **does** conflict with a later server-side apply. **CLI v0.11.0** [removes its own field-manager entry](#dorgu-leaves-no-field-manager-behind) after patching, and **operator v0.10.0** counts a foreign `Update`-op owner of a container's `resources` as ownership.
  * The kustomize rule never fired on real kustomize output. **Operator v0.10.0** matches the markers kustomize actually emits, and [the limitation is stated plainly](#the-kustomize-limitation) instead of being papered over.

  Both are described in place below. Run the newest pair: on operator v0.9.0 and CLI v0.10.0, this page's ownership guard works but a heal still leaves a field-manager entry behind.
</Note>

## Why Dorgu refuses

Patching an owned Deployment does not merely get reverted. It breaks the owner's *next* deploy.

Under server-side apply, the field manager that last set a field owns it. When the Dorgu CLI patches `resources.limits.memory` on a Deployment that Helm installed, that patch claims the field away from Helm's field manager. The fix works, the pod recovers, and everything looks fine. Then someone runs `helm upgrade` and it **hard-fails on a field-manager conflict**, because Helm still believes it owns a field it no longer does.

A fix that breaks your next deploy is not a fix. Clean-room testing hit exactly that failure: Dorgu healed a Helm-managed app, printed a green checkmark, and left the release un-upgradable.

The failure mode differs by owner, and none of them are good:

| Owner     | What a direct patch does                                                                                                                                                  |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Helm      | Claims the fields away from Helm's field manager. The next `helm upgrade` fails outright with a field-manager conflict.                                                   |
| ArgoCD    | Reverted on the next sync, or rejected outright under server-side apply.                                                                                                  |
| Flux      | Reverted the next time Flux reconciles the Deployment.                                                                                                                    |
| kustomize | Overwritten the next time somebody runs `kubectl apply -k`. Detected only when the overlay opts into a marker: see [the kustomize limitation](#the-kustomize-limitation). |
| unknown   | An unseen owner is still an owner. A patch that collides with one breaks their next deploy.                                                                               |

## `spec.workloadRef`

At proposal time the operator reads the **live** Deployment and records what it found on `RemediationAction.spec.workloadRef`:

```yaml theme={null}
spec:
  workloadRef:
    kind: Deployment
    name: frontend-podinfo          # the workload's name, not the persona's
    namespace: apps
    container: podinfo              # the container whose resources were read
    managedBy: helm
    managedByDetail: 'Helm release "frontend" in namespace apps'
    observedResources:
      limits:
        memory: 32Mi
        cpu: ""                     # empty means the workload does not set it
      requests:
        memory: 16Mi
        cpu: 50m
    observedImage: ghcr.io/stefanprodan/podinfo:6.14.1
    observedAt: "2026-08-23T09:14:02Z"
```

| Field               | Meaning                                                                                                                                                                                                     |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `kind`              | The workload kind. Only `Deployment` is resolved today.                                                                                                                                                     |
| `name`              | The live Deployment's `metadata.name`. On a brownfield cluster this is usually **not** the persona name: persona `frontend` resolves to Deployment `frontend-podinfo`.                                      |
| `namespace`         | The Deployment's namespace.                                                                                                                                                                                 |
| `container`         | The container whose resources were observed, and the one a fix would target.                                                                                                                                |
| `managedBy`         | One of `helm`, `argocd`, `flux`, `kustomize`, `unmanaged`, `unknown`. Defaults to `unknown`. `kustomize` is only ever set from an opt-in marker; see [the kustomize limitation](#the-kustomize-limitation). |
| `managedByDetail`   | The specific owner in prose, so a refusal can name what owns the workload rather than saying that something does.                                                                                           |
| `observedResources` | The live container's `requests` and `limits`. An **empty string means the workload does not set that key**, which is a different fact from a value of zero.                                                 |
| `observedImage`     | The container's live image reference including its tag. The only image Dorgu has actually read.                                                                                                             |
| `observedAt`        | When the live workload was read.                                                                                                                                                                            |

<Note>
  `workloadRef` is also what grounds every number Dorgu states. The `ApplicationPersona` is a point-in-time import that drifts from the running Deployment, so quoting persona values as current reality is how Dorgu once claimed a 96Mi limit on a container that had 32Mi. Facts, blast-radius caps, and diffs all come from `observedResources` now.
</Note>

## How ownership is detected

The operator reads server-side-apply field managers plus labels and annotations, **most specific owner first**, because owners layer on top of each other. A Flux `HelmRelease` renders Helm's own labels, so Flux has to win; an ArgoCD-managed Helm chart likewise carries Helm metadata that ArgoCD, not Helm, actually reconciles.

| Order | `managedBy` | Evidence                                                                                                                                                                                                                                                                                                                          |
| ----- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1     | `argocd`    | `argocd.argoproj.io/tracking-id` annotation, `argocd.argoproj.io/instance` label, or the `argocd-controller` / `argocd-application-controller` field manager                                                                                                                                                                      |
| 2     | `flux`      | `kustomize.toolkit.fluxcd.io/name` or `helm.toolkit.fluxcd.io/name` label, or the `kustomize-controller` / `helm-controller` field manager                                                                                                                                                                                        |
| 3     | `helm`      | `meta.helm.sh/release-name` annotation, `app.kubernetes.io/managed-by: Helm`, or the `helm` field manager                                                                                                                                                                                                                         |
| 4     | `kustomize` | `app.kubernetes.io/managed-by: kustomize-*` (the versioned value kustomize generates) or a bare `kustomize` written by hand, or the `config.kubernetes.io/origin` / `alpha.config.kubernetes.io/transformations` build-metadata annotations. **All three are opt-in:** see [the kustomize limitation](#the-kustomize-limitation). |
| 5     | `unknown`   | Another field manager already owns the fields a fix would write, and Dorgu does not recognise it. That is any **`Apply`**-operation entry, or an **`Update`**-operation entry holding a container's `resources` block. `managedByDetail` names it.                                                                                |
| 6     | `unmanaged` | None of the above. Nothing Dorgu can see reconciles this workload.                                                                                                                                                                                                                                                                |

Three details are worth knowing:

* **An `Update`-operation field manager counts as an owner when it holds the fields a fix would write.** This page used to say the opposite: that the `Update`-operation entry `kubectl patch` and `kubectl set` leave behind "claims no ongoing ownership of the fields, so it says nothing about whether a future patch will conflict". That was wrong, and it was reproduced twice against a real API server. Apply conflict detection is about *who owns the field*, not about how they came to own it:

  ```
  $ kubectl set resources deploy/probe --limits=memory=32Mi      # kubectl-set:Update
  $ kubectl apply --server-side --field-manager=some-gitops-tool -f probe.yaml
  error: Apply failed with 1 conflict: conflict with "kubectl-set" using apps/v1:
    .spec.template.spec.containers[name="probe"].resources.limits.memory
  ```

  So a foreign manager holding a container's `resources` reads as `unknown` whichever operation put it there. The check is scoped to `resources` on purpose: a manager that owns only `spec.replicas` (an autoscaler) or a pod-template annotation (a sidecar injector) is not in the way of a resource patch, and treating it as an owner would make Dorgu refuse to heal on most real clusters for no safety gain. An entry whose field set cannot be parsed is treated as owning, on the same house rule as `unknown`.

* **Your own `kubectl` is not an owner, and neither is Dorgu.** Managers named `kubectl-*`, `dorgu`, and `kube-controller-manager` are skipped by that rule. A human with kubectl is the *definition* of `unmanaged` rather than a counter-example to it, and refusing there would leave you worse off than healing. A leftover `kubectl-set` claim is a genuine conflict waiting for the next server-side apply, and a heal **clears** it rather than adding to it: see [Dorgu leaves no field manager behind](#dorgu-leaves-no-field-manager-behind).

* **A Flux `HelmRelease` reads as `flux`, not `helm`,** because Flux is what reconciles it. Editing chart values without going through the Flux source would get reverted.

<Warning>
  **`unknown` is treated as owned.** Where the Deployment cannot be resolved, or its server-side applier is not one Dorgu recognises, `managedBy` is `unknown` and Dorgu explains rather than writes. Absence of evidence that patching is safe is not evidence that it is, and nothing is patched on a guess.

  One consequence worth knowing: a workload the operator cannot resolve also loses its advisory `kubectl` commands, so a resolution failure costs plan usefulness as well as grounding.
</Warning>

## The kustomize limitation

Dorgu cannot reliably tell that a Deployment came from a kustomize overlay. Rather than advertise a protection it does not have, it says so.

**kustomize is a client-side renderer with no controller.** It builds YAML and hands it to `kubectl`; nothing of kustomize is left running in your cluster, and nothing reconciles its output until a human runs `kubectl apply -k` again. A Deployment created that way is indistinguishable at the API level from one created with `kubectl apply -f`: the same `kubectl-client-side-apply` field manager, no label, no annotation. So **a plain `kubectl apply -k` reads as `unmanaged`, and Dorgu will patch it.**

The three markers in the detection table above are all set by the *kustomization*, not by kustomize:

| Marker                                           | Written when the kustomization sets       |
| ------------------------------------------------ | ----------------------------------------- |
| `app.kubernetes.io/managed-by: kustomize-v5.8.1` | `buildMetadata: [managedByLabel]`         |
| `config.kubernetes.io/origin`                    | `buildMetadata: [originAnnotations]`      |
| `alpha.config.kubernetes.io/transformations`     | `buildMetadata: [transformerAnnotations]` |

<Warning>
  **The rule this replaces never fired on real kustomize output.** Detection required `app.kubernetes.io/managed-by: kustomize` **exactly**, and the label kustomize generates is **versioned**: `kustomize-v5.8.1`, checked against the version shipped inside `kubectl`. So the exact match only ever matched a bare `kustomize` somebody had hand-written into `commonLabels`. In clean-room testing a plain `kubectl apply -k` was classified `unmanaged`, which is precisely what the published claim said could not happen. The match is now on the `kustomize` prefix so both forms are recognised, and the honest statement is the one above: **an overlay that opts into no build metadata is invisible to Dorgu.**
</Warning>

What that costs you is narrower than it sounds, and Dorgu names it at the moment it matters. Because nothing reconciles an overlay on its own, a patch is not *fought*: it survives until somebody re-runs `kubectl apply -k`, at which point the overlay's value wins and the fix is reverted. There is no conflict and no broken deploy, because Dorgu [leaves no field-manager entry behind](#dorgu-leaves-no-field-manager-behind). So immediately before it writes, the CLI prints the limit of the classification that got it there:

```
  Nothing Dorgu can see reconciles this Deployment. A kustomize overlay leaves no marker,
  so if this app is rendered by one, re-applying it will revert this change.
```

Mirror the change back into your overlay, exactly as you would into Helm values.

## Dorgu leaves no field manager behind

*We change it, we do not own it.* On the one class of workload Dorgu will write to, it takes the fields it needs and then hands them back.

A heal is two writes, not one:

1. A strategic-merge patch under the field manager **`dorgu`**, rather than kubectl's default `kubectl-patch`. Using its own name is what makes the entry distinguishable from a `kubectl patch` *you* ran, which Dorgu has no business deleting.
2. A second patch removing that `dorgu` entry from `metadata.managedFields`, followed by a read-back to confirm it is gone.

The second write is the point, and it does more than avoid harm. An `Update` takes the fields it writes *away* from whoever held them, so a heal moves any pre-existing `kubectl-set` or `kubectl-patch` claim on those fields onto `dorgu` and then drops it. The fields end up owned by nobody, which is the state a server-side apply can claim without a conflict. **A heal now clears a conflict that was already there rather than merely avoiding a new one.** Bring a healed Deployment under Helm, ArgoCD, or Flux afterwards and that first apply succeeds.

<Note>
  **Why not `--server-side --field-manager=dorgu --force-conflicts`?** Because it is the worse of the two. Forcing the conflict works once and makes Dorgu a *persistent* `Apply`-operation owner of those fields, which is exactly what the next `helm upgrade` would then have to fight. Patch and release leaves nothing behind to fight.
</Note>

<Warning>
  **If the entry cannot be removed, Dorgu tells you.** The removal is a read-modify-write over state Dorgu does not own, so it carries the `resourceVersion` as a precondition, retries against fresh state on a conflict, and reads the object back afterwards. The read-back is not ceremony: the API server accepts a client-supplied `managedFields` list on some paths and recomputes it on others, so the only honest way to report "Dorgu owns nothing here" is to look.

  The workload is patched either way, so a failure here is not a failed heal. It is a future apply conflict, and it is printed as a warning naming the fields Dorgu now owns and the command to clear them:

  ```bash theme={null}
  kubectl patch deployment <name> -n <ns> --type merge \
    -p '{"metadata":{"managedFields":[{}]}}'
  ```

  That clears the whole `managedFields` list; every other manager reclaims its fields on its next apply. Leaving the footprint in silence was never an option.
</Warning>

## What changes about the plan

For an owned Deployment, the operator rewrites every step whose command would write to the cluster. The command is dropped, the description becomes what to change **at the source**, and the step's rationale gains one line on what a direct patch would have broken.

<Tabs>
  <Tab title="Helm">
    ```
    [1] Set resources.limits.memory: 128Mi in the values for Helm release
        "frontend" in namespace apps (the key is chart-specific, commonly
        under `resources`), then run your usual `helm upgrade` for that release.
        A direct patch would claim the fields it sets away from Helm release
        "frontend" in namespace apps, and your next helm upgrade would then
        fail with a field-manager conflict.
    ```

    The chart values key is hedged deliberately. Dorgu has not read your chart, so it says "commonly under `resources`" rather than asserting a path it cannot verify.
  </Tab>

  <Tab title="ArgoCD">
    ```
    [1] Set resources.limits.memory: 128Mi in the Git manifests for ArgoCD
        application "frontend", then commit and let ArgoCD sync.
        A direct patch would be reverted on the next sync by ArgoCD
        application "frontend", or rejected outright under server-side apply.
    ```
  </Tab>

  <Tab title="Flux">
    ```
    [1] Set resources.limits.memory: 128Mi in the Git source reconciled by
        Flux HelmRelease "frontend", then commit and let Flux reconcile it.
        A direct patch would be reverted the next time Flux HelmRelease
        "frontend" reconciles this Deployment.
    ```
  </Tab>

  <Tab title="unmanaged">
    ```
    [2] workload-apply (low; advisory): Correct the mistyped image tag
        nginx:1.27-alpineX does not exist; the published tag is nginx:1.27-alpine
        Run: kubectl set image deployment/web web=nginx:1.27-alpine -n demo
    ```

    An `unmanaged` workload keeps its direct `kubectl` command exactly as planned. This is the only case where Dorgu hands you a command that writes.
  </Tab>
</Tabs>

### Read-only commands survive

On an owned workload, a step's `Run:` line is kept when the command only reads:

```
[2] manual (low; advisory): Check the container's recent memory pattern
    Run: kubectl logs deployment/frontend-podinfo -n apps --previous
```

`kubectl logs`, `kubectl get events`, `kubectl describe`, `kubectl top`, `kubectl rollout status` and the rest of the read-only verbs are printed unchanged, because reading matters *most* on exactly the workloads Dorgu will not patch. It is what is left to hand over.

Classification is positive, not inferred: read-only verbs are listed explicitly, and anything unrecognised is refused rather than assumed harmless. That includes a bare `kubectl rollout` with no subcommand. The verb is found by scanning for the first token that is a known kubectl subcommand, so `kubectl -n apps patch ...` cannot hide `patch` behind a flag argument.

<Note>
  The operator strips workload-writing commands before it persists the object, and the CLI checks read-only-ness **again** before printing. The duplication is deliberate: the command field is model-authored, and the CLI reads `RemediationAction` objects straight out of the cluster, where an older operator or anything with permission to create the CRD could have put a `kubectl patch` in there.
</Note>

## What you see at the CLI

`dorgu remediation diff` names the workload and its owner in the header, and diffs **workload against workload** rather than persona against persona:

```
Remediation: fix-oom-frontend
════════════════════════════

Target:     ApplicationPersona/frontend (apps)
Workload:   Deployment apps/frontend-podinfo (container podinfo)
Owner:      Helm release "frontend" in namespace apps
Type:       persona-update
Confidence: 0.85
Plan:       ai-anthropic
Phase:      Pending
Incident:   im-oom-frontend

...

Deployment change (apps/frontend-podinfo, container podinfo):
  resources.limits.memory  32Mi -> 128Mi

Dorgu will not patch this Deployment: Helm release "frontend" in namespace apps owns it.
Make the change in the values for Helm release "frontend" in namespace apps (the key is
chart-specific, commonly under `resources`), then run your usual helm upgrade.

Actions:
  dorgu remediation approve fix-oom-frontend -n apps --no-heal   (record the decision, patch no workload)
  dorgu remediation reject fix-oom-frontend -n apps --reason "..."
```

Two things to notice. The `Owner:` line is there on unmanaged workloads too, reading `unmanaged (nothing reconciles it, so Dorgu may patch it)`, so you never have to infer which mode you are in. And the suggested action is **not** `approve`: printing a command that will be declined is how a guard that is working reads as a guard that is broken.

### The refusal

If you run `approve` or `heal` on an owned workload anyway, you get the refusal rather than a patch:

```
Dorgu will not patch this workload.

  Workload: Deployment apps/frontend-podinfo (container podinfo)
  Owner:    Helm release "frontend" in namespace apps

  A direct patch would claim the fields it sets away from Helm release "frontend"
  in namespace apps, and your next helm upgrade would then fail with a
  field-manager conflict.

Deployment change (apps/frontend-podinfo, container podinfo):
  resources.limits.memory  32Mi -> 128Mi

Apply it where this workload's desired state lives:
  [1] Set resources.limits.memory: 128Mi in the values for Helm release "frontend"
      in namespace apps (the key is chart-specific, commonly under `resources`),
      then run your usual helm upgrade for that release.

Nothing was approved and nothing in the cluster was changed.
To record the decision without a workload patch: dorgu remediation approve fix-oom-frontend -n apps --no-heal
```

**Approval is withheld along with the patch.** The gate sits in the preflight, ahead of any write, so on an owned workload `approve` writes nothing at all: no Deployment patch and no status patch. That is deliberate. Approving is what tells the operator to patch the persona and start the verification clock, so approving a change the CLI will not apply would leave the persona at 128Mi, the workload at 32Mi, and a ten-minute verification window running over a fix that was never coming.

## Exit code 4: declined by design

A refusal exits **`4`** (`ExitDeclined`), not `1`.

| Code | Meaning                                                                                                                                                                              |
| ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `0`  | Success                                                                                                                                                                              |
| `1`  | Failure: the command could not run                                                                                                                                                   |
| `4`  | **Declined.** Dorgu understood the request, could see exactly what to do, and chose not to write, because the workload belongs to somebody else. Nothing failed and nothing changed. |

The command ran, the plan was understood, and the decision was not to write. A script should be able to tell that apart from breakage.

<Warning>
  **Any wrapper that treats every non-zero exit as breakage will now report a successful refusal as an error.** Update it to accept `4`, or use `--no-heal` (which exits `0`) where you want the decision recorded without a workload patch.
</Warning>

## Recording the decision: `--no-heal`

`--no-heal` is how you say "I have read this, I agree, and I will apply it myself":

```bash theme={null}
dorgu remediation approve fix-oom-frontend -n apps --no-heal
```

It records `phase: Approved` on the `RemediationAction`, lets the operator patch the `ApplicationPersona` spec, and skips the Deployment patch entirely. Exit code `0`.

<Note>
  The CLI warns that the persona and the running workload will disagree until you apply the change at the source. That warning is accurate and worth heeding: the operator's verification window will run against a workload that has not changed yet.

  If you manage personas through GitOps, the persona patch will also show as drift on your next sync. Mirror it back into your repo so your pipeline does not revert it.
</Note>

## `--workload` no longer redirects the patch

`--workload` still resolves the Deployment, but it must **agree with the one recorded in `spec.workloadRef`**. A mismatch is refused:

```
refusing to patch Deployment frontend-canary: Dorgu checked ownership for
frontend-podinfo, and a decision about one workload does not carry to another
```

Ownership is a fact about one specific object. A flag that aims the patch at a Deployment the operator never observed is the guard with a hole in it: Dorgu would clear `frontend` as unmanaged and then write to `frontend-canary`, which Helm owns.

<Note>
  **`--container` still overrides freely,** because ownership is per-Deployment, not per-container. When `--container` is omitted, the container the operator actually read is used, so the patch targets the same container whose values were the diff's before-state.
</Note>

## What is *not* gated

Ownership governs one thing: the CLI patching your Deployment. Everything else is unchanged.

| Write                                                         | Actor                          | Gated on ownership?                                                |
| ------------------------------------------------------------- | ------------------------------ | ------------------------------------------------------------------ |
| `persona-update` step patches the `ApplicationPersona` spec   | Operator                       | **No.** Always safe, and `autoExecutable` semantics are unchanged. |
| `RemediationAction` / `IncidentMemory` / `DorguEvent` records | Operator                       | **No.** These are Dorgu's own CRDs.                                |
| Deployment patch on approval or heal                          | CLI, with **your** credentials | **Yes.** Only `managedBy: unmanaged` permits it.                   |

Detection, diagnosis, proposal, guardrails, verification, and incident memory all run identically on owned and unmanaged workloads. The operator [could not write your workloads if it wanted to](/security-and-permissions): its ClusterRole grants no `create`, `update`, or `patch` on Deployments.

## Version coupling

<Note>
  **Current versions: CLI `v0.12.0` with operator `v0.11.1`** (Helm chart `0.11.1`; the chart version, the chart `appVersion`, and the operator version are always the same number). Newest of each is always the right answer, and this note is the only place in these docs that states which that is.

  **On arm64 nodes, `0.11.1` is a floor and not a preference.** Every operator image published before it, up to and including `0.11.0`, was pushed as a single-platform `amd64` manifest rather than a manifest list. That covers AWS Graviton nodegroups and any kind or k3d cluster on an Apple Silicon Mac. On containerd there is no manifest list to select from, so the pull **succeeds** and the container dies immediately with `exec /manager: exec format error` and exit code 255. The pod reads `CrashLoopBackOff`, not `ImagePullBackOff`. `0.11.1` is that image and nothing else: no API, CRD, controller, or chart-template change, which is why everything below still names `v0.11.0` as the release each behaviour landed in.

  **There is no version pinning between the CLI and the operator.** Every field either side has added is optional and additive, so a half that does not know about a field simply sees it absent and renders exactly as it did before. Nothing has to be upgraded in lockstep.

  Three mismatches change behaviour. Only the first is unsafe, and it is the old **CLI**, not the old operator:

  | Mismatch                                                     | What happens                                                                                                                                                                                                                                                                                                                                    |
  | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
  | **CLI `v0.9.0` or older against operator `v0.9.0` or newer** | **Upgrade the CLI.** An older CLI never reads `spec.workloadRef`, so `approve` will patch a Deployment that Helm, ArgoCD, or Flux owns and break the owner's next apply. The operator supplies the ownership facts; declining the patch is the CLI's job. This is the one combination that silently does the thing the guard exists to prevent. |
  | CLI `v0.10.0` or newer against operator `v0.8.1` or older    | Conservative rather than dangerous. The operator writes no `workloadRef`, an absent record is treated as owned, and every remediation carrying a workload change is declined with exit `4`. `dorgu remediation approve <name> -n <ns> --no-heal` records the decision until the operator is upgraded.                                           |
  | CLI `v0.12.0` against operator `v0.10.0` or older            | No guardrail verdicts. They are read from the optional [`spec.steps[].safety`](/cli/architecture/crds#step-safety-fields) field that operator `v0.11.0` adds. The field is absent, `remediation list` grows no `GUARDRAIL` column, and every command prints what it printed before.                                                             |

  One operator upgrade is worth doing on its own merits, with no CLI implication at all: **with `aiRemediation.enabled=true`, operator `v0.11.0` is the release that made AI-planned remediations appliable.** On `v0.10.0` and older, a plan that diagnosed a resource change could be persisted with nothing to apply. See [AI setup](/operator/configuration/ai-setup).

  Both mismatch directions in full, with the ownership reasoning behind them: [version coupling](/ownership-model#version-coupling). Newest published releases: [CLI](https://github.com/dorgu-ai/dorgu/releases) and [operator](https://github.com/dorgu-ai/dorgu-operator/releases).
</Note>

The rest of this section is the history behind that note: which release introduced each half of the guard, and what each mismatch actually does. It states no "current" version of its own, so there is one place in these docs to correct when a release lands.

### The ownership guard: CLI v0.10.0 with operator v0.9.0

A missing `workloadRef` is deliberately treated as owned, which makes both mismatch directions predictable rather than silent.

| Combination                         | What happens                                                                                                                                                                                                                                                                                                                                                                                             |
| ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **CLI v0.10.0 + operator v0.9.0**   | Correct. Owned workloads are declined with owner-shaped steps; unmanaged ones are patched as before.                                                                                                                                                                                                                                                                                                     |
| **CLI v0.10.0 + operator ≤ v0.8.1** | The operator never writes `workloadRef`, and an absent record means either an operator too old to know or a workload that could not be read. So **every remediation carrying a workload change is declined**, exit `4`. Advisory-only remediations are unaffected, since there is no workload change to refuse. Use `--no-heal` to record decisions and apply changes yourself, or upgrade the operator. |
| **CLI ≤ v0.9.0 + operator v0.9.0**  | **The guard does not apply.** Older CLIs do not read `workloadRef` at all, so `approve` will still patch an owned Deployment and break the owner's next apply. The operator supplies the facts; refusing the patch is the CLI's job. Upgrade the CLI.                                                                                                                                                    |

<Warning>
  Of the two mismatches, **CLI ≤ v0.9.0 against operator v0.9.0 is the dangerous one**. The other direction is merely conservative: it declines things it could have patched. This one silently does the thing the guard exists to prevent.
</Warning>

### The field-manager footprint: CLI v0.11.0 with operator v0.10.0

The two halves of the footprint fix are independent rather than coupled. Neither requires the other to run, and there is no dangerous mismatch between them, only a partial one.

| Combination                        | What happens                                                                                                                                                                                                                                                               |
| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **CLI v0.11.0 + operator v0.10.0** | Correct. A heal leaves no field-manager entry, and a foreign `Update`-op owner of a container's `resources` is recognised as ownership.                                                                                                                                    |
| **CLI v0.11.0 + operator v0.9.0**  | The footprint is still stripped, so a healed workload stays safe to bring under Helm, ArgoCD, or Flux. But the operator still counts `Apply`-op managers only, so a workload whose `resources` are held by a foreign `Update`-op manager reads `unmanaged` and is patched. |
| **CLI v0.10.0 + operator v0.10.0** | Ownership is classified correctly, but a heal on an `unmanaged` workload still leaves a `kubectl-patch` entry owning the fields it wrote, so a later `helm upgrade` or server-side apply conflicts with it. This is the case worth upgrading out of.                       |

### Guardrail verdicts: CLI v0.12.0 with operator v0.11.0

Not coupled at all, and worth saying so explicitly, because it is the pair a reader is most likely to assume needs pinning. Operator v0.11.0 records what its guardrails decided on the optional [`spec.steps[].safety`](/cli/architecture/crds#step-safety-fields) field; CLI v0.12.0 prints it. Either half runs alone.

| Combination                          | What happens                                                                                                                                                                                                                                                                      |
| ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **CLI v0.12.0 + operator v0.11.0**   | Verdicts are printed as Dorgu's own arithmetic in `remediation diff`, in the approve and heal preview, and in the owned-workload refusal, which is the reader who most needs to know that a ceiling was Dorgu's and not the plan's.                                               |
| **CLI v0.12.0 + operator ≤ v0.10.0** | The field is absent, so no guardrail block is printed, `remediation list` grows no `GUARDRAIL` column, and `--json` gains no `safety` key. Every command renders exactly as it did before.                                                                                        |
| **CLI ≤ v0.11.0 + operator v0.11.0** | The operator writes the field and the CLI ignores it. Nothing is lost that was previously shown: the operator also writes the verdict into the step's description, so the refusal still reaches the reader, just as prose rather than under a heading with the numbers beside it. |

The reason this one exists at all is a reporting failure rather than a safety failure. The verdict used to be spliced onto the front of the model's `rationale` as `[safety:blast-radius] …`, which in clean-room run #4 put Dorgu's measurement one line below the model's own claim that the same 16x change was "well within a 2x ceiling", with nothing to tell the reader which of the two had been computed.

## Trying it

The [greenfield quickstart](/cli/quickstart) heals end to end, because its demo app is applied with `kubectl apply` and is therefore `unmanaged`. To see the ownership path instead, point Dorgu at anything you installed with Helm and break it **through Helm**:

```bash theme={null}
# Any Helm release will do. Import a persona so Dorgu can see it.
dorgu persona import -n apps --all --apply

# Give one of them a limit it cannot live within. Set it through Helm, so Helm
# stays the only manager of that field. The values key is chart-specific;
# `resources.limits.memory` is the common one.
helm upgrade <release> <chart> -n apps --reuse-values \
  --set resources.limits.memory=32Mi

# One detection cycle later
dorgu remediation list -n apps
dorgu remediation diff <remediation-name> -n apps
```

Pick a limit the container genuinely cannot live within. If the pod comes back healthy, halve it and try again.

<Warning>
  **Break it through the owner, not with `kubectl set resources`.** An earlier version of this walkthrough used `kubectl -n apps set resources deploy/<deployment> --limits=memory=32Mi`. It reaches the same broken state and Dorgu behaves identically: it detects, diagnoses, names the Helm release, and writes nothing at all. But `kubectl set` leaves a `kubectl-set` field manager owning `resources.limits.memory`, so the `helm upgrade` this walkthrough ends on **fails with a field-manager conflict against `kubectl-set`** and reads as though Dorgu broke the release. Dorgu never touched it, and the guard held. Setting the limit through `helm upgrade` leaves no conflicting manager, so the closing step tests the thing it is there to test.

  If you already ran the old version and are stuck on that conflict, clear the stale manager first:

  ```bash theme={null}
  kubectl patch deployment <deployment> -n apps --type merge \
    -p '{"metadata":{"managedFields":[{}]}}'
  ```

  Every manager, Helm included, reclaims its fields on its next apply.
</Warning>

The diff names the Helm release, shows the Deployment change, and offers `--no-heal` and `reject` instead of `approve`. Apply the fix where that chart's values live, then run your usual upgrade and record the decision:

```bash theme={null}
helm upgrade <release> <chart> -n apps --reuse-values \
  --set resources.limits.memory=128Mi

dorgu remediation approve <remediation-name> -n apps --no-heal
```

That `helm upgrade` succeeding afterwards is the whole point.

## Next steps

<CardGroup cols={2}>
  <Card title="Security and permissions" icon="shield-check" href="/security-and-permissions">
    The operator's actual ClusterRole, and the verbs it does not have
  </Card>

  <Card title="Self-healing" icon="heart-pulse" href="/operator/features/self-healing">
    Detection, diagnosis, guardrails, and verification
  </Card>

  <Card title="dorgu remediation" icon="terminal" href="/cli/commands/remediation">
    Every flag for diff, approve, reject, and heal
  </Card>

  <Card title="Trust model" icon="user-check" href="/cli/architecture/trust-model">
    Where ownership sits in the progressive trust levels
  </Card>
</CardGroup>
