Skip to content

feat(git-providers): native GitHub integration via Decobot App#3475

Open
vibegui wants to merge 1 commit into
mainfrom
vibegui/git-provider-settings
Open

feat(git-providers): native GitHub integration via Decobot App#3475
vibegui wants to merge 1 commit into
mainfrom
vibegui/git-provider-settings

Conversation

@vibegui

@vibegui vibegui commented May 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Replaces the legacy shared-token deco/mcp-github MCP connection — where every org member acted on GitHub as the connection creator — with a first-class Git Provider subsystem backed by a Decobot GitHub App.

The change fixes two problems:

  1. Wrong identity on writes. Today every PR/issue/comment shows up as the connection creator's GitHub user (there's a TODO acknowledging this at apps/mesh/src/mcp-clients/outbound/headers.ts:104-108).
  2. Effective impersonation. Any org member calling the connection acted as the creator on GitHub.

After this PR, GitHub actions are attributed to the calling Studio user whenever they trigger an agent. Unattended runs (cron, event-bus) act as Decobot.

How identity is resolved

Every native GitHub tool routes through ctx.gitProviders.resolveClient(ctx, { owner }), which implements:

                     calling context
                  user session?      no user (cron, event-bus)
linked GitHub?  ─────────────     ──────────────────────────
yes             user-to-server    installation token (bot)
no              ERROR (link req)  installation token (bot)
  • Real user + linked GitHub → uses Better Auth's account row to mint a user-to-server token. Actions show as the user on GitHub.
  • Real user + unlinked → throws GitProviderUserLinkRequiredError (code GIT_PROVIDER_USER_LINK_REQUIRED) with a link CTA URL. No silent fallback to the bot token — that would defeat the point of the feature.
  • No user in context → mints an installation token (Decobot). Write tools append a via Decobot — Studio request <id> footer so attribution stays traceable on GitHub itself.

What ships

ConfigDECOBOT_APP_ID, DECOBOT_PRIVATE_KEY (PEM or DECOBOT_PRIVATE_KEY_BASE64), DECOBOT_CLIENT_ID, DECOBOT_CLIENT_SECRET, DECOBOT_APP_SLUG. Self-hosters bring their own App; the adapter reports available=false when env is missing and the UI shows a "not configured" message.

Schema — new git_provider_installations table (migration 093). Holds GitHub account metadata only — installation tokens are short-lived and minted on demand from the App's private key (no secrets stored).

Subsystemapps/mesh/src/git-providers/ mirrors the shape of apps/mesh/src/ai-providers/:

  • types.tsGitProviderAdapter, ResolvedGitClient, structured error classes.
  • registry.ts + factory.tsGitProviderFactory.resolveClient (the four-quadrant logic above).
  • adapters/github/ — RS256 App JWT signing (no jsonwebtoken dep), in-memory installation-token cache with concurrent-refresh dedup, octokit-style fetch helpers, Decobot adapter with user-token refresh.

Tools — all registered in CORE_TOOLS:

  • Management: GIT_PROVIDERS_LIST, GIT_PROVIDER_INSTALL_URL, GIT_PROVIDER_INSTALL_COMPLETE, GIT_PROVIDER_INSTALLATION_LIST, GIT_PROVIDER_INSTALLATION_DELETE, GIT_PROVIDER_USER_LINK_STATUS.
  • Native GitHub: GITHUB_READ_FILE, GITHUB_LIST_REPO_CONTENTS, GITHUB_CREATE_ISSUE, GITHUB_COMMENT, GITHUB_LIST_PRS, GITHUB_READ_PR.

Better AuthsocialProviders.github now falls back to DECOBOT_CLIENT_ID/DECOBOT_CLIENT_SECRET so users can link their personal GitHub via the standard Better Auth sign-in/social endpoint.

UISettings → Git Providers:

  • Install dialog (popup → postMessage → GIT_PROVIDER_INSTALL_COMPLETE).
  • Installations list with disconnect + GitHub revoke link.
  • Per-user "Link your personal GitHub" card.
  • New /oauth/callback/git-provider route handles the App install redirect.

Tests — 14 new tests covering:

  • RS256 App JWT signature verified against a freshly generated keypair.
  • Installation token cache TTL behavior + concurrent-refresh deduplication + invalidation.
  • All four quadrants of resolveClient + edge cases (no org, no installation, unavailable provider, case-insensitive owner lookup). The Q2 test explicitly asserts we throw rather than falling back to the bot — that's the regression-prevention bit.

Out of scope (follow-ups)

  • Deprecating deco/mcp-github connections and the legacy GITHUB_LIST_USER_ORGS tool (both still work).
  • Webhook ingest from Decobot → event-bus republishing.
  • GitLab/Bitbucket adapters (the abstraction is shaped for them but only GitHub ships now).
  • Inline "Link GitHub to continue" CTA rendering in the agent chat tool-call error UI.

Test plan

  • Register a test Decobot-dev App on github.com with permissions Contents (read), Issues (write), Pull Requests (read+write). Enable "Request user authorization (OAuth) during installation" and "Expire user authorization tokens". Callback http://localhost:4000/oauth/callback/git-provider and http://localhost:4000/api/auth/callback/github.
  • Export DECOBOT_* env vars locally. bun run --cwd=apps/mesh migrate && bun run dev.
  • Settings → Git Providers → "Install Decobot" → pick a personal test GitHub org with one repo selected. Confirm a row lands in git_provider_installations.
  • Click "Link my GitHub" on the same page. Confirm an account row appears with providerId="github".
  • From any agent in that org, invoke GITHUB_CREATE_ISSUE against the repo. Verify the issue on github.com is opened by your user, not Decobot-dev.
  • Delete the account row and try again. Verify the tool throws GIT_PROVIDER_USER_LINK_REQUIRED instead of silently using anyone else's token.
  • Publish an event-bus event whose handler invokes GITHUB_COMMENT. Verify the comment shows as Decobot-dev with the workflow footer.

🤖 Generated with Claude Code


Summary by cubic

Adds native GitHub integration via the Decobot App and a first‑class Git Provider subsystem. Fixes identity attribution so GitHub actions run as the calling user; unattended runs use Decobot.

  • New Features

    • Replaces shared‑token deco/mcp-github with a Git Provider backed by the Decobot GitHub App.
    • Correct identity: user calls use user-to-server tokens; unattended uses Decobot with a workflow footer; unlinked users get GIT_PROVIDER_USER_LINK_REQUIRED with a link URL.
    • New storage git_provider_installations (migration 093); no secrets stored, tokens minted on demand and cached.
    • Management tools: GIT_PROVIDERS_LIST, GIT_PROVIDER_INSTALL_URL, GIT_PROVIDER_INSTALL_COMPLETE, GIT_PROVIDER_INSTALLATION_LIST, GIT_PROVIDER_INSTALLATION_DELETE, GIT_PROVIDER_USER_LINK_STATUS.
    • Native GitHub tools routed through the factory: read file, list repo contents, list/read PRs, create issue, comment.
    • Better Auth falls back to DECOBOT_CLIENT_ID/DECOBOT_CLIENT_SECRET for the per-user GitHub link flow.
    • Settings → Git Providers UI (install dialog, installations list, user link card) and /oauth/callback/git-provider.
    • Tests for RS256 App JWT, installation-token cache, and the four-quadrant identity logic.
  • Migration

    • Set DECOBOT_APP_ID, DECOBOT_PRIVATE_KEY or DECOBOT_PRIVATE_KEY_BASE64, DECOBOT_CLIENT_ID, DECOBOT_CLIENT_SECRET, DECOBOT_APP_SLUG.
    • Run DB migration 093 and restart.
    • In Settings → Git Providers, install Decobot and select repos.
    • Have users link their personal GitHub; otherwise user-initiated calls will error.
    • Legacy deco/mcp-github and GITHUB_LIST_USER_ORGS remain available for now.

Written for commit d7eb702. Summary will update on new commits. Review in cubic

Replaces the legacy shared-token `deco/mcp-github` MCP connection (where
every org member acted on GitHub as the connection creator) with a first-
class Git Provider subsystem that gives each Studio user proper GitHub
identity attribution.

Key pieces:

- Decobot GitHub App credentials read from DECOBOT_APP_ID, DECOBOT_PRIVATE_KEY
  (PEM or DECOBOT_PRIVATE_KEY_BASE64), DECOBOT_CLIENT_ID, DECOBOT_CLIENT_SECRET,
  DECOBOT_APP_SLUG. Self-hosters bring their own App; the adapter reports
  available=false when env is missing.

- New `git_provider_installations` table (migration 093) recording which
  GitHub accounts each org has installed Decobot on. Holds no secrets —
  installation tokens are short-lived and minted on demand.

- `GitProviderFactory.resolveClient(ctx, { owner })` implements the
  four-quadrant identity matrix:
    real user + linked GitHub → user-to-server token (action attributed
                                to the calling user on GitHub)
    real user + unlinked      → throws GitProviderUserLinkRequiredError
                                with a CTA URL (no silent bot fallback)
    no user (cron, event-bus) → installation token (Decobot), with a
                                "via Decobot — Studio request <id>" footer
                                on write payloads

- Better Auth `socialProviders.github` falls back to DECOBOT_CLIENT_ID/
  CLIENT_SECRET so the user-link flow works out of the box.

- New management tools: GIT_PROVIDERS_LIST, GIT_PROVIDER_INSTALL_URL,
  GIT_PROVIDER_INSTALL_COMPLETE, GIT_PROVIDER_INSTALLATION_LIST,
  GIT_PROVIDER_INSTALLATION_DELETE, GIT_PROVIDER_USER_LINK_STATUS.

- New native GitHub tools, always registered in CORE_TOOLS, routed through
  the factory: GITHUB_READ_FILE, GITHUB_LIST_REPO_CONTENTS,
  GITHUB_CREATE_ISSUE, GITHUB_COMMENT, GITHUB_LIST_PRS, GITHUB_READ_PR.

- Settings → Git Providers UI: install dialog (popup + postMessage),
  installations list, per-user link card. New /oauth/callback/git-provider
  route handles the GitHub App install redirect.

- Tests cover RS256 App JWT signing (signature verified against a
  generated keypair), installation-token cache TTL + concurrent-refresh
  dedup, and the full four-quadrant matrix in resolveClient.

Legacy `deco/mcp-github` connection and `GITHUB_LIST_USER_ORGS` left in
place for now; deprecation is a follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

🧪 Benchmark

Should we run the Virtual MCP strategy benchmark for this PR?

React with 👍 to run the benchmark.

Reaction Action
👍 Run quick benchmark (10 & 128 tools)

Benchmark will run on the next push after you react.

@github-actions

Copy link
Copy Markdown
Contributor

Release Options

Suggested: Minor (2.350.0) — based on feat: prefix

React with an emoji to override the release type:

Reaction Type Next Version
👍 Prerelease 2.349.4-alpha.1
🎉 Patch 2.349.4
❤️ Minor 2.350.0
🚀 Major 3.0.0

Current version: 2.349.3

Note: If multiple reactions exist, the smallest bump wins. If no reactions, the suggested bump is used (default: patch).

@cubic-dev-ai cubic-dev-ai Bot 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.

2 issues found

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/mesh/src/tools/github/list-repo-contents.ts">

<violation number="1" location="apps/mesh/src/tools/github/list-repo-contents.ts:24">
P2: Require non-empty `owner` and `repo` in the input schema so invalid GitHub requests are rejected early.

(Based on your team's feedback about validating string fields strictly.) [FEEDBACK_USED]</violation>
</file>

<file name="apps/mesh/migrations/093-git-provider-installations.ts">

<violation number="1" location="apps/mesh/migrations/093-git-provider-installations.ts:32">
P2: `organization_id + provider_id + account_login` should be unique to prevent duplicate installation rows for the same account and ambiguous account-based lookups.</violation>
</file>

Tip: cubic used a learning from your PR history. Let your coding agent read cubic learnings directly with the cubic MCP.

Re-trigger cubic

openWorldHint: true,
},
inputSchema: z.object({
owner: z.string(),

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.

P2: Require non-empty owner and repo in the input schema so invalid GitHub requests are rejected early.

(Based on your team's feedback about validating string fields strictly.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/tools/github/list-repo-contents.ts, line 24:

<comment>Require non-empty `owner` and `repo` in the input schema so invalid GitHub requests are rejected early.

(Based on your team's feedback about validating string fields strictly.) </comment>

<file context>
@@ -0,0 +1,78 @@
+    openWorldHint: true,
+  },
+  inputSchema: z.object({
+    owner: z.string(),
+    repo: z.string(),
+    path: z.string().default("").describe("Directory path (empty = root)."),
</file context>

.execute();

await db.schema
.createIndex("idx_git_provider_installations_org_account")

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.

P2: organization_id + provider_id + account_login should be unique to prevent duplicate installation rows for the same account and ambiguous account-based lookups.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/migrations/093-git-provider-installations.ts, line 32:

<comment>`organization_id + provider_id + account_login` should be unique to prevent duplicate installation rows for the same account and ambiguous account-based lookups.</comment>

<file context>
@@ -0,0 +1,47 @@
+    .execute();
+
+  await db.schema
+    .createIndex("idx_git_provider_installations_org_account")
+    .on("git_provider_installations")
+    .columns(["organization_id", "provider_id", "account_login"])
</file context>

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