Skip to content

fix(cursor-review): download-then-execute the CLI installer + assert the binary (BE-5646) - #105

Open
mattmillerai wants to merge 4 commits into
mainfrom
matt/be-5646-cursor-review-install-hardening
Open

fix(cursor-review): download-then-execute the CLI installer + assert the binary (BE-5646)#105
mattmillerai wants to merge 4 commits into
mainfrom
matt/be-5646-cursor-review-install-hardening

Conversation

@mattmillerai

Copy link
Copy Markdown
Contributor

ELI-5

The workflow installed the Cursor CLI with curl … | bash. If the download failed, bash still ran (on nothing) and succeeded, so the install step went green — and the run only fell over a step later with cursor-agent: command not found, under an error message blaming the model pins. Now we download the installer to a file first, run it, and then check that the binary actually exists. If either half fails, the install step itself goes red with the real reason.

What changed

All three identical Install Cursor agent CLI steps in .github/workflows/cursor-review.yml (preflight, review matrix, consolidate) now:

  1. curl -fsSL --retry 3 --retry-all-errors --retry-delay 2 … -o /tmp/cursor-install.sh — a failed download is now the step's own exit status, and up to 3 retries ride out a transient 5xx.
  2. bash /tmp/cursor-install.sh — separate command, so its status stands on its own.
  3. An in-step assertion that cursor-agent actually exists, emitting an explicit ::error:: if it does not.

The preflight step keeps its CURSOR_API_KEY skip guard verbatim as its first lines. The three bodies are otherwise byte-identical (verified by parsing the YAML and comparing the step bodies) — GitHub Actions has no YAML anchors and the preflight job has no checkout, so a local composite action is not an option; the triplication matches the rest of the file.

Not done, deliberately: no workflow-level defaults: run: shell: bash was added — it would flip every run step in the file to -eo pipefail, and this workflow deliberately relies on last-command-wins pipelines and && … || … exit-capture idioms elsewhere. The Log Cursor agent version step, the model pins, and every caller repo are untouched. Vendor-outage soft-skip is out of scope: hard-red stays, now correctly attributed to the install step.

Deviation from the ticket — the assertion path (please read)

The ticket prescribed asserting on $HOME/.cursor/bin/cursor-agent. That assertion fires on a successful install, which would have hard-red'd every cursor-review run across every pinned caller repo. The current cursor.com/install script installs to ~/.local/bin:

ln -s ~/.local/share/cursor-agent/versions/<ver>/cursor-agent ~/.local/bin/cursor-agent

There is no .cursor/bin path anywhere in the installer. I ran the real installer against an isolated HOME and confirmed it creates ~/.local/bin/{agent,cursor-agent} and no ~/.cursor directory at all, and a recent successful CI run of this repo's own review panel shows the same in its install log (✓ Detected linux/x64✓ Symlink createdStart using Cursor Agent: agent), followed by cursor-agent --version2026.07.23-e383d2b.

So the assertion here targets the real location, and is written to err toward not failing — it only errors when both the PATH lookup and the explicit path come up empty:

if ! command -v cursor-agent >/dev/null 2>&1 \
  && [ ! -x "$HOME/.local/bin/cursor-agent" ]; then

A false positive on this check would break the review panel in every consumer repo at once, so the lenient direction is the right one.

Related finding, left alone as out of scope: the pre-existing echo "$HOME/.cursor/bin" >> "$GITHUB_PATH" line is vestigial — that directory is never created, and cursor-agent resolves today only because ~/.local/bin is already on the GitHub-hosted runner's default PATH. The ticket says the success path and the PATH line must be unchanged, so I left it (and said so in a code comment). Worth a follow-up to either point it at ~/.local/bin or drop it; today it is a harmless no-op, but it is also the reason the ticket's assertion path looked plausible.

Verification

  • actionlint (with shellcheck integration) clean on the whole repo and on the changed file; shellcheck -s bash clean on the extracted step body.
  • python3 -m unittest discover -s .github/cursor-review/tests — 40 tests, OK (untouched, sanity only). AGENTS.md integrity checker passes (one pre-existing CODEOWNERS warning, unrelated).
  • Three step bodies confirmed byte-identical apart from the preflight guard, by loading the YAML and diffing the parsed run: strings.
  • Success path, executed for real: ran the new step body against the live installer with an isolated HOME whose .local/bin was deliberately not on PATH (so the explicit-path fallback was the branch under test) → exit 0, symlinks created.
  • Failure path, executed for real: pointed the URL at a 500 endpoint → curl: (56) The requested URL returned error: 500 four times (initial + 3 retries), step exits 56. Pointed it at a URL serving a 200 HTML page → bash fails parsing it, step exits 2. Both go red at the install step, which is the whole point.
  • Not run: an end-to-end panel run on a fork PR. The preflight guard is unchanged, so fork behavior is untouched.

