Commit Graph

7 Commits

Author SHA1 Message Date
David Blass fe2746198c fix: 6 unaddressed log-audit / run-audit findings (#840)
* fix: 6 unaddressed log-audit / run-audit findings

- #836 + #818 (clerk middleware SyntaxError on action-runtime endpoints):
  narrow proxy.ts matcher to exclude /api/repo/<owner>/<repo>/run-context,
  /api/runtime/*, /api/proxy-token. these are server-to-server with their
  own auth and have no Clerk session to evaluate, so clerkMiddleware's
  decodeJwt throws turn into 500s on every request.

- #837 (npx EBADDEVENGINES on customer's package.json): change runCli's
  bootstrap cwd from $GITHUB_WORKSPACE to os.tmpdir() so npm v11+ doesn't
  enforce devEngines.packageManager from the customer's tree before our
  bootstrap can install pullfrog. CLI process.chdir's to payload.cwd
  internally, so the runtime work still happens in $GITHUB_WORKSPACE.

- #838 (createWorkflowDispatch silently dropped 39 user runs on a 5xx
  spike): add bounded retry (3 attempts, ~750ms total) on Octokit 5xx and
  network errors inside dispatchReservedRun. preserves the existing 422
  "Unexpected inputs" path. retry budget stays well under GitHub's 10s
  webhook redelivery window.

- #833 (bail() redirect("/signout") propagated NEXT_REDIRECT into webhook
  handlers, 40+ 500s/24h): drop the redirect side-effect; bail now just
  classifies bad-credentials as non-retryable and propagates. UI flows
  that wanted auto-signout on revoked tokens can detect it themselves;
  the side-effect was wrong for any non-page caller.

- #835 (BYOK provider billing-exhausted fell through to raw error
  renderer, 37 review-mode runs/24h with no PR-side signal):
  - extend providerErrors patterns with Anthropic "credit balance is too
    low" + extract isProviderBillingExhausted / extractProviderId helpers
  - add a renderer branch in runErrorRenderer.ts that names the provider
    (parsed from providerID=) and links to its billing dashboard
  - route handleAgentResult's !result.success path through the renderer
    + reportErrorToComment with createIfMissing, so review-mode and
    silent triggers get an actionable PR comment regardless of mode

- #834 (post-hook ERR_MODULE_NOT_FOUND already fixed on main, hardening):
  - add a vitest invariant that walks the entryPost.ts import graph and
    refuses any non-relative / non-node: specifier — catches the next
    `@actions/core` slip-up before publish
  - add an analyze-logs classifier so future entryPost crashes surface as
    failure:post-hook-module-not-found instead of hiding inside
    failure:unknown / failure:git-lock-file

Co-authored-by: Cursor <cursoragent@cursor.com>

* anneal: fix dead-code matcher, 404 url, silent-trigger gate, and grammar regression

Round-1 anneal pass on the audit-fixes PR surfaced two critical issues +
several majors that the original fixes shipped with:

- proxy.ts matcher 1 (`(?!_next|[^?]*\.(...))`) still caught every
  /api/... route because matcher arrays are OR'd. The narrowing in
  matcher 2 was dead code → middleware still 500'd on /api/runtime/*,
  /api/proxy-token, /api/repo/.../run-context. Carve-out now lives in
  BOTH matchers.

- opencode.ai/billing returns 404; canonical top-up surface is /zen.
  deepseek /usage is the consumption page, not the top-up flow (/top_up
  is correct). google /apikey is the keys list, not billing (/usage is
  the spend dashboard). All three URL strings updated.

- handleAgentResult gated reportErrorToComment behind `if (!ctx.silent)`,
  contradicting the createIfMissing intent — silent IncrementalReview /
  pull_request_synchronize / auto-label still got zero PR signal on BYOK
  billing exhaustion, the exact failure mode #835 was meant to fix.
  Moved createIfMissing into finalizeSuccessRun's existing render-and-
  post block (single source of truth), reverted handleAgentResult to its
  prior shape. Side benefit: drops the double-PATCH that fired on every
  non-silent !success path with an existing progress comment.

- Anthropic-direct error rendered "**Your your provider account is out
  of credit.**" because Anthropic SDK has no providerID= tag, so
  extractProviderId returned null and the headline composed
  "Your " + "your provider". Added detectProviderId Anthropic fallback
  (matches "Anthropic API" / "credit balance is too low") so the link
  is reachable AND made the headline conditional on whether a provider
  id was detected.

- Reordered renderRunError classifier: BYOK billing-exhausted now runs
  BEFORE api-key auth detection. Providers commonly return 401 for
  billing exhaustion (DeepSeek, Gemini), and the OpenCode harness logs
  often include "API Error: 401" in the raw error body, which
  isApiKeyAuthError would otherwise match — surfacing "rotate your key"
  when the actual fix is "top up credits".

- isTransientUpstreamError missed ENOTFOUND / ENETUNREACH / EHOSTUNREACH
  (undici DNS-class failures Octokit doesn't wrap with a status). Added
  to the prefix alternation.

- Tightened "10s webhook redelivery budget" / "GitHub redelivers"
  wording in triggerWorkflow.ts and bail.ts JSDoc — GitHub's 10s is the
  response timeout (it doesn't auto-redeliver); upstream webhook proxy
  retries are what multiplied the failure.

Co-authored-by: Cursor <cursoragent@cursor.com>

* anneal r2: unbreak proxy.ts matcher 2, extend carve-out, mkdtemp bootstrap

Round-2 anneal (security + cross-cutting lenses) on top of the round-1
audit-fixes commit caught a critical regression + several majors:

- proxy.ts matcher 2 from r1 (`/(api|trpc)(?!...)(.*)`) does NOT compile.
  path-to-regexp rejects a top-level `(?!` after `)` as "Pattern cannot
  start with '?'", and Next.js's SourceSchema runs the same validator at
  build time and aborts via `process.exit(1)`. PR #840's Vercel + preview
  deployments have been failing since 978eca26 for exactly this reason.
  Fixed by nesting the lookahead inside an outer parameter group:
  `/(api|trpc)((?!...).*)`. Same shape matcher 1 already uses, which is
  why m1 always compiled. Verified `pnpm next build` succeeds end-to-end.

- proxy.ts carve-out was incomplete relative to its own justification.
  The "no Clerk session, decodeJwt 500" failure mode applies to ALL
  server-to-server action-runtime endpoints — five more share the exact
  shape: /api/repo/.../learnings, /api/repo/.../pr/.../summary-comment,
  /api/repo/.../issue/.../plan-comment, /api/workflow-run/, and
  /api/github/installation-token. Extended both matchers. /api/upload/
  signed-url stays in (dual auth: Clerk session OR bearer JWT — needs
  middleware for the user path).

- proxy.ts carve-outs were unanchored: a future /api/proxy-token-info,
  /api/proxy-tokens, or /api/repo/X/Y/run-context-foo would silently
  bypass Clerk. Added `(?:$|/)` for path-prefix carve-outs (allow exact
  match or sub-path), `$` for routes with browser-callable siblings
  (e.g. `learnings/history` is browser-Clerk, `learnings$` is action-
  bearer-JWT). Verified 30/31 routes via path-to-regexp test harness.

- runCli.ts cwd flipped from $GITHUB_WORKSPACE to os.tmpdir() in r1
  (#837 fix for npm v11 devEngines.packageManager EBADDEVENGINES). But
  $TMPDIR is overridable from a prior $GITHUB_ENV step — a customer-
  authored or compromised prior step can plant /atk/node_modules/
  pullfrog/ and `echo "TMPDIR=/atk" >> $GITHUB_ENV`, and our npx
  --yes pullfrog@<v> bootstrap resolves the local install first,
  executing attacker code with full action env (provider keys, OIDC,
  installation token, CODEX_AUTH_JSON). Switched to mkdtempSync(join
  (tmpdir(), "pullfrog-bootstrap-")) — fresh per-invocation 0700 dir,
  not pre-writable by anything earlier in the job.

- runLifecycle.ts: writeRunErrorOutputs (catch-path) didn't pass
  createIfMissing: true, contradicting the symmetric intent of the
  r1 finalizeSuccessRun fix. Silent triggers (IncrementalReview /
  pull_request_synchronize / auto-label) that throw past the success
  path still got zero PR signal — exact failure mode #835 was meant
  to close. Now both paths pass createIfMissing: true.

- runErrorRenderer.ts detectProviderId regex `/Anthropic API|credit
  balance is too low/i` could mis-tag a non-Anthropic billing-exhausted
  error that mentioned "Anthropic API" in passing (fallback-chain agent
  prompt text, OpenCode harness logs). Tightened to /credit balance is
  too low/i — Anthropic-specific phrasing, sufficient for the direct-
  Anthropic SDK case the fallback exists to handle.

- runErrorRenderer.ts JSDoc: r1's classifier reorder put hang at #6 in
  code but the JSDoc still listed it at #2. Reordered the doc to match
  dispatch order, with explicit note that hang is a sub-source for the
  api-key check (which is why hangBody is precomputed early).

- analyze-logs.ts: ERR_MODULE_NOT_FOUND.*entryPost regex needs `s`
  flag so it survives Node v23+ stack-trace reformatting onto multiple
  lines. One-char fix to defend the #834 classification bucket.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-26 16:53:18 +00:00
Colin McDonnell 1a9d3c1f82 fix bootstrap ETARGET when customer has npm min-release-age policy (#725)
* fix bootstrap ETARGET when customer has npm min-release-age policy

set npm_config_min_release_age=0 in the action runtime env so
`npx --yes pullfrog@<spec>` doesn't get rejected by a customer-side
release-age gate (npm 11.5+'s min-release-age / pnpm's
minimumReleaseAge). env vars beat .npmrc in npm config precedence,
so this neutralises the policy regardless of where it's defined.

pullfrog's npm version is server-stamped from a SHA-pinned action
ref customers already vet at the action layer — it isn't a
customer-vetted dep, so the release-age policy is the wrong
affordance for our bootstrap and would otherwise hard-fail every
run while the latest publish ages into the customer's window.

closes #713

* also cover pnpm's minimumReleaseAge key for corepack fallback path

* correct pnpm env var (pnpm v11+ uses pnpm_config_*, not npm_config_*)

the prior commit set `npm_config_minimum_release_age=0` to cover the
pnpm corepack-dlx fallback path, but pnpm v11+ only reads env vars
prefixed `pnpm_config_*` / `PNPM_CONFIG_*` (the v10→v11 migration
explicitly renamed the prefix). swap to the correct env var so the
fallback path actually neutralises pnpm's `minimumReleaseAge`.

also tighten the comment block, and add an AGENTS.md rule reminding
us to fetch top-level reviews AND inline review comments together —
they live on different endpoints and the inline set is easy to miss
with `gh pr view --json reviews,comments` alone.

* add scripts/pr-reviews.ts for one-shot review evaluation

dumps top-level reviews + inline review threads (with resolved/outdated
state) + PR-level conversation in a single GraphQL round trip, so agents
don't miss inline-comment feedback. fixes the trap where
`gh pr view --json reviews,comments` silently omits the inline
`pulls/{n}/comments` set.

borrows `gh auth token` so no env vars are required. registered in
`wiki/scripts.md`; AGENTS.md rule updated to point at the script
instead of the two-step gh-CLI workaround.

* pr-reviews: dump raw JSON for jq piping
2026-05-14 01:48:14 +00:00
Colin McDonnell 6607112d0b Exclude GITHUB_WORKSPACE and relative entries from PATH walk (#558)
* Exclude GITHUB_WORKSPACE and relative entries from PATH walk

resolveExecutable previously walked any directory listed in process.env.PATH,
which trusts that nothing earlier in the workflow prepended an
attacker-controlled location. A malicious PR could land bin/npx in the repo
and add `echo "$GITHUB_WORKSPACE/bin" >> $GITHUB_PATH` to a prior step,
causing pullfrog to exec the attacker's binary with our scoped tokens in env.

Filter out (a) any non-absolute PATH entry (., bin, .., etc., which resolve
against cwd) and (b) any entry equal to or under GITHUB_WORKSPACE. The walk
then continues to the next legitimate system tooling dir.

* Address PR #558 review: comment typo + Windows case bypass

- Drop double space in the threat-model comment.
- Lowercase paths on Windows before comparing against GITHUB_WORKSPACE.
  Without this, an attacker can bypass the filter by varying case in their
  injected PATH entry (`d:\a\repo\bin` vs `D:\a\repo`) — string compare
  misses but NTFS still resolves the executable inside the workspace.
2026-05-03 17:33:13 +00:00
Colin McDonnell 55c95e6f50 Fix Node 24 action bootstrap fallback (#556)
* Fix Node 24 action bootstrap fallback

Resolve the published CLI launcher through PATH so runners missing a sibling Node 24 npx can still start, and make post cleanup recognize prefixed leaping comments.

* Bump Pullfrog action package version

Ensure the Node 24 bootstrap and post-cleanup fixes publish to npm and move the v0 action tag.

* Walk PATH for corepack and npx in action bootstrap

ensureActionDependencies and runPackageCli now resolve corepack/npx through
PATH the same way as the npx-via-PATH fix, so Node 24 runner pools missing
either sibling can still bootstrap. Also adds a Zod-mirror settings helper
for the preview-556 repo and documents the per-PR settings workflow.

* log when corepack PATH fallback is used
2026-05-01 15:59:46 +00:00
Colin McDonnell 56a5d29598 add diff coverage preflight for PR review submissions (#544)
* add one-time diff coverage preflight for PR reviews

track diff read coverage from agent tool-use events and run a one-time pre-flight before review submission, with explicit coverage skip reasons for low-value files like lockfiles.

Made-with: Cursor

* add manual dispatch fallback for preview deploy workflow

allow preview repo and preview sync jobs to be run via workflow_dispatch with explicit PR number and branch inputs, so preview provisioning can be retriggered when pull_request events fail to fire.

Made-with: Cursor

* fix manual preview dispatch PR input wiring

use normalized PR number and branch env values for comment creation and script env wiring so workflow_dispatch preview runs can create and update PR-specific preview resources.

Made-with: Cursor

* remove obsolete snapshots invalidated by checkout instructions change

* fix diff coverage read offset handling and add local sanity-check guidance

normalize read offset semantics for diff coverage tracking, reuse shared range counting in review preflight, add focused diff coverage unit tests, and document the local play.ts testing workflow in AGENTS.md.

Made-with: Cursor

* add regenerated mcp test snapshots

capture snapshot files generated by the review comment and checkout formatting tests during pre-push validation so the branch remains clean and reproducible.

Made-with: Cursor

* add diff coverage preflight instrumentation logs

log diff coverage initialization in checkout_pr and emit preflight state/breakdown diagnostics in create_pull_request_review to debug missing coverage enforcement in preview e2e runs.

Made-with: Cursor

* add env override to force local cli execution in action runtime

support explicit local-cli execution via PULLFROG_FORCE_LOCAL_CLI so preview workflows can run branch action code instead of the npm fallback package during e2e debugging.

Made-with: Cursor

* add preview e2e debugging learnings for action runtime validation

capture the preview execution-path gotchas and one-time preflight verification pattern in AGENTS.md so future investigations validate the real runtime and avoid npm fallback confusion.

Made-with: Cursor

* reduce diff coverage log noise while preserving failure visibility

downgrade verbose diff coverage lifecycle diagnostics to debug, keep a concise info-level pre-flight failure signal, and document preview runtime debugging learnings in AGENTS.md.

Made-with: Cursor

* WIP

* tune sync.md: ff override + softer overlap verification

Made-with: Cursor

* chore: bump models snapshot for claude-opus-4-7

Made-with: Cursor

* rip out coverage_skips waiver from diff coverage pre-flight

Made-with: Cursor

---------

Co-authored-by: pullfrog[bot] <226033991+pullfrog[bot]@users.noreply.github.com>
2026-04-16 21:51:44 +00:00
Colin McDonnell d1e075fa3b fix npx binary resolution: run in workspace, not action directory
npx was running with cwd set to the action's own directory, which has
package.json with "name": "pullfrog". npm treats the local package as
satisfying the request and skips the registry fetch, then fails to find
the binary (sh: 1: pullfrog: not found). use GITHUB_WORKSPACE instead.

Made-with: Cursor
2026-04-12 19:17:09 +00:00
Colin McDonnell 421607cf97 fix push-to-action: use CLI direct invocation for token acquisition
the inline `node -e` + `TOKEN=$(...)` approach broke because
`core.getIDToken()` in @actions/core writes `::debug::` and
`::add-mask::` to stdout, polluting the captured value.

`node cli.ts gha token` uses `core.setOutput()` which writes to
the $GITHUB_OUTPUT file instead of stdout.

Made-with: Cursor
2026-04-12 00:47:41 +00:00