Deep Review: 20260416-144208-pr-316

Date2026-04-16 14:42
Reporancher-sandbox/rancher-desktop-daemon
Round3
Author@jandubois
PR#316 — engine: replace Container.spec.state with action annotations
Branchcontainer-action-annotations
Commits397d40a engine: replace Container.spec.state with action annotations
ReviewersClaude Opus 4.7, Codex GPT 5.4, Gemini 3.1 Pro
VerdictMerge with fixes — one webhook gap leaves a stated safeguard unenforced on the update path; stale comments need cleanup.
Wall-clock time27 min 8 s


Executive Summary

The core design lands cleanly. Replacing the level-triggered Container.spec.state field with a one-shot containers.rancherdesktop.io/action annotation plus a status.lastAction outcome correctly recognizes that Docker (restart policy, out-of-band docker start/stop) is a concurrent writer that a level-triggered mirror cannot co-own. The implementation handles crash-replay (dispatch before status patch, annotation persists until cleanup), informer-cache staleness (pre-checks on pause/unpause), concurrent annotation overwrites (observed-vs-current comparison in removeActionAnnotation), and webhook bypass at dispatch time (IsValid guard in processContainerAction). The SSA/finalizer simplification enabled by the now-empty ContainerSpec is a genuine cleanup.

One real correctness gap: the webhook's stated safeguard that "a hand-written Container cannot drive a Docker action" is only enforced on Create. A caller can create a placeholder Container without the annotation and then PATCH the annotation in, and ValidateUpdate will accept it. The remaining findings are documentation: two stale code comments still reference the removed spec.state / reconcileContainerSpecs identifiers, and a third comment ("prevents all modification") no longer describes what the webhook does.

Structure: 0 critical · 1 important · 6 suggestions.


Critical Issues

None.


Important Issues

I1. Webhook safeguard enforced only on create, not on update Codex

// ValidateCreate implements [ctrlwebhookadmission.Validator].
// The engine watcher creates Container mirrors and never sets the action
// annotation; reject any create that carries one so a hand-written
// Container cannot drive a Docker action against its metadata.name.
func (c *immutableValidator) ValidateCreate(_ context.Context, newContainer *v1alpha1.Container) (warnings ctrlwebhookadmission.Warnings, err error) {
	if _, ok := newContainer.Annotations[v1alpha1.AnnotationAction]; ok {
		return nil, fmt.Errorf("%s annotation is not allowed on create", v1alpha1.AnnotationAction)
	}
	return nil, nil
}

// ValidateDelete implements [ctrlwebhookadmission.Validator].
func (c *immutableValidator) ValidateDelete(context.Context, *v1alpha1.Container) (warnings ctrlwebhookadmission.Warnings, err error) {
	return nil, errors.New("webhook does not implement delete")
}

