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โ
| File | Action | Responsibility |
|---|---|---|
api/src/integrations/credentials/__tests__/credentials.github-provider.spec.ts | Create | Pins the github Connect-session contract (incl. NO user_scopes) + confirm binding |
api/src/integrations/credentials/__tests__/credentials.delete.spec.ts | Modify | Pins GitHub Nango revoke-before-local-delete behavior |
api/src/integrations/credentials/credentials.dto.ts | Modify | ChannelType union + NANGO_CONNECT_PROVIDERS |
api/src/integrations/credentials/credentials.service.ts | Modify | CHANNEL_REQUIRED_KEYS, NANGO_BACKED_TYPES, validateNangoProvider |
api/src/integrations/credentials/sources.service.ts | Modify | Tile gating tools + display name |
api/src/integrations/tool-permissions/tool-permissions.guard.spec.ts | Modify | Pins GitHub connect/confirm permission gate against the two-tool any-enabled mapping |
frontend/src/lib/api.ts | Modify | SourceInfo.provider union + source-provider classifier |
frontend/src/lib/__tests__/api.test.ts | Modify | Pins Nango helper route selection for source-provider vs org-id calls |
frontend/src/app/client/settings/sources/page.tsx | Modify | siGithub brand icon + tile hint |
frontend/src/app/client/settings/sources/__tests__/page.github.test.tsx | Create | Renders 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)โ
- GitHub OAuth App (org-owned), callback = Nango's callback URL.
- Nango dashboard: integration key
github, providergithub, client id/secret, scoperepo. - SuperAdmin enables
github_read(+ optionallygithub_write) for the pilot org. - Rendered-UI smoke on dev against the Railway-hosted Nango: connect โ tile shows connected + scopes โ
github_readon a private repo โ disconnect revokes at Nango.