Deep Review: 20260428-102700-pr-333

Date2026-04-28 10:27
Reporancher-sandbox/rancher-desktop-daemon
Round3 (of target)
Author@Nino-K
PR#333 — Docker Engine: improve Docker context probe safety and isolation
Commits751d6e5 Docker Engine: improve Docker context probe safety and isolation
ReviewersClaude Opus 4.7 (effort: xhigh), Codex GPT 5.5 (effort: xhigh), Gemini 3.1 Pro (effort: default)
VerdictMerge with fixes — round 2's DOCKER_CONTEXT gap is closed by an env-skip in manageDockerContext, but the BATS suite does not isolate the daemon's inherited env from the dev's parent shell, so the round-2 test fix becomes flaky on hosts that export DOCKER_HOST or DOCKER_CONTEXT. Plus several round-2 housekeeping items remain unaddressed.
Wall-clock time25 min 42 s


Executive Summary

Round 3 closes the round-2 DOCKER_CONTEXT false-override path by skipping the entire probe-and-set when the daemon process inherits either DOCKER_HOST or DOCKER_CONTEXT (engine_reconciler.go:372-381), and adds a four-case unit suite for probeNamedDockerContext covering missing, SSH, unreachable unix, and unreachable tcp endpoints. The author's choice to short-circuit on either env var (rather than the narrower "honor DOCKER_CONTEXT as the named context") is a defensible conservative read: the daemon cannot tell whether the inherited env reflects the user's global preference, so it declines to write config.json at all when in doubt.

The round-3 changes introduce one regression and surface several housekeeping items the prior rounds left open:

  1. The new env-skip silently disables the round-2 BATS test fix on dev machines. local_setup_file does not unset DOCKER_HOST / DOCKER_CONTEXT before spawning the service, and service.Start passes no explicit cmd.Env, so the daemon inherits whatever the parent shell happened to have set. A developer who runs BATS from a shell with DOCKER_CONTEXT=staging exported (a common case for users with multiple Docker contexts) sees the new test fail because the env-skip bails out before the seeded named context is probed. CI runners typically lack these vars, so this surfaces only on local runs.
  2. Round-2 housekeeping items remain. The manageDockerContext function-level docstring and docs/design/bootstrap.md:58,86 still describe the pre-refactor "absent or unhealthy → promote ours" contract; the round-2 S6 TODO(windows) wording still names getCurrentDockerContext despite that helper not needing Windows work; and the docker_context_dir BATS helper docstring still references ~/.docker/contexts/... even though the body returns ${DOCKER_CONFIG}/contexts/....
  3. A pre-existing WaitGroup race remains in manageDockerContext / removeDockerContext. Gemini's round-1 finding was dismissed in the round-1 retro on incorrect reasoning (the retro claimed stopWatcher holds watcherMu across the Wait() call; in fact stopWatcher releases watcherMu before invoking removeDockerContext, so the Add(1) in manageDockerContext and the Wait() in removeDockerContext are not serialized by watcherMu). The race is narrow — it requires an in-flight Reconcile to call manageDockerContext after the shutdown hook released watcherMu — but it is real and out-of-scope for this PR.

Report structure: 0 Critical, 1 Important, 9 Suggestions, 2 Design Concerns, 4 Design Strengths.


Critical Issues

None.


Important Issues

I1. local_setup_file does not isolate the daemon from DOCKER_HOST / DOCKER_CONTEXT in the parent shell Claude
# Engine controller tests — verify that the engine controller mirrors Docker
# containers, images, and volumes into Container, Image, and Volume
# resources, and that deletions and action annotations on Container
# resources are forwarded to Docker.