// ValidateUpdate implements [ctrlwebhookadmission.Validator].
// The whole Container spec is immutable on update: actions are requested via
// the AnnotationAction annotation on metadata, not via a level-triggered
// desired-state field.
//
// Provenance of the annotation is not checked. ValidateCreate rejects
// creates that carry the action annotation, but a caller can create an
// empty Container and then PATCH the annotation in. Closing that bypass
// would require a managedFields check, which is more cost than benefit on
// a single-user desktop where the principal already has Docker socket
// access.
func (c *immutableValidator) ValidateUpdate(_ context.Context, oldContainer, newContainer *v1alpha1.Container) (warnings ctrlwebhookadmission.Warnings, err error) {
	if !equality.Semantic.DeepEqual(oldContainer.Spec, newContainer.Spec) {
		return nil, fmt.Errorf("spec is immutable: old: %v, new: %v", oldContainer.Spec, newContainer.Spec)
	}
	if raw, ok := newContainer.Annotations[v1alpha1.AnnotationAction]; ok {
		if !v1alpha1.ContainerAction(raw).IsValid() {
			return nil, fmt.Errorf("invalid %s annotation %q", v1alpha1.AnnotationAction, raw)
		}

The comment on ValidateCreate (lines 24–26) states the intent explicitly: "The engine watcher creates Container mirrors and never sets the action annotation; reject any create that carries one so a hand-written Container cannot drive a Docker action against its metadata.name." ValidateUpdate does not enforce that invariant — it only rejects invalid action values. A two-step bypass defeats the guard:

type immutableValidator struct {
	Client client.Client
}

// ValidateCreate implements [ctrlwebhookadmission.Validator].
// The engine watcher creates Container mirrors and never sets the action
// annotation; reject any create that carries one so a hand-written
// Container cannot drive a Docker action against its metadata.name.
func (c *immutableValidator) ValidateCreate(_ context.Context, newContainer *v1alpha1.Container) (warnings ctrlwebhookadmission.Warnings, err error) {
	if _, ok := newContainer.Annotations[v1alpha1.AnnotationAction]; ok {
		return nil, fmt.Errorf("%s annotation is not allowed on create", v1alpha1.AnnotationAction)
	}
	return nil, nil
  1. Create a Container with metadata.name=<some docker container id> and no annotation. ValidateCreate accepts it (spec is empty, no annotation).
  2. PATCH the same object to add metadata.annotations[containers.rancherdesktop.io/action]=stop. ValidateUpdate accepts it.

The reconciler path does not distinguish mirrors from hand-written objects. reconcileContainerActions at engine_reconciler.go:366-385 lists every Container in the namespace, and processContainerAction at docker_watcher.go:356-403 dispatches the Docker call against c.Name. If the chosen name matches a real Docker container ID, the action runs.

	r.watcherMu.Unlock()
	if w == nil {
		return nil
	}

	var containers containersv1alpha1.ContainerList
	if err := r.List(ctx, &containers, client.InNamespace(r.apiNamespace)); err != nil {
		return err
	}

	var errs []error
	for i := range containers.Items {
		c := &containers.Items[i]
		if c.DeletionTimestamp != nil {
			continue
		}
		if _, ok := c.Annotations[containersv1alpha1.AnnotationAction]; !ok {
			continue
		}
		if err := w.processContainerAction(ctx, c); err != nil {
			errs = append(errs, fmt.Errorf("container %s: %w", c.Name, err))
		}
	}
	return errors.Join(errs...)
}

// processFinalizers handles resources with a deletion timestamp by deleting
// the corresponding Docker object and removing the finalizer.
func (r *EngineReconciler) processFinalizers(ctx context.Context) error {
	r.watcherMu.Lock()
// replays the action. Start, stop, pause, and unpause are idempotent
// against a container already in the target state, so replay is safe.
// Restart has no target state to match: a replay sends SIGTERM and waits
// the grace period a second time, which the controller cannot distinguish
// from a deliberate re-request.
func (w *dockerWatcher) processContainerAction(ctx context.Context, c *containersv1alpha1.Container) error {
	raw, ok := c.Annotations[containersv1alpha1.AnnotationAction]
	if !ok {
		return nil
	}

	log := logf.FromContext(ctx).WithName("docker-watcher")
	action := containersv1alpha1.ContainerAction(raw)
	observedAt := metav1.Now()

	// The webhook rejects invalid action values, but one written while the
	// webhook is offline can still reach storage. Drop such values here;
	// otherwise the CRD enum rejects the status.lastAction write, the
	// annotation stays in place, and every reconcile retries forever.
	if !action.IsValid() {
		log.Info("Dropping invalid container action annotation", "id", c.Name, "action", raw)
		return w.removeActionAnnotation(ctx, c, raw)
	}

	dockerErr := w.dispatchContainerAction(ctx, log, c.Name, action)

	lastAction := &containersv1alpha1.ContainerLastAction{
		Action:      action,
		ObservedAt:  observedAt,
		CompletedAt: metav1.Now(),
	}
	if dockerErr == nil {
		lastAction.State = containersv1alpha1.ContainerActionSucceeded
	} else {
		lastAction.State = containersv1alpha1.ContainerActionFailed
		lastAction.Error = dockerErr.Error()
		log.Info("Container action failed", "id", c.Name, "action", action, "error", dockerErr)
	}

	latest, err := w.patchContainerLastAction(ctx, c.Name, lastAction)
	if err != nil {
		return fmt.Errorf("failed to patch lastAction for %s: %w", c.Name, err)
	}
	if latest == nil {
		// Mirror was deleted between dispatch and the status patch; nothing
		// left to clean up.
		return nil
	}
	if err := w.removeActionAnnotation(ctx, latest, raw); err != nil {
		return fmt.Errorf("failed to remove action annotation for %s: %w", c.Name, err)
	}
	return nil
}

// dispatchContainerAction executes the Docker call for a single action. The
// caller pre-validates the action name; the default branch triggers only
// when a new ContainerAction value is added to the type but not to the switch.
//

The impact in practice is bounded — on a single-user desktop tool the same principal already has Docker socket access — but the webhook's stated safeguard does not hold. Either the safeguard should hold end-to-end, or the create-time rejection should be dropped so the inconsistency does not mislead future readers.

Fix: apply the same provenance check on update, or drop the Create rejection. The cleanest enforcement is to require an engine-owned marker (the engine.rancherdesktop.io/mirror finalizer is already added on every applyContainer, though a caller can set it themselves) or to inspect managedFields to confirm the object was originally created by engine-controller before accepting an action annotation.


Suggestions

S1. Stale comment references the removed reconcileContainerSpecs / spec.state Claude Codex
	// no-ops when nothing changed, so stable reconciles pay nothing.
	if err := r.setEngineCondition(ctx, &app, metav1.ConditionTrue, "Connected", "Container engine synced"); err != nil {
		return ctrl.Result{}, err
	}

	// Two passes run per Reconcile: reconcileContainerActions drives
	// Docker actions from the action annotation, and
	// processFinalizers forwards K8s-side deletes to Docker for any
	// mirror still carrying the mirror finalizer.
	//
	// Both passes issue List calls per reconcile, and every
	// Container/Image/Volume watch event triggers a reconcile — so
	// cost is O(N) per child event. The long-term fix is to split
	// these into per-resource reconcilers with watch predicates

The function was renamed to reconcileContainerActions in this PR (called at line 199) and Container.spec.state no longer exists. The rest of the block (lines 188-197) was updated to reference the new "action annotation present" watch predicate, but lines 183-184 were missed. A reader following this comment is pointed at code that was deleted in the same commit. Both Claude and Codex flagged this; Codex also included container_controller.go:81 (see S3) under the same theme.

		Scheme:   mgr.GetScheme(),
		Recorder: mgr.GetEventRecorder(ControllerName + "-controller"),
	}).SetupWithManager(mgr)
}

