Deep Review: 20260412-003842-pr-295

Date2026-04-12 00:38
Reporancher-sandbox/rancher-desktop-daemon
Round1
Author@jandubois
PR#295 — Container Engine watcher
Branchdocker
Commitscc5875e ci: capture support bundle when BATS targets time out
02d4d91 Make rdd set wait for the desired state by default
4b062fb Add BATS integration tests for the engine controller
86969f8 Document engine mirroring and container state transitions
6dfa5ad Allow container spec.state transitions via webhook
174fc06 Add engine controller that mirrors Docker state into K8s
87f1df1 Add DockerSocket() helper and Docker client dependency
32a0109 Work around distro proxy bug; update docker socket path
536ccb2 Rosetta is now a vz driver option, not a global option
ReviewersClaude Opus 4.6, Codex GPT 5.4, Gemini 3.1 Pro
VerdictRework — one critical data-path bug breaks fullSync for any Docker volume with _ or uppercase, plus eight important regressions around finalizer/cleanup error handling, containerd backend support, the new rdd set wait semantics, and per-reconcile list sweeps.
Wall-clock time1 h 26 min 58 s


Executive Summary

This PR introduces the engine controller that mirrors Docker state into Kubernetes resources, adds a "wait for desired state" default to rdd set, ships BATS coverage for the new flow, and adds a CI support-bundle wrapper that captures diagnostics when BATS targets hang. The design cleanly separates user intent (spec.state) from engine reality (status.status), and the finalizer/mirror contract is sensibly documented. However, the implementation ships one critical regression (volume name sanitization allows characters that K8s rejects, crashing the entire full sync on common volume names), plus eight important issues clustered around finalizer error handling (dropped K8s resources on Docker errors, silently-swallowed Conflict errors, edge-triggered cleanup that can't retry), containerd backend support, the semantics of the new wait logic, and per-reconcile O(N) list sweeps that multiply under event load.

Structure: 1 critical, 8 important, 8 suggestions.


Critical Issues

C1. sanitizeKubernetesObjectName leaves volume names that fail RFC 1123 validation, breaking the entire fullSync Gemini
	containersv1alpha1 "github.com/rancher-sandbox/rancher-desktop-daemon/pkg/apis/containers/v1alpha1"
	containersv1alpha1apply "github.com/rancher-sandbox/rancher-desktop-daemon/pkg/apis/containers/v1alpha1/applyconfiguration/containers/v1alpha1"
)

// sanitizeKubernetesObjectName replaces characters not allowed in K8s names.
func sanitizeKubernetesObjectName(input string) string {
	return strings.NewReplacer("/", "-", ":", ".").Replace(input)
}

// syncAllImages lists all Docker images and creates/updates K8s resources,
// then removes stale ones.
//
// Images are Inspected sequentially for the same reason as

Docker permits underscores and uppercase letters in volume names (e.g., My_Volume, project_db_data, any Compose-created volume), but sanitizeKubernetesObjectName only replaces / and :. When applyVolume calls w.k8s.Apply(...) with a name containing _ or A-Z, the Kubernetes API server rejects it due to RFC 1123 subdomain validation on metadata.name.

