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.
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 after patching, and operator v0.10.0 counts a foreignUpdate-op owner of a container’sresourcesas 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 instead of being papered over.
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 patchesresources.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:
spec.workloadRef
At proposal time the operator reads the live Deployment and records what it found on RemediationAction.spec.workloadRef:
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.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 FluxHelmRelease 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.
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 theUpdate-operation entrykubectl patchandkubectl setleave 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:So a foreign manager holding a container’sresourcesreads asunknownwhichever operation put it there. The check is scoped toresourceson purpose: a manager that owns onlyspec.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 asunknown. -
Your own
kubectlis not an owner, and neither is Dorgu. Managers namedkubectl-*,dorgu, andkube-controller-managerare skipped by that rule. A human with kubectl is the definition ofunmanagedrather than a counter-example to it, and refusing there would leave you worse off than healing. A leftoverkubectl-setclaim 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. -
A Flux
HelmReleasereads asflux, nothelm, because Flux is what reconciles it. Editing chart values without going through the Flux source would get reverted.
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 tokubectl; 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:
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. So immediately before it writes, the CLI prints the limit of the classification that got it there:
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:- A strategic-merge patch under the field manager
dorgu, rather than kubectl’s defaultkubectl-patch. Using its own name is what makes the entry distinguishable from akubectl patchyou ran, which Dorgu has no business deleting. - A second patch removing that
dorguentry frommetadata.managedFields, followed by a read-back to confirm it is gone.
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.
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.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.- Helm
- ArgoCD
- Flux
- unmanaged
resources” rather than asserting a path it cannot verify.Read-only commands survive
On an owned workload, a step’sRun: line is kept when the command only reads:
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.
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.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:
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 runapprove or heal on an owned workload anyway, you get the refusal rather than a patch:
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 exits4 (ExitDeclined), not 1.
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.
Recording the decision: --no-heal
--no-heal is how you say “I have read this, I agree, and I will apply it myself”:
phase: Approved on the RemediationAction, lets the operator patch the ApplicationPersona spec, and skips the Deployment patch entirely. Exit code 0.
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.
--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:
frontend as unmanaged and then write to frontend-canary, which Helm owns.
--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.What is not gated
Ownership governs one thing: the CLI patching your Deployment. Everything else is unchanged.
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: its ClusterRole grants no
create, update, or patch on Deployments.
Version coupling
Current versions: CLI
v0.12.0 with operator v0.11.0 (Helm chart 0.11.0; 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.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: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.Both mismatch directions in full, with the ownership reasoning behind them: version coupling. Newest published releases: CLI and operator.The ownership guard: CLI v0.10.0 with operator v0.9.0
A missingworkloadRef is deliberately treated as owned, which makes both mismatch directions predictable rather than silent.
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.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 optionalspec.steps[].safety field; CLI v0.12.0 prints it. Either half runs alone.
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 heals end to end, because its demo app is applied withkubectl apply and is therefore unmanaged. To see the ownership path instead, point Dorgu at anything you installed with Helm and break it through Helm:
--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:
helm upgrade succeeding afterwards is the whole point.
Next steps
Security and permissions
The operator’s actual ClusterRole, and the verbs it does not have
Self-healing
Detection, diagnosis, guardrails, and verification
dorgu remediation
Every flag for diff, approve, reject, and heal
Trust model
Where ownership sits in the progressive trust levels