Skip to content

fix(installer): a stopped Docker forced the admin path, and the copy said we were starting it - #741

Merged
LukasWodka merged 9 commits into
developfrom
fix/runtime-down-start-docker
Aug 19, 2026
Merged

fix(installer): a stopped Docker forced the admin path, and the copy said we were starting it#741
LukasWodka merged 9 commits into
developfrom
fix/runtime-down-start-docker

Conversation

@LukasWodka

@LukasWodkaLukasWodka commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Closes#740.

The trap

A Mac with Docker Desktop installed but not running — the state every machine is in after a reboot — could not be set up by a user who couldn't give an administrator password. And the installer told them it was starting Docker.

· Docker isn't running yet — starting it, then checking your environment.
Container runtime none usable as this user –
→ Install tier Tier 2 — Docker isn't running; start/install Docker Desktop (needs admin once).
[13:20:57] step b: admin check passed
[13:20:57] FAILED at .../common.sh:527 — exit 1 — command: sudo -n true
  1. assess.sh printed "Docker isn't running yet — starting it" and fell through to return 0. Nothing started anything.
  2. The probe found no usable runtime → macOS → Tier 2, the admin-password path.
  3. preflight_sudo asked for the password. Dead end.

The circularity is the bug: the onlyopen -a Docker in the tree lives in install_docker_desktop, behindpreflight_sudo. The installer could only start Docker after taking a password it needed solely because Docker wasn't started.