Notes

  • /tmp/cursor-install.sh is a predictable path, which would matter on a self-hosted runner; every job in this workflow is runs-on: ubuntu-latest (GitHub-hosted, single-tenant per job), so there is no shared-runner race here.
  • --retry-all-errors needs curl ≥ 7.71; the ubuntu-latest images ship 8.x.
  • Merging this triggers bump-cursor-review-callers.yml, which opens SHA-bump PRs in the pinned caller repos automatically — no caller-side edits in this PR.

…the binary (BE-5646)

`curl https://cursor.com/install -fsSL | bash` exits 0 when curl fails: this
workflow sets no `shell:`/`defaults:`, so run steps get `bash -e {0}` with no
pipefail, and a pipeline's status is its last command's. When the install
endpoint returned HTTP 500 the install step went green and the failure
resurfaced one step later as `cursor-agent: command not found` under a
misleading annotation about the model pins.

Replace all three identical `Install Cursor agent CLI` steps (preflight,
review matrix, consolidate) with download-then-execute plus an in-step binary
assertion, and add `--retry 3 --retry-all-errors --retry-delay 2` so a
transient 5xx is ridden out. GitHub Actions has no YAML anchors and the
preflight job has no checkout (so no local composite action), so the three
copies stay triplicated to match the rest of the file.

The assertion targets `$HOME/.local/bin/cursor-agent`, which is where the
current installer symlinks the binary — verified by running the live installer
against an isolated HOME and by a successful CI run's own install log. The
pre-existing `$HOME/.cursor/bin` GITHUB_PATH line is left untouched (it is
vestigial: `~/.local/bin` is already on the runner PATH, which is why the
version step resolves today).

Success path is unchanged. Failure path now goes red at the install step with
curl's own error or the explicit missing-binary annotation.
@mattmillerai mattmillerai added cursor-review Multi-model cursor review agent-coded Authored by the agent-work loop labels Aug 1, 2026
@mattmillerai
mattmillerai marked this pull request as ready for review August 1, 2026 23:54
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 5 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: fb46cf62-0393-4836-9959-c91bb9305d8b

📥 Commits

Reviewing files that changed from the base of the PR and between bcde90f and afb541f.

📒 Files selected for processing (1)
  • .github/workflows/cursor-review.yml

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot 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 Review — Consolidated panel

Triggered by @mattmillerai.

Found 7 finding(s).

Severity Count
🟠 High 1
🟡 Medium 2
🟢 Low 3
⚪ Nit 1

Panel: 8/8 reviewers contributed findings.

Comment thread .github/workflows/cursor-review.yml Outdated
Comment thread .github/workflows/cursor-review.yml Outdated
Comment thread .github/workflows/cursor-review.yml Outdated
Comment thread .github/workflows/cursor-review.yml Outdated
Comment thread .github/workflows/cursor-review.yml Outdated
Comment thread .github/workflows/cursor-review.yml Outdated
Comment thread .github/workflows/cursor-review.yml
…nstaller fetch (BE-5646)

Review-panel follow-ups on the install hardening, applied to all three
identical `Install Cursor agent CLI` steps:

- Export `$HOME/.local/bin` to `$GITHUB_PATH` instead of the vestigial
  `$HOME/.cursor/bin`. The assertion passes when the binary exists at the
  explicit path but is not on PATH; in that case later steps still died with
  the misleading `cursor-agent: command not found` the change set out to
  remove. `.cursor/bin` is provably dead here — the live installer only ever
  creates `~/.local/bin/{agent,cursor-agent}`, and any run reaching that line
  has already proven cursor-agent is on PATH or in `~/.local/bin`.
- Download to `"${RUNNER_TEMP:-/tmp}"` rather than a fixed world-writable
  `/tmp` path.