local_setup_file() {
    # The Docker socket access pattern used by these tests is not yet wired
    # up for Windows/WSL2.
    skip_on_windows

    # Isolate all Docker config reads and writes from the developer's real
    # ~/.docker directory. Set DOCKER_CONFIG before starting the service so
    # the controller process inherits it (service.Start uses exec.Command
    # without an explicit Env, so it inherits the caller's environment).
    export DOCKER_CONFIG="${BATS_FILE_TMPDIR}/docker-config"
    mkdir -p "${DOCKER_CONFIG}"

    # Deliberately skip setup_rdd_control_plane here: `rdd set` internally
    # calls ensureServiceRunning, which is exactly the CLI path we want to
    # exercise — the engine controller is part of the default controller
    # set, so no explicit --controllers selection is needed.
    rdd svc delete
    rdd set running=true
    run -0 rdd svc paths docker_socket
    export DOCKER_HOST="unix://${output}"
    # Mirror resources live in App.spec.namespace. Override RDD_NAMESPACE
    # to whatever the App was created with so the test queries the same
    # namespace the engine controller uses, regardless of CRD defaults.
    RDD_NAMESPACE=$(rdd ctl get app app -o jsonpath='{.spec.namespace}')
    export RDD_NAMESPACE
}

# Make a WebSocket request, with the endpoint given as the first argument in the
# same form as `kubectl get --raw`; the output is captured in `${output}`.
do_websocket() { # endpoint
    local endpoint=$1

pkg/service/cmd/service.go spawns the daemon via exec.CommandContext(...) with no explicit cmd.Env, so the daemon inherits the parent shell's environment verbatim. The new skip in engine_reconciler.go:379-381 then short-circuits the entire probe-and-set goroutine when the daemon's process env carries DOCKER_HOST or DOCKER_CONTEXT:

		// vars: the daemon inherits them frozen from the shell that ran
		// rdd svc start and they may not reflect the user's global
		// preference. Rewriting config.json based on them could clobber a
		// correct currentContext for sessions that don't have those vars
		// set. Skip the probe-and-set entirely when either is present.
		if os.Getenv("DOCKER_HOST") != "" || os.Getenv("DOCKER_CONTEXT") != "" {
			return
		}

		var healthy bool
		if current != "" && current != "default" {
			healthy = probeNamedDockerContext(probeCtx, current)
		} else {
if os.Getenv("DOCKER_HOST") != "" || os.Getenv("DOCKER_CONTEXT") != "" {
    return
}

The round-2 test fix moby engine sets currentContext when no healthy context exists (engine-docker.bats:779-809) seeds currentContext: test-unreachable, restarts the watcher, and asserts the reconciler promotes rancher-desktop-${RDD_INSTANCE}. A developer who ran bats ... from a shell that exported either env var (a common case for anyone using a remote/secondary Docker context) trips the skip; currentContext stays at test-unreachable; the try --max 6 --delay 1 -- ... loop times out and the test fails. CI runners typically lack these vars, so this surfaces only on local runs.

    local socket_path=${output}
    run -0 jq -r '.Endpoints.docker.Host' "${meta_file}"
    assert_output "unix://${socket_path}"
}

@test "moby engine sets currentContext when no healthy context exists" {
    local context_name="rancher-desktop-${RDD_INSTANCE}"

    # Seed a named context pointing at an unreachable socket and make it
    # current. This forces the reconciler into the probeNamedDockerContext
    # branch with a guaranteed-failing endpoint, independent of whether the
    # host has a running Docker daemon at the default socket (e.g.
    # ubuntu-latest runners ship Docker pre-installed). Using a named context
    # also avoids the stale-env issue: the service process's DOCKER_HOST is
    # frozen at first start, so FromEnv-based probing of "" / "default" is
    # host-dependent and unreliable from inside a long-lived daemon.
    probe_target="test-unreachable"
    run_e -0 docker_context_dir "${probe_target}"
    probe_dir="${output}"
    mkdir -p "${probe_dir}"
    cat >"${probe_dir}/meta.json" <<EOF
{"Name":"${probe_target}","Endpoints":{"docker":{"Host":"unix:///nonexistent/does-not-exist.sock","SkipTLSVerify":false}}}
EOF
    printf '{"currentContext":"%s"}\n' "${probe_target}" >"${DOCKER_CONFIG}/config.json"

    rdd set running=false
    rdd set running=true containerEngine.name=moby
    rdd ctl wait --for=condition=ContainerEngineReady app/app --timeout=30s

    # The probe goroutine runs asynchronously; give it a moment.
    try --max 6 --delay 1 -- \
        bash -c "jq -r '.currentContext' '${DOCKER_CONFIG}/config.json' | grep -qx '${context_name}'"

    run -0 jq -r '.currentContext' "${DOCKER_CONFIG}/config.json"
    assert_output "${context_name}"
}

@test "stopping moby engine removes Docker context and clears currentContext" {
    rdd set running=false

    local context_name="rancher-desktop-${RDD_INSTANCE}"

The in-test export DOCKER_HOST="unix://${output}" at line 31 fires after rdd set running=true on line 29, so it does not reach the already-spawned daemon — only the BATS shell's child docker CLIs see it. The daemon's env is whatever was inherited at spawn time.

Fix: clear the env vars before spawning the daemon.

 local_setup_file() {
     skip_on_windows
     export DOCKER_CONFIG="${BATS_FILE_TMPDIR}/docker-config"
     mkdir -p "${DOCKER_CONFIG}"
+    # The daemon inherits these env vars at spawn (service.Start does not
+    # set cmd.Env). Clear them so manageDockerContext exercises the
+    # probe-and-set path regardless of the parent shell.
+    unset DOCKER_HOST DOCKER_CONTEXT
     rdd svc delete
     rdd set running=true

(important, regression)


Suggestions

S1. Comment explaining the named-context detour misstates why it works Claude
}

@test "moby engine sets currentContext when no healthy context exists" {
    local context_name="rancher-desktop-${RDD_INSTANCE}"

    # Seed a named context pointing at an unreachable socket and make it
    # current. This forces the reconciler into the probeNamedDockerContext
    # branch with a guaranteed-failing endpoint, independent of whether the
    # host has a running Docker daemon at the default socket (e.g.
    # ubuntu-latest runners ship Docker pre-installed). Using a named context
    # also avoids the stale-env issue: the service process's DOCKER_HOST is
    # frozen at first start, so FromEnv-based probing of "" / "default" is
    # host-dependent and unreliable from inside a long-lived daemon.
    probe_target="test-unreachable"
    run_e -0 docker_context_dir "${probe_target}"
    probe_dir="${output}"
    mkdir -p "${probe_dir}"
    cat >"${probe_dir}/meta.json" <<EOF

The "Using a named context also avoids the stale-env issue" claim does not hold once round 3's env-skip lands. The skip at engine_reconciler.go:379-381 runs before the named-context probe, so a frozen DOCKER_HOST in the daemon's env disables both branches equally — the named-context detour buys nothing on that axis. The detour only escapes the FromEnv flake when the daemon inherits no env vars at all. Pair this rewrite with the I1 unset so the comment describes the actual contract.

		// vars: the daemon inherits them frozen from the shell that ran
		// rdd svc start and they may not reflect the user's global
		// preference. Rewriting config.json based on them could clobber a
		// correct currentContext for sessions that don't have those vars
		// set. Skip the probe-and-set entirely when either is present.
		if os.Getenv("DOCKER_HOST") != "" || os.Getenv("DOCKER_CONTEXT") != "" {
			return
		}

		var healthy bool
		if current != "" && current != "default" {
			healthy = probeNamedDockerContext(probeCtx, current)
		} else {
-    # Using a named context
-    # also avoids the stale-env issue: the service process's DOCKER_HOST is
-    # frozen at first start, so FromEnv-based probing of "" / "default" is
-    # host-dependent and unreliable from inside a long-lived daemon.
+    # Both probe branches (named and FromEnv) require the daemon's env to
+    # have neither DOCKER_HOST nor DOCKER_CONTEXT set, otherwise
+    # manageDockerContext skips the probe entirely. local_setup_file
+    # unsets both before spawning the daemon.

(suggestion, gap)

S2. probeDockerContext docstring overstates what the function reaches in production Claude
	defer cli.Close()
	_, err = cli.Ping(ctx, dockerclient.PingOptions{})
	return err == nil
}

// probeDockerContext pings the implicit default Docker endpoint
// (DOCKER_HOST or the platform default socket) within dockerContextProbeTimeout.
// Used when currentContext is "" or "default".
func probeDockerContext(ctx context.Context) bool {
	probeCtx, cancel := context.WithTimeout(ctx, dockerContextProbeTimeout)
	defer cancel()
	return pingDocker(probeCtx, dockerclient.FromEnv)
}

// probeNamedDockerContext pings the Docker endpoint for the named context
// within dockerContextProbeTimeout and returns true if the endpoint is healthy.
//
// When we cannot determine whether the context is healthy — because the store

By the time probeDockerContext(probeCtx) runs at engine_reconciler.go:387, the goroutine has already returned if either DOCKER_HOST or DOCKER_CONTEXT was set (lines 379-381). So FromEnv's WithHostFromEnv only ever resolves to the platform default socket — DOCKER_HOST is empty by construction here. The docstring's "DOCKER_HOST or the platform default socket" phrasing is reachable from unit tests but never on the production path. Tighten the comment so a future refactor that removes the env-skip does not silently change probe semantics.


		var healthy bool
		if current != "" && current != "default" {
			healthy = probeNamedDockerContext(probeCtx, current)
		} else {
			healthy = probeDockerContext(probeCtx)
		}
		// Guard against writing currentContext after removeDockerContext has
		// already cancelled probeCtx and deleted the context directory.
		if !healthy && probeCtx.Err() == nil {
			if err := setCurrentDockerContext(contextName); err != nil {
 // probeDockerContext pings the implicit default Docker endpoint
-// (DOCKER_HOST or the platform default socket) within dockerContextProbeTimeout.
-// Used when currentContext is "" or "default".
+// (the platform default socket) within dockerContextProbeTimeout. Used
+// when currentContext is "" or "default". The caller short-circuits when
+// DOCKER_HOST or DOCKER_CONTEXT is set, so FromEnv's host-from-env path
+// is unreachable on the production path and the probe always targets
+// the platform default socket.
 func probeDockerContext(ctx context.Context) bool {

(suggestion, documentation)

S3. probeNamedDockerContext lacks coverage for the four "assume healthy" library-error branches Claude

The new Test_probeNamedDockerContext covers four common outcomes (missing, SSH, unreachable unix, unreachable tcp), but the four "assume healthy" branches at docker_context.go:172-174, 178-180, 188-191, 193-196 (metadata read error, decode error, TLS load error, ClientOpts error) and the TCP+TLS / mTLS / SkipTLSVerify paths motivating the PR have no unit anchor. The malformed-meta case is the cheapest to lock in: write a meta.json with an unparseable endpoint payload and assert the function returns true.

		_, hasAuths := cfg["auths"]
		assert.Assert(t, hasAuths, "auths key must be preserved across clear")
	})
}

func Test_probeNamedDockerContext(t *testing.T) {
	// seedContext writes a named docker context with the given Host into the
	// store. It uses the same store config as production code so the context
	// is readable by probeNamedDockerContext.
	seedContext := func(t *testing.T, name, host string) {
		t.Helper()
		s, err := newContextStore()
		assert.NilError(t, err)
		assert.NilError(t, s.CreateOrUpdate(store.Metadata{
			Name:     name,
			Metadata: map[string]any{},
			Endpoints: map[string]any{
				docker.DockerEndpoint: docker.EndpointMeta{Host: host},
			},
		}))
	}

	t.Run("missing context returns false", func(t *testing.T) {
		newDockerTestDir(t)
		result := probeNamedDockerContext(context.Background(), "does-not-exist")
		assert.Assert(t, !result, "missing context must be treated as unhealthy")
	})

	t.Run("ssh context returns true (non-probed scheme)", func(t *testing.T) {
		newDockerTestDir(t)
		seedContext(t, "ssh-ctx", "ssh://user@remote-host")
		result := probeNamedDockerContext(context.Background(), "ssh-ctx")
		assert.Assert(t, result, "ssh context must be assumed healthy to avoid clobbering user's choice")
	})

	t.Run("unix context with unreachable socket returns false", func(t *testing.T) {
		newDockerTestDir(t)
		seedContext(t, "local-ctx", "unix:///nonexistent/does-not-exist.sock")
		result := probeNamedDockerContext(context.Background(), "local-ctx")
		assert.Assert(t, !result, "unreachable unix socket must be treated as unhealthy")
	})

	t.Run("tcp context with unreachable endpoint returns false", func(t *testing.T) {
		newDockerTestDir(t)
		// Port 1 is reserved and will be refused on any sane host.
		seedContext(t, "tcp-ctx", "tcp://127.0.0.1:1")
		result := probeNamedDockerContext(context.Background(), "tcp-ctx")
		assert.Assert(t, !result, "unreachable tcp endpoint must be treated as unhealthy")
	})
}

A fake context store seeded with TLS data plus an unreachable TLS endpoint would pin the TLS construction without needing a live TLS daemon and close the largest residual gap from round 2 S2.

(suggestion, gap)

S4. The env-skip and the empty/default probe routing have no unit test Claude

The four-line env-var skip and the two-branch probe selector are entirely behavioral additions to the goroutine in manageDockerContext and have no direct test. The BATS path covers a subset (named-unhealthy promotes ours), but no test asserts:

  1. setCurrentDockerContext is never called when DOCKER_HOST is set;
  2. setCurrentDockerContext is never called when DOCKER_CONTEXT is set;
  3. current == "default" routes through probeDockerContext (FromEnv), not the named branch.

A small refactor that extracts the goroutine body into a testable helper (taking getEnv func(string) string and the probe functions as parameters) would make all three reachable from _test.go. Worth doing because the env-skip is the headline behavior of this PR — silent regression of it would not be caught.

(suggestion, gap)

S5. getCurrentDockerContext early-return comment misses the recovery path Claude
			}
			r.contextMu.Unlock()
			cancel()
		}()

		current, err := getCurrentDockerContext()
		if err != nil {
			// An error here means the config file exists but could not be
			// read or parsed. Do not proceed — writing currentContext now
			// would overwrite a file we cannot safely round-trip.
			log.Error(err, "Failed to read current Docker context")
			return
		}

		// DOCKER_HOST and DOCKER_CONTEXT take precedence over config.json
		// in the Docker CLI's resolution order. Both are shell-local env
		// vars: the daemon inherits them frozen from the shell that ran
		// rdd svc start and they may not reflect the user's global

The early return is the safe choice — setCurrentDockerContext would have failed on the same config.Load call in the old flow. A future reader could mistake the early return for "RDD ignores broken config.json forever"; in fact the next reconcile (e.g., another rdd set) re-enters manageDockerContext, so repairing the file and triggering any reconcile is enough to recover.

     // An error here means the config file exists but could not be
     // read or parsed. Do not proceed — writing currentContext now
-    // would overwrite a file we cannot safely round-trip.
+    // would overwrite a file we cannot safely round-trip. The next
+    // reconcile (e.g. another `rdd set`) re-enters this function, so
+    // repairing config.json restores the probe-and-set behavior.

(suggestion, documentation)

S6. Use t.Context() in new tests Gemini
		}))
	}

	t.Run("missing context returns false", func(t *testing.T) {
		newDockerTestDir(t)
		result := probeNamedDockerContext(context.Background(), "does-not-exist")
		assert.Assert(t, !result, "missing context must be treated as unhealthy")
	})

	t.Run("ssh context returns true (non-probed scheme)", func(t *testing.T) {
		newDockerTestDir(t)

Go 1.25 (go.mod) gives every *testing.T a per-test Context() that cancels when the test finishes. Switch the four context.Background() callers in Test_probeNamedDockerContext so test timeouts and cancellations propagate into the probe.

-result := probeNamedDockerContext(context.Background(), "does-not-exist")
+result := probeNamedDockerContext(t.Context(), "does-not-exist")

(suggestion, enhancement)

S7. manageDockerContext docstring and docs/design/bootstrap.md still describe the pre-refactor contract
		e.stop()
	}
	r.removeDockerContext()
}

