paperclipai/paperclip · error

Managed GitHub connection is unavailable

Error message

Managed GitHub connection is unavailable

What it means

During git credential resolution, Paperclip checks the managed GitHub identity first. If a managed GitHub connection is configured (selected as the identity source) but a usable credential cannot be produced, `resolveManagedGitHubCredential` returns `configured: true` with an `error` message instead of a credential. `resolveCredential` then fails closed and throws this error rather than silently falling back to legacy company secrets or the server's GITHUB_TOKEN/GH_TOKEN environment variables.

Source

Thrown at server/src/services/git-credentials.ts:238

    secrets?: GitCredentialSecretsDeps;
    env?: NodeJS.ProcessEnv;
    secretNames?: readonly string[];
  },
): GitRemoteAuthProvider {
  const secrets: GitCredentialSecretsDeps = deps?.secrets ?? secretService(db);
  const env = deps?.env ?? process.env;
  const secretNames = deps?.secretNames ?? DEFAULT_GITHUB_TOKEN_SECRET_NAMES;
  let credentialPromise: Promise<GitCredential | null> | null = null;

  const resolveCredential = async (): Promise<GitCredential | null> => {
    // Unit callers historically pass a null DB through the typed test seam. Production
    // always supplies a real DB and therefore always checks managed identities before
    // considering legacy secrets or process environment credentials.
    const managed = db
      ? await resolveManagedGitHubCredential(db, secrets, companyId, context ?? {})
      : { configured: false as const };
    if (managed.configured) {
      if (!managed.credential) throw new Error(managed.error ?? "Managed GitHub connection is unavailable");
      return managed.credential;
    }
    for (const secretName of secretNames) {
      const secret = await Promise.resolve(secrets.getByName(companyId, secretName)).catch(() => null);
      if (!secret) continue;
      // A resolution failure (inactive secret, provider outage) records its own failure audit
      // event; fall through to the next source instead of failing the whole git operation here.
      const token = await secrets
        .resolveSecretValue(companyId, secret.id, "latest", {
          accessContext: {
            consumerType: "system",
            consumerId: "workspace-git-credential",
            actorType: "system",
            issueId: context?.issueId ?? null,
            heartbeatRunId: context?.heartbeatRunId ?? null,
            responsibleUserId: context?.responsibleUserId ?? null,
          },
        })

View on GitHub (pinned to 01ad858492)

Solutions

  1. Inspect the thrown message / `managed.error`: it carries the specific reason (unauthorized member, incomplete identity, no repository access, unresolvable personal credential) and fix that underlying condition.
  2. Reconnect the managed GitHub connection: reinstall the GitHub App on the organization/repositories and re-authorize the OAuth grant so a fresh access token exists.
  3. If the identity is a personal (user) connection, ensure the owner is an active company member with a non-viewer role and that their GitHub user secret is still present and resolvable.
  4. If managed identity is not wanted, disconnect/remove the managed GitHub connection for the company so resolution falls through to company secrets or GITHUB_TOKEN/GH_TOKEN env credentials.
  5. For self-hosted operators, as a non-managed fallback, set GITHUB_TOKEN or GH_TOKEN in the server process environment or store the token as a company secret with one of the well-known names.

Example fix

// before: managed GitHub App uninstalled, resolution fails closed
const provider = createGitRemoteAuthProvider(db, companyId, { issueId });
const credential = await provider(remoteUrl); // throws 'Managed GitHub connection is unavailable'

// after: operator reinstalls/re-authorizes the GitHub App (or disconnects it so env token applies)
// env: GITHUB_TOKEN=ghp_...
const credential = await provider(remoteUrl); // returns { token, source: "server_env" }
Defensive patterns

Strategy: fallback

Validate before calling

const managed = await resolveManagedGitHubCredential(db, secretService(db), companyId, { issueId });
if (managed.configured && !managed.credential) {
  console.error("Managed GitHub identity unusable:", managed.error); // fix before any git op
}

Type guard

function hasManagedCredential(m: { configured: boolean; credential?: unknown }): m is { configured: true; credential: NonNullable<unknown> } {
  return m.configured && m.credential != null;
}

Try / catch

try {
  const credential = await provider(remoteUrl);
} catch (err) {
  if (err instanceof Error && err.message.includes("Managed GitHub connection")) {
    // alert operator to reconnect the GitHub App; do not silently fall back
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling `createGitRemoteAuthProvider(db, companyId, context)(remoteUrl)` (or `resolveCredential`) when: (1) a managed GitHub connection exists for the company but the OAuth grant is missing/expired and refresh via `refreshOAuthGrantCredentials` fails; (2) the identity owner is not an active non-viewer company member; (3) the grant's credential secret ref for `oauth.access_token` or the GitHub tenant record is missing ('identity is incomplete'); (4) the GitHub app installation or repository access was revoked (installationCount/repositoryCount < 1); (5) the personal credential secret cannot be resolved.

Common situations: A GitHub App installation was uninstalled from the org; the user who owns a personal GitHub connection left the company or was downgraded to viewer; the OAuth token expired and the refresh token was revoked; the connection was partially deleted leaving no access_token secret ref; an operator configured a managed connection expecting secret fallback that intentionally never happens (fails closed by design).

Related errors


AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10). Data as JSON: /api/errors/a1eea9a05a3b1a82. Report an issue: GitHub.