// setupWebhookWithRuntimeConfig registers a webhook that enforces an immutable
// spec, rejects action annotations on create, and validates action annotations
// on update.
func (c *controller) setupWebhookWithRuntimeConfig(mgr ctrl.Manager) error {
	mgr.GetLogger().Info("Setting up container webhook")
	mutatingConfig := base.WebhookConfig[*v1alpha1.Container]{
	// Two passes run per Reconcile: reconcileContainerActions drives
	// Docker actions from the action annotation, and
	// processFinalizers forwards K8s-side deletes to Docker for any
	// mirror still carrying the mirror finalizer.
	//
	// Both passes issue List calls per reconcile, and every
	// Container/Image/Volume watch event triggers a reconcile — so
	// cost is O(N) per child event. The long-term fix is to split
	// these into per-resource reconcilers with watch predicates
	// (deletionTimestamp set, action annotation present).
	//
	// Run both passes unconditionally and join their errors. Early
	// return on actionsErr would stall every mirror's finalizer behind
	// a single stuck container action: the next reconcile would hit
	// the same broken container first and skip finalizers again.
	var actionsErr, finErr error
	if err := r.reconcileContainerActions(ctx); err != nil {
		actionsErr = fmt.Errorf("failed to reconcile container actions: %w", err)
	}
	if err := r.processFinalizers(ctx); err != nil {
		finErr = fmt.Errorf("failed to process finalizers: %w", err)
	}

Fix:

-	// Two passes run per Reconcile: reconcileContainerSpecs drives
-	// Docker start/stop from Container.spec.state, and
+	// Two passes run per Reconcile: reconcileContainerActions drives
+	// Docker actions from the action annotation, and
 	// processFinalizers forwards K8s-side deletes to Docker for any
 	// mirror still carrying the mirror finalizer.
S2. ContainerSpec Godoc first line contradicts the rest of the comment Claude
	// IPv4 and IPv6 bindings.
	//
	// +required
	Bindings []ContainerPortBinding `json:"bindings"`
}

// ContainerSpec is currently empty. Actions are requested via the AnnotationAction
// annotation, not a level-triggered desired-state field. Docker's restart
// policy and out-of-band `docker start/stop` are competing writers, so a
// level-triggered desired state would fight them.
type ContainerSpec struct{}

// ContainerStatus defines the observed state of the container.
type ContainerStatus struct {
	// Name of the container; this is distinct from the container ID.
	//

Line 124 predates the PR but is now directly contradicted by lines 125-128 (added by the PR): the struct is not "the configuration the container was created with," it is intentionally empty. A reader hitting the first line is told to expect created-time configuration. The PR extended the doc but did not rewrite the opener.

Fix: delete or rewrite the first line so the opener matches the rest.

-// ContainerSpec defines the configuration the container was created with.
-// Container spec is empty today: actions are requested via the AnnotationAction
+// ContainerSpec is currently empty. Actions are requested via the AnnotationAction
 // annotation, not a level-triggered desired-state field. Docker's restart
 // policy and out-of-band `docker start/stop` are competing writers, so a
 // level-triggered desired state would fight them.
 type ContainerSpec struct{}
S3. "Prevents all modification" comment no longer matches the webhook's behavior Codex
		Scheme:   mgr.GetScheme(),
		Recorder: mgr.GetEventRecorder(ControllerName + "-controller"),
	}).SetupWithManager(mgr)
}