- Add `--connect-timeout 10 --max-time 60` so a stalled cursor.com fails at
  the install step instead of hanging each of the 8 matrix cells to the
  job timeout.

actionlint + shellcheck clean; cursor-review script tests 40/40 OK.
@mattmillerai mattmillerai added cursor-review Multi-model cursor review and removed cursor-review Multi-model cursor review labels Aug 2, 2026

@github-actions github-actions Bot 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 Review — Consolidated panel

Triggered by @mattmillerai.

Found 8 finding(s).

Severity Count
🟡 Medium 2
🟢 Low 3
⚪ Nit 3

Panel: 8/8 reviewers contributed findings.

Comment thread .github/workflows/cursor-review.yml Outdated
Comment thread .github/workflows/cursor-review.yml
Comment thread .github/workflows/cursor-review.yml Outdated
Comment thread .github/workflows/cursor-review.yml
Comment thread .github/workflows/cursor-review.yml
Comment thread .github/workflows/cursor-review.yml Outdated
Comment thread .github/workflows/cursor-review.yml Outdated
Comment thread .github/workflows/cursor-review.yml
…temp the download (BE-5646)

Second review-panel round, applied to all three `Install Cursor agent CLI`
steps:

- `timeout 300 bash "$installer"`. The previous timeouts bounded only the
  ~6 KB bootstrap; the bootstrap itself downloads the cursor-agent payload,
  which was unbounded — so a stalled cursor.com could still burn the whole
  job budget and surface as an opaque cancelled job.
- `[ -s "$installer" ]` before executing. A 200 with an empty body made the
  installer a silent no-op, which the post-install assertion could not
  distinguish from a real install when cursor-agent was already on PATH.
- `mktemp` instead of a fixed filename, so nothing can pre-place a symlink at
  a predictable path for `curl -o` to write through.
- `--retry-max-time 120`: `--max-time` is per attempt, so curl had no overall
  bound. Worst case is now ~2 min of curl + 300s of installer, inside the
  tightest job cap (preflight, 10 min).
- `</dev/null` keeps stdin closed as it effectively was under `curl | bash`.
- Corrected the timeout comment, which was copy-pasted from the review job and
  described a "30-minute cap (x8 matrix cells)" in the 10-minute non-matrix
  preflight job and the 15-minute consolidate job.

actionlint + shellcheck clean; cursor-review script tests 40/40 OK. Step body
executed against the live installer with an isolated HOME whose .local/bin was
off PATH (exercising the fallback branch): exit 0, symlinks created,
$GITHUB_PATH written, `cursor-agent --version` -> 2026.07.23-e383d2b. Empty-body
and installer-nonzero paths both exit red at this step with the explicit error.
@mattmillerai mattmillerai added cursor-review Multi-model cursor review and removed cursor-review Multi-model cursor review labels Aug 2, 2026

@github-actions github-actions Bot 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 Review — Consolidated panel

Triggered by @mattmillerai.

Found 10 finding(s).

Severity Count
🟡 Medium 2
🟢 Low 5
⚪ Nit 3

Panel: 8/8 reviewers contributed findings.

Comment thread .github/workflows/cursor-review.yml Outdated
Comment thread .github/workflows/cursor-review.yml
Comment thread .github/workflows/cursor-review.yml
Comment thread .github/workflows/cursor-review.yml
Comment thread .github/workflows/cursor-review.yml Outdated
Comment thread .github/workflows/cursor-review.yml
Comment thread .github/workflows/cursor-review.yml
Comment thread .github/workflows/cursor-review.yml Outdated
Comment thread .github/workflows/cursor-review.yml
Comment thread .github/workflows/cursor-review.yml Outdated
… accurate timeout math (BE-5646)

Third review-panel round, applied to all three `Install Cursor agent CLI`
steps:

- `timeout -k 30 240` instead of `timeout 300`. Plain `timeout` sends only
  SIGTERM, so an installer that traps or ignores it outlives the cap the line
  exists to guarantee; `-k` escalates to SIGKILL.
- The post-install assertion now requires `-f` as well as `-x`. `-x` alone is
  true for a *searchable directory*, so a stray directory at
  `$HOME/.local/bin/cursor-agent` satisfied the fallback and let the step pass
  with no binary. Confirmed both ways against a directory at that path.
