Skip to content

Commit 870610c

Browse files
jonathanKingstoncursoragentgithub-actions[bot]
authored
Content scope dependabot (#2727)
* Update Dependabot cooldown * Gate Dependabot auto-approval on Cursor checks * Add Anthropic gate for Dependabot auto-approval * Auto-format: prettier + stylelint fix * Fix Anthropic gate type annotations * Use Dependabot npm ecosystem metadata * Wait for checks before Anthropic Dependabot gate * Auto-format: prettier + stylelint fix * Fix Dependabot gate lint issue * Simplify Dependabot automation gates * Exclude current workflow's check runs by ID, not by name The previous self-exclusion compared run.run_id (a field that does not exist on the Check Runs API response) and then fell through to a hardcoded run.name === 'dependabot' filter. That created a fragile coupling to the YAML job key: any rename would silently include the gate's own check run in waitForOtherChecksToPass and deadlock until the 30-minute timeout. Resolve the current workflow run's job IDs via the Actions Jobs API (GitHub Actions sets job.id == check_run.id) and exclude any matching check run from the wait set. Co-authored-by: Jonathan Kingston <jonathanKingston@users.noreply.github.com> * Restrict Anthropic gate evidence to trusted GitHub App authors matchedCursorSources() previously matched any review or issue comment whose body contained the public Cursor agent id. Because the agent id appears in plain text in PR comments, anyone with comment access could echo it (or a fabricated transcript around it) and have the Anthropic gate treat that text as authenticated Cursor-authored evidence — prompt injection against the auto-merge decision. Filter sources at construction time: drop any review/comment whose author is not a GitHub App bot in the explicit TRUSTED_AUTOMATION_AUTHORS allow-list (currently just 'cursor[bot]'). Untrusted authors are returned as null and stripped before sources reach matchedCursorSources(). Also tighten the Anthropic system prompt to reflect that filtering happens upstream. Co-authored-by: Jonathan Kingston <jonathanKingston@users.noreply.github.com> * Validate Cursor check-run identity by app slug + details URL host latestCheckRunsByName() previously matched expected Cursor checks solely by display name. Any installed GitHub App with checks:write permission could have published a later success with the same display name, and the Anthropic gate would have treated it as authenticated Cursor evidence. Convert EXPECTED_CHECKS to structured entries that pin each check to: - run.name (display name) - run.app.slug (globally unique GitHub App slug 'cursor') - URL(run.details_url).host ('cursor.com') A new matchExpectedCheck() returns the matching expected entry only when all three signals line up; latestCheckRunsByName() ignores anything else. Updated main()'s missingChecks computation to read expected.name from the new shape. Co-authored-by: Jonathan Kingston <jonathanKingston@users.noreply.github.com> * Drop third-party wait action; gate script waits for Cursor checks itself Removed the three fountainhead/action-wait-for-check@v1.2.0 steps and the Validate Cursor checks step from dependabot-auto-merge.yml. That action was referenced by mutable tag and was being handed GITHUB_TOKEN on a pull_request_target job with contents:write / pull-requests:write, which would let a tag retag or repo compromise of fountainhead use the write-scoped token during Dependabot PR processing. Folded the wait inline into dependabot-anthropic-gate.mjs, which is already checked out from the trusted base branch via sparse-checkout and runs with the same permissions: - Renamed waitForOtherChecksToPass -> waitForChecksToSettle and made it block until (a) every non-current check run on the head SHA is completed and passing, (b) commit statuses are non-pending and non-failed, AND (c) every EXPECTED_CHECKS entry has appeared as a matching trusted check run and reached 'completed' state. Without (c) the loop could exit before Cursor's checks were even registered, then fail with 'Missing expected Cursor checks'. - Renamed OTHER_CHECK_TIMEOUT_MS / OTHER_CHECK_POLL_INTERVAL_MS to CHECK_WAIT_TIMEOUT_MS / CHECK_WAIT_POLL_INTERVAL_MS to match. - Added missingExpectedCheckNames() and pendingExpectedCheckRuns() helpers reused for both the wait and the timeout error messages. The Anthropic step now runs whenever the npm classification matches; the script itself enforces all the previous gating, so failure modes are unchanged from a caller's perspective. Co-authored-by: Jonathan Kingston <jonathanKingston@users.noreply.github.com> * Replace greedy JSON regex with brace-balanced candidate scan parseAnthropicDecision used /\{[\s\S]*\}/, which matches greedily from the first '{' to the *last* '}' in the response. Any preamble containing curly braces — e.g. 'Based on {evidence}, here is the JSON: {...}' — would make the match span non-JSON content, JSON.parse would throw, and a legitimate safe_to_merge=true decision would be lost. Replace it with a generator candidateJsonObjects() that walks the text emitting every brace-balanced '{...}' substring, tracking JSON string literals and escapes so braces inside quoted values do not disturb depth. parseAnthropicDecision now iterates candidates and returns the first one that both parses as JSON and has the expected {safe_to_merge:boolean, reason:string} shape, surfacing the last parse/shape error if no candidate matches. Co-authored-by: Jonathan Kingston <jonathanKingston@users.noreply.github.com> * Fetch inline PR review comments for Anthropic gate evidence The gate previously assembled evidence from two sources only: - GET /pulls/{pr}/reviews (top-level review bodies) - GET /issues/{pr}/comments (issue / conversation comments) It did not pull inline review comments (those attached to a diff hunk) from GET /pulls/{pr}/comments. Cursor Bugbot publishes its blocking findings as inline review comments with an empty parent review body, so omitting that feed would let the Anthropic gate auto-approve while concrete blocking findings sit unread on the PR. Add a sourceFromInlineReviewComment builder (typed as 'inline_review_comment', carrying path/line/in_reply_to_id for audit) gated by the existing isTrustedAutomationActor allow-list, and include the new feed in main()'s Promise.all + sources merge. Trust boundary is unchanged: untrusted authors still drop to null and are filtered out before evidence is sent to Anthropic. Co-authored-by: Jonathan Kingston <jonathanKingston@users.noreply.github.com> * Grant actions: read so the gate can list its own workflow's jobs fetchCurrentWorkflowCheckRunIds() calls GET /actions/runs/{run_id}/jobs to map the current workflow run's jobs to their corresponding check-run IDs, which is what lets waitForChecksToSettle() ignore its own check run when deciding whether 'other' checks are pending. Once a workflow declares an explicit permissions block, every scope not listed defaults to 'none'. The block listed checks/contents/ pull-requests but omitted actions, so GITHUB_TOKEN would 403 against the Actions Jobs API and the gate would error out before reaching its fail-closed Anthropic decision. Add 'actions: read' with an explanatory comment so the dependency is documented at the permission site. Co-authored-by: Jonathan Kingston <jonathanKingston@users.noreply.github.com> * Attribute Cursor Bugbot evidence by check head_sha matchedCursorSources() only matched bodies containing the agent id extracted from a check run's details_url (/agents/<id>). That works for the two 'Cursor Automation: ...' runs, but Cursor Bugbot uses a generic https://cursor.com/docs/bugbot URL with no agent id, so cursorAgentId() returned null and matchedCursorSources() returned [] for every Bugbot check — even after the inline-comment fetch landed. The Anthropic gate could therefore auto-approve without ever seeing the Bugbot inline findings on the PR. Add a small sourceMatchesCheckRun() helper. Default match is still the agent id; when the run is 'Cursor Bugbot' specifically, fall back to matching trusted-author bodies that mention the check run's head_sha. Bugbot's review summary, per-finding inline comments, and threaded follow-ups all include a 'Reviewed by Cursor Bugbot for commit <head_sha>' footer, so head_sha presence is a reliable attribution signal that: - never re-trusts arbitrary comment authors (isTrustedAutomationActor has already filtered sources to cursor[bot] only), - scopes findings to the current revision (Bugbot comments for earlier commits with a different head_sha don't match), - does not depend on Bugbot's URL scheme or the 'BUGBOT_BUG_ID' / 'BUGBOT_REVIEW' marker shape, so it survives Bugbot UI tweaks. Verified with 5 cases: Bugbot inline finding for this head_sha matches; Bugbot review summary for this head_sha matches; Bugbot comment referencing a different head_sha is rejected; Cursor Automation runs still match by agent id; Cursor Automation does not fall back to head_sha. Co-authored-by: Jonathan Kingston <jonathanKingston@users.noreply.github.com> * Add fixture coverage for the Dependabot Anthropic gate helpers The last Cursor automation review reported no remaining security findings but recommended fixture coverage for the gate script, particularly: spoofed check names from another GitHub App, untrusted comments containing agent ids, inline-only Bugbot findings with details_url=https://cursor.com/docs/bugbot, and brace-balanced Anthropic JSON parsing. Make the script importable so it can be unit-tested without invoking main(): - Export the per-helper functions and the configuration constants (EXPECTED_CHECKS, TRUSTED_AUTOMATION_AUTHORS, PASSING_CHECK_CONCLUSIONS). - Guard the previous unconditional 'await main()' behind a fileURLToPath(import.meta.url) === process.argv[1] check so the workflow invocation still runs the gate but tests can import helpers safely. Add dependabot-anthropic-gate.test.mjs following the existing .github/scripts/review-helpers.test.mjs node:test / node:assert convention, with 41 assertions across 16 suites covering: - matchExpectedCheck: trusted check accepted; spoofed app slug, wrong host, and unknown display name rejected. - latestCheckRunsByName: picks the latest matching run per name, rejects spoofed apps. - latestOtherCheckRunsByName / checkRunState: excludes own workflow's check-run ids; pending/failed/passing classification. - commitStatusState: pending vs failed. - missingExpectedCheckNames / pendingExpectedCheckRuns: report which expected Cursor checks have not yet appeared or are still in flight. - isTrustedAutomationActor and the three source builders: untrusted authors map to null, trusted authors produce the expected shape (path/line/inReplyToId on inline comments). - sourceMatchesCheckRun: Cursor Automation runs match by agent id; Cursor Bugbot runs match by head_sha; cross-commit Bugbot findings rejected; non-Bugbot runs do not fall back to head_sha. - matchedCursorSources: only surfaces the expected source per run. - evidenceForRun: long output text is truncated. - candidateJsonObjects / parseAnthropicDecision: preamble braces, multiple top-level objects, escape sequences, garbage input. - truncate, parseLinkHeader: utility behavior. Co-authored-by: Jonathan Kingston <jonathanKingston@users.noreply.github.com> * Strict Anthropic decision parsing + dedupe pending Cursor runs Two unresolved review threads on the PR were still valid: 1. parseAnthropicDecision accepted the first valid-shape JSON object anywhere in the model response. Because evidence sent to Anthropic includes untrusted review/comment bodies, the model could be induced to quote {"safe_to_merge":true,...} from an attacker's comment before its real answer, and the parser would treat the quoted snippet as the merge decision. Replace candidateJsonObjects / first-match parsing with strict validation: the trimmed response must be exactly a single JSON object whose keys are exactly {safe_to_merge, reason, confidence}, with type and enum validation on each field. Any preamble, code fence, trailing prose, extra key, or wrong type fails closed. candidateJsonObjects is no longer needed and is removed. 2. pendingExpectedCheckRuns previously returned every matching pending run without deduplicating by name, while the rest of the wait loop reasons about the latest run per name. A stale in_progress Cursor check sitting alongside a newer completed one for the same name would keep pendingCursor non-empty even when otherIdle was true, deadlocking waitForChecksToSettle() until the 30-minute timeout fired. Route through latestCheckRunsByName() so only the latest matching run per name is considered, then filter for non-completed runs. Test coverage updated: 11 stricter parseAnthropicDecision cases (bare object, whitespace, preamble, trailing prose, code fence, embedded snippet, extra keys, missing/invalid required fields, invalid confidence enum, non-object JSON, empty input, garbage) and a new stale-run dedup case for pendingExpectedCheckRuns. 46/46 pass on node --test. Co-authored-by: Jonathan Kingston <jonathanKingston@users.noreply.github.com> * Make dependabot-anthropic-gate.test.mjs typecheck under strictNullChecks The repo's tsconfig.json includes .github/scripts with checkJs:true and strictNullChecks:true, so the gate test file participates in 'npm run tsc'. Three categories of errors: - matchExpectedCheck() returns the entry or null; the EXPECTED_CHECKS.includes(matched) assertion was passing {entry}|null where {entry} was expected. Add an assert.ok(matched) null guard before the includes() call. - The local externalRun() helper inferred its conclusion parameter type from its default of null, so callers passing 'success' / 'skipped' / 'failure' tripped TS2345. Annotate the helper with JSDoc so conclusion is typed as string | null. - sourceFromInlineReviewComment() returns the source object or null. The captures-path/line/in_reply_to_id test asserted on properties of the result without first narrowing it. Add assert.ok(src) before the property accesses. No behavior change. tsc still reports three pre-existing errors in injected/src/features/* about generated build files (build/locales/*-locales.js, surrogates-generated.js); those are unrelated to this PR and only appear when 'npm run build' has not been run. Co-authored-by: Jonathan Kingston <jonathanKingston@users.noreply.github.com> * Key non-Cursor check-run dedup by (app, name) so cross-app successes can't mask failures latestOtherCheckRunsByName() previously deduped non-Cursor check runs by display name only, so an installed GitHub App with checks:write could publish a newer 'lint: success' check run and have it supersede a failing 'lint' check run from github-actions before checkRunState() saw the failure. With the gate auto-asking Anthropic on success, that would silently drop a real CI failure from the gate's evidence. Add a checkRunIdentityKey() that combines run.app.slug (falling back to run.app.id, then null) with run.name, and dedupe on that composite key. Each app's runs for a given name are tracked independently, so a failure from any app still surfaces while reruns within the same app still collapse to the latest run. Tests: - new regression: github-actions reports 'lint: failure', another app posts a newer 'lint: success'; the failure must still appear in checkRunState().failed. - new regression: two reruns of 'lint' from the same app collapse to the latest run (status preserved). - existing tests updated to attach an explicit app.slug to the fixture so the new key works (default 'github-actions'). 48/48 node --test cases pass; node --check, prettier, and eslint clean. Pre-existing tsc errors in injected/src/features/* about generated build files are unrelated and unchanged. Co-authored-by: Jonathan Kingston <jonathanKingston@users.noreply.github.com> * Fail closed on PR head drift before Dependabot auto-approve/merge The Anthropic gate can wait up to 30 minutes while evaluating a specific head SHA. Re-read the current PR head before approving or enabling auto-merge, bind approvals to commit_id, and pass --match-head-commit so an older workflow run cannot approve or queue merge for a newer commit. Co-authored-by: Jonathan Kingston <jonathanKingston@users.noreply.github.com> * Harden Dependabot gate: CI tests, evidence checks, concurrency - Run dependabot-anthropic-gate.test.mjs in CI (no tokens required) - Reject blank Cursor evidence before calling Anthropic; poll up to 3m for cursor[bot] comments after checks complete - Fail the approve step with API output instead of swallowing errors - Serialize gate runs per PR via concurrency group - Switch from pull_request_target to pull_request with in-repo fork guard Co-authored-by: Jonathan Kingston <jonathanKingston@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Jonathan Kingston <jonathanKingston@users.noreply.github.com>
1 parent 3bfd158 commit 870610c

5 files changed

Lines changed: 1326 additions & 18 deletions

File tree

.github/dependabot.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ updates:
33
- package-ecosystem: 'npm'
44
directory: '/'
55
schedule:
6-
interval: 'weekly'
6+
interval: 'daily'
77
cooldown:
8-
default-days: 2
8+
default-days: 14
99
target-branch: 'main'
1010
open-pull-requests-limit: 20
1111
labels:

0 commit comments

Comments
 (0)