// setupWebhookWithRuntimeConfig registers a webhook that enforces an immutable
// spec, rejects action annotations on create, and validates action annotations
// on update.
func (c *controller) setupWebhookWithRuntimeConfig(mgr ctrl.Manager) error {
	mgr.GetLogger().Info("Setting up container webhook")
	mutatingConfig := base.WebhookConfig[*v1alpha1.Container]{

After this PR, ValidateCreate allows creates without the action annotation, and ValidateUpdate allows updates that add or change a valid action annotation. The webhook no longer "prevents all modification" — it enforces a spec-immutability rule and a valid-action rule. Rewrite the comment to describe what the webhook actually does.

S4. Invalid-action fallback is silent to the caller Claude

	// The webhook rejects invalid action values, but one written while the
	// webhook is offline can still reach storage. Drop such values here;
	// otherwise the CRD enum rejects the status.lastAction write, the
	// annotation stays in place, and every reconcile retries forever.
	if !action.IsValid() {
		log.Info("Dropping invalid container action annotation", "id", c.Name, "action", raw)
		return w.removeActionAnnotation(ctx, c, raw)
	}

	dockerErr := w.dispatchContainerAction(ctx, log, c.Name, action)

	lastAction := &containersv1alpha1.ContainerLastAction{
		Action:      action,

The webhook normally rejects invalid values at admission, so this branch only fires when the webhook is bypassed (failurePolicy: Ignore, or the startup window before registration). When it does fire, the caller gets no feedback: lastAction is untouched and the annotation disappears. An Info log is invisible to a GUI client. Two options: raise the log to Error with a clear message, or emit a Kubernetes Event on the Container so a UI can surface it. Low priority — the webhook catches the normal case — but worth doing because the dropped-annotation branch is specifically the "webhook was bypassed" path, which is exactly when observability matters.

S5. ObservedAt semantics are ambiguous in the doc comment and could be clearer Claude
	State ContainerActionState `json:"state"`
	// Error is the error message if the action failed.
	//
	// +optional
	Error string `json:"error,omitempty"`
	// ObservedAt is when the reconciler began processing the action annotation.
	// Backlog may delay this relative to the user's write time, and dispatch
	// (especially restart's grace period) extends the gap to CompletedAt.
	//
	// +optional
	ObservedAt metav1.Time `json:"observedAt,omitempty,omitzero"`
	// CompletedAt is when the action completed, regardless of outcome.
	//
	// +optional
	CompletedAt metav1.Time `json:"completedAt,omitempty,omitzero"`

In processContainerAction, observedAt := metav1.Now() is captured after the annotation has already been seen via List, just before the IsValid check and dispatch. CompletedAt is captured after dispatchContainerAction returns. If a caller computes CompletedAt - ObservedAt to measure user-visible latency, it will get the dispatch duration (which includes a multi-second grace period on restart or stop), not the observe-to-process backlog. Either rename the field to StartedAt/ProcessingStartedAt, or clarify the Godoc to "when the reconciler began processing the annotation." Keep the design doc text (docs/design/api_containers.md:160-161) in sync.

  # direct `docker stop`) in between. The `error` field is set only
  # when `state` is `Failed`.
  lastAction:
    action: pause
    state: Succeeded
    observedAt: "2026-04-15T10:30:00Z"
    completedAt: "2026-04-15T10:30:00Z"
```

### Container state

`status.status` always reflects the actual Docker state. The engine
S6. removeActionAnnotation comment asserts an invariant the invalid-action branch does not satisfy Claude
		return nil
	})
	return result, err
}

// removeActionAnnotation clears the AnnotationAction annotation only if
// its value still matches the action just processed. A concurrent writer
// may replace the annotation with a different action between dispatch
// and cleanup; that new value must survive so the next reconcile picks
// it up. Callers pass either a fresh Container from patchContainerLastAction
// (which bypasses the informer cache, not yet showing the preceding status
// write) or a cached one (which may 409 on the first Patch). On conflict,
// the retry re-reads from the cache; by then it has usually caught up.
func (w *dockerWatcher) removeActionAnnotation(ctx context.Context, latest *containersv1alpha1.Container, observed string) error {
	firstAttempt := true
	return retry.RetryOnConflict(retry.DefaultRetry, func() error {
		if !firstAttempt {
			if err := w.k8s.Get(ctx, client.ObjectKeyFromObject(latest), latest); err != nil {

The !action.IsValid() branch in processContainerAction passes c directly from the cache-backed List in reconcileContainerActions, not a fresh Container from patchContainerLastAction. Functionally the RetryOnConflict loop still works (its second attempt re-reads on conflict), but the comment's claim that "the caller passes a fresh Container" is not universally true. Either do a direct (non-cache) Get in the invalid-action branch before calling removeActionAnnotation, or soften the comment to "callers may pass either a fresh Container or a cached one; the retry loop tolerates cache lag."

S7. No unit test for the dispatchContainerAction default branch Claude
		return err
	case containersv1alpha1.ContainerActionRestart:
		log.Info("Restarting container", "id", id)
		_, err := w.cli.ContainerRestart(ctx, id, dockerclient.ContainerRestartOptions{})
		return err
	}
	return fmt.Errorf("unknown container action %q", action)
}

// containerRunState reports whether Docker currently shows the container as
// running and as paused. Pause uses paused to skip a no-op dispatch; unpause
// also checks running so that unpause on a stopped container reports a

IsValid and the webhook both gate input, so this path is unreachable under normal operation. The comment at lines 406-408 correctly notes that the default branch only triggers when a new ContainerAction constant is added without a matching switch case. A tiny unit test that constructs a value not in the switch and asserts the error message would catch future drift between the enum, IsValid, and this switch. Low priority.

	}
	return nil
}

// dispatchContainerAction executes the Docker call for a single action. The
// caller pre-validates the action name; the default branch triggers only
// when a new ContainerAction value is added to the type but not to the switch.
//
// Pause and unpause pre-check the current Docker state and return nil when
// the container is already in the target state. Two reconcile ticks can read
// the same action annotation through the informer cache before the cache
// sees the removal that follows the first dispatch — the pre-check keeps
// the second dispatch from flipping lastAction to Failed. Start, stop, and

Design Observations

Strengths

Concerns


Testing Assessment

BATS coverage is solid for the happy paths: start, stop, pause/unpause toggle, pause-on-stopped and unpause-on-stopped failures, restart (with pre/post StartedAt comparison), webhook rejection of an invalid action on update, and webhook rejection of the annotation on create. Gaps, in rough order of risk:

  1. No test for the create-then-update bypass path (see I1). Adding a test that creates a Container without the annotation and then patches in a valid action, asserting the update is rejected, would pin the invariant the webhook claims to enforce. Codex
  2. No test for the webhook-bypass drop-path — the !action.IsValid() branch in processContainerAction has no integration test because the webhook catches invalid values first. A unit test constructing a Container with a bogus annotation and calling processContainerAction directly would provide cheap coverage. Claude
  3. No test that lastAction persists across a direct docker stop. The design doc (docs/design/api_containers.md:153-156) asserts "Persists ... regardless of any observable state changes (e.g. a direct docker stop) in between." Simple to add: request start, verify lastAction=start/Succeeded, docker stop, verify lastAction unchanged. Claude
  4. No test for concurrent annotation overwrite during dispatch. The current != observed guard in removeActionAnnotation is covered only by code comment. Hard to race deterministically in BATS; possible as a unit test with a mock client returning different values on subsequent Gets. Claude
  5. lastAction timestamp fields and overwrite behavior across consecutive actions are not asserted. The BATS suite checks action and state but never reads observedAt/completedAt. Codex
  6. Restart double-dispatch race has no coverage. Acknowledged as a known trade-off; likely not worth testing. Claude

Documentation Assessment

The design doc (docs/design/api_containers.md) is comprehensive and matches the implementation: the action table, crash-replay semantics, failure example, and lastAction schema are all accurate. The CRD matches the Go types.

Stale code-level comments (captured as S1–S3) are the only documentation issues: engine_reconciler.go:183-186 still names reconcileContainerSpecs, container_types.go:124 still opens with "defines the configuration the container was created with," and container_controller.go:81 still says the webhook "prevents all modification." Codex grouped these into one finding; Claude split the first two across I-level items; the consolidation treats all three at Suggestion severity.

The PR description's state=InProgress|Succeeded|Failed and requestedAt nomenclature does not match the landed code (Succeeded|Failed and observedAt), but only the PR description (not the commit message, design doc, or CRD) is outdated. Worth noting but not actionable in-repo.


Commit Structure

Single commit. The commit message describes the change and motivation accurately, with sections on "API and SSA fallout" and "Docs" that match what the diff does. Clean history.


Acknowledged Limitations


Unresolved Feedback


Agent Performance Retro

Claude

Claude produced the most specific catalogue — six findings with precise line anchors and fix diffs — and was the only agent to notice the ObservedAt-vs-dispatch-duration semantics gap (S5) and the removeActionAnnotation comment-vs-branch invariant mismatch (S6). Its severity calibration ran hot: the two stale-comment findings were flagged Important, but neither affects correctness, and Codex's independent Suggestion rating held up better on review. Documentation misdirection in this project would normally rank Important only when it materially misleads design decisions, which stale identifier names in a comment block do not.

Codex

Codex landed the only finding on the webhook bypass path, and its fix sketch (require an engine-owned marker before accepting an action annotation on update) is the right shape. It also grouped the three stale-comment locations under one finding at Suggestion severity — a tighter framing than Claude's split. It produced no critical-severity false positives and no unique insight beyond the two findings, so it was narrower than Claude in breadth but deeper on the one item it flagged.

Gemini

Gemini returned zero findings. Its Coverage Summary was complete (all 16 changed files accounted for with per-file disposition) and its stderr held only the YOLO-mode banner, so the "None." result reflects its actual assessment, not a truncated run. It did not pick up the webhook-update gap (Codex's I1) despite having container_webhooks.go in scope, and its design observations read more as a summary of the PR description than as independent analysis. Gemini's known skip of git blame is not the explanation here — I1 is visible from the diff alone, so the miss is analytical.

Summary

Claude Opus 4.7Codex GPT 5.4Gemini 3.1 Pro
Duration21m 19s6m 23s5m 24s
Findings6S1I 1Snone
Tool calls41 (Read 20, Bash 12, Grep 9)43 (shell 42, stdin 1)16 (runshellcommand 9, readfile 4, grepsearch 3)
Design observations434
False positives000
Unique insights210
Files reviewed161616
Coverage misses001
Totals6S1I 1Snone
Downgraded2 (I→S)00
Dropped000

Reconciliation. Claude P1 "Stale comment references the removed function and field" (engine_reconciler.go:183-184): Important → Suggestion S1. Claude P1 "ContainerSpec doc comment is internally contradictory" (container_types.go:124-128): Important → Suggestion S2. Both were stale-identifier comments with no correctness impact; Codex independently rated the same class of findings at Suggestion, and that calibration held up.

Codex provided the most valuable output: one real finding nobody else caught. Claude provided the most volume of genuinely actionable items, though two were miscalibrated to Important. Gemini's "no findings" result, on a PR where Codex found a webhook bypass visible in the diff, is a weak signal for this review.


Review Process Notes

Skill improvements

Repo context updates


Resolution

Addressed2026-04-16
Commit31bc715 engine: replace Container.spec.state with action annotations (amended into the original)
#FindingAction
1Important #1: Webhook safeguard enforced only on create, not on updateCommented
2Suggestion #1: Stale comment references removed reconcileContainerSpecs / spec.stateFixed
3Suggestion #2: ContainerSpec Godoc opener contradicts the restFixed
4Suggestion #3: "Prevents all modification" webhook comment no longer accurateFixed
5Suggestion #4: Invalid-action fallback is silent to the callerSkipped
6Suggestion #5: ObservedAt semantics ambiguous in godocFixed (sharpened godoc, regenerated CRD/apply config)
7Suggestion #6: removeActionAnnotation comment vs invalid-action branchFixed (softened comment)
8Suggestion #7: No unit test for dispatchContainerAction default branchSkipped
9Testing Gap #1: No test for create-then-update bypassSkipped (consistent with I1 commented, not fixed)
10Testing Gap #2: No test for !action.IsValid() fallbackSkipped
11Testing Gap #3: No test that lastAction persists across direct docker stopTest written
12Testing Gap #4: No test for concurrent annotation overwrite during dispatchSkipped
13Testing Gap #5: lastAction timestamps and overwrite behavior not assertedTest written
14Testing Gap #6: Restart double-dispatch race has no coverageSkipped (acknowledged limitation)