The failure is not localized. syncAllVolumes collects errors from each applyVolume and returns errors.Join(errs...) at pkg/controllers/app/engine/controllers/sync_volumes.go:89; fullSync at pkg/controllers/app/engine/controllers/docker_watcher.go:316-329 joins and returns that to newDockerWatcher, which aborts the watcher entirely. One volume with an uppercase letter or underscore means the engine never connects, ContainerEngineReady stays False, and rdd set running=true hangs for its full 5-minute timeout.

		if err := w.applyVolume(ctx, v); err != nil {
			errs = append(errs, err)
		}
	}

	// Remove stale K8s volumes.
	var k8sVolumes containersv1alpha1.VolumeList
	if err := w.k8s.List(ctx, &k8sVolumes, client.InNamespace(apiNamespace)); err != nil {
		return fmt.Errorf("failed to list K8s volumes: %w", err)
	}
	for i := range k8sVolumes.Items {

// deleteVolume removes a volume from Docker. See deleteContainer for the
// error-handling contract.
func (w *dockerWatcher) deleteVolume(ctx context.Context, name string) error {
	_, err := w.cli.VolumeRemove(ctx, name, dockerclient.VolumeRemoveOptions{Force: true})
	if cerrdefs.IsNotFound(err) {
		return nil
	}
	return err
}

// fullSync lists all containers, images, and volumes from Docker and creates
// corresponding K8s resources. It also removes stale K8s resources.
func (w *dockerWatcher) fullSync(ctx context.Context) error {
	log := logf.FromContext(ctx).WithName("docker-watcher")
	log.Info("Starting full sync")

	if err := w.ensureNamespace(ctx); err != nil {
		return fmt.Errorf("failed to ensure namespace: %w", err)
	}

	var errs []error

	if err := w.syncContainerNamespace(ctx); err != nil {

Fix: produce a deterministic encoding that always yields a valid RFC 1123 subdomain — e.g. fmt.Sprintf("vol-%x", sha256.Sum256([]byte(vol.Name))) — and keep the original Docker name in status.name (which deleteVolume already uses for the reverse path via v.Status.Name). The same treatment is needed for image tags since they can contain uppercase characters as well.


Important Issues

I1. Engine controller unconditionally targets Docker, so containerd backends never satisfy ContainerEngineReady Codex Gemini

// Reconcile handles App condition changes, Docker watcher lifecycle, container
// spec.state transitions, and finalizer processing for mirror resources.
func (r *EngineReconciler) Reconcile(ctx context.Context, _ ctrl.Request) (ctrl.Result, error) {
	log := logf.FromContext(ctx)

	var app appv1alpha1.App
	if err := r.Get(ctx, client.ObjectKey{Name: appName}, &app); err != nil {
		return ctrl.Result{}, client.IgnoreNotFound(err)
	}

	running := meta.IsStatusConditionTrue(app.Status.Conditions, "Running")
	engineIsDocker := app.Spec.ContainerEngine.Name == engineMoby

	r.watcherMu.Lock()
	watcherRunning := r.watcher != nil
	watcherDied := watcherRunning && !r.watcher.alive()
	if watcherDied {
		r.watcher.stop()
		r.watcher = nil
		watcherRunning = false
	}
	r.watcherMu.Unlock()

	// Watcher died unexpectedly (Docker socket gone, engine restarted, etc.).
	// Clean up mirror resources and set the condition to Disconnected.
	if watcherDied {
		log.Info("Docker watcher died, cleaning up mirror resources")
		if err := r.cleanupMirrorResources(ctx); err != nil {
			log.Error(err, "Failed to clean up mirror resources")
			return ctrl.Result{}, err
		}
		if err := r.setEngineCondition(ctx, &app, metav1.ConditionFalse, "Disconnected", "Container engine connection lost"); err != nil {
			return ctrl.Result{}, err
		}
		return ctrl.Result{}, nil
	}

	// The watcher should only run when the App is Running with the moby
	// backend. In every other state (stopped, containerd, both) we stop
	// the watcher and sweep mirror resources. Cleanup runs on every
	// reconcile in that state so transient errors are retried on the

The engine reconciler decides the watcher lifecycle purely from the App's Running condition; it never consults app.Spec.ContainerEngine.Name. Meanwhile lima-template.yaml:47-52 explicitly disables docker.service when PARAM_CONTAINER_ENGINE=containerd, and newDockerWatcher unconditionally pings the Docker socket. So for a containerd-backed VM, the reconciler enters an endless ConnectFailed loop.


    # Write the selected container engine; docker.service and containerd.service
    mkdir -p /etc/sysconfig
    echo "CONTAINER_ENGINE=${PARAM_CONTAINER_ENGINE}" >/etc/sysconfig/rancher-desktop

    # Enable the requested container engine and disable the other.
    # containerd.service has a Conflicts=docker, so we must not start both.
    CONTAINERD_ACTION=$([[ $PARAM_CONTAINER_ENGINE = containerd ]] && echo enable || echo disable)
    DOCKER_ACTION=$([[ $PARAM_CONTAINER_ENGINE = moby ]] && echo enable || echo disable)
    systemctl "$CONTAINERD_ACTION" containerd.service buildkitd.service
    systemctl "$DOCKER_ACTION" docker.service docker.socket

    if [[ $PARAM_KUBERNETES_ENABLED == "true" ]]; then
        export INSTALL_K3S_VERSION="v${PARAM_KUBERNETES_VERSION}+k3s1"
        INSTALLED_VERSION=""
        [[ -x /usr/local/bin/k3s ]] && INSTALLED_VERSION=$(/usr/local/bin/k3s --version | awk '{print $3; exit}')

This intersects directly with the new rdd set wait path: the help text at cmd/rdd/set.go:70-72 advertises rdd set running=true containerEngine.name=containerd as a supported example, and waitForDesiredState at cmd/rdd/set.go:450-454 waits for ContainerEngineReady=True. In containerd mode that condition can never become True, so the command always hangs until the 300-second timeout.

			runtime. If the App resource does not exist, it is created with
			default settings before applying the specified values.

			By default, rdd set waits for the desired state: when running=true
			is set, it waits for the container engine to be ready; when
			running=false, it waits for the VM to stop. Use --timeout=0 to
			skip waiting.

			Examples:
			  rdd set running=true
			  rdd set running=true containerEngine.name=containerd
			  rdd set kubernetes.enabled=true kubernetes.version=1.32.2
			  rdd set --dry-run running=true
	runningVal, ok := properties["running"]
	if !ok {
		return nil
	}

	ctx, cancel := context.WithTimeout(ctx, timeout)
	defer cancel()

	if runningVal == "true" {
		logrus.Info("Waiting for container engine to be ready")
		return watchCondition(ctx, config, func(obj *unstructured.Unstructured) bool {
			return conditionStatus(obj, "ContainerEngineReady") == metav1.ConditionTrue
		})
	}
	logrus.Info("Waiting for the VM to stop")

Fix: gate the watcher on app.Spec.ContainerEngine.Name == "moby" (or any other Docker-compatible engine you add later). For containerd, either short-circuit ContainerEngineReady to a distinct terminal reason (e.g. Unsupported / NotApplicable) so the CLI can detect "nothing to wait for," or skip the condition entirely and drive the CLI off the App's Running condition instead. Either way, the "waiting for container engine" branch in waitForDesiredState needs a matching adjustment.

I2. ContainerNamespace mirror finalizer is never processed on user delete — objects stick in Terminating until the next VM shutdown Codex
	var ns corev1.Namespace
	if err := w.k8s.Get(ctx, client.ObjectKey{Name: apiNamespace}, &ns); err != nil {
		if !apierrors.IsNotFound(err) {
			return err
		}
		ns = corev1.Namespace{
			ObjectMeta: metav1.ObjectMeta{Name: apiNamespace},
		}
		if err := w.k8s.Create(ctx, &ns); err != nil && !apierrors.IsAlreadyExists(err) {
			return fmt.Errorf("failed to create namespace %s: %w", apiNamespace, err)
		}
	}
	return nil
}

// syncContainerNamespace creates the "moby" ContainerNamespace resource.
// Unlike Container / Image / Volume mirrors, this resource has no mirror

syncContainerNamespace now adds the engine.rancherdesktop.io/docker-mirror finalizer to the moby ContainerNamespace.

processFinalizers at engine_reconciler.go:266-281 only covers Containers, Images, and Volumes, and SetupWithManager at lines 354-361 does not watch ContainerNamespaces.

				}
				return err
			}
			if !removeFinalizer(latest, mirrorFinalizer) {
				return nil
			}
			return r.Update(ctx, latest)
		})
		if retryErr != nil {
			errs = append(errs, fmt.Errorf("failed to remove finalizer from %s/%s: %w",
				kind, obj.GetName(), retryErr))
			continue
		}
		if err := client.IgnoreNotFound(r.Delete(ctx, obj)); err != nil {
			errs = append(errs, fmt.Errorf("failed to delete %s/%s: %w",
				kind, obj.GetName(), err))
		}
	}
	return errors.Join(errs...)
}

// reconcileContainerSpecs handles container spec.state changes by calling
// Docker start/stop.
func (r *EngineReconciler) reconcileContainerSpecs(ctx context.Context) error {
	r.watcherMu.Lock()
	w := r.watcher

The existing ValidateDelete webhook at pkg/controllers/containers/containernamespace/container_namespace_webhooks.go:24-29 returns only a warning, so users can still issue kubectl delete containernamespace/moby. The object gets a deletion timestamp, retains the mirror finalizer, and is never unstuck until the next VM stop (when cleanupMirrorResources strips finalizers wholesale). The pre-existing "namespace delete TODO" just graduated into a visible dead end.

func (c *deleteValidator) ValidateCreate(context.Context, *v1alpha1.ContainerNamespace) (ctrlwebhookadmission.Warnings, error) {
	return nil, errors.New("webhook does not implement create")
}

// ValidateDelete implements [ctrlwebhookadmission.Validator].
func (c *deleteValidator) ValidateDelete(context.Context, *v1alpha1.ContainerNamespace) (ctrlwebhookadmission.Warnings, error) {
	// TODO: We should validate that:
	// The namespace to be deleted must be empty before allowing the delete.
	// Additionally, when using moby backend: we do not delete the `moby` namespace.
	return ctrlwebhookadmission.Warnings{"namespace delete validation not implemented"}, nil
}

// ValidateUpdate implements [ctrlwebhookadmission.Validator].
func (c *deleteValidator) ValidateUpdate(context.Context, *v1alpha1.ContainerNamespace, *v1alpha1.ContainerNamespace) (ctrlwebhookadmission.Warnings, error) {
	return nil, errors.New("webhook does not implement update")
}

Fix: either stop adding the mirror finalizer to ContainerNamespace at all (it isn't reverse-managed and cleanupMirrorResources can still sweep it at stop time), or add a dedicated ContainerNamespace watch + finalizer handler and tighten the delete webhook to reject user-initiated deletes of system-managed namespaces up front.

I3. Finalizer is removed even when the Docker-side delete fails, silently orphaning the real Docker object Codex
	}
	return errors.Join(errs...)
}

