Make the dev account an ordinary user of the box rather than a tenant on it - #14
Merged
Merged
Conversation
… on it The role assumed the account was a guest on somebody else's machine: it did not own Homebrew, its home was unreadable to the account that did, and sudo was a per-host grant that defaulted off. Every mechanism worth deleting here descended from one of those three, and the visible result was an account that could run brew but not install with it -- `brew install` as the provisioned user failed on an unwritable prefix, and the documented workaround was to install as somebody else. The account now owns the Homebrew prefix, installing it itself and taking over one another account left behind. That is Homebrew's only supported arrangement, and it removes the shared-group alternative along with the setgid pass on directories, the group-write pass on files, and the repair pass for the combination of the two -- which mattered, because group-write reached the login shell of an account holding sudo, so membership was equivalent to root and the comment saying so was the argument against the feature. Taking ownership copies nothing: the cost the sharing existed to avoid was 3.6G on disk, and a chown moves no bytes at all. With one owner there is nobody to hand a file to, so the Brewfile is read out of the account's own clone and the root-owned staging directory is gone. The home stays at 0750, which is HOME_MODE on this distribution and now simply the default for a normal user rather than something the role works around. The login shell moves to the distribution's zsh. It is the same release as Homebrew's, and the only thing the Homebrew one bought was matching the cloud image's own account -- which stopped being a consideration when this became the account you log into. Naming a shell that exists before Homebrew does is what lets the account be created first and then install Homebrew as itself, which is what removes the connection-account discovery and the refusal to install over a root connection. Passwordless sudo stops being a flag. The account is created with a locked password, so password sudo could never have worked: the choice was never which kind of sudo but whether the account had any. It is also a precondition rather than a preference, because the Homebrew installer probes `sudo -n` before it will create anything under /home. Two defects fixed on the way, both of which made a stock cloud image fail where a CI runner passed. install.sh generates settings.json through claude/build-settings, which requires jq; jq comes from the Brewfile, so the Brewfile now installs before the home is configured rather than after it. That alone was not enough -- `command` runs no login shell, so the Homebrew prefix was never on PATH and the jq just installed was still not found. The installer task names the prefix explicitly. A GitHub runner ships apt's jq on the default PATH, which is why neither showed up in CI; both were found by running the role in a bare ubuntu:24.04 container. CI stops pinning the login shell to /bin/bash, so it exercises the real one, and asserts that the account ends up owning the prefix and can install a formula with it -- the operation that ownership actually gates, and the one that did not work before. 33 tasks where there were 43, and five variables gone. Claude-Session: https://claude.ai/code/session_01XBpcNj3TG2iV63QjJ1P4SD
A step runs in $GITHUB_WORKSPACE, which is under the runner's own home at 0750, and brew refuses to start when it cannot stat its working directory. citest is not the runner, so it cannot -- the check failed on the cwd rather than on anything it was testing. The ownership assertion above it passed, which is the part that had never been exercised on a real runner. Claude-Session: https://claude.ai/code/session_01XBpcNj3TG2iV63QjJ1P4SD
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The role assumed the provisioned account was a guest on somebody else's machine: it did not own Homebrew, its home was unreadable to the account that did, and sudo was a per-host grant that defaulted off. Every mechanism deleted here descends from one of those three. The visible symptom was an account that could run brew but not install with it —
brew installfailed on an unwritable prefix, and the documented workaround was to install as somebody else.The account owns Homebrew
It installs the prefix itself, and takes over one another account left behind. That is Homebrew's only supported arrangement, and it removes the shared-group alternative: the setgid pass on directories, the group-write pass on files, and the repair pass for the combination of the two. That last one mattered — group-write reached the login shell of an account holding sudo, so brew-group membership was equivalent to root, and the comment saying so was really the argument against the feature.
Taking ownership copies nothing. The cost the sharing existed to avoid was 3.6G on disk; a chown moves no bytes. Measured at 0s over a 206M prefix.
With one owner there is nobody to hand a file to, so the Brewfile is read from the account's own clone and the root-owned staging directory is gone. The home stays 0750 — that is
HOME_MODEon this distribution, so it is now simply what every normal user gets rather than something the role arranges and then works around.The login shell moves to the distribution's zsh
Same release as Homebrew's (5.9). The only thing the Homebrew one bought was matching the cloud image's own account, which stopped being a consideration when this became the account you log into. Naming a shell that exists before Homebrew is what lets the account be created first and then install Homebrew as itself — which is what removes the connection-account discovery task and the refusal to install over a root connection.
Passwordless sudo stops being a flag
The account is created with a locked password, so password sudo could never have worked; the choice was never which kind of sudo but whether the account had any. It is also a precondition rather than a preference: the Homebrew installer probes
sudo -nbefore it will create anything under/home.Two defects fixed on the way
Both made a stock cloud image fail where a CI runner passed.
install.shgeneratessettings.jsonthroughclaude/build-settings, which requiresjq.jqcomes from the Brewfile, so the Brewfile now installs before the home is configured rather than after it. That alone was not enough —commandruns no login shell, so the Homebrew prefix was never onPATHand thejqjust installed still was not found. The installer task now names the prefix explicitly.A GitHub runner ships apt's
jqon the defaultPATH, which is why neither showed up in CI. Both were found by running the role in a bareubuntu:24.04container.Verification
Run in containers, not just linted:
brew installworks as the accountinstall.shtake_ownership: falseagainst a foreign prefixchanged=0 failed=0ansible-lintproductionprofileCI
Stops pinning the login shell to
/bin/bash, so it exercises the real one, and asserts the account ends up owning the prefix and can install a formula with it — the operation ownership actually gates, and the one that did not work before. Thejq/PATHblind spot is documented in the workflow rather than left implicit.33 tasks where there were 43;
dev_user_brew_share_mode,dev_user_brew_group,dev_user_brew_owner,dev_user_brew_stage_diranddev_user_sudo_nopasswdare gone.The one real trade-off
Whoever owned the prefix before can still run every binary in it, but can no longer
brew install. On a box where another account needs to install its own packages, this is the wrong role —dev_user_brew_take_ownership: falsemakes it refuse rather than degrade silently.https://claude.ai/code/session_01XBpcNj3TG2iV63QjJ1P4SD