Skip to content

feat(install): add auto-selecting curl|sh installer + streamline README - #387

Open
mogul wants to merge 1 commit into
mainfrom
docs/npm-install-fresh-macos
Open

feat(install): add auto-selecting curl|sh installer + streamline README#387
mogul wants to merge 1 commit into
mainfrom
docs/npm-install-fresh-macos

Conversation

@mogul

@mogul mogul commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Ready for review. Rebased on current main; the ADR is numbered (0026, accepted) and the npm path is now wired. Please still weigh in on the ADR design, not just the code.

Context

Onboarding surfaced two recurring pains for non-technical users (see the ADR's "Observed evidence" section):

  • The ./acq-from-the-wrong-folder trap — running ./acq from a project/home dir yields "no such file", with no obvious recovery. Multiple users hit it.
  • No gentle "just install it" front door — Homebrew needs admin (managed GFE accounts often lack it), and npm/brew dead-end on a bare Mac.

This PR gives acq a real install path that puts it on PATH.

What's here

  • install.sh — one hardened curl | sh front door that auto-selects the best method already on the host: Homebrew → npm → managed git clone.
    • npm branch is FUNCTIONAL (wired via package.json bin/files; installs place acq on PATH and the launcher resolves its acq.backends/ tree through the npm symlink).
    • brew branch is STUBBED (detects brew, explains the tap isn't published yet, falls back to clone) pending a published Homebrew tap (deferred below).
    • clone fallback is fully functional: shallow clone → ~/.local/share/acq, symlink → ~/.local/bin/acq, idempotent, no sudo. Fails closed if a requested --ref can't be checked out, cleaning up any partial clone.
    • Optional commit-SHA pinning (--sha / ACQ_INSTALL_SHA): checks out a full 40-char commit and verifies HEAD matches it, failing closed (and cleaning up) on a missing/mismatched SHA — git objects are content-addressed, so a matching SHA is itself an integrity check.
    • Proactive Command Line Tools setup: probes git by running it, triggers xcode-select --install (no admin), guides the user to the dialog (incl. "look in your Dock"), and waits until git is usable (5s poll, 30-min cap → actionable timeout).
    • PATH is never changed without consent; declining prints the exact line.
    • Flags: --method brew|npm|clone, --ref, --sha, --no-msb, --dry-run, --yes/-y.
  • release-please-config.json — an extra-files ($.version) entry so release-please bumps package.json's version on release; package.json now carries the real manifest version (2.0.0) instead of a placeholder.
  • README — rewritten to a 3-step quickstart (open terminal → install → run) for a non-technical audience; prerequisites/manual-clone/tagged-release/brew/npm moved into <details>; new troubleshooting for the wrong-folder trap and CLT/Dock.
  • ADR (docs/adr/0026-installation-and-distribution.md, status: accepted) — the design rationale, incl. why the hash-confirmed tarball was rejected (git/CLT is needed at runtime anyway; a full commit-SHA checkout is itself content-addressed integrity), and a carve-out noting the msb leg is an unpinned upstream curl | sh outside our control.

Deferred (to be filed as issues after ADR sign-off)

  • Create GSA-TTS/homebrew-tap + acq formula → un-stub brew branch. (External repo — cannot be created from this repo.)
  • Release automation to publish SHA256SUMS and a canonical commit SHA per release; then swap README URL main → pinned tag, and default the installer to that SHA (the --sha mechanism is already in place).

Verification transcript

Run in the acq sandbox (Linux; macOS-specific CLT path exercised via stubs + --dry-run):

Check Result
shellcheck --severity=warning install.sh clean
markdownlint-cli2 (our files) 0 issues
scripts/test-acq-bats (bats-core, per ADR-0025) 368 passed, 0 failed
gitleaks protect --staged no leaks
npm pack --dry-run ships exactly acq + acq.backends/ (7) + package.json/README/LICENSE (11 files)
npm install -g (local) places acq, launcher resolves through symlink
auto-select (host has npm, no brew) picks npm
--method clone --yes (real, local repo) launcher linked, PATH consent line added, idempotent
bad --ref (clone) fails closed (exit 1), partial clone removed
--sha <matching> (real, local repo) checks out + prints "verified HEAD matches pinned commit"
--sha <well-formed but absent> fails closed (exit 1), partial clone removed
malformed --sha / --sha with --method npm rejected at parse with clear error
release-please version sync npm pack reports manifest version 2.0.0 (no placeholder)
invalid --method bogus exits 1 with clear error
no-usable-git on "Darwin" (stubbed, dry-run) shows CLT trigger step, no hang
non-macOS no-git fails closed with guidance

Rollback

Revert this branch/PR. install.sh is additive and the README/ADR are docs; nothing in acq's runtime dispatch changed.

Security impact

No auth/authz changes. Installer uses no sudo, writes only under \$HOME, gates PATH edits and msb install on consent, fails closed on an unresolvable --ref, and (once releases publish it) will verify integrity via a pinned commit SHA. The msb leg delegates to msb's own upstream installer, which is unpinned and outside our control (documented in the ADR).

AI-assisted (OpenCode). Human owner: @mogul.

Testing this PR before merge

The README one-liner points at main, which won't have install.sh until this merges. To try it now, run it from a checkout of this branch. Safe (non-destructive) checks that never touch your real ~/.local or PATH:

git fetch origin docs/npm-install-fresh-macos
git checkout docs/npm-install-fresh-macos

# 1. See what it would do — makes no changes, no network mutations:
sh install.sh --help
sh install.sh --dry-run
sh install.sh --method clone --dry-run
sh install.sh --method npm   --dry-run

# 2. Real clone install into a throwaway HOME (does NOT touch your real dotfiles),
#    using this local checkout as the source so it's offline:
TMP=$(mktemp -d)
HOME="$TMP" ACQ_INSTALL_REPO_URL="$PWD" ACQ_INSTALL_REF=docs/npm-install-fresh-macos \
  sh install.sh --method clone --no-msb --yes
ls -l "$TMP/.local/bin/acq"                    # launcher symlink
git -C "$TMP/.local/share/acq" rev-parse HEAD  # pinned to the ref
rm -rf "$TMP"

# 3. Fail-closed check: a bad --ref must exit non-zero and leave nothing behind:
TMP=$(mktemp -d)
HOME="$TMP" ACQ_INSTALL_REPO_URL="$PWD" ACQ_INSTALL_REF=does-not-exist \
  sh install.sh --method clone --no-msb --yes ; echo "exit=$?"   # expect exit=1
ls "$TMP/.local/share/acq" 2>&1 || echo "clone absent (good)"
rm -rf "$TMP"

For a real, non-throwaway install on macOS, just run sh install.sh (auto-selects a method, asks before editing PATH or installing msb). On a bare Mac it will trigger the Command Line Tools dialog — accept it (look in your Dock if you don't see the window).

@mogul
mogul force-pushed the docs/npm-install-fresh-macos branch 4 times, most recently from 9c86028 to a160f6c Compare August 27, 2026 02:32
@mogul
mogul marked this pull request as ready for review August 27, 2026 02:35
@mogul
mogul requested a review from a team as a code owner August 27, 2026 02:35
@mogul
mogul force-pushed the docs/npm-install-fresh-macos branch from a160f6c to 48296c9 Compare August 27, 2026 06:46
Add install.sh: a no-sudo, no-admin front door that auto-selects an
install method (brew -> npm -> git clone) so non-technical users need not
choose. The clone fallback is fully functional (shallow clone to
~/.local/share/acq, consent-gated PATH edit, idempotent); the npm path is
wired via package.json bin/files; the brew path is stubbed pending a
published homebrew tap.

Support optional commit-SHA pinning (--sha / ACQ_INSTALL_SHA): the clone
method checks out the pinned commit and verifies HEAD matches it, failing
closed and cleaning up on mismatch. Git objects are content-addressed, so
a matching SHA is itself an integrity check.

Wire release-please (extra-files, $.version) to bump package.json's
version on release so the npm package tracks the release manifest instead
of a hand-maintained placeholder.

Proactively trigger and explain the macOS Command Line Tools install
(the git dependency is unavoidable at acq runtime), noting the pop-up may
hide in the Dock. Fail closed when a requested --ref cannot be checked
out, cleaning up any partial clone.

Streamline the README quickstart to three steps for a non-technical
audience and record the decision in docs/adr/0026-installation-and-distribution.md.

Co-authored-by: OpenCode [claude-opus-4] <bret.mogilefsky@gsa.gov>

@wz-gsa wz-gsa 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.

Adversarial review — one blocking finding, two non-blocking

Blocking: the default install is unpinned, contradicting the script's and ADR's own security claims

install.sh:

# Pin to a release tag by default so `curl | sh` never runs a moving target.
# Override with --ref for testing a branch or specific tag.
REF="${ACQ_INSTALL_REF:-main}"

The comment says "pin to a release tag by default." The code's default is main — a moving target, the opposite of what the comment claims.

This isn't a stale comment on a minor path — it's the headline, README-recommended install method this PR is built around:

curl -fsSL https://raw.githubusercontent.com/GSA-TTS/agentic-coding-quickstart/main/install.sh | sh

Note the URL itself already fetches the script from main; the script then clones the repo at main too (or, on the npm path, npm install -g github:GSA-TTS/agentic-coding-quickstart#mainREF feeds spec unconditionally whenever non-empty, so the same unpinned default reaches all three install methods, not just clone). Nothing in the README's quickstart flow tells a user to pass --ref <tag> or --sha <commit> — those exist, but they're opt-in, not the default.

The ADR states the opposite of what the code does:

"Our own install.sh and the acq clone are pinned; this carve-out applies only to the third-party msb bootstrap."

That's only true if a user overrides REF. As shipped, install.sh's own default is exactly as unpinned as the third-party msb installer it explicitly calls out as the exception — the Control Mapping cites SR-3 (Supply Chain Controls) and SR-11 (Component Authenticity) for a property the default path doesn't provide. release-please-config.json's change doesn't rewrite REF's default at release time either, so there's no mechanism that flips this to a real tag automatically.

Suggested fix: either (a) default REF to a real pinned tag (bump it as part of the release process, the way release-please-config.json's new extra-files entry already keeps package.json's version in sync — the same mechanism could point at this script or a generated constant), or (b) if main is genuinely intended as the default (rolling-release philosophy), fix the comment and the ADR to say so honestly rather than claim a pinning guarantee that isn't there. Either is fine; the current mismatch between claim and code is not.

Non-blocking: ensure_git's Command Line Tools wait has no non-interactive bypass

ensure_git unconditionally runs xcode-select --install and polls for up to 30 minutes when git isn't usable — with no ASSUME_YES/--yes check. --yes is documented as "Intended for non-interactive/CI use," but on a git-less macOS runner this launches a GUI dialog nothing can click and then blocks the job for up to 30 minutes before finally dying with a timeout message, rather than failing fast. Worth gating the wait (or skipping straight to the actionable die message) when ASSUME_YES=1 or stdin isn't a TTY.

Verified as sound (not flagged)

  • The --sha integrity check is real: content-addressed git rev-parse HEAD comparison, rm -rf + fail-closed on mismatch, correctly restricted to the clone method (npm/brew reject --sha).
  • confirm() fails closed (no way to ask → decline) and correctly never edits PATH without consent.
  • git_is_usable probes by running git, not just command -v — correctly catches the macOS shim-without-CLT-installed case.
  • No sudo, no writes outside $HOME, npm PATH-detection via command -v after npm install -g is not a race (POSIX sh doesn't hash between commands the way an interactive login shell does).
  • The Homebrew stub path is honestly labeled and safely falls through to the functional clone method rather than silently no-op'ing.

All 4 CI checks green; this is a review-blocker on the security-claim mismatch, not the implementation quality, which is otherwise careful.

AI-assisted (OpenCode).

Sign up for free to 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.

2 participants