Skip to content

feat(mcp): Sprint B — on-chain tools (submit_pr) — GHB-187#102

Merged
Gastonfoncea merged 21 commits into
mainfrom
gastonfoncea09/ghb-187-sprint-b-mcp-on-chain-tools-submit_pr-check_status
May 19, 2026
Merged

feat(mcp): Sprint B — on-chain tools (submit_pr) — GHB-187#102
Gastonfoncea merged 21 commits into
mainfrom
gastonfoncea09/ghb-187-sprint-b-mcp-on-chain-tools-submit_pr-check_status

Conversation

@Gastonfoncea
Copy link
Copy Markdown
Collaborator

Summary

Implements Sprint B (GHB-187): the MCP server's first write-capable tools that close the AI-agent loop — find bounty → resolve → submit PR on-chain autonomously.

  • submissions.create (a.k.a. submit_pr) — new dev-only tool
  • submissions.list — new dev-only tool
  • Hard role gating (requireRole) on all on-chain tools
  • Privy delegated server-signing for Solana via @privy-io/node
  • Consent UI in /app/credentials (AgentDelegationCard)
  • Defense-in-depth fix for GHB-182 (PR ownership): MCP pre-check + relayer post-check
  • New table agent_delegations (migration 0026)

Spec: docs/superpowers/specs/2026-05-18-mcp-sprint-b-onchain-tools-design.md
Plan: docs/superpowers/plans/2026-05-18-mcp-sprint-b-onchain-tools.md
Runbook: docs/superpowers/runbooks/2026-05-18-sprint-b-smoke.md

Related: closes GHB-114; mitigates GHB-182 off-chain (on-chain redeploy stays in scope of GHB-182).