With Docker running, PROBE_RUNTIME_USABLE=1 wins first and unconditionally in _classify_from_probesTier 0, where the admin gate and preflight_sudo never run. Tier 0 (client#704) was unreachable for exactly the machines it was written for.

The fix

_assess_handle_runtime_down either does the thing or doesn't claim it:

  • macOSopen -a Docker (a GUI launch, no privileges whatsoever), wait bounded, let the probe reclassify to Tier 0. If it doesn't come up: say so, and name the cost — the fallback needs a password. That sentence is the one the old copy hid.
  • Anywhere else — starting the daemon needs root, so don't claim to be starting it.

_wait_for_docker is extracted so the nudge and install_docker_desktop share one loop instead of growing a second that drifts.

The report named a healthy check

Stopped at common.sh:527 (exit 1) — command: sudo -n true

Line 527 is _real_sudo; sudo -n true is the passwordless-sudo probe, which is supposed to fail on a normal Mac — the host check prints its failure as a normal row ("sudo needs a password"). The real cause was never recorded, because error() calls exit, and exit fires no ERR trap, so TB_ERR_* still held the last benign probe.

A report that confidently names a working line is worse than one that names nothing. error() now records its own caller location and message first — BASH_SOURCE[1], not [0], which would name common.sh for every refusal in the tree — with an explicit exit code so it can't latch whatever $? happened to be.

Verified end-to-end against the failing scenario:

last recorded cause
beforecmd=sudo -n true
aftercmd=error: Could not obtain administrator privileges (sudo authentication failed)

The coverage gap that let it ship

preflight_sudo has four branches. Three were tested — root, no sudo binary, passwordless. The ordinary Mac — an admin user whose sudo wants a password — was exercised by nothing: the tier-2 lifecycle tests stub preflight_sudo out via _tier0_mocks, and common.bats never covered it. Both branches are now tested, each asserting its specific message rather than a bare non-zero.

Tests

Full suite: 1102 passed, 0 failed (see the correction comment — my first report of this was read off a stale background run that exited 1; two failures, both mine, are fixed in the follow-up commit). shellcheck --severity=error and check-style.sh clean.

scripts/manifest.sha256 is regenerated in this PR: it carries the checksums of the installer libs edited here and is the installer's own integrity check over what it downloads, so a stale one breaks verification at install time.

Mutations, each asserted to have applied:

mutationreddens
error() stops recording3 tests
error() records BASH_SOURCE[0]1
the old "starting it" copy restored7

Three things this cost, worth recording

  • One assertion passed for the wrong reason.assess.bats wasn't sourcing setup-macos.sh, so run _try_start_docker_desktop exited 127 (command not found) and a bare -ne 0 accepted it — the bare-assertRaises trap. Now run -1 asserts the one code.
  • A branch was unreachable on any macOS dev box._try_start_docker_desktop's "not installed" path can't be hit on a machine that has Docker — i.e. the machine of whoever changes this code. _docker_app_installed is now its own function and the test stubs it, so it isn't a CI-only branch.
  • An assertion failed against a log that plainly contained the string. bats runs a preprocessed copy (N-common.bats.src) and rewrites those paths back in its own output — so cat printed something the file does not contain, and every assertion written against what I saw on screen failed. The needle is now what is actually there, with a comment saying why.

Workaround until this ships

Start Docker Desktop, wait for "running", re-run. The installer then takes Tier 0 and never asks for a password.

🤖 Generated with Claude Code


Note

Medium Risk
Changes first-run classification and macOS Docker startup before the admin gate, plus fatal-error logging; scope is installer shell only with heavy test coverage, but misclassification or a wedged-daemon edge case could still affect real installs.

Overview
Fixes the case where Docker Desktop is installed but stopped on macOS: the assess gate used to print that it was “starting” Docker while doing nothing, then the probe still saw no runtime and the install fell into Tier 2 (admin password)—even though the only open -a Docker lived behind that same prompt.

Runtime-down handling adds _assess_handle_runtime_down: on Darwin, when Docker.app is present, it calls _try_start_docker_desktop (GUI open -a Docker, up to ~60s, bounded probes). Messaging either matches success or admits failure and that continuing may need an administrator password; non-macOS and Macs without Docker Desktop no longer claim to start anything.

Shared Docker liveness centralizes checks in _docker_answers (bounded docker info) and _wait_for_docker (wall-clock deadline); install-time Docker wait is guarded with || true so a timeout does not abort under set -e before user-facing guidance.

Failure reporting: error() records the caller site and message via _record_err before exit, so install logs no longer blame the last benign sudo -n true probe when sudo auth actually failed.

Bats coverage for runtime-down copy, bounded routing, error()/preflight_sudo, and updated manifest.sha256 checksums for touched installer libs.

Reviewed by Cursor Bugbot for commit e8cfbcb. Bugbot is set up for automated code reviews on this repo. Configure here.

…said we were starting it
The assess branch for a down runtime printed
Docker isn't running yet — starting it, then checking your environment.
and then `return 0`. Nothing started anything. The probe found no usable
runtime, macOS classified Tier 2 — the admin-password path — and the only
`open -a Docker` in the tree sat inside install_docker_desktop, BEHIND
preflight_sudo. So the installer could only start Docker after taking an
administrator password it needed solely because Docker wasn't started.
A Mac with Docker installed but stopped therefore had no path at all for a
user who couldn't give that password, and had been told Docker was being
started. That is the state a machine is in after every reboot.
With Docker running, PROBE_RUNTIME_USABLE=1 wins first and unconditionally
in _classify_from_probes → Tier 0, where the admin gate and preflight_sudo
never run. Tier 0 (client#704) was unreachable for exactly the machines it
was written for.
_assess_handle_runtime_down now does the thing or doesn't claim it: on
macOS `open -a Docker` (a GUI launch, no privileges whatsoever), wait
bounded, and let the probe reclassify. If it doesn't come up, say so and
name the cost — the fallback needs a password. Off macOS, starting the
daemon needs root, so don't claim to be starting it.
_wait_for_docker is extracted so the nudge and install_docker_desktop share
one loop rather than growing a second one that drifts.
── the report named a healthy check ──
Stopped at .../common.sh:527 (exit 1) — command: sudo -n true
Line 527 is _real_sudo, and `sudo -n true` is the passwordless-sudo probe:
it is SUPPOSED to fail on a normal Mac, and the host check prints its
failure as a normal row ("sudo needs a password"). The real cause — "Could
not obtain administrator privileges (sudo authentication failed)" — was
never recorded, because error() calls `exit` and `exit` fires no ERR trap,
so TB_ERR_* still held the last benign probe.
A failure report that confidently names a healthy check is worse than one
that names nothing: it sends the reader to a line that is working. error()
now records its own CALLER location and message first (BASH_SOURCE[1], not
[0] — [0] would name common.sh for every refusal in the tree), with an
explicit exit code so it can't latch whatever $? happened to be.
Verified against the failing scenario end to end: the last recorded cause
goes from `cmd=sudo -n true` to `cmd=error: Could not obtain administrator
privileges (sudo authentication failed)`.
── the coverage gap that let it ship ──
preflight_sudo has four branches. Three were tested: root, no sudo binary,
passwordless. The ordinary Mac — an admin user whose sudo wants a password
— was exercised by NOTHING, because the tier-2 lifecycle tests stub
preflight_sudo out via _tier0_mocks and common.bats never covered it. Both
branches are now tested, each asserting its specific message rather than a
bare non-zero.
Mutations, each asserted to have applied:
error() stops recording -> 3 tests fail
error() records BASH_SOURCE[0] -> 1 fails
the old "starting it" copy back -> 7 fail
Three things this cost, worth recording:
- One assertion passed for the wrong reason first: assess.bats wasn't
sourcing setup-macos.sh, so `run _try_start_docker_desktop` exited 127
and a bare `-ne 0` accepted it. Now `run -1` asserts the one code.
- _try_start_docker_desktop's "not installed" branch was unreachable on any
macOS dev box, so _docker_app_installed is its own function and the test
stubs it — otherwise only CI would ever run that branch.
- A test assertion failed for an hour against a log that plainly contained
the string: bats runs a preprocessed copy (`N-common.bats.src`) and
rewrites those paths back in its OWN output, so `cat` prints something
the file does not contain. The needle is now what is really there.
Closes#740
@LukasWodkaLukasWodka self-assigned this Aug 17, 2026
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

Correction — my "1102 passed, 0 failed" line was wrong

I read that off a background suite run that had finished before the last round of test edits. A second, later run against the final tree exited 1, with two failures, both mine. Fixed in the follow-up commit; the PR body is updated.

1. scripts/manifest.sha256 was stale — the important one

gen-manifest.bats "a clean tree passes --check" failed because this PR edits common.sh, assess.sh and setup-macos.sh, and the manifest carries their checksums. That manifest is the installer's own integrity check over the libs it downloads, so merging it stale would have broken verification at install time on every platform — a considerably worse bug than the one this PR fixes.

Regenerated with scripts/gen-manifest.sh; --check now passes.

2. Two advisory assertions

bats-hygiene.bats caught two ! grep -qF "LAUNCHED" <<<"$output" lines without || return 1. Both are the last statement in their test, so they did determine the result — but the repo's rule is unconditional for good reason, and I shouldn't ship the exact class of defect this PR spends its tests arguing against. Hardened.

Worth naming the process error rather than just the two bugs: I reported a green suite from a run that predated the code it was testing. That run didn't capture its exit code in a form I actually checked — I read a tail showing the last test passing and treated it as the suite passing. Those are not the same thing, which is the entire reason the exit code exists.

Two failures from the full suite, both introduced by the previous commit
and both missed because I reported a green run that had finished BEFORE
the last round of test edits.
scripts/manifest.sha256 carries the checksums of the installer libs this
branch edits, and it is the installer's own integrity check over the files
it downloads. Merging it stale would have broken verification at install
time on every platform — a worse bug than the one this branch fixes.
Regenerated; gen-manifest.sh --check passes.
bats-hygiene flagged two `! grep -qF "LAUNCHED" <<<"$output"` lines with no
`|| return 1`. Both happened to be the last statement in their test, so
they did decide the result — but the rule is unconditional for a reason,
and an advisory assertion is precisely the defect class this branch's tests
exist to argue against.
The process error is the one worth recording: I read a tail showing the
last test passing and called the suite green. The suite's exit code says
whether the suite passed; the last line says what ran last. Those are not
the same claim, which is why the exit code exists.
…down
The caller-location test passed on macOS bats 1.13 and failed on CI. The
needle was the literal "common.bats", but bats executes a PREPROCESSED copy
of the file whose name varies by version — and rewrites those paths back in
its OWN output, so the failure message shows a path the log does not
contain. That display cost an hour locally before CI cost another round.
The expectation now comes from the same mechanism under test: whatever bash
calls this file (BASH_SOURCE[0] at test scope) is exactly what error() must
record as BASH_SOURCE[1] when called from a function defined here. True on
every platform and every bats version, because both sides read the same
thing rather than agreeing with a string I typed.
Still mutation-proven: pointing error() back at BASH_SOURCE[0] reddens it.
Manifest regenerated for the test-only change — gen-manifest --check passes.
client#736 (SS3 escape stripping) landed on develop and also touches
common.sh. The code merged cleanly — both its escape handling and this
branch's error() recording are present.
scripts/manifest.sha256 conflicted, as it must: both branches regenerate
the checksums of the libs they edit. It is a generated file, so it was
regenerated over the merged tree rather than hand-resolved — hand-picking
lines there would produce a manifest that matches neither branch's actual
bytes, and the installer verifies itself against it.
Comment threadscripts/lib/setup-macos.sh
…exists for
Bugbot, High, #741. _wait_for_docker and _try_start_docker_desktop called a
bare `docker info`, which does not return against a WEDGED daemon — only a
stopped one.
Wedged is precisely the state that reaches them. _assess_runtime_down
classifies runtime-down from _bounded's 124, i.e. from the bounded probe
having ALREADY timed out; the new assess-time path then re-entered an
unbounded probe. So it hung on the one input that routes to it, and the
operator saw "starting it" followed by nothing, forever.
The shape of that is worth naming: this branch exists because a message
promised an action the code never took. Unbounded, it promised the action
and then froze — a worse version of the same defect.
_docker_answers (common.sh, beside _bounded) is now the single probe every
"is the runtime up?" check routes through, defaulting to the same 10s as
TB_ASSESS_DOCKER_TIMEOUT: both answer the same question about the same
daemon and must not disagree. Wired into all four probes on this path,
including install_docker_desktop's two, which bracket the loop extracted
here and hang identically.
_wait_for_docker also moves from a poll COUNT to a wall-clock deadline. Now
that each probe is bounded, counting iterations would make "20 polls" mean
60s against a live daemon and ~260s against the wedged one this exists to
survive. $SECONDS keeps the caller's polls*3s budget true either way.
Three tests, four mutations, each anchor confirmed to apply (7 passing ->
6, 6, 4, 6). The fourth sets the bound to 0: still routed through _bounded,
bound disabled — covered-looking and not covered.
Two things the fix surfaced, both mine:
- The first test asserted on a stub's stdout, but _docker_answers redirects
its whole call to /dev/null, so the assertion was invisible either way.
It passed. Now a marker file.
- Bounding the probe runs it through timeout(1) as an EXTERNAL process, so
a `docker() { … }` shell-function stub no longer intercepts — that broke
a passing test. Worse, _bounded runs bare when neither timeout nor
gtimeout exists, so such a stub works on some hosts and not others and
the test's meaning depends on the machine. Stubs now sit at
_docker_answers, the seam present everywhere.
16 more unbounded probes remain across preflight.sh, setup-linux.sh,
diagnose.sh and setup-macos.sh's other branches, plus probe.sh hand-rolling
its own copy of _bounded. Filed as #744 with a proposed check-style guard,
rather than widened into this PR.
Full suite green; manifest regenerated.
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

Fixed in 616e967 — correct finding, and a pointed one.

Why it mattered more than it looks:_assess_runtime_down classifies runtime-down from _bounded's 124 — i.e. from the bounded probe having already timed out on a wedged daemon. The new path then re-entered an unboundeddocker info. So it hung on exactly the input that routes to it. This branch exists because a message promised an action the code never took; unbounded, it promised the action and then froze.

Fix: one _docker_answers helper beside _bounded, defaulting to the same 10s as TB_ASSESS_DOCKER_TIMEOUT (same question, same daemon, should not disagree). Wired into all four probes on this path, including install_docker_desktop's two that bracket the extracted loop.

_wait_for_docker also moved from a poll count to a wall-clock deadline: with each probe bounded, counting iterations would make "20 polls" mean 60s against a live daemon and ~260s against the wedged one it exists to survive. $SECONDS keeps the caller's budget true either way.

Three tests, four mutations, each anchor confirmed to apply (7 passing → 6, 6, 4, 6). The fourth sets the bound to 0 — still routed through _bounded, bound disabled: covered-looking and not covered.

Two flaws the fix exposed in my own tests, worth recording:

  • The first assertion read a stub's stdout, but _docker_answers redirects its whole call to /dev/null — invisible either way, and it passed regardless. Now a marker file.
  • A bounded probe runs through timeout(1) as an external process, so a docker() shell-function stub stops intercepting; that broke a previously-passing test. And _bounded runs the command bare when neither timeout nor gtimeout is installed, so such a stub works on some hosts and not others — a test whose meaning depends on the machine. Stubs now sit at _docker_answers, the seam present everywhere.

Scope: 16 more unbounded probes remain (preflight.sh, setup-linux.sh, diagnose.sh, and setup-macos.sh's other branches), plus probe.sh hand-rolling its own three-branch copy of _bounded. Filed as #744 with a proposed check-style.sh guard — since Bugbot carries a learned rule for this, it recurs, and the org standard says a recurring review finding becomes tooling rather than a repeated comment.

Full suite green, manifest regenerated.

Comment threadscripts/lib/setup-macos.sh
No code change. GitHub's 2026-08-17 incident (Actions in major outage from
13:40 UTC) dropped the push event for 616e967: the remote is at that sha and
`actions/runs?head_sha=616e967…` reports total_count 0, while unrelated
workflows on other branches ran normally half an hour ago. So it is that one
event, not the platform.
An empty commit rather than the alternatives, deliberately:
* `--amend` + force-push would work but force-pushing needs an explicit
human instruction, so it is not mine to do.
* closing and reopening the PR would fire `pull_request`, but closing a PR
is also not mine to do on a nudge.
* `workflow_dispatch` exists on standard-checks and installer-tests but NOT
on version-bump-gate-caller — and `version-bump-gate / version-check` is
one of develop's eight required contexts, so dispatching would leave the
PR permanently short of a required check while looking busy.
A commit is the only nudge that reaches all eight.
Comment threadscripts/lib/assess.sh
#738 and #739 landed; #738 also touches common.sh. Code merged cleanly and
all three of this branch's changes are intact (_docker_answers,
_assess_handle_runtime_down, error()'s BASH_SOURCE[1] record).
manifest.sha256 conflicted again, as it must when two branches edit libs.
Regenerated over the merged tree rather than hand-resolved.
This merge is also the fix for the missing CI, and my earlier diagnosis of
that was wrong. I blamed a dropped push event from the 08-17 outage. The
evidence says otherwise: another PR on this repo got a full run set at 08:03
UTC, well after my 07:40 push. What my branch had that it did not was
mergeable_state DIRTY — and `pull_request` workflows run against the merge
ref, which GitHub cannot create for a conflicted PR, so no runs fire at all.
Not a dropped event, and not something a force-push or a PR reopen would
have fixed. The empty commit 72c529e was therefore a no-op against the real
cause; it stays as history rather than being amended away, since amending
means force-pushing.

@cursorcursorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit e872350. Configure here.

Comment threadscripts/lib/assess.sh
Comment threadscripts/tests/assess.bats
… level down
── the regression this branch introduced ──
_wait_for_docker returns non-zero on timeout, and install_docker_desktop
called it as a BARE statement under `set -e`. So the script exited there —
before the whale-icon guidance and the deliberate error() that exist
precisely for "Docker didn't come up". A silent death replacing a helpful
message, in a branch about messages that lie.
The old inline loop ended on printf/tput and so always fell through;
extracting it moved the timeout's status somewhere errexit could see. `||
true` now guards it, and the `if ! _docker_answers` below remains the
verdict. Guarded by a source-level assertion: driving install_docker_desktop
end to end would need brew, hdiutil and a DMG.
── the same defect, one layer down ──
On a Colima-only or headless Mac, _assess_handle_runtime_down announced
"starting it" and only then found there was no Docker Desktop to start —
and the failure copy went on to tell someone WITHOUT Docker Desktop to open
Docker Desktop. _docker_app_installed is now part of the condition, not just
of the nudge.
The off-macOS branch said "start it, then re-run this installer" and then
returned 0 and carried on into the privileged flow: guidance contradicting
the next thing on screen. It now names its continue, as the macOS failure
branch already did.
Both are this branch's own bug reproduced inside the fix for it. That is
worth stating plainly rather than filing under "review feedback": extracting
and re-routing a path is exactly where message-and-behaviour drift gets
reintroduced.
── the host-dependent test ──
CI failed on "won't launch an app that isn't installed": it stubbed
`docker()`, but a bounded probe runs through timeout(1) as an external
process, so on the ubuntu runner — which has a live daemon — the real probe
answered and the function returned 0 at "already up" without reaching the
branch under test. It passed here only because macOS ships no timeout(1), so
_bounded fell through to the bare call and the stub worked.
The fix is this file's existing convention, `_bounded() { shift; "$@"; }`,
which the _assess_runtime_down tests have used all along. I documented this
exact trap in #744 and in the previous commit message, then shipped it.
Verified the other way round too: local Docker is running here, so the test
now passes under the same condition that failed on CI.
Mutations, each anchor confirmed to apply (13 passing -> 12 each):
bare _wait_for_docker under set -e -> 1 fail
announce without checking the app exists -> 1 fail
drop the off-macOS "Continuing" line -> 1 fail
Full suite 1141 passed, 0 failed. Manifest regenerated, style guard clean.
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

All four addressed in 8096970. Three were real, and worth naming rather than filing under "review feedback".

The regression this branch introduced

_wait_for_docker returns non-zero on timeout, and install_docker_desktop called it as a bare statement under set -e — so the script exited there, before the whale-icon guidance and the deliberate error() that exist precisely for "Docker didn't come up". A silent death replacing a helpful message, in a branch about messages that lie.

The old inline loop ended on printf/tput and always fell through; extracting it moved the timeout's status somewhere errexit could see. || true now guards it, with the if ! _docker_answers below remaining the verdict. Asserted by a source-level guard — driving install_docker_desktop end to end would need brew, hdiutil and a DMG.

The same defect, one layer down

Both of the assess findings are this branch's own bug reproduced inside the fix for it:

  • On a Colima-only or headless Mac, _assess_handle_runtime_down announced "starting it" and only then discovered there was no Docker Desktop to start — and the failure copy went on to tell someone without Docker Desktop to open Docker Desktop. _docker_app_installed is now part of the condition, not just of the nudge.
  • The off-macOS branch said "start it, then re-run this installer", then returned 0 and carried on into the privileged flow — guidance contradicting the next thing on screen. It now names its continue, as the macOS failure branch already did.

Extracting and re-routing a path is exactly where message-and-behaviour drift gets reintroduced. Good catches.

The host-dependent test

Correct, and CI had just failed on it independently. It stubbed docker(), but a bounded probe runs through timeout(1) as an external process — so on the ubuntu runner, which has a live daemon, the real probe answered and _try_start_docker_desktop returned 0 at "already up" without reaching the branch under test. It passed locally only because macOS ships no timeout(1), so _bounded fell through to the bare call and the stub worked.

Fixed with this file's existing convention, _bounded() { shift; "$@"; }, which the _assess_runtime_down tests have used all along. Verified the other way round too: Docker is running on the machine I fixed it on, so the test now passes under the same condition that failed on CI.

I had documented this exact trap in #744 and in the preceding commit message, and then shipped it anyway.

Verification

Mutations, each anchor confirmed to apply (13 passing → 12 each):

mutationreddens
bare _wait_for_docker under set -e1
announce without checking the app exists1
drop the off-macOS "Continuing" line1

Full suite 1141 passed, 0 failed. Manifest regenerated, style guard clean.

saadqbal
saadqbal previously approved these changes Aug 18, 2026

@saadqbalsaadqbal left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean — I tried to break it and could not. Verified independently on the branch rather than taking the report: shasum -a 256 -c scripts/manifest.sha256 passes, assess.bats + common.bats are 157/157 both with and without a timeout(1) on PATH (the host-dependence that bit you twice — it is genuinely gone now that the stubs sit at _docker_answers/_bounded), shellcheck --severity=error and check-style are clean, and every new path behaves identically under bash 3.2. The error() fix does exactly what the table claims: seeding TB_ERR_* with a benign sudo -n true and then refusing gives FAILED at <caller>:8 exit 1 cmd=error: Could not obtain administrator privileges — caller frame, right code, benign probe displaced.

I also checked the two places the extraction could plausibly still bite and they are fine: cmd && return 0 in _try_start_docker_desktop does not trip errexit (the left operand of an AND list is exempt), and _wait_for_docker is ERR-trap-safe in both callers because one is behind || and the other is inside an if.

Two stale-comment notes inline. Neither blocks.

Comment threadscripts/lib/assess.sh
Comment threadscripts/lib/common.sh
Both are the defect this branch exists to remove — a statement that no
longer describes the behaviour — so they belong in the same PR rather than
a follow-up.
── assess.sh says STRICTLY NON-MUTATING, and this branch mutates ──
`open -a Docker` plus a 60s wait is a mutation by the header's own
definition, and the header still forbade it. The risk is not the stale
sentence, it is which way the next reader resolves the contradiction: revert
the nudge as an obvious breach, or read the ban as lifted and start the k3d
cluster from `cluster-stopped` — the one mutation the header exists to
forbid, and the one _assess_cluster_servers_running explains is off-limits.
Restated as "non-mutating, with exactly ONE narrow exception", with the four
properties that make this one admissible written down so the boundary is
checkable rather than a matter of taste: unprivileged (a GUI launch, no
sudo), idempotent (launching a running Docker is a no-op), bounded (a
wall-clock deadline, every probe through _docker_answers), and load-bearing
(without it a stopped-but-installed Docker takes Tier 2 and demands a
password to start a runtime that is already installed). Plus the explicit
non-consequence: this does not lift the ban, cluster-stopped still only
prints.
── TB_ERR_CMD promises UNEXPANDED text, and error() stores interpolated ──
The declaration said BASH_COMMAND yields `cmd "$VAR"` and never the value,
"so this cannot leak a credential into the log". error() now stores
"error: $*" — interpolated — and it reaches LOG_FILE twice, via _record_err
and via install_cleanup's `FAILED at … command:` line.
Nothing leaks today; every interpolating error() call carries paths, sizes,
versions and arch names. But that comment is exactly what a future author
would rely on when writing an error message, and it was false for the
error() path. It now documents both producers and their different
guarantees, and carries the rule forward: never interpolate a credential
into an error() message — name the path, not the value.
Considered making that rule a machine check, since a load-bearing claim
should be one. Rejected, deliberately: the only name-shaped hit today is
`error "…($cred_file)"`, a PATH — the safe pattern the comment recommends.
A name-based guard would fire solely on that false positive, and silencing
it needs a hand-maintained allowlist, which is the restate-the-rule defect.
All noise, no signal.
Comments only; no behaviour change. Full suite 1141 passed, 0 failed;
manifest regenerated, style guard clean.
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

Thanks for going at it independently rather than reading the report back — the bash 3.2 pass and the with/without-timeout(1) run are the two things I could not have checked convincingly from my own machine, and the cmd && return 0 / _wait_for_docker errexit analysis is the part I would have most liked a second pair of eyes on.

Both notes fixed in e8cfbcb. Neither is a nit as far as I am concerned — they are the exact defect this branch exists to remove, a statement that no longer describes the behaviour, so they belong in this PR rather than a follow-up.

assess.sh's non-mutating header

You are right that the danger is not the stale sentence but which way the next reader resolves the contradiction — and "reads the ban as lifted and starts the k3d cluster from cluster-stopped" is the failure I would not have predicted and is plainly the worse of the two.

Restated as non-mutating with exactly ONE narrow exception, and I wrote down the four properties that make this one admissible so the boundary is checkable rather than a matter of taste:

  • unprivileged — a GUI app launch as the current user, no sudo, nothing mistakable for the privileged path
  • idempotent — launching a running Docker Desktop is a no-op, so a re-run cannot compound
  • bounded — a wall-clock deadline, every liveness probe through _docker_answers
  • load-bearing — without it a stopped-but-installed Docker classifies Tier 2 and demands a password to start a runtime that is already installed

Plus the explicit non-consequence you asked for: this does not lift the ban, cluster-stopped still only prints, and a second exception is the moment to ask whether this file is still the right home.

TB_ERR_CMD

Also right, and sharper than a nit — the comment does not merely go stale, it becomes a false safety guarantee at precisely the moment someone is writing a new error message. It now documents both producers and their different guarantees (ERR trap → BASH_COMMAND, unexpanded; error()"error: $*", interpolated, because a deliberate refusal fires no ERR trap), notes that it reaches LOG_FILE twice, and carries the rule forward: never interpolate a credential into an error() message — name the path, not the value.

I considered making that a machine check, since the standard says a load-bearing claim should be one. Rejected, and worth saying why rather than leaving it silent: the only name-shaped hit across all 58 interpolating error() calls is error "…($cred_file)" — a path, i.e. the safe pattern the comment recommends. A name-based guard would fire solely on that false positive, and silencing it needs a hand-maintained allowlist — the restate-the-rule defect. All noise, no signal. If you would rather have it anyway, say so and I will file it.

Comments only, no behaviour change. Full suite 1141 passed, 0 failed; manifest regenerated, style guard clean.

@saadqbalsaadqbal left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both notes addressed, and better than I asked for. Carving the exception out by its four properties — unprivileged, idempotent, bounded, load-bearing — and then explicitly re-stating that cluster-stopped is still banned is the version that survives the next reader; “if you are adding a second exception, question whether this file is still the right place” is the right guard rail. Same for TB_ERR_CMD: naming the two producers and turning the lost guarantee into a rule (never interpolate a credential, say “the credential file at $path”) is more useful than the blanket claim it replaces.

Re-verified at e8cfbcb: the diff is comment-only, manifest -c passes, assess.bats + common.bats 157/157, shellcheck --severity=error and check-style clean.

One cosmetic slip from the edit itself, inline. Not blocking.

Comment threadscripts/lib/assess.sh
@saadqbal

Copy link
Copy Markdown
Contributor

Agreed on both, and your reasoning on the guard is right as far as it goes — I checked it rather than taking it: 59 interpolating error() calls, and the only credential-shaped name across all of them is $cred_file, a path. A bare name-match guard would have exactly one hit and it would be a false positive, so declining it was the right call.

One shape does answer the allowlist objection though, if you want it: exempt the safe pattern in the rule rather than in a list — match credential-ish names but drop anything ending _file/_path/_dir. That is what your comment already tells people to write, so it is a rule, not a hand-maintained set.

grep -rinE '(^|[^_[:alnum:]])error "[^"]*\$\{?[a-z_]*(cred|token|passw|secret|key|auth)[a-z0-9_]*' scripts/ \
| grep -viE '\$\{?[a-z_][a-z0-9_]*_(file|path|dir)\b'

Zero hits on the tree at e8cfbcb, and it fires on error "…$TRACEBLOC_CLIENT_PASSWORD…". Worth a follow-up ticket if you agree; genuinely fine either way and it should not hold this PR. Approved as it stands — the only thing left is the orphaned sentence on assess.sh:36.

@LukasWodka
LukasWodka merged commit 598ccb6 into developAug 19, 2026
94 of 99 checks passed
@LukasWodka
LukasWodka deleted the fix/runtime-down-start-docker branch August 19, 2026 10:23
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

/fr-pass

Advanced at @LukasWodka's explicit direction. Held in the automated pass for a verification gap (interactive/TTY, GPU-only, or journey-dependent while e2e journey is red — backend#2206); Lukas is accepting that gap for this card.

aptracebloc added a commit that referenced this pull request Aug 26, 2026
Resolves the manifest.sha256 + BUGBOT.md conflicts from develop (#832/#842),
and folds in both CHANGES_REQUESTED findings:
- HIGH: `_bounded … sudo docker info` execs the real `sudo` binary via `timeout`,
bypassing common.sh's root-aware `sudo()` shadow — on the root-run --prepare-host
path (RFC 0001, often no sudo binary) a live daemon read as dead. New `_bounded_root`
(id-0 → bare, else real sudo) at the two prepare-host probes; unit-tested in common.bats.
- MEDIUM: rule 5 missed `docker info # comment` (no redirect) — added `#` to the
follow-set; new bare-comment fixture reddens it. Discriminator kept (whole-line
comments still filtered; no exempt mention has `#` as its next char).
- Exempt _docker_answers_bounded's background-PID probe (common.sh) — it is bounded by
spin's deadline, not lexically by _bounded, so rule 5 can't see it (style-guard: allow).
- Harmonize BUGBOT.md: keep develop's macOS-coreutils-trap rule, add the rule-5 gate,
the #741 test trap, and the _bounded_root/sudo-shadow lesson.
- Clean setup-linux.bats id() mocks to answer `id -u` numerically (else _bounded_root's
root check errored into the else-branch — passing for the wrong reason).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
aptracebloc added a commit that referenced this pull request Aug 26, 2026
…the next (#744) (#844)
* fix(installer): bound every remaining 'docker info' probe, and guard the next (#744)
A bare `docker info` never returns against a WEDGED daemon — the exact state
that lands a machine in the probes that check for it. #741 bounded the four on
the assess-time path; this bounds the 16 remaining in scripts/lib/ and stops the
class recurring.
- Route every `docker info` through the common.sh seam: `_docker_answers`
(bounded yes/no) for the liveness checks (setup-macos ×4, setup-linux ×1,
preflight ×3), `_bounded` for the probes that read a value (preflight ×3,
setup-linux ×2 incl. the two `sudo docker info`, diagnose ×1).
- probe.sh: `_probe_runtime_usable` calls `_bounded` instead of hand-rolling the
same timeout/gtimeout/bare fallback a second time (keeps its 5s cap).
- check-style.sh rule 5: fail CI on any unbounded `docker info` under
scripts/lib/, encoding Bugbot's learned rule as a gate (mutation-proven both
ways in check-style.bats). .cursor/BUGBOT.md records it.
- Tests: preflight/diagnose setups shadow timeout/gtimeout so the bounded
probe's external `timeout` can't bypass the docker() mock (the #741 trap).
- Regenerate scripts/manifest.sha256 (bootstrap integrity surface over the five
edited libs).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(installer): bound the macOS-reachable docker probes coreutils-free (#744)
Bugbot High on abef442: setup-macos's 4 probes and preflight's runtime readers
called `_docker_answers`, which bounds through `_bounded` — a no-op on a stock Mac
(no timeout/gtimeout), so a wedged Docker Desktop still hung there, defeating the
fix on the platform it targets. Route them through `_docker_answers_bounded`
(background-PID + spin deadline, coreutils-free, per backend#2521):
- setup-macos.sh ×4 (_kill_lingering_docker, the two Colima liveness checks, the
headless-Mac branch) — visible spinner, matching the colima-stop recovery site.
- preflight.sh ×3 (_pf_runtime_mem_kb / _pf_runtime_ncpu / _pf_docker_root) — the
liveness guard is silenced (>/dev/null): these readers' stdout is captured, and a
passing bounded guard proves the daemon responsive so the --format read can't hang.
Left as-is (not flagged, and both need output-capture a yes/no probe can't give):
diagnose's bundle read and probe.sh's Tier-0 check — the latter unchanged from its
pre-existing timeout/gtimeout form.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(installer): bound the --diagnose docker-info read coreutils-free (#744)
Bugbot High on 7aee2fe: the support-bundle `docker info` still went through
`_bounded`, a no-op bound on a stock Mac — so `--diagnose` (a Darwin-reachable
path) hung forever on a wedged daemon, from a machine already broken. Gate it
behind the same coreutils-free `_docker_answers_bounded` liveness probe preflight
uses (silenced with >/dev/null so its spinner stays out of the bundle file); once
the daemon is proven responsive the piped read can't hang.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(installer): bound the host-audit runtime probe coreutils-free (#744)
Bugbot High on afeb9ec: `_probe_runtime_usable` routed `docker info` through
`_bounded`, a no-op bound on a stock Mac — and host_audit runs run_host_probes on
EVERY install (macOS included), so this is the FIRST thing to freeze at "Checking
your machine" on a wedged Docker Desktop, before _kill_lingering_docker can run.
The preflight/diagnose gates added earlier make that hang the reachable one.
Route it through `_docker_answers_bounded` (background-PID + spin deadline,
coreutils-free) with the probe's own 5s TB_PROBE_TIMEOUT as the deadline arg —
Bugbot's own note: it takes a seconds argument, so the 5s cap needs no `_bounded`.
Silenced (>/dev/null): the rc is the verdict, the host-audit panel says the rest.
probe.bats: the three `_bounded`/timeout-mechanics tests are rewritten to the new
mechanism — one asserts the 5s cap reaches `_docker_answers_bounded`, one proves the
probe still works with no timeout/gtimeout on PATH, one keeps the never-fatal contract.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

installer: a stopped Docker forces the admin path on macOS, and the installer says it is starting Docker when it isn't

2 participants

@LukasWodka@saadqbal