// manageDockerContext creates the instance Docker context and, in a goroutine,
// probes the user's current context; if it is absent or unhealthy, switches
// the default to the new context. At most one probe runs at a time.
func (r *EngineReconciler) manageDockerContext(socketPath string) {
	// TODO(windows): Docker context management uses unix:// sockets; Windows
	// requires npipe:// in the context meta file, an npipe-aware probe client,
	// and platform-specific path handling in getCurrentDockerContext and
	// createReplaceDockerContext. Track in a follow-up issue before shipping

After round 2 the empty / "default" branch routes through probeDockerContext (FromEnv) and treats a foreign default daemon as healthy; round 3 adds a further env-skip for DOCKER_HOST / DOCKER_CONTEXT. Neither the docstring nor docs/design/bootstrap.md:58,86 reflects either change. Round 2 S3 flagged the same drift and was not picked up.


    * The `LimaVM` controller creates the VM. Since the status is set to `running` it also starts the VM. Once the VM has completed startup the controller sets the status to "ready".

  * The `App` controller is notified of the status change of the `LimaVM` object and detects that the VM is running. It now monitors the VM to check when the `docker.sock` is accepting requests.

  * It creates a `rancher-desktop-2` docker context. If there is no current context, or if the context isn't accepting connections, then it makes the new context the default.

  * It sets the status of the `App` object to `ready`.

* `rdd start` is notified that the `App` status has changed. It verifies that the status is "ready" and exits.
docs/design/bootstrap.md:58
   "It creates a `rancher-desktop-2` docker context. If there is no
   current context, or if the context isn't accepting connections, then
   it makes the new context the default."

docs/design/bootstrap.md:86
   "`rdd start` has created the `rancher-desktop-2` docker context, but
   it will only activate it if the current context doesn't exist, or is
   non-functional."

Fix: rewrite the docstring and both bullets to spell out the new contract — RDD probes the current named context, falls back to probing the implicit default endpoint when none is set, and skips the entire promotion when DOCKER_HOST or DOCKER_CONTEXT is set in the daemon's env.

(suggestion, documentation, round 2 S3 unaddressed)

S8. TODO(windows) comment overstates what needs platform-specific handling

// manageDockerContext creates the instance Docker context and, in a goroutine,
// probes the user's current context; if it is absent or unhealthy, switches
// the default to the new context. At most one probe runs at a time.
func (r *EngineReconciler) manageDockerContext(socketPath string) {
	// TODO(windows): Docker context management uses unix:// sockets; Windows
	// requires npipe:// in the context meta file, an npipe-aware probe client,
	// and platform-specific path handling in getCurrentDockerContext and
	// createReplaceDockerContext. Track in a follow-up issue before shipping
	// Windows support.
	if runtime.GOOS == "windows" {
		return
	}
	contextName := instance.Name()
	log := logf.FromContext(r.watcherCtx).WithName("docker-context")

getCurrentDockerContext delegates to docker/cli's config.Load, and createReplaceDockerContext plus newContextStore delegate to the store's path handling — both resolve %USERPROFILE%\.docker on Windows through os.UserHomeDir() with no extra work. The Windows-specific work concentrates in one line of createReplaceDockerContext (the hard-coded "unix://" prefix) and in the probe client selection. Round 1 S5 and round 2 S6 flagged this and were not picked up.

-    // TODO(windows): Docker context management uses unix:// sockets; Windows
-    // requires npipe:// in the context meta file, an npipe-aware probe client,
-    // and platform-specific path handling in getCurrentDockerContext and
-    // createReplaceDockerContext. Track in a follow-up issue before shipping
-    // Windows support.
+    // TODO(windows): createReplaceDockerContext hard-codes "unix://"; Windows
+    // needs "npipe://" plus an npipe-aware probe client in
+    // probeDockerContext / probeNamedDockerContext. Track in a follow-up
+    // issue before shipping Windows support.

(suggestion, documentation, round 2 S6 / round 1 S5 unaddressed)

S9. Stale path reference in docker_context_dir helper docstring
    refute_output
}

# --- Docker context management ---

# docker_context_dir returns the ~/.docker/contexts/meta/<hash> directory for
# the named context. Docker derives the sub-directory from sha256(name).
docker_context_dir() {
    local name="$1"
    local hash
    # sha256sum on Linux, shasum on macOS
    if command -v sha256sum &>/dev/null; then

The body returns ${DOCKER_CONFIG}/contexts/meta/${hash}, but the docstring still says ~/.docker/contexts/meta/<hash>. The two diverged when the round-2 fix isolated tests via DOCKER_CONFIG, and the docstring still has not been updated.

-# docker_context_dir returns the ~/.docker/contexts/meta/<hash> directory for
-# the named context. Docker derives the sub-directory from sha256(name).
+# docker_context_dir returns the contexts/meta/<hash> directory under
+# DOCKER_CONFIG for the named context. Docker derives the sub-directory
+# from sha256(name).

(suggestion, documentation, round 2 S8 unaddressed)


Design Observations

Concerns

Add(1) and Wait() on contextProbeWg are not serialized by an outer lock (in-scope, pre-existing) Gemini

manageDockerContext calls r.contextProbeWg.Add(1) at engine_reconciler.go:350 after releasing contextMu; removeDockerContext calls r.contextProbeWg.Wait() at line 420 after releasing contextMu. The two are not on the same mutex, and stopWatcher releases watcherMu before invoking removeDockerContext (line 311 vs line 316), so watcherMu does not bridge them either. Round 1 dismissed Gemini's identical finding on the rationale that "stopWatcher takes watcherMu before calling Wait()" — that is incorrect; the lock is released first.

The exploit window is narrow: it needs an in-flight Reconcile to acquire watcherMu after the shutdown hook released it from stopWatcher, then succeed in newDockerWatcher(r.watcherCtx, ...) despite watcherCtx already being cancelled, then reach Add(1) while the shutdown hook's removeDockerContext is at Wait() with the counter at 0. If this lands, the WaitGroup panics with "Add called concurrently with Wait" and the daemon crashes during graceful shutdown rather than cleaning up.

This is pre-existing — predates PR #333 — so it does not block this PR. Mention it here so the round-1 dismissal does not bury the issue. The right fix is to swap the shared WaitGroup for a per-probe state struct holding both the cancel func and a per-probe done channel, both swappable atomically under contextMu.

Daemon's frozen env is a long-lived property masquerading as a per-invocation choice (future) Claude

The env-skip uses the daemon process's own environment — whatever shell happened to spawn rdd svc start minutes or hours earlier. A developer who once started rdd from a shell exporting DOCKER_CONTEXT=staging permanently disables RDD's context promotion on that machine — even from new shells without those vars — until they rdd svc delete && rdd svc start from a clean shell. The author's PR comment acknowledges this is conservative; a follow-up could either (a) document the gotcha in user-facing docs, (b) re-resolve the env at probe time from a stable source (e.g., the user's ~/.docker/config.json environment block — Docker CLI honors this), or (c) emit a one-shot info log on startup recording which env vars are inherited so the daemon's behavior stays observable.

Strengths

DOCKER_CONTEXT env-skip closes the round-2 false-override path (in-scope) Codex Gemini

The author chose the most conservative reading of round-2 I1: rather than honor DOCKER_CONTEXT as a named-context selector (which would still write config.json based on shell-local state), the daemon now declines to write config.json at all when either DOCKER_HOST or DOCKER_CONTEXT is set in its env. This eliminates the "user unsets DOCKER_CONTEXT and finds RDD's context as their new default" surprise that round 2 enumerated.

Conservative "assume healthy" stance with explicit logging at every fall-through (in-scope) Claude Gemini

probeNamedDockerContext returns true on every error path that cannot decide healthiness — store unreadable, metadata missing, endpoint malformed, scheme not tcp/unix, TLS load failed, ClientOpts failed — and logs each at default level (round 2 S7 was picked up). For a tool that mutates user config, this is the right default: refuse to clobber a config the controller cannot evaluate.

Test_probeNamedDockerContext covers the four common probe outcomes (in-scope) Codex

The new four-case suite (missing context → false, SSH scheme → true, unreachable unix → false, unreachable tcp → false) anchors the contract for the most common shapes a real user config produces. Round 1 S2 and round 2 S2 raised this gap; the round-3 addition closes most of it (S3 above lists the residual library-error branches).

Two-function probe split makes the transport decision explicit (in-scope) Claude Codex

Splitting the old single-signature probeDockerContext(ctx, host) into probeDockerContext(ctx) (FromEnv-backed default probe) and probeNamedDockerContext(ctx, name) (store-backed named probe) makes the "did we load TLS for this?" question unmissable at each call site. The old signature silently called WithHost(host) even when TLS data was required; the new signatures foreclose that class entirely.


Testing Assessment

Coverage gaps, ranked by risk:

  1. Daemon env contamination from the parent shell — the round-2 BATS test fix flakes on dev machines where DOCKER_HOST or DOCKER_CONTEXT is exported (per I1).
  2. DOCKER_HOST / DOCKER_CONTEXT env-skip path — the headline behavior of this PR, untested at any layer (per S4).
  3. probeNamedDockerContext "assume healthy" library-error branches — TLS load failure, ClientOpts failure, malformed meta (per S3).
  4. TCP+TLS / mTLS / SkipTLSVerify probe paths — the original motivation for the PR has no unit anchor (per S3, residual round 2 S2).
  5. current == "default" routing — neither unit nor BATS tests assert that this routes through probeDockerContext, not probeNamedDockerContext (Docker treats "default" as a magic non-name).
  6. Early return on getCurrentDockerContext errorTest_getCurrentDockerContext_malformedAuth verifies the helper surfaces the error, but no test confirms the reconciler refuses to call setCurrentDockerContext when the config is malformed.

Codex ran go test ./pkg/controllers/app/engine/controllers and go test -race ./pkg/controllers/app/engine/controllers during its review and reported both passed.


Documentation Assessment


Commit Structure

Single commit (751d6e5), self-contained, message accurately describes the change. The bundling — env-skip, unit-test additions, BATS env-isolation choice — is internally consistent. No objection.


Acknowledged Limitations


Unresolved Feedback


Agent Performance Retro

Claude

Carried this round single-handed: produced the only Important finding (I1, the BATS env-contamination flake) and four of the five new suggestions. The I1 trace is the strongest piece of work in the round — it spotted that the round-3 env-skip silently disables the round-2 BATS test fix, and tracked the failure through service.Start having no explicit cmd.Env to the parent-shell inheritance window. Also caught the round-2 leftovers (S7 docstring/bootstrap.md drift, S8 TODO Windows wording, S9 docker_context_dir comment) that both peers missed. No false positives, no coverage misses.

Codex

Returned an empty findings list — 0 critical, 0 important, 0 suggestions — with all four files marked "Reviewed, no issues" and a brief Design Strengths / Testing Assessment section. The review ran a real go test -race and reported both passed; that is genuinely useful evidence for the verdict but does not by itself justify a slot in this round's report. Codex missed I1 entirely (the env-skip / BATS interaction was directly in its strike zone — round 2 was Codex's headline finding) and missed every round-2 leftover. Below threshold.

Gemini

Two suggestions (one valid t.Context() modernization in S6; one local-in-@test re-flag that this round dropped as out-of-scope, since the offending line is pre-existing and not modified by this PR). The unique contribution is the WaitGroup race re-raise — Gemini was correct in round 1 and is correct now; the round-1 retro dismissed it on incorrect mutex reasoning. Surfacing the race a second time was the right call. Gemini again skipped git blame (known daily-quota behavior); Claude and Codex covered that gap.

Summary

Claude Opus 4.7Codex GPT 5.5Gemini 3.1 Pro
Duration12m 17s3m 39s7m 34s
Findings1I 5Snone1S
Tool calls36 (Bash 16, Read 13, Grep 7)37 (shell 36, stdin 1)11 (runshellcommand 7, read_file 4)
Design observations122
False positives000
Unique insights502
Files reviewed444
Coverage misses011
Totals1I 5Snone1S
Downgraded000
Dropped001

Reconciliation:


Review Process Notes

Skill improvements

Repo context updates