Key design decisions

  1. Signing model — Privy delegated server-signing. The user authorizes our server once via delegateWallet; subsequent submit_pr calls have Privy sign on their behalf without a browser popup. Dismissed alternatives: agent-owned wallet (changes data model, breaks authz), two-step client signing (AI agents don't have wallets natively), per-action browser handoff (defeats the point of an agent).
  2. GHB-182 fix off-chain. MCP rejects mismatched pr.author / pr.base.repo before signing; relayer re-checks and marks auto_rejected if MCP bypassed. No Anchor redeploy.
  3. Server-side gas station. MCP uses SolanaGasStation from @ghbounty/shared directly (not via HTTP). Removed a hop + a second auth path.
  4. Idempotent submit_pr. Triple key (solver, issue_pda, pr_url); retries after timeout return the existing submission, including when the bounty has since closed.
  5. opus_report_hash stays zeros. Same as the web app — relayer never commits a hash on-chain in v1.

State of devnet

  • ✅ Migration 0026_agent_delegations applied (Gaston, 2026-05-19)
  • ⚠️ drizzle.__drizzle_migrations was empty before this PR (all prior migrations had been applied manually). Reconciled by inserting marker rows; documented as tech debt in memory for 0023/0024 (those remain outside Drizzle's tracking, not introduced here).

Test plan

Unit / integration (already verified via pre-commit hook)

  • MCP tests: 54 pass, 1 skipped (pre-existing)
    • submissions.create: 13 tests (happy + idempotency + role guard + delegation guard + bounty not open + PR ownership failure + Privy 404 + revocation flow + transient GitHub errors + pr_url length)
    • submissions.list: 2 tests (happy + role guard)
    • role-guard: 2 tests
    • delegation-guard: 3 tests
    • delegated-signer: 5 tests (including getWalletByAddress 404 path)
    • build-submit-solution-tx: 2 tests (length validation + v0 + 2 signature slots)
  • Shared lib: 7 tests on verifyPrOwnership (happy + each fail reason + network error)
  • Frontend route-core: 10 tests on agent-delegation-route-core
  • Relayer: 9 new tests on the ownership pre-check (definitive vs transient handling)
  • Workspace-wide: typecheck clean, all 641 tests pass

Manual smoke (post-deploy, Gaston runs)

Full runbook: docs/superpowers/runbooks/2026-05-18-sprint-b-smoke.md

  • Delegate wallet via /app/credentialsagent_delegations row appears
  • submissions.list returns a row for an existing submission
  • submissions.create with a real PR → returns { submission_id, status: "pending", tx_signature, submission_pda }
  • Solana Explorer shows the submit_solution invocation
  • After ~30-60s, submissions.get returns state: "scored"
  • Negative: PR by another author → Forbidden: PR ownership check failed: author_mismatch
  • Negative: revoke delegation then retry → Forbidden: Wallet delegation required
  • Negative: company-role API key → Forbidden: This tool requires \dev` role.`
  • Idempotency: same (bounty_id, pr_url) twice → same submission_id, idempotent: true

Verification of the central assumption

walletId in Privy is the internal Privy wallet ID, NOT the on-chain pubkey. Fixed in commit 2671b1d: signSolanaTransaction now calls client.wallets.getWalletByAddress({ address }) to resolve the pubkey to the internal id before signing. The smoke test will verify this works against real Privy.

Known follow-ups (not blocking this PR)

  • GHB-182 on-chain redeploy. Off-chain mitigation is in place; on-chain ownership check stays in GHB-182.
  • 0023 / 0024 migration journal gap. Pre-existing tech debt — these SQL files are applied to devnet but absent from _journal.json / __drizzle_migrations. Doesn't break current/future migrations.
  • Privy pricing. Server-side signing is in the Core (free) tier — 50K signatures/mo, 499 MAU. Verified in memory project_privy_pricing_2026_05_19. No action needed for v1; revisit when approaching MAU cap.
  • Cache for Privy getWalletByAddress lookup. Currently +1 HTTP roundtrip per submit_pr. If it becomes a hot path, cache the mapping pubkey → walletId in Redis with TTL.

Files changed

44 files, +10,566 / -310 (excluding lockfile churn from Privy SDK install).

🤖 Generated with Claude Code

Gastonfoncea and others added 20 commits May 18, 2026 17:53
Linear issues (spec, plan, code, migrations, everything tied to the
issue) must live on the issue's feature branch and reach main only via
a merged PR. Never commit issue work directly to main.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Design spec para Sprint B: submit_pr + submissions.list + hard role gating + fix GHB-182.
Resuelve decisión central de signing model via Privy delegated server-signing (Opción A).
Defense-in-depth para PR ownership (MCP pre-check + relayer post-check).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
TDD-style bite-sized tasks for: agent_delegations migration, verifyPrOwnership shared lib, role/delegation guards, Privy delegated-signer, submit_solution tx builder, submissions.list + submissions.create MCP tools, frontend consent UI, relayer post-check, smoke runbook, and PR handoff.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the agent_delegations table to track which users have granted
server-side Privy wallet signing consent for the MCP submit_pr flow.
Includes FK to profiles(user_id) ON DELETE CASCADE, a partial index
on active delegations (revoked_at IS NULL), and an RLS policy for
end-user session reads. Migration ready for human-applied db:migrate.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Gaston must run pnpm db:migrate against devnet before opening the PR.
Without it, agent_delegations doesn't exist and Sprint B fails at runtime.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements buildSubmitSolutionTx helper that builds the submit_solution
Anchor instruction and packs it into an unsigned v0 VersionedTransaction
with gas station as fee payer, ready for Privy signing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Full auth → role guard → delegation guard → idempotency → PR ownership
→ build tx → Privy sign → gas station submit → DB mirror insert flow.
Adds SolanaGasStation singleton wrapper, registers the tool, and covers
happy path + 4 guard cases with vitest.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add 4 missing error-case tests: mcp_status suspended → Forbidden,
no active delegation → Forbidden, bounty not found → NotFound, and
Privy delegation_revoked → Forbidden with DB update verification.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
POST /api/agent-delegation handles delegate (upsert) and revoke (set
revoked_at) actions; GET returns the caller's current delegation row.
Auth via Privy JWT (same pattern as stake/api-keys routes). Core logic
extracted to lib/agent-delegation-route-core.ts with 10 vitest tests.
Also adds agent_delegations table types to db.types.ts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds defense-in-depth ownership verification to the relayer's submission
handler (GHB-182). Before calling the analyzer, the handler now calls
verifyPrOwnership (from @ghbounty/shared) with the solver's GitHub handle
and the bounty's repo URL looked up from DB. Definitive failures
(author_mismatch, repo_mismatch, pr_not_found, invalid_url) → auto_rejected;
transient failures (rate_limited, upstream_error) → skip poll cycle, retry.
Also fixes @ghbounty/shared's internal imports to use .js extensions so it
works for both bundler and NodeNext module resolution consumers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Privy's signTransaction expects the internal wallet id, not the on-chain
pubkey. Resolve walletAddress → walletId via getWalletByAddress before
signing; map 404 to delegation_revoked. Rename SignInput.walletId →
walletAddress throughout signer, caller, and tests (5 tests total).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 19, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
gh-bounty-frontend Ready Ready Preview, Comment May 19, 2026 9:27pm
ghbounty-mcp Ready Ready Preview, Comment May 19, 2026 9:27pm

…GHB-187

Task 14 added .js extensions to @ghbounty/shared internal imports to make
NodeNext resolution happy when the relayer started consuming the package.
That undid the GHB-180 fix that made the package consumable by Next.js
(Turbopack), breaking both MCP and frontend builds on Vercel.

Revert the .js extensions in the shared package and align the relayer's
tsconfig to use moduleResolution: bundler (same pattern as
packages/shared/tsconfig.json). tsx handles either mode at runtime; the
workspace tests + typecheck still pass for all packages.

Verified locally:
  pnpm --filter @ghbounty/mcp build  → ok
  pnpm --filter frontend build       → ok
  pnpm typecheck                     → ok (7/7 packages)
  pnpm test                          → ok (641 tests pass)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Gastonfoncea Gastonfoncea marked this pull request as ready for review May 19, 2026 21:32
@Gastonfoncea Gastonfoncea requested a review from tomazzi14 as a code owner May 19, 2026 21:32
@Gastonfoncea Gastonfoncea merged commit 0e485ad into main May 19, 2026
5 checks passed
@Gastonfoncea Gastonfoncea deleted the gastonfoncea09/ghb-187-sprint-b-mcp-on-chain-tools-submit_pr-check_status branch May 19, 2026 21:32
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.

1 participant