- Corrected the timeout arithmetic in the comment: `--retry-max-time` bounds
  only when a new attempt may *start*, so an attempt beginning just under 120s
  still gets its full `--max-time 60` and curl's worst case is ~3 min, not the
  ~2 min claimed. 240s + a 30s grace keeps the total inside preflight's
  10-minute cap.
- The exit-124 message no longer asserts a timeout unconditionally, since an
  installer can natively exit 124.

actionlint + shellcheck clean; cursor-review script tests 40/40 OK.
@mattmillerai

Copy link
Copy Markdown
Contributor Author

Review resolution — three panel rounds, all threads resolved

25 findings across three cursor-review panel rounds; all threads replied to and resolved. Base is main (not stacked). Fixes landed in 951e093, 5cdbeee, afb541f.

Fixed (13). The one High: the assertion could pass via the explicit-path fallback while only the vestigial $HOME/.cursor/bin went onto $GITHUB_PATH, so later steps still died with the exact cursor-agent: command not found this PR exists to eliminate — now exports the real install dir. Plus: timeout -k 30 240 around the installer (the earlier timeouts bounded only the ~6 KB bootstrap, not the payload download it triggers); [ -s ] gate so an empty 200 can't be a silent no-op; mktemp instead of a predictable path; -f as well as -x on the binary check (-x alone is true for a directory at that path — a real bug, verified both ways); --retry-max-time; </dev/null; and corrected timeout arithmetic in a comment that had been copy-pasted into two jobs it misdescribed.

Declined with reasoning (10). Mostly premises that aren't reachable here — runs-on: ubuntu-latest is hardcoded in all seven jobs with no runner input, so the self-hosted/old-curl scenarios can't occur — plus one suggestion (drop $HOME/.local/bin from $GITHUB_PATH) that directly contradicts the first round's own High finding. Details are on each thread.

Deferred to follow-ups (2), recorded for filing rather than dropped:

  1. Pin the Cursor CLI install to a verified version. The installer is fetched unpinned and executed with CURSOR_API_KEY in scope. Pre-existing and unchanged by this PR (it replaced curl … | bash, same vendor and trust). The panel's proposed sha256sum gate against cursor.com/install would be actively harmful — that URL is regenerated on every Cursor release, so a pinned digest hard-reds the panel in every caller repo the day Cursor ships. Needs version pinning with a refresh path: a design decision.
  2. De-duplicate the triplicated install step. It's now ~35 lines in three jobs; every fix in all three rounds had to be applied three times, which is the argument. Non-trivial because preflight has no checkout and the "logic loads from a pinned ref, never the caller's checkout" invariant has to be preserved.

Verification. actionlint + shellcheck -s bash clean; script tests 40/40. The step body was executed against the live installer with an isolated HOME whose .local/bin was off PATH — exercising the fallback branch specifically — giving exit 0, symlinks created, $GITHUB_PATH written, cursor-agent --version2026.07.23-e383d2b. Empty-body and installer-nonzero paths both go red at this step with the explicit error.

One caveat for whoever merges: the panel is label-triggered, so it ran against 5cdbeee, not the final afb541f. That last commit is four small edits (-k 30, 240, -f+-x, two comment corrections), locally verified but not panel-reviewed — I stopped re-triggering deliberately, since each round was returning findings about the previous round's fixes at steadily lower severity. Re-apply the cursor-review label if you'd rather see a clean pass on the final commit.

@mattmillerai

Copy link
Copy Markdown
Contributor Author

🤖 The reviews loop filed Linear follow-up ticket(s) for review thread(s) deferred as out of scope for this PR:

  • BE-5864 — Pin the Cursor CLI install to a verified version instead of fetching cursor.com/install unpinned — filed as agent-spike (premise unverified)
  • BE-5865 — De-duplicate the triplicated 'Install Cursor agent CLI' step in cursor-review.yml — filed as agent-spike (premise unverified)

The following carry agent-spike instead of agent-ok because their reachability claim was not backed by evidence (BE-5378) — the claim is investigated before any code is written, and "the premise does not hold" is a valid, successful outcome:

  • Pin the Cursor CLI install to a verified version instead of fetching cursor.com/install unpinned — no reachability block in the proposal
  • De-duplicate the triplicated 'Install Cursor agent CLI' step in cursor-review.yml — no reachability block in the proposal

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent-coded Authored by the agent-work loop cursor-review Multi-model cursor review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants