Skip to main content

GitHub Source Connector Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Make GitHub a client-connectable Source: org admins authorize via OAuth on the Sources page, the token lives in Nango, and the already-shipped github_read/github_write tools consume the org's connection.

Architecture: Add 'github' to the existing Nango-provider registries (NANGO_CONNECT_PROVIDERS, NANGO_BACKED_TYPES, CHANNEL_REQUIRED_KEYS, SOURCE_TOOLS_BY_PROVIDER) โ€” every downstream surface (Connect session/confirm, delete/revoke, test, Sources read model, connect-token flow, permission guard) keys off these sets and picks GitHub up automatically. No migration (integration_type is varchar), no executor changes (the github_read Nango path and public fallback already ship), no new UI components (the Sources tile renders from the API response).

Tech Stack: NestJS (api), Jest, Next.js 16 + React 19 (frontend), simple-icons, self-hosted Nango v0.71.

Spec: docs/superpowers/specs/2026-08-03-github-source-connector-design.md

Worktree/branch: .claude/worktrees/github-source-connector, feat/github-source-connector off origin/dev. All commands below run from the worktree root.


File mapโ€‹

FileActionResponsibility
api/src/integrations/credentials/__tests__/credentials.github-provider.spec.tsCreatePins the github Connect-session contract (incl. NO user_scopes) + confirm binding
api/src/integrations/credentials/__tests__/credentials.delete.spec.tsModifyPins GitHub Nango revoke-before-local-delete behavior
api/src/integrations/credentials/credentials.dto.tsModifyChannelType union + NANGO_CONNECT_PROVIDERS
api/src/integrations/credentials/credentials.service.tsModifyCHANNEL_REQUIRED_KEYS, NANGO_BACKED_TYPES, validateNangoProvider
api/src/integrations/credentials/sources.service.tsModifyTile gating tools + display name
api/src/integrations/tool-permissions/tool-permissions.guard.spec.tsModifyPins GitHub connect/confirm permission gate against the two-tool any-enabled mapping
frontend/src/lib/api.tsModifySourceInfo.provider union + source-provider classifier
frontend/src/lib/__tests__/api.test.tsModifyPins Nango helper route selection for source-provider vs org-id calls
frontend/src/app/client/settings/sources/page.tsxModifysiGithub brand icon + tile hint
frontend/src/app/client/settings/sources/__tests__/page.github.test.tsxCreateRenders GitHub tile and pins org-scoped Sources-page Connect flow

Existing loop-driven guards that extend automatically (run, never edit): credentials.nango-provider-lockstep.spec.ts, sources.service.spec.ts. tool-permissions.guard.spec.ts also pins the body-provider permission gate for multi-tool providers, including GitHub. credentials.delete.spec.ts pins GitHub disconnect/revoke semantics because Nango-backed deletes are fail-closed on provider revoke.


Task 1: Backend provider registration (github in the Nango sets)โ€‹

Files:

  • Test (create): api/src/integrations/credentials/__tests__/credentials.github-provider.spec.ts

  • Modify: api/src/integrations/credentials/credentials.dto.ts:20-38 (ChannelType), :124-132 (NANGO_CONNECT_PROVIDERS)

  • Modify: api/src/integrations/credentials/credentials.service.ts:56-86 (CHANNEL_REQUIRED_KEYS), :89-97 (NANGO_BACKED_TYPES), :286-301 (validateNangoProvider)

  • Step 1: Write the failing spec

Create api/src/integrations/credentials/__tests__/credentials.github-provider.spec.ts. The harness is copied verbatim from credentials.slack-search-provider.spec.ts (same constructor arg order: credRepo, orgRepo, slackBindingRepo, nangoClient, telegramOnboarding, featureFlags, rlsSessionContext, auditService โ€” passed as 9 positional args below):

import { BadRequestException } from "@nestjs/common";
import { CredentialsService } from "../credentials.service";

// Harness mirrors credentials.slack-search-provider.spec.ts. Constructor arg order:
// (credRepo, orgRepo, slackBindingRepo, nangoClient, telegramOnboarding, featureFlags, rls, audit)
function makeService(overrides: { credRepo?: any } = {}) {
const createConnectSession = jest.fn().mockResolvedValue({ token: "sess-token" });
const getConnection = jest.fn().mockResolvedValue({
end_user: { id: "u1", organization: { id: "org-1" } },
});
const deleteConnection = jest.fn().mockResolvedValue(undefined);
const nangoClient = { createConnectSession, getConnection, deleteConnection } as any;
const orgRepo = { findOne: jest.fn().mockResolvedValue({ id: "org-1", slug: "acme" }) } as any;
const credRepo = overrides.credRepo ?? {
findOne: jest.fn().mockResolvedValue(null),
create: jest.fn((x: any) => x),
save: jest.fn(async (x: any) => ({ id: "cred-1", ...x })),
};
const svc = new CredentialsService(
credRepo, orgRepo, {} as any, {} as any, nangoClient,
{ setup: jest.fn() } as any, { isEnabled: jest.fn() } as any,
{ setLocalContext: jest.fn() } as any, { tryLog: jest.fn() } as any,
);
return { svc, createConnectSession, getConnection, deleteConnection, credRepo };
}

describe("CredentialsService โ€” github Nango provider", () => {
it("creates a Connect session for github (no pinned connection id โ€” Nango generates it)", async () => {
const { svc, createConnectSession } = makeService();
const res = await svc.createNangoSession("org-1", { userId: "u1", email: "a@b.com" }, { provider: "github" } as any);
expect(res.sessionToken).toBe("sess-token");
expect(res).not.toHaveProperty("connectionId");
expect(createConnectSession).toHaveBeenCalledWith(
expect.objectContaining({
end_user: { id: "u1", email: "a@b.com" },
organization: expect.objectContaining({ id: "org-1" }),
allowed_integrations: ["github"],
}),
);
});

it("sends NO integrations_config_defaults โ€” GitHub's `repo` scope lives on the Nango integration, not the session", async () => {
// The user/bot scope split is Slack-specific: GitHub OAuth scopes ride the
// provider-level scope= param, configured on the Nango integration itself.
// This is the inverse of the slack_search/slack_archive assertions and
// pins the design decision (spec 2026-08-03, Decisions table).
const { svc, createConnectSession } = makeService();
await svc.createNangoSession("org-1", { userId: "u1" }, { provider: "github" } as any);
const call = createConnectSession.mock.calls[0][0];
expect(call).not.toHaveProperty("integrations_config_defaults");
});

it("persists a github credential row on confirm when the connection's end_user.organization matches this org", async () => {
const { svc, credRepo } = makeService();
await svc.confirmNangoConnection("org-1", { provider: "github", nango_connection_id: "generated-id-1" } as any);
expect(credRepo.create).toHaveBeenCalledWith(expect.objectContaining({ orgId: "org-1", integrationType: "github" }));
});

it("rejects confirm when the connection's end_user.organization belongs to a different org", async () => {
const getConnection = jest.fn().mockResolvedValue({
end_user: { id: "u1", organization: { id: "some-other-org" } },
});
const { svc } = makeService();
(svc as any).nangoClient.getConnection = getConnection;
await expect(
svc.confirmNangoConnection("org-1", { provider: "github", nango_connection_id: "generated-id-1" } as any),
).rejects.toBeInstanceOf(BadRequestException);
});
});
  • Step 2: Run the spec to verify it fails

Run: cd api && npx jest src/integrations/credentials/__tests__/credentials.github-provider.spec.ts --runInBand Expected: FAIL โ€” validateNangoProvider throws BadRequestException: Invalid channel type: github (github is not in CHANNEL_REQUIRED_KEYS), all 4 cases.

  • Step 3: Widen the DTO

In api/src/integrations/credentials/credentials.dto.ts:

Add to the ChannelType union after | 'google_workspace' (line 35):

| 'google_workspace'
| 'github'

Add to NANGO_CONNECT_PROVIDERS after 'google_workspace', (line 129):

'google_workspace',
'github',
  • Step 4: Widen the service

In api/src/integrations/credentials/credentials.service.ts:

CHANNEL_REQUIRED_KEYS โ€” insert before jumio (line 85):

// GitHub Source connector: OAuth App token lives in Nango; only
// nango_connection_id is stored locally. Feeds BOTH github_read (Nango
// proxy + public-repo fallback) and github_write (first-party server, same
// connection). Spec: docs/superpowers/specs/2026-08-03-github-source-connector-design.md
github: [],

NANGO_BACKED_TYPES โ€” add after "google_workspace", (line 94):

"google_workspace",
"github",

validateNangoProvider (lines 286-301) โ€” add "github" to BOTH Extract<...> unions (after "google_workspace") and to the error string:

private validateNangoProvider(
provider: string,
): Extract<
ChannelType,
"quickbooks" | "xero" | "notion" | "google_drive" | "google_workspace" | "github" | "slack_search" | "slack_archive"
> {
const type = this.validateChannelType(provider);
if (!NANGO_BACKED_TYPES.has(type)) {
throw new BadRequestException(
`Invalid Nango provider: ${provider}. Valid providers: quickbooks, xero, notion, google_drive, google_workspace, github, slack_search, slack_archive`,
);
}
return type as Extract<
ChannelType,
"quickbooks" | "xero" | "notion" | "google_drive" | "google_workspace" | "github" | "slack_search" | "slack_archive"
>;
}

Do NOT touch NANGO_USER_SCOPES (the Slack-only map at line ~118) and do NOT add github to CONVERSATION_CHANNEL_TYPES.

  • Step 5: Run the new spec to verify it passes

Run: cd api && npx jest src/integrations/credentials/__tests__/credentials.github-provider.spec.ts --runInBand Expected: PASS (4 tests).

  • Step 6: Run the lockstep guard (it loops over the sets โ€” must pass untouched)

Run: cd api && npx jest src/integrations/credentials/__tests__/credentials.nango-provider-lockstep.spec.ts --runInBand Expected: PASS, with new auto-generated cases accepts provider "github" for both DTOs. If it FAILS with a set-difference assertion, Step 3/4 missed one of the two registries โ€” fix that, never the spec.

  • Step 7: Commit
git add api/src/integrations/credentials/credentials.dto.ts api/src/integrations/credentials/credentials.service.ts api/src/integrations/credentials/__tests__/credentials.github-provider.spec.ts
git commit -m "feat(integrations): github joins the Nango connect/backed provider sets"

Task 2: Sources page read model (tile gating + display name)โ€‹

Files:

  • Modify: api/src/integrations/credentials/sources.service.ts:27-48

  • Test (existing, unmodified): api/src/integrations/credentials/__tests__/sources.service.spec.ts

  • Step 1: Run the map-completeness guard to verify it now fails

Run: cd api && npx jest src/integrations/credentials/__tests__/sources.service.spec.ts --runInBand Expected: FAIL โ€” maps every NANGO_CONNECT_PROVIDER to โ‰ฅ1 gating tool (map completeness guard) โ€” SOURCE_TOOLS_BY_PROVIDER keys no longer equal NANGO_CONNECT_PROVIDERS (Task 1 added github to the set). This is the designed tripwire.

Note: TypeScript already fails compilation here too (Record<NangoConnectProvider, ...> missing the github key) โ€” same signal, either proof is fine.

  • Step 2: Add github to both maps

In api/src/integrations/credentials/sources.service.ts:

SOURCE_TOOLS_BY_PROVIDER โ€” add after the google_workspace entry (line 34):

google_workspace: ['google_docs_search', 'google_calendar_read', 'gmail_search'],
// github_read and github_write consume the SAME connection (the first-party
// write server resolves the identical integrationType:'github' row), so
// either tool being SA-enabled shows the tile โ€” same any-enabled rule as
// google_workspace's three tools.
github: ['github_read', 'github_write'],

SOURCE_DISPLAY_NAME โ€” add after google_workspace: 'Google Workspace', (line 47):

google_workspace: 'Google Workspace',
github: 'GitHub',
  • Step 3: Run the sources spec to verify it passes

Run: cd api && npx jest src/integrations/credentials/__tests__/sources.service.spec.ts --runInBand Expected: PASS.

  • Step 4: Run the neighboring suites the sets feed

Run: cd api && npx jest src/integrations/credentials src/integrations/connect-tokens src/integrations/tool-permissions --runInBand Expected: PASS. These consume NANGO_CONNECT_PROVIDERS / SOURCE_TOOLS_BY_PROVIDER directly (connect-token mint, permission guard) โ€” any failure here means a hardcoded provider list drifted; fix the production code, not the spec.

  • Step 5: Commit
git add api/src/integrations/credentials/sources.service.ts
git commit -m "feat(sources): GitHub tile โ€” gated on github_read/github_write, display name"

Task 3: Frontend (SourceInfo union + tile icon/hint)โ€‹

Files:

  • Modify: frontend/src/lib/api.ts:2571-2584 (SourceInfo)

  • Modify: frontend/src/app/client/settings/sources/page.tsx:32 (import), :40-52 (SOURCE_ICONS), :57-64 (SOURCE_HINTS)

  • Step 1: Widen SourceInfo.provider

In frontend/src/lib/api.ts, the union mirrors NANGO_CONNECT_PROVIDERS (the comment above it says update together). Add the missing google_workspace entry and github, and keep NANGO_SOURCE_PROVIDERS in lockstep so one-argument source-provider calls route through the authenticated /v1 source endpoints:

provider:
| "slack_search"
| "slack_archive"
| "quickbooks"
| "xero"
| "notion"
| "google_drive"
| "google_workspace"
| "github";
  • Step 2: Add the tile icon + hint

In frontend/src/app/client/settings/sources/page.tsx:

Extend the simple-icons import (line 32):

import { siNotion, siGoogledrive, siQuickbooks, siXero, siGithub } from "simple-icons";

SOURCE_ICONS โ€” add after the xero entry (line 51):

xero: { brandIcon: { path: siXero.path, hex: siXero.hex } },
github: { brandIcon: { path: siGithub.path, hex: siGithub.hex } },

SOURCE_HINTS โ€” add after the slack_archive entry (line 63):

github:
"Lets your Specialist read issues, pull requests, and commits from " +
"your GitHub repositories, including private ones.",
  • Step 3: Type-check + targeted tests

Run: cd frontend && npx tsc --noEmit Expected: clean exit (0). If tsc isn't wired standalone, npm run build is the fallback proof.

Run: cd frontend && npx jest src/lib/__tests__/api.test.ts src/app/client/settings/sources/__tests__/page.github.test.tsx --runInBand --runTestsByPath Expected: PASS (covers the Nango helper overloads for google_workspace/github source-provider routing, the org-scoped GitHub card route, and the Sources-page GitHub tile flow).

  • Step 4: Commit
git add frontend/src/lib/api.ts frontend/src/lib/__tests__/api.test.ts frontend/src/app/client/settings/sources/page.tsx frontend/src/app/client/settings/sources/__tests__/page.github.test.tsx
git commit -m "feat(frontend): GitHub source tile โ€” provider union, brand icon, hint"

Task 4: Full verification + PRโ€‹

  • Step 1: API โ€” run every suite that touches the provider sets in one pass

Run: cd api && npx jest src/integrations src/agent-api src/runtime-control-plane --runInBand Expected: PASS. Notably tool-registry / runtime-tool-executor specs (the connect_integration tool enumerates NANGO_CONNECT_PROVIDERS in its JSON schema โ€” github now appears there by construction) and tool-permissions.guard.

  • Step 2: Frontend build

Run: cd frontend && npm run build Expected: clean build.

  • Step 3: Push and open the PR (do NOT merge โ€” standing user rule)
git push -u origin feat/github-source-connector
gh pr create --base dev --title "feat: GitHub source connector (Nango-brokered)" --body-file - <<'EOF'
Adds `github` to the Nango connect/backed provider sets so orgs connect GitHub
from the Sources page; the shipped `github_read`/`github_write` tools then use
the org's connection instead of script-imported ones or the public fallback.

Spec: docs/superpowers/specs/2026-08-03-github-source-connector-design.md

- No migration (integration_type is varchar), no executor changes.
- Tile gated on ANY of github_read/github_write (one connection feeds both).
- No NANGO_USER_SCOPES entry: GitHub's `repo` scope is configured on the
Nango integration (dashboard), the user/bot scope split is Slack-specific.
- Also closes the frontend provider-mirror drift: `google_workspace` is now in
`SourceInfo.provider` and the Nango source-provider classifier, with API
helper tests pinning `/v1` source-route selection and a Sources-page GitHub
tile test pinning the org-scoped Connect flow.
- GitHub disconnect is covered: Nango revoke uses the stored generated
connection id, runs before local row removal, and leaves the credential intact
when provider revoke fails.

Out-of-repo follow-up (owner: alexdel): GitHub OAuth App + Nango dashboard
integration (key `github`, scope `repo`), then SA-enable github_read/github_write
for the pilot org. Smoke: connect โ†’ private-repo github_read โ†’ disconnect revokes.
EOF

Expected: PR URL printed. STOP โ€” await user approval before any merge.


Out-of-repo handoff (user, after PR)โ€‹

  1. GitHub OAuth App (org-owned), callback = Nango's callback URL.
  2. Nango dashboard: integration key github, provider github, client id/secret, scope repo.
  3. SuperAdmin enables github_read (+ optionally github_write) for the pilot org.
  4. Rendered-UI smoke on dev against the Railway-hosted Nango: connect โ†’ tile shows connected + scopes โ†’ github_read on a private repo โ†’ disconnect revokes at Nango.