// reconcileContainerSpecs handles container spec.state changes by calling
// Docker start/stop.
func (r *EngineReconciler) reconcileContainerSpecs(ctx context.Context) error {
	r.watcherMu.Lock()
	w := r.watcher
	r.watcherMu.Unlock()
	if w == nil {
		return nil
	}

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

	for i := range containers.Items {
		c := &containers.Items[i]
		if c.DeletionTimestamp != nil {
			continue
		}
		if err := w.reconcileContainerState(ctx, c); err != nil {
			logf.FromContext(ctx).Error(err, "Failed to reconcile container state",
				"container", c.Name)
		}
	}
	return nil
}

// 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()
	w := r.watcher
	r.watcherMu.Unlock()
	if w == nil {
		return nil
	}

	if err := r.processContainerFinalizers(ctx, w); err != nil {
		return err
	}
	if err := r.processImageFinalizers(ctx, w); err != nil {
		return err
	}
	return r.processVolumeFinalizers(ctx, w)
}

// processContainerFinalizers deletes the Docker-side container for every
// K8s Container pending deletion and only strips the mirror finalizer
// when the Docker delete succeeds. Per-item errors are collected so one
// stuck container does not block the rest; the reconciler retries the
// remaining stuck items on the next reconcile.
func (r *EngineReconciler) processContainerFinalizers(ctx context.Context, w *dockerWatcher) error {
	var containers containersv1alpha1.ContainerList
	if err := r.List(ctx, &containers, client.InNamespace(apiNamespace)); err != nil {
		return err
	}
	var errs []error
	for i := range containers.Items {
		c := &containers.Items[i]
		if c.DeletionTimestamp == nil || !hasFinalizer(c, mirrorFinalizer) {
			continue
		}
		if err := w.deleteContainer(ctx, c.Name); err != nil {

deleteContainer / deleteImage / deleteVolume at docker_watcher.go:273-304 swallow all Docker errors (V(1) log only). The finalizer callers (processContainerFinalizers, processImageFinalizers, processVolumeFinalizers) then unconditionally strip the finalizer and delete the K8s mirror.

	// desired != actual is guaranteed by the early return above, so each
	// branch just dispatches to Docker. ContainerStop handles paused /
	// restarting containers natively — filtering by actual == running
	// would silently drop the user's intent in those states.
	switch desired {
	case containersv1alpha1.ContainerStatusRunning:
		log.Info("Starting container", "id", c.Name)
		_, err := w.cli.ContainerStart(ctx, c.Name, dockerclient.ContainerStartOptions{})
		return err
	case containersv1alpha1.ContainerStatusCreated:
		log.Info("Stopping container", "id", c.Name)
		_, err := w.cli.ContainerStop(ctx, c.Name, dockerclient.ContainerStopOptions{})
		return err
	}
	return nil
}

// deleteContainer removes a container from Docker. NotFound errors are
// treated as success (the container is already gone); all other errors
// are returned so the caller keeps the mirror finalizer in place and
// retries on the next reconcile.
func (w *dockerWatcher) deleteContainer(ctx context.Context, id string) error {
	_, err := w.cli.ContainerRemove(ctx, id, dockerclient.ContainerRemoveOptions{Force: true})
	if cerrdefs.IsNotFound(err) {
		return nil
	}
	return err
}

// deleteImage removes an image from Docker. See deleteContainer for the
// error-handling contract.
func (w *dockerWatcher) deleteImage(ctx context.Context, img *containersv1alpha1.Image) error {
	// Use the tag if available, otherwise the raw image ID.
	ref := img.Status.ID
	if img.Status.RepoTag != "" {
		ref = img.Status.RepoTag
	}
	_, err := w.cli.ImageRemove(ctx, ref, dockerclient.ImageRemoveOptions{})
	if cerrdefs.IsNotFound(err) {
		return nil
	}
	return err

This directly violates the contract in docs/design/api_containers.md:56-59: "the finalizer handler deletes the corresponding object in the container engine, then removes the finalizer to allow K8s garbage collection." A transient Docker daemon error, permission error, or conflict (container still running, image in use, volume mounted) leaves the real Docker object in place while the Kubernetes mirror is gone for good. Eventual divergence is guaranteed unless the next fullSync rebuilds the mirror — and a user who tried to delete it via the API will be surprised that it has been silently recreated.

controller removes all mirror resources and sets `ContainerEngineReady` to
`False`.

### Finalizer lifecycle

Each mirror resource carries the `engine.rancherdesktop.io/docker-mirror`
finalizer.  When a user deletes a resource through the K8s API, the finalizer
handler deletes the corresponding object in the container engine, then removes
the finalizer to allow K8s garbage collection.

When the container engine deletes an object (the user ran `docker rm` or
`nerdctl rm`, for example), the engine controller strips the finalizer and
deletes the K8s resource directly, so no reverse engine call occurs.

Fix: make the delete helpers return errors. Ignore only the "not found" / already-gone cases (e.g. errdefs.IsNotFound), treat all other errors as a failure, and leave the finalizer in place so processFinalizers retries on the next reconcile. If the error is persistently terminal (e.g. "container is in use"), surface it on the Container's conditions so the user can see why deletion is blocked.

I4. rdd set running=false wait returns on ContainerEngineReady change, not on the VM actually stopping Codex
}

// waitForDesiredState waits for the App to reach the state implied by the
// properties that were set. If running=true was set, it waits for the
// container engine to be ready. If running=false was set, it waits for the
// App's Running condition to go to False — i.e. the VM has actually
// stopped, which is stricter than "container engine disconnected".
// Other property changes do not wait.
//
// TODO: changes that trigger a VM restart without changing "running" (e.g.
// containerEngine.name) are not waited on. This requires a "Reconciled"
// condition with observedGeneration on the App resource so the CLI can
// detect when the full reconcile chain has settled.
func waitForDesiredState(ctx context.Context, config *rest.Config, properties map[string]string, timeout time.Duration) error {
	runningVal, ok := properties["running"]
	if !ok {
		return nil
	}

	ctx, cancel := context.WithTimeout(ctx, timeout)
	defer cancel()

	if runningVal == "true" {
		logrus.Info("Waiting for container engine to be ready")
		return watchCondition(ctx, config, func(obj *unstructured.Unstructured) bool {
			return conditionStatus(obj, "ContainerEngineReady") == metav1.ConditionTrue
		})
	}
	logrus.Info("Waiting for the VM to stop")
	return watchCondition(ctx, config, func(obj *unstructured.Unstructured) bool {
		// Running=False means the VM has stopped. Running absent means
		// the VM was never started (nothing to wait for).
		return conditionStatus(obj, "Running") != metav1.ConditionTrue
	})
}

// watchCondition watches the App singleton until the supplied predicate

The help text at cmd/rdd/set.go:65-67 promises that running=false "waits for the VM to stop", but waitForDesiredState returns as soon as ContainerEngineReady is absent or False. That condition is already False on the engine controller's ConnectFailed path, can be False while the App controller is still propagating running=false to the LimaVM, and can flip to False long before the LimaVM actually finishes shutting down. In those windows the command reports success while the VM is still running.


			Properties are specified as PROPERTY=VALUE pairs. Property names use
			dot notation for nested fields (e.g., containerEngine.name).

			Valid property names and types are derived from the App CRD at
			runtime. If the App resource does not exist, it is created with
			default settings before applying the specified values.

			By default, rdd set waits for the desired state: when running=true
			is set, it waits for the container engine to be ready; when
			running=false, it waits for the VM to stop. Use --timeout=0 to
			skip waiting.

Fix: for running=false, wait on the App's Running condition going to False (or the LimaVM's equivalent stopped condition). Reserve ContainerEngineReady for the running=true / Docker branch. This also sidesteps the I1 containerd hang on the running=true side once the gating there is fixed.

I5. Cleanup on VM stop is fragile end-to-end: no Conflict retry, first-error short-circuit, and an edge-triggered outer guard that makes retries impossible Claude Gemini
// spec.state transitions, and finalizer processing for mirror resources.
func (r *EngineReconciler) Reconcile(ctx context.Context, _ ctrl.Request) (ctrl.Result, error) {
	log := logf.FromContext(ctx)

	var app appv1alpha1.App
	if err := r.Get(ctx, client.ObjectKey{Name: appName}, &app); err != nil {
		return ctrl.Result{}, client.IgnoreNotFound(err)
	}

	running := meta.IsStatusConditionTrue(app.Status.Conditions, "Running")
	engineIsDocker := app.Spec.ContainerEngine.Name == engineMoby

	r.watcherMu.Lock()
	watcherRunning := r.watcher != nil
	watcherDied := watcherRunning && !r.watcher.alive()
	if watcherDied {
		r.watcher.stop()
		r.watcher = nil
		watcherRunning = false
	}
	r.watcherMu.Unlock()

	// Watcher died unexpectedly (Docker socket gone, engine restarted, etc.).
	// Clean up mirror resources and set the condition to Disconnected.
	if watcherDied {
		log.Info("Docker watcher died, cleaning up mirror resources")
		if err := r.cleanupMirrorResources(ctx); err != nil {
			log.Error(err, "Failed to clean up mirror resources")
			return ctrl.Result{}, err
		}
		if err := r.setEngineCondition(ctx, &app, metav1.ConditionFalse, "Disconnected", "Container engine connection lost"); err != nil {
			return ctrl.Result{}, err
		}
		return ctrl.Result{}, nil
	}

	// The watcher should only run when the App is Running with the moby
	// backend. In every other state (stopped, containerd, both) we stop
	// the watcher and sweep mirror resources. Cleanup runs on every
	// reconcile in that state so transient errors are retried on the
	// next requeue without relying on the in-memory watcher pointer as
	// the retry trigger.
	wantWatcher := running && engineIsDocker
	if !wantWatcher {
		if watcherRunning {
			log.Info("Stopping Docker watcher",
				"running", running, "engine", app.Spec.ContainerEngine.Name)
			r.stopWatcher()
		}
		if err := r.cleanupMirrorResources(ctx); err != nil {
			log.Error(err, "Failed to clean up mirror resources")
			return ctrl.Result{}, err
		}
		switch {
		case running && !engineIsDocker:
			return ctrl.Result{}, r.setEngineCondition(ctx, &app, metav1.ConditionTrue, "NotApplicable",
				"Engine mirroring is only supported with the moby backend")
		default:
			return ctrl.Result{}, r.setEngineCondition(ctx, &app, metav1.ConditionFalse, "Stopped",

Three stacked weaknesses turn any transient problem during cleanup into lasting damage:

  1. Inner loop has no Conflict retry Claude. deleteAllOfType calls r.Update(ctx, obj) to strip finalizers with no retry.RetryOnConflict wrapper. A stale cache or concurrent modification during shutdown returns Conflict, which propagates up as a generic error.
  2. First error short-circuits the remaining types Claude. cleanupMirrorResources at engine_reconciler.go:190-207 calls deleteAllOfType four times in sequence for Container / Volume / Image / ContainerNamespace. Any error in the first call returns before the remaining three run, leaving a half-cleaned state across kinds.
  3. Outer retry is edge-triggered on in-memory watcher state Gemini. stopWatcher() sets r.watcher = nil before cleanupMirrorResources runs at engine_reconciler.go:115-124. When cleanup returns an error the reconcile requeues — but on the retry, watcherRunning is now false, the !running && watcherRunning branch is skipped entirely, and no further cleanup attempt happens until the user stops the VM again.
	// backend. In every other state (stopped, containerd, both) we stop
	// the watcher and sweep mirror resources. Cleanup runs on every
	// reconcile in that state so transient errors are retried on the
	// next requeue without relying on the in-memory watcher pointer as
	// the retry trigger.
	wantWatcher := running && engineIsDocker
	if !wantWatcher {
		if watcherRunning {
			log.Info("Stopping Docker watcher",
				"running", running, "engine", app.Spec.ContainerEngine.Name)
			r.stopWatcher()
		}
		if err := r.cleanupMirrorResources(ctx); err != nil {
			log.Error(err, "Failed to clean up mirror resources")
			return ctrl.Result{}, err
		}
		switch {
		case running && !engineIsDocker:
			return ctrl.Result{}, r.setEngineCondition(ctx, &app, metav1.ConditionTrue, "NotApplicable",
				"Engine mirroring is only supported with the moby backend")
	r.watcherMu.Lock()
	w := r.watcher
	r.watcher = nil
	r.watcherMu.Unlock()

	if w != nil {
		w.stop()
	}
}

// setEngineCondition updates the ContainerEngineReady condition on the App resource.
func (r *EngineReconciler) setEngineCondition(ctx context.Context, app *appv1alpha1.App, status metav1.ConditionStatus, reason, message string) error {
	changed := meta.SetStatusCondition(&app.Status.Conditions, metav1.Condition{
		Type:               conditionContainerEngineReady,
		Status:             status,
		Reason:             reason,
		Message:            message,
		ObservedGeneration: app.Generation,
	})
	if !changed {
		return nil
	}
	return r.Status().Update(ctx, app)
}

// cleanupMirrorResources removes every mirror resource the engine
// controller owns. Errors are collected across all four kinds with
// errors.Join so one stuck resource type does not block the rest — the

A successful next start partially self-heals via the syncAllContainers / syncAllImages / syncAllVolumes stale-item pruning, so this is not permanent orphaning — but between stop and that next start, the resources are visible to rdd ctl get and violate the BATS assertion at bats/tests/32-app-controllers/engine.bats:160-168. Objects mid-deletion (finalizer already stripped, Delete not yet issued) can also land in a broken state.

    assert_output "engine.rancherdesktop.io/docker-mirror"

    # Remove the container so Docker permits the image removal. The
    # next reconcile's finalizer retry succeeds and the K8s Image is
    # finally collected.
    docker rm --force test-inuse
    rdd ctl wait --for=delete --namespace="${NAMESPACE}" \
        "${image_ref}" --timeout=30s
}

# --- Container state transitions via spec ---

@test "patching spec.state=created stops Docker container" {
    docker run -d --name test-state busybox sleep 3600

    run -0 docker inspect test-state --format '{{.Id}}'
    cid=${output}

    rdd ctl wait --for=jsonpath='{.status.status}'=running \

Fix: (1) wrap the inner Update in retry.RetryOnConflict with the default backoff; (2) collect errors across the four types with errors.Join instead of returning on the first; (3) decouple "cleanup needed" from the in-memory watcher pointer — run cleanup whenever !running regardless of watcherRunning, or persist a short-lived "cleanup pending" condition on the App.

I6. Initial fullSync issues N sequential Inspect calls per container/image, amplifying cold-start latency Gemini
// concern on loaded machines, parallelise with errgroup + a small
// worker pool here rather than redesigning the sync.
func (w *dockerWatcher) syncAllContainers(ctx context.Context) error {
	log := logf.FromContext(ctx).WithName("docker-watcher")

	listResult, err := w.cli.ContainerList(ctx, dockerclient.ContainerListOptions{All: true})
	if err != nil {
		return fmt.Errorf("failed to list containers: %w", err)
	}

	// Track which container IDs exist in Docker.
	dockerIDs := make(map[string]bool, len(listResult.Items))

	var errs []error
	for _, dc := range listResult.Items {
		dockerIDs[dc.ID] = true
		if err := w.syncContainer(ctx, dc.ID); err != nil {

syncAllContainers calls ContainerList, then loops and calls ContainerInspect once per container. syncAllImages does the same with ImageList + ImageInspect. A developer laptop with several hundred images (not uncommon on a long-lived machine) hits the Docker socket hundreds of times sequentially before the watcher goroutine enters its event loop, and every hit goes through the Lima port-forwarded Unix socket. Observed startup latency for rdd set running=true will grow linearly with the image count, and the operation is entirely serial — newDockerWatcher holds watcherMu the whole time.

This also intersects with I1: even in the correct Docker case, any developer with a bloated local cache pays this cost on every VM restart.

Fix: seed the initial mirror from List responses where the shape is sufficient (image list has RepoTags, Size, Labels, etc.), and defer Inspect to event-driven updates or lazy per-resource fetches when a user actually queries a field that only Inspect exposes. If an Inspect is still needed for full status, do it concurrently with a small worker pool so startup isn't bounded by serial round-trips.

I7. Every App reconcile re-lists Containers, Images, and Volumes; per-event work scales O(N) with mirror size Claude Codex
			log.Error(err, "Failed to clean up mirror resources")
			return ctrl.Result{}, err
		}
		switch {
		case running && !engineIsDocker:
			return ctrl.Result{}, r.setEngineCondition(ctx, &app, metav1.ConditionTrue, "NotApplicable",
				"Engine mirroring is only supported with the moby backend")
		default:
			return ctrl.Result{}, r.setEngineCondition(ctx, &app, metav1.ConditionFalse, "Stopped",
				"Container engine stopped")
		}
	}

	if !watcherRunning {
		log.Info("App is running, starting Docker watcher")
		if err := r.startWatcher(ctx); err != nil {
			log.Error(err, "Failed to start Docker watcher")
			_ = r.setEngineCondition(ctx, &app, metav1.ConditionFalse, "ConnectFailed", err.Error())
			return ctrl.Result{}, err
		}
		if err := r.setEngineCondition(ctx, &app, metav1.ConditionTrue, "Connected", "Container engine synced"); err != nil {
			return ctrl.Result{}, err
		}

Every reconcile with running=true executes four unfiltered list operations:

		return err
	}

	items, err := meta.ExtractList(list)
	if err != nil {
		return err
	}

	var errs []error
	for _, item := range items {
		obj := item.(client.Object)
}

// reconcileContainerSpecs handles container spec.state changes by calling
// Docker start/stop.
func (r *EngineReconciler) reconcileContainerSpecs(ctx context.Context) error {
	r.watcherMu.Lock()
	w := r.watcher
	r.watcherMu.Unlock()
	if w == nil {
		return nil
	}

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

	for i := range containers.Items {
		c := &containers.Items[i]
		if c.DeletionTimestamp != nil {
			continue
		}
		if err := w.reconcileContainerState(ctx, c); err != nil {
			logf.FromContext(ctx).Error(err, "Failed to reconcile container state",
				"container", c.Name)
		}
	}
	return nil
}

// 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()
	w := r.watcher
	r.watcherMu.Unlock()
	if w == nil {
		return nil
	}

	if err := r.processContainerFinalizers(ctx, w); err != nil {
		return err
	}
	if err := r.processImageFinalizers(ctx, w); err != nil {
		return err
	}
	return r.processVolumeFinalizers(ctx, w)
}

// processContainerFinalizers deletes the Docker-side container for every
// K8s Container pending deletion and only strips the mirror finalizer

The reconcile fires on every Container / Image / Volume watch event via Watches(&containersv1alpha1.Container{}, enqueueApp) at engine_reconciler.go:358-360, and each status update the Docker watcher writes back generates one such event. So every container status change on a host with N mirrored containers performs 3·N K8s reads and a nested reconcileContainerState sweep that calls ContainerStart / ContainerStop against Docker for every container with a non-unknown spec. On a busy system with dozens of containers cycling the multiplier is immediate and user-visible.

		}
	}
	return errors.Join(errs...)
}

func (r *EngineReconciler) processImageFinalizers(ctx context.Context, w *dockerWatcher) error {
	var images containersv1alpha1.ImageList
	if err := r.List(ctx, &images, client.InNamespace(apiNamespace)); err != nil {
		return err
	}
	var errs []error
	for i := range images.Items {
		img := &images.Items[i]

The same architecture also made it easy to miss ContainerNamespace coverage entirely (see I2) — the App-level sweep approach has no compile-time guarantee that every mirrored kind has a finalizer handler.

Fix: split the engine controller into (a) an App-level session manager that only owns the Docker watcher lifecycle and (b) per-resource reconcilers (Container, Image, Volume, ContainerNamespace) that handle their own finalizer and spec updates with For() predicates filtering to "deletion timestamp set" or "spec generation changed." That removes the O(N) sweeps and makes coverage explicit per kind.

I8. removeMirrorResource uses IgnoreNotFound on Update, silently swallowing Conflict / Forbidden / every other error Claude
	}
}

// removeMirrorResource removes the finalizer from a mirror resource and deletes
// it. This is used when Docker has already deleted the object.
//
// The finalizer-strip Update is wrapped in retry.RetryOnConflict to
// survive a stale cache during concurrent reconciles; NotFound means
// the mirror is already gone and is treated as success. Any other
// Update error propagates so the event handler retries on the next
// reconcile instead of stripping the mirror while leaving a stale
// object behind.
func (w *dockerWatcher) removeMirrorResource(ctx context.Context, obj client.Object, name string) error {
	key := client.ObjectKey{Name: name, Namespace: apiNamespace}
	retryErr := retry.RetryOnConflict(retry.DefaultRetry, func() error {
		if err := w.k8s.Get(ctx, key, obj); err != nil {
			if apierrors.IsNotFound(err) {
				return nil
			}
			return err
		}
		if !removeFinalizer(obj, mirrorFinalizer) {

client.IgnoreNotFound returns nil only for IsNotFound errors and passes everything else through — but here it is being used as a generic "swallow if it fails" on Update, which is wrong. Conflict on the finalizer-strip Update (the common optimistic-concurrency outcome when two goroutines race on the same mirror object) is silently treated as success, and the subsequent Delete then runs against a stale snapshot. The only expected benign error on Update here is NotFound (object deleted out from under us); everything else should surface.

This path is reachable for every Docker Destroy event through handleContainerEvent / handleImageEvent / handleVolumeEvent, which makes it a frequent trigger in normal use. Combined with I5 above, a Conflict here leads to a finalizer that was never actually removed plus a Delete that appears to succeed locally — exactly the kind of divergence the finalizer contract is meant to prevent.

Fix: drop IgnoreNotFound on the Update and either (a) propagate the error so the event loop retries, or (b) wrap in retry.RetryOnConflict with a bounded backoff and only propagate on terminal errors. Keep IgnoreNotFound on the final Delete — that one is correct.


Suggestions

S1. Design doc still documents ContainerCreateRequest.spec.state as defaulting to running Codex
apiVersion: containers.rancherdesktop.io/v1alpha1
kind: ContainerCreateRequest
metadata:
  name: whatever-12345
  namespace: rancher-desktop
spec:
  name: magical_gates # If unset, generate one randomly
  namespace: k8s.io # Refers to a `ContainerNamespace` object
  state: running # Default to `running`
  path: /bin/sh # defaults to image entry point / command
  args: [-c, 'sleep inf'] # defaults to image entry point / command
  image: "sha256:999adf320e40662dc96119a14f07459af9959a081d10ccab7c405257030ab96b" # accepts image tag
  ports: # merged with image defaults
    - name: 80/tcp
      bindings:

The API default in pkg/apis/containers/v1alpha1/container_types.go:217-220 and the generated CRD at pkg/controllers/containers/container/crd.yaml:133 both say default: unknown. Consumers reading the design doc will get the wrong startup semantics for create requests.

	// +optional
	Labels map[string]string `json:"labels"`

	// State is the desired state of the container.
	//
	// +required
	// +kubebuilder:default:=running
	// +kubebuilder:validation:Enum=created;running
	State ContainerStatusValue `json:"state"`
}

// ContainerCreateRequestStatus defines the status for a container creation request.
type ContainerCreateRequestStatus struct {
	// Name is the name of the created container; this is the container ID.
                  - unknown
                - enum:
                  - created
                  - running
                default: running
                description: State is the desired state of the container.
                type: string
            required:
            - image
            - state
            type: object

Fix: update the example to state: unknown (and drop the Default to running comment), or, if create-requests really should auto-start, change the API default back to running and document why the Container default is different.

S2. applyContainer only sets the finalizer on first create; a manually stripped finalizer never gets restored Gemini
}

// applyContainer creates or updates a Container resource from a Docker
// InspectResponse. The spec is only set on creation; subsequent syncs
// update only the status subresource so user-initiated spec.state
// changes (start/stop) are not overwritten.
//
// Pure server-side Apply is not enough here: if we always applied
// WithSpec(unknown), the engine-controller field owner would keep
// reasserting spec.state=unknown, clobbering any user patch to
// running/created. Gating the WithSpec call behind a Get — so spec is
// written exactly once, on creation — keeps the invariant "subsequent
// syncs never touch spec" without needing a second field owner.
func (w *dockerWatcher) applyContainer(ctx context.Context, inspect mobycontainer.InspectResponse) error {
	namespace, name := parseContainerName(inspect.Name)

	// Create the resource if it doesn't exist. spec.state is always set to
	// "unknown" on creation — meaning the engine mirrors Docker state without
	// expressing intent. The user can later set it to "running" or "created"
	// to control the container; the reconciler ignores "unknown".
	var existing containersv1alpha1.Container
	err := w.k8s.Get(ctx, client.ObjectKey{Name: inspect.ID, Namespace: apiNamespace}, &existing)

applyContainer deliberately only writes spec on first create so that later syncs do not clobber a user-initiated spec.state. That's correct for the spec, but as a side effect it also means the mirror finalizer is only written once — if it is ever missing on a subsequent sync (manually stripped, or a new API server behavior), it never gets restored. Since Apply supports setting only specific fields, the structural metadata (finalizers, labels) can be applied unconditionally on every sync without touching spec.state.

Fix: split the apply into two field-owner scopes: one that always writes finalizers and any owner-managed metadata, and one that only writes spec.state=unknown on the not-found path.

S3. rdd set watch loop aborts on any watch-channel closure instead of reconnecting Gemini
			if obj, ok := item.(*unstructured.Unstructured); ok && satisfied(obj) {
				return true, nil
			}
		}
		return false, nil
	}

	condition := func(event watch.Event) (bool, error) {
		obj, ok := event.Object.(*unstructured.Unstructured)
		if !ok {
			return false, nil
		}
		return satisfied(obj), nil
	}

Kubernetes watches routinely close for benign reasons: API server timeouts, proxies cutting idle connections, resource-version compaction. Returning a hard error on closure means any sufficiently long-running rdd set may fail even though the desired state would eventually be reached. The rdd set timeout is 300s by default, which is already in range for a typical watch disconnect.

Fix: wrap the watch in a retry loop, ideally via k8s.io/client-go/tools/watch (watchtools.UntilWithSync or similar), so a benign channel close resumes from the last observed resource version.

S4. reconcileContainerState ignores intermediate container states (paused, restarting), so spec.state=created silently no-ops on them Claude
	return client.IgnoreNotFound(w.k8s.Delete(ctx, obj))
}

// reconcileContainerState checks if the user set spec.state to "running" or
// "created" and calls Docker start/stop accordingly. The engine creates
// containers with spec.state="unknown", which the reconciler ignores.
func (w *dockerWatcher) reconcileContainerState(ctx context.Context, c *containersv1alpha1.Container) error {
	desired := c.Spec.State
	if desired == containersv1alpha1.ContainerStatusUnknown {
		return nil
	}

	actual := c.Status.Status
	if desired == actual {
		return nil
	}

	log := logf.FromContext(ctx).WithName("docker-watcher")

	// desired != actual is guaranteed by the early return above, so each
	// branch just dispatches to Docker. ContainerStop handles paused /
	// restarting containers natively — filtering by actual == running
	// would silently drop the user's intent in those states.
	switch desired {
	case containersv1alpha1.ContainerStatusRunning:

The desired=created branch only stops the container when actual == ContainerStatusRunning. If a user patches spec.state=created on a container that happens to be in paused or restarting state, the reconciler returns nil and waits for a running status it may never see — the user's intent is dropped silently. Docker's ContainerStop handles paused containers natively.

Fix: drop the narrow actual == running guard and call ContainerStop for any non-created, non-exited / non-dead state (or just unconditionally and let Docker short-circuit).

S5. bats-with-timeout.sh hand-rolls instance.LogDir() logic, with a comment acknowledging the duplication Claude
# Usage: bats-with-timeout.sh <seconds> <label> <command> [args...]

set -o errexit -o nounset -o pipefail

timeout_seconds=$1
label=$2
shift 2

instance="${RDD_INSTANCE:-2}"

# `rdd svc paths log_dir` is a pure local computation (see
# cmd/rdd/service_paths.go): it resolves the path from the instance
# suffix without touching the running service, so it is safe to call
# even when the target under test is hung.
log_dir=$(rdd svc paths log_dir)
mkdir -p "${log_dir}"
bundle_file="${log_dir}/support-bundle.log"

The shell script duplicates the OS → log-dir mapping because rdd itself may be hung at timeout time. That reason is legitimate — but the two implementations will drift.

Fix: expose the log-dir path as a lightweight helper that doesn't touch the control plane (e.g., rdd debug log-dir or a standalone rdd-log-dir printing binary) so the shell script can call it even when the service is hung. As a cheaper alternative, add a CI test that asserts the shell mapping matches instance.LogDir() across all three platforms.

S6. Support-bundle output is capped at 30–50 lines via head, which silently truncates the exact diagnostic detail we'd want after a hang Claude
# `RDD_INSTANCE=<instance>`.
matches_our_instance() {
    case "$1" in
        *"${instance}"*) return 0 ;;
    esac
    return 1
}

dump_linux_proc() {
    local pid_dir pid comm
    for pid_dir in /proc/[0-9]*; do
        [ -d "$pid_dir" ] || continue

The bundle already exists as a post-mortem artifact rather than a live log stream, so the reason to cap output is unclear — and the cap hides the specific file descriptors or sockets that would identify the cause of a hang. head -30 on lsof -p of a busy process is almost certain to drop the interesting fd.

Fix: remove the head caps, or hoist them to a configurable BATS_SUPPORT_BUNDLE_MAX_LINES that defaults high (or to 0 for unlimited) and stays low only on explicit opt-in.

S7. Add a comment explaining why applyContainer uses a Get-then-Apply pattern instead of pure server-side Apply Claude

The Get-then-Apply pattern is correct: server-side Apply with field ownership would still reassert spec.state=unknown on every sync (a field the engine controller does not own after creation), which would clobber user-initiated spec.state=running / spec.state=created. The Get check ensures WithSpec is only called on the not-found path.

Fix: a two-line comment above the Get call explaining the invariant — "spec.state must be written exactly once, on creation; subsequent syncs must not reassert it to avoid clobbering user intent." Saves the next reader from puzzling out why the Apply omits WithSpec unconditionally.

S8. Dead scheme field in dockerWatcher Claude
)

// dockerWatcher manages a Docker client connection and event stream. It
// performs a full sync on connect and then watches for incremental changes.
type dockerWatcher struct {
	cli *dockerclient.Client
	k8s client.Client

	cancel context.CancelFunc
	done   chan struct{}

scheme *runtime.Scheme is plumbed through newDockerWatcher, stored on the struct, and never read anywhere in the package (verified with grep -n 'w\.scheme'). Remove the field and propagate the removal up through EngineReconciler.startWatcher at engine_reconciler.go:154 and the controller wiring at pkg/controllers/app/engine/controller.go:59.

	// reconcileContainerSpecs + processFinalizers each issue one or more
	// List() calls across every mirrored object on every reconcile, and
	// every Container/Image/Volume watch event triggers a reconcile via
	// SetupWithManager below. Cost is therefore O(N) per child event.
	// The long-term fix is to split these into per-resource reconcilers
	// with watch predicates ("deletion timestamp set", "spec.state
	// changed"); until then the sweep runs on every event.
	if err := r.reconcileContainerSpecs(ctx); err != nil {
		return ctrl.Result{}, fmt.Errorf("failed to reconcile container specs: %w", err)
	}
	if err := r.processFinalizers(ctx); err != nil {
	}

	return (&controllers.EngineReconciler{
		Client: mgr.GetClient(),
	}).SetupWithManager(mgr)
}

Design Observations

Concerns

Strengths


Testing Assessment

The new bats/tests/32-app-controllers/engine.bats covers the happy-path end-to-end flow well: full sync of the moby namespace, image/container/volume create & delete events, spec.state transitions in both directions, and VM-stop cleanup with a post-stop "every list returns empty" assertion. That is a strong foundation.

Gaps the findings above would have caught:

  1. Volume / image names outside [a-z0-9.-] (C1): no test creates a Docker volume with an uppercase letter or underscore (e.g., docker volume create My_Volume). Adding one would have immediately surfaced the K8s naming crash.
  2. containerd backend startup (I1): no test runs rdd set running=true containerEngine.name=containerd to assert either success or a clear "unsupported" error. The unconditional Docker-watcher path is currently unexercised for non-Docker engines.
  3. Docker-side delete failures (I3): no test covers the case where ContainerRemove / ImageRemove / VolumeRemove returns a transient error. A lightweight way to force this is to delete a container that's mid-stop, or an image referenced by a running container.
  4. ContainerNamespace user delete (I2): a test that does rdd ctl delete containernamespace/moby would have immediately shown the finalizer hang.
  5. running=false wait with degraded engine (I4): no test exercises rdd set running=false from a state where ContainerEngineReady is already False, which is exactly the false-positive return case.
  6. Cleanup failure retry (I5): no test exercises what happens when cleanupMirrorResources fails partway through (e.g., deleting rancher-desktop namespace concurrently) and the VM is then left in running=false.
  7. Watcher-died mid-session recovery Claude: no test exercises the watcherDied path — e.g., restart the Docker daemon while the App stays running and confirm the reconciler cleans up mirror resources, sets ContainerEngineReady=Disconnected, and recovers on the next reconcile. Docker-engine restarts are common enough to matter.
  8. Concurrent user patch vs. controller status update Claude: no test locks in the Server-Side Apply field-ownership guarantee — i.e., that a user kubectl patch spec.state=running concurrent with an engine status update does not clobber either write.
  9. parseContainerName edge cases Claude — the helper at pkg/controllers/app/engine/controllers/sync_containers.go:157-164 has no direct test; names with extra slashes (/ns/name/with/slashes) are not exercised.
  10. reconcileContainerState against intermediate states (S4): no test patches spec.state=created while the container is paused or restarting.
  11. Non-default RDD_INSTANCE Codex — The engine BATS suite hardcodes DOCKER_HOST=unix://${HOME}/.rd2/docker.sock at engine.bats:14, which matches the hardcoded instance in pkg/instance/instance.go:128-136 and lima-template.yaml:30. Once those TODOs are resolved, the BATS setup will also need to derive the socket path from the instance suffix.

There are also no unit tests for the reconciler or watcher internals; all coverage rides on the BATS end-to-end flow. Claude specifically called out envtest-level coverage of the sync/apply/finalizer logic as the biggest testing gap.


Documentation Assessment

docs/design/api_containers.md gained useful new sections on engine mirroring, finalizer lifecycle, and spec.state transitions, and they match the implementation for the moby / Docker case. Two accuracy problems remain:

  1. The ContainerCreateRequest example at docs/design/api_containers.md:156-160 still shows state: running # Default to running, but the code now defaults to unknown (S1).
  2. The "engine-specific namespaces for containerd" phrasing at docs/design/api_containers.md:37-38 overstates current implementation — the controller hardcodes the moby namespace and, as called out in I1, cannot run against containerd at all today. Either add a "Docker only in the current release" qualifier, or qualify the sentence so readers don't expect containerd support.

The new CLI help in cmd/rdd/set.go:65-72 similarly promises behavior that the code doesn't yet deliver for running=false (I4) and for containerEngine.name=containerd (I1). Updating it along with the fix is worth doing as part of the same change.


Commit Structure

The branch is nine commits, each focused and individually reviewable (Rosetta option cleanup, socket path + distro workaround, Docker client dep, engine controller, webhook, design doc, BATS tests, rdd set wait, CI timeout wrapper). Commit order is logical — dependencies first, feature on top, tests and CI last — and most commits build on each other cleanly. No commit mixes unrelated concerns. Nothing to fix here.


Acknowledged Limitations


Agent Performance Retro

Claude

Claude's review was initially lost to a launcher bug (see Review Process Notes): the claude --print mode only captures the last assistant text block, and Claude emitted its findings across many text blocks as it worked. The full structured review was recovered post-hoc from the session JSONL and folded into this report. Claude's unique contributions are the strongest set of the three: the removeMirrorResource IgnoreNotFound-masking bug (I8), the compound cleanup weakness (Conflict-retry + first-error short-circuit) that strengthened I5, and the O(N) per-reconcile sweep concern that promoted the architectural observation into an actionable Important finding (I7). It also contributed four useful Suggestions (S4–S7) and a small dead-code cleanup (S8) that neither other agent caught. Its execution pattern — read a file, build a theory, verify with a targeted grep, write a finding — is visible in the tool-call mix and delivered zero false positives.

Codex

Codex produced the widest "contract-violation" set: the ContainerNamespace finalizer stuckness (I2), the running=false wait-semantics regression (I4), and the "finalizer stripped even when Docker delete fails" contract violation (I3) are all unique to it. Its severity calibration was restrained — it intentionally chose zero criticals and classified everything as Important, which in hindsight left C1 slightly underweighted compared to Gemini's read but was otherwise well-tuned.

Gemini

Gemini was fast and aggressive and caught the one true critical in the change — the volume-name sanitization crash (C1). It also raised real important issues: the edge-triggered cleanup bug (I5), the N+1 startup latency (I6), and the watch-channel-closure concern. Its weakness was an obvious false positive: it claimed a bash syntax error "$ @" in scripts/bats-with-timeout.sh:143 that does not exist in the file (the actual code is "$@"). That finding carried a critical label and an Executive Summary pull quote, so without verification it would have completely dominated the consolidated output. Gemini also upgraded the edge-trigger cleanup bug to critical on a plausible-but-overblown "permanent orphaning" argument — partial self-healing on the next fullSync brings it back to Important. Net: strong unique findings, but the only agent that needed a correction on severity and hallucinated a finding outright.

Summary

Claude Opus 4.6Codex GPT 5.4Gemini 3.1 Pro
Duration15m 59s6m 54s6m 33s
Findings3I 5S4I 1S1C 2I 2S
Tool calls88 (Bash 39, Read 28, Grep 13)50 (shell 50)6 (grepsearch 3, runshell_command 3)
Design observations232
False positives001
Unique insights533
Files reviewed212121
Coverage misses000
Totals3I 5S4I 1S1C 2I 2S
Downgraded1 (I→S)01 (I→S)
Dropped001

Reconciliation: Gemini C1 ("bash syntax error in bats-with-timeout.sh:144"): rejected as a false positive — verified the file directly, the actual code is "$@" & with no space. Gemini C2 ("edge-triggered cleanup permanently orphans resources"): critical → important (merged into I5) — the failure mode is real but syncAllContainers / syncAllImages / syncAllVolumes stale-item pruning partially self-heals on the next successful start, so "permanent" is overstated. Gemini C3 ("volume naming"): kept as critical (C1 in the consolidated report), since one misnamed volume is enough to break the entire engine startup. Claude I4 ("dead scheme field"): important → suggestion (S8) — a hygiene cleanup rather than a correctness or performance concern.

Overall assessment: Claude and Codex each contributed the highest-signal set for their respective problem classes — Claude for cleanup / concurrency error handling and perf architecture, Codex for contract violations and CLI semantics. Gemini contributed the data-path critical (C1) and the reconciler state-machine concern (I5) plus the efficiency finding (I6). No two agents alone would have produced the full consolidated set; all three were necessary.


Review Process Notes

Skill improvements

Repo context updates