import { companySecrets, heartbeatRuns, companyMemberships, connectionGrantDelegations, connectionGrants, toolConnectionInstalls, toolConnections, userSecretDefinitions, type Db, } from "@paperclipai/db"; import { and, eq, inArray, or } from "drizzle-orm"; import { isGitHubDotCom } from "./github-fetch.js"; import { secretService } from "./secrets.js"; import { toolAccessService } from "./tool-access.js"; /** * Server-side git credentials for managed project checkouts and execution-workspace base * refreshes. Operators store a GitHub token as a company secret under one of the well-known * names below (the same convention the GitHub external-object provider reads); this module * resolves it and turns it into a git invocation that authenticates clone/fetch against * github.com over HTTPS without ever placing the token in argv, URLs, or on disk. * * The provider factory is deliberately the single seam for future credential sources (for * example a brokered GitHub connection): swap the factory, keep every call site unchanged. */ /** Company-secret names probed for a GitHub token, in priority order. */ export const DEFAULT_GITHUB_TOKEN_SECRET_NAMES = ["GITHUB_TOKEN", "GH_TOKEN", "PAPERCLIP_GITHUB_TOKEN"] as const; /** Env var the credential helper reads the token from; never appears in argv. */ export const GIT_CREDENTIAL_TOKEN_ENV_KEY = "PAPERCLIP_GIT_TOKEN"; // `!`-prefixed helpers run via `sh -c` with the credential action appended as "$1". Only the // `get` action answers; store/erase drain stdin and exit 0 silently. `x-access-token` // authenticates classic PATs, fine-grained PATs, and GitHub App installation tokens alike. // // The helper re-validates the credential request from its stdin description and answers only // for `protocol=https` + `host=github.com`/`www.github.com`. The pre-invocation URL check // runs before git applies configuration like repository-local `url..insteadOf` // rewrites, so a rewritten remote could otherwise request the token for an arbitrary host. // The helper is additionally installed URL-scoped (`credential.https://github.com.helper`) // so git does not consult it for other hosts in the first place — two independent gates. const GIT_CREDENTIAL_HELPER = `!f() { ok=; proto=; while IFS= read -r l && [ -n "$l" ]; do case "$l" in host=github.com|host=www.github.com) ok=1;; protocol=https) proto=1;; esac; done; if [ "$1" = get ] && [ -n "$ok" ] && [ -n "$proto" ]; then printf 'username=x-access-token\\npassword=%s\\n' "$PAPERCLIP_GIT_TOKEN"; fi; }; f`; export type GitCredential = { token: string; source: "managed_connection" | "company_secret" | "server_env"; /** The company-secret name the token came from; null for a server-environment token. */ secretName: string | null; githubIdentity?: { userId: string; login: string }; identitySource?: "personal" | "dedicated"; connectionId?: string; grantId?: string; }; /** A prepared, credential-bearing git invocation: config args plus the env that carries the token. */ export type GitAuthInvocation = { configArgs: string[]; env: Record; source: GitCredential["source"]; secretName: string | null; }; /** * Resolve auth for one remote URL. Returns null when the URL is out of scope (non-GitHub, * ssh, or already credentialed) or when no token is available — callers then run git with * ambient behavior, exactly as before this module existed. */ export type GitRemoteAuthProvider = (remoteUrl: string) => Promise; /** * True only for `https://github.com/...` (or `www.`) URLs without inline userinfo. GHES and * other hosts are out of scope for now — sending a github.com token to an arbitrary host * would leak it, and an operator's inline URL credential must never be overridden. */ export function isGitHubHttpsRemoteUrl(remoteUrl: string): boolean { let parsed: URL; try { parsed = new URL(remoteUrl); } catch { return false; } if (parsed.protocol !== "https:") return false; if (parsed.username || parsed.password) return false; return isGitHubDotCom(parsed.hostname); } function isSupportedGitHubRemoteUrl(remoteUrl: string): boolean { if (isGitHubHttpsRemoteUrl(remoteUrl)) return true; if (/^git@(?:www\.)?github\.com:[^\s]+$/i.test(remoteUrl)) return true; try { const parsed = new URL(remoteUrl); return parsed.protocol === "ssh:" && parsed.username === "git" && !parsed.password && isGitHubDotCom(parsed.hostname); } catch { return false; } } /** * Mask credential material embedded in URLs so it never reaches warnings, run errors, or * persisted payloads: userinfo on any scheme (`https://user:token@host`, * `ssh://user:pass@host`) and the entire query string of any URL (`?access_token=…` and * every other parameter — masked wholesale rather than by an inevitably incomplete * parameter-name list). Scp-style remotes (`git@host:path`) carry no password and are left * alone. */ export function scrubGitCredentialText(text: string): string { return text .replace(/([a-z][a-z0-9+.-]*:\/\/)[^/@\s]+@/gi, "$1***@") .replace(/([a-z][a-z0-9+.-]*:\/\/[^\s"'?]*)\?[^\s"']*/gi, "$1?***"); } export function buildGitAuthInvocation(credential: GitCredential): GitAuthInvocation { const identity = credential.githubIdentity; const noreplyEmail = identity ? `${identity.userId}+${identity.login}@users.noreply.github.com` : null; const configEntries = [ ["credential.helper", ""], ["credential.https://github.com.helper", GIT_CREDENTIAL_HELPER], ["credential.https://www.github.com.helper", GIT_CREDENTIAL_HELPER], ["url.https://github.com/.insteadOf", "git@github.com:"], ["url.https://github.com/.insteadOf", "ssh://git@github.com/"], ["url.https://github.com/.insteadOf", "git@www.github.com:"], ["url.https://github.com/.insteadOf", "ssh://git@www.github.com/"], ...(identity ? [ ["user.name", identity.login], ["user.email", noreplyEmail!], ] : []), ]; return { // The leading empty helper clears ambient helpers (gh, osxkeychain, credential-store) so // they neither outrank the resolved token nor receive store/erase callbacks for it. The // token helper is installed URL-scoped: git consults it only for credential requests // whose context matches github.com over https, so an `insteadOf`-rewritten remote never // reaches it (and the helper itself re-checks the request host — see above). configArgs: [ "-c", "credential.helper=", "-c", `credential.https://github.com.helper=${GIT_CREDENTIAL_HELPER}`, "-c", `credential.https://www.github.com.helper=${GIT_CREDENTIAL_HELPER}`, ], env: { [GIT_CREDENTIAL_TOKEN_ENV_KEY]: credential.token, GH_TOKEN: credential.token, GITHUB_TOKEN: credential.token, GIT_TERMINAL_PROMPT: "0", ...(identity ? { GIT_AUTHOR_NAME: identity.login, GIT_AUTHOR_EMAIL: noreplyEmail!, GIT_COMMITTER_NAME: identity.login, GIT_COMMITTER_EMAIL: noreplyEmail!, } : {}), GIT_CONFIG_COUNT: String(configEntries.length), ...Object.fromEntries(configEntries.flatMap(([key, value], index) => [ [`GIT_CONFIG_KEY_${index}`, key], [`GIT_CONFIG_VALUE_${index}`, value], ])), }, source: credential.source, secretName: credential.secretName, }; } const GIT_AUTH_FAILURE_PATTERN = /authentication failed|could not read username|could not read password|invalid username or password|terminal prompts disabled|repository not found|not accessible|permission denied|HTTP 40[13]|The requested URL returned error: 40[13]/i; /** * Turn a failed git network operation into an actionable suffix for the error message. * Returns null when the failure does not look auth-related — a credential that was merely * present during an unrelated failure (network outage, target-path collision) must not be * blamed for it. */ export function describeGitAuthFailure(input: { error: string; used: { source: GitCredential["source"]; secretName: string | null } | null; }): string | null { if (!GIT_AUTH_FAILURE_PATTERN.test(input.error)) { return null; } if (input.used) { const label = input.used.secretName ? `the ${input.used.secretName} company-secret GitHub credential` : input.used.source === "managed_connection" ? "the resolved GitHub connection" : "the server-environment GitHub credential"; return `The operation authenticated with ${label}, which was rejected or lacks access to this repository.`; } return "No GitHub credential is configured — add a GITHUB_TOKEN or GH_TOKEN company secret in Settings → Secrets, or configure a local checkout cwd for this project workspace."; } type SecretServiceLike = ReturnType; type GitCredentialSecretsDeps = { getByName: ( companyId: string, name: string, ) => Promise<{ id: string } | null | undefined> | ReturnType; resolveSecretValue: SecretServiceLike["resolveSecretValue"]; resolveUserSecretValue?: SecretServiceLike["resolveUserSecretValue"]; }; /** * Build the credential provider for one run. Resolution order: the managed GitHub identity * resolver, then a company secret by well-known name, then the server process environment * (`GITHUB_TOKEN`/`GH_TOKEN`) for self-hosted operators. A configured managed identity fails * closed instead of falling through to legacy credentials. The lookup is memoized per * provider instance so one run performs at most one secret resolution (and writes at most * one audit event) no matter how many git operations it authenticates. */ export function createGitRemoteAuthProvider( db: Db, companyId: string, context?: { issueId?: string | null; heartbeatRunId?: string | null; responsibleUserId?: string | null; agentId?: string | null; }, deps?: { 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 | null = null; const resolveCredential = async (): Promise => { // 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, }, }) .then((value) => value.trim()) .catch(() => ""); if (token) return { token, source: "company_secret", secretName }; } const envToken = env.GITHUB_TOKEN?.trim() || env.GH_TOKEN?.trim() || ""; if (envToken) return { token: envToken, source: "server_env", secretName: null }; return null; }; return async (remoteUrl: string) => { if (!isSupportedGitHubRemoteUrl(remoteUrl)) return null; if (db && context?.heartbeatRunId && context.agentId) { const [run] = await db.select({ contextId: heartbeatRuns.activeIdentityContextId }).from(heartbeatRuns).where(and( eq(heartbeatRuns.id, context.heartbeatRunId), eq(heartbeatRuns.companyId, companyId), eq(heartbeatRuns.agentId, context.agentId), )); if (run?.contextId) { const { resolveGitHubOperationCredentials } = await import("./github-operation-credentials.js"); const result = await resolveGitHubOperationCredentials(db, { companyId, runId: context.heartbeatRunId, agentId: context.agentId, }); if (result.status === "absent") { const credential = await resolveCredential(); return credential ? buildGitAuthInvocation(credential) : null; } const anonymous = buildGitAuthInvocation({ token: "", source: "managed_connection", secretName: null }); return { ...anonymous, env: { ...anonymous.env, GIT_CONFIG_GLOBAL: "/dev/null", GIT_CONFIG_SYSTEM: "/dev/null", GIT_AUTHOR_NAME: "", GIT_AUTHOR_EMAIL: "", GIT_COMMITTER_NAME: "", GIT_COMMITTER_EMAIL: "", ...result.env, } }; } } credentialPromise ??= resolveCredential(); const credential = await credentialPromise; if (!credential) return null; return buildGitAuthInvocation(credential); }; } export async function resolveManagedGitHubIdentitySelection( db: Db, companyId: string, context: { responsibleUserId?: string | null; agentId?: string | null; allowStandingDelegation?: boolean; excludeGrantId?: string; }, ): Promise<{ configured: boolean; identitySource?: "personal" | "dedicated"; grant?: typeof connectionGrants.$inferSelect; error?: string; }> { const connections = await db.select().from(toolConnections).where(and( eq(toolConnections.companyId, companyId), )); const githubConnections = connections.filter((connection) => { const config = connection.config && typeof connection.config === "object" ? connection.config as Record : {}; const transportConfig = connection.transportConfig && typeof connection.transportConfig === "object" ? connection.transportConfig as Record : {}; return config.sourceTemplateKey === "github" || transportConfig.sourceTemplateKey === "github"; }); if (githubConnections.length === 0) return { configured: false }; const connectionIds = githubConnections.map((connection) => connection.id); const installs = await db.select().from(toolConnectionInstalls).where(and( eq(toolConnectionInstalls.companyId, companyId), inArray(toolConnectionInstalls.connectionId, connectionIds), )); const eligibleConnectionIds = new Set(githubConnections.filter((connection) => installs.some((install) => install.connectionId === connection.id && ( (install.targetType === "company" && install.targetId === companyId) || (install.targetType === "agent" && install.targetId === context.agentId) ) )).map((connection) => connection.id)); // A GitHub connection installed only for another agent is not configured for // this run. Treating the company-wide connection as configured here would // make unrelated agents fail before their adapter starts and would also // suppress their otherwise-eligible legacy credential fallback. if (eligibleConnectionIds.size === 0) return { configured: false }; const grants = await db.select().from(connectionGrants).where(and( eq(connectionGrants.companyId, companyId), inArray(connectionGrants.connectionId, [...eligibleConnectionIds]), or(eq(connectionGrants.kind, "agent"), eq(connectionGrants.kind, "user")), )); const dedicated = context.agentId ? grants.filter((grant) => grant.kind === "agent" && grant.subjectAgentId === context.agentId) : []; // Connections are already restricted above to the owner-selected install // targets. Within that consent boundary the server-resolved responsible user // is authoritative; standing delegation is only an ownerless-run fallback. const personal = context.responsibleUserId ? grants.filter((grant) => grant.kind === "user" && grant.subjectUserId === context.responsibleUserId) : []; const delegated = context.allowStandingDelegation !== false && !context.responsibleUserId && context.agentId ? await db.select({ grantId: connectionGrantDelegations.grantId }).from(connectionGrantDelegations).where(and( eq(connectionGrantDelegations.companyId, companyId), eq(connectionGrantDelegations.agentId, context.agentId), inArray(connectionGrantDelegations.grantId, grants.map((grant) => grant.id)), )).then((rows) => { const delegatedIds = new Set(rows.map((row) => row.grantId)); return grants.filter((grant) => grant.kind === "user" && delegatedIds.has(grant.id)); }) : []; const candidates = dedicated.length > 0 ? dedicated : personal.length > 0 ? personal : delegated; const identitySource = dedicated.length > 0 ? "dedicated" as const : "personal" as const; // Reconnecting can create another connection/grant for the same GitHub // account. Ambiguity is about provider identities, not the number of rows. // Only trust GitHub's stable account ID; equal logins or missing metadata // cannot establish that two grants belong to the same person. const githubUserIds = candidates.map((candidate) => candidate.providerTenant?.github?.userId?.trim()); if (candidates.length === 0 || (candidates.length > 1 && ( githubUserIds.some((id) => !id) || new Set(githubUserIds).size !== 1 ))) { return { configured: true, identitySource, error: candidates.length === 0 ? "No managed GitHub identity is available for this run" : "More than one managed GitHub identity matches this run", }; } const credentialIds = candidates.flatMap((grant) => grant.credentialSecretRefs .filter((ref) => ref.configPath === "oauth.access_token").map((ref) => ref.secretId)); const credentialRecords = candidates.length > 1 && credentialIds.length > 0 ? await db.select({ id: companySecrets.id, status: companySecrets.status, deletedAt: companySecrets.deletedAt, scope: companySecrets.scope, ownerUserId: companySecrets.ownerUserId, definitionStatus: userSecretDefinitions.status, definitionDeletedAt: userSecretDefinitions.deletedAt, }).from(companySecrets).leftJoin(userSecretDefinitions, and( eq(userSecretDefinitions.id, companySecrets.userSecretDefinitionId), eq(userSecretDefinitions.companyId, companyId), )).where(and( eq(companySecrets.companyId, companyId), inArray(companySecrets.id, credentialIds), )) : []; const hasCredentialRecord = (grant: typeof connectionGrants.$inferSelect) => { if (candidates.length === 1) return true; const github = grant.providerTenant?.github; const ref = grant.credentialSecretRefs.find((ref) => ref.configPath === "oauth.access_token"); return Boolean(github && github.installationCount > 0 && github.repositoryCount > 0 && ref && credentialRecords.some((secret) => secret.id === ref.secretId && secret.status === "active" && !secret.deletedAt && (grant.kind === "user" ? secret.scope === "user" && secret.ownerUserId === grant.subjectUserId && secret.definitionStatus === "active" && !secret.definitionDeletedAt : secret.scope === "company"))); }; const isAvailable = (grant: typeof connectionGrants.$inferSelect) => grant.status === "active" && hasCredentialRecord(grant) && githubConnections.some((connection) => connection.id === grant.connectionId && connection.enabled && connection.status === "active", ); // Prefer an available, healthy authorization for this account, then the newest // connection grant. Do not rank by updatedAt: refreshes/webhooks change it. // Select one grant, preserving its credential and connection policy intact. const healthRank = (candidate: typeof connectionGrants.$inferSelect) => { const health = githubConnections.find((connection) => connection.id === candidate.connectionId)?.healthStatus; return health === "ok" || health === "healthy" ? 2 : health === "unknown" ? 1 : 0; }; const grant = candidates.filter((candidate) => candidate.id !== context.excludeGrantId).sort((a, b) => Number(isAvailable(b)) - Number(isAvailable(a)) || healthRank(b) - healthRank(a) || b.createdAt.getTime() - a.createdAt.getTime() || a.id.localeCompare(b.id), )[0]!; if (!grant) return { configured: true, identitySource, error: "No alternative managed GitHub authorization is available" }; const connection = githubConnections.find((candidate) => candidate.id === grant.connectionId); if (!connection?.enabled || connection.status !== "active") { return { configured: true, identitySource, error: "The managed GitHub connection is unavailable" }; } if (grant.status !== "active") return { configured: true, identitySource, error: "The managed GitHub identity must be reconnected" }; return { configured: true, identitySource, grant }; } export async function filterResolvedGitHubConnectionsForRun(input: { db: Db; companyId: string; agentId: string; responsibleUserId?: string | null; connections: T[]; }): Promise { const githubConnections = input.connections.filter((connection) => { const config = connection.config && typeof connection.config === "object" ? connection.config as Record : {}; const transportConfig = connection.transportConfig && typeof connection.transportConfig === "object" ? connection.transportConfig as Record : {}; return config.sourceTemplateKey === "github" || transportConfig.sourceTemplateKey === "github"; }); if (githubConnections.length === 0) return input.connections; const selection = await resolveManagedGitHubIdentitySelection(input.db, input.companyId, { agentId: input.agentId, responsibleUserId: input.responsibleUserId ?? null, }); const selectedConnectionId = selection.grant?.connectionId ?? null; const githubIds = new Set(githubConnections.map((connection) => connection.id)); return input.connections.filter((connection) => !githubIds.has(connection.id) || connection.id === selectedConnectionId, ); } export async function resolveManagedGitHubCredential( db: Db, secrets: GitCredentialSecretsDeps, companyId: string, context: { issueId?: string | null; heartbeatRunId?: string | null; responsibleUserId?: string | null; agentId?: string | null; allowStandingDelegation?: boolean; }, ): Promise<{ configured: boolean; identitySource?: "personal" | "dedicated"; credential?: GitCredential; error?: string }> { const selection = await resolveManagedGitHubIdentitySelection(db, companyId, context); if (!selection.configured) return { configured: false }; if (!selection.grant) return { configured: true, identitySource: selection.identitySource, error: selection.error }; const acquire = async (selection: Awaited>) => { let grant = selection.grant!; if (grant.kind === "user" && grant.subjectUserId) { const [membership] = await db.select({ id: companyMemberships.id, role: companyMemberships.membershipRole }).from(companyMemberships).where(and( eq(companyMemberships.companyId, companyId), eq(companyMemberships.principalType, "user"), eq(companyMemberships.principalId, grant.subjectUserId), eq(companyMemberships.status, "active"), )).limit(1); if (!membership || membership.role === "viewer") return { configured: true, identitySource: selection.identitySource, error: "The managed GitHub identity owner is not an authorized company member" }; } const expiresAt = grant.providerTenant?.oauth?.accessTokenExpiresAt; const refreshedAt = grant.providerTenant?.oauth?.refreshedAt; const expiryMs = typeof expiresAt === "string" ? Date.parse(expiresAt) : Number.NaN; const refreshedMs = typeof refreshedAt === "string" ? Date.parse(refreshedAt) : Number.NaN; if (Number.isFinite(expiryMs) && ( expiryMs <= Date.now() + 60 * 60_000 || !Number.isFinite(refreshedMs) || refreshedMs <= Date.now() - 30 * 24 * 60 * 60_000 )) { grant = await toolAccessService(db).refreshOAuthGrantCredentials({ companyId, connectionId: grant.connectionId, grantId: grant.id, actor: { actorType: "system", actorId: "workspace-git-credential" }, issueId: context.issueId, heartbeatRunId: context.heartbeatRunId, }); } const accessRef = grant.credentialSecretRefs.find((ref) => ref.configPath === "oauth.access_token"); const github = grant.providerTenant?.github; if (!accessRef || !github) return { configured: true, identitySource: selection.identitySource, error: "The managed GitHub identity is incomplete" }; if (github.installationCount < 1 || github.repositoryCount < 1) { return { configured: true, identitySource: selection.identitySource, error: "The managed GitHub identity no longer has repository access" }; } const accessContext = { consumerType: "system" as const, consumerId: "workspace-git-credential", actorType: "system" as const, actorId: context.agentId ?? undefined, issueId: context.issueId ?? null, heartbeatRunId: context.heartbeatRunId ?? null, responsibleUserId: context.responsibleUserId ?? null, }; let token: string; if (grant.kind === "user") { if (!grant.subjectUserId || !secrets.resolveUserSecretValue) { return { configured: true, identitySource: selection.identitySource, error: "The personal GitHub credential cannot be resolved" }; } const [secret] = await db.select({ userSecretDefinitionId: companySecrets.userSecretDefinitionId, }).from(companySecrets).where(and( eq(companySecrets.companyId, companyId), eq(companySecrets.id, accessRef.secretId), eq(companySecrets.ownerUserId, grant.subjectUserId), )).limit(1); if (!secret?.userSecretDefinitionId) return { configured: true, identitySource: selection.identitySource, error: "The personal GitHub credential is invalid" }; const resolved = await secrets.resolveUserSecretValue(companyId, { definitionId: secret.userSecretDefinitionId, responsibleUserId: grant.subjectUserId, version: accessRef.versionSelector ?? "latest", required: true, }, accessContext); if (!resolved) return { configured: true, identitySource: selection.identitySource, error: "The personal GitHub credential is missing" }; token = resolved.value; } else { token = await secrets.resolveSecretValue(companyId, accessRef.secretId, accessRef.versionSelector ?? "latest", { accessContext }); } return { configured: true, identitySource: selection.identitySource, credential: { token, source: "managed_connection" as const, secretName: null, githubIdentity: { userId: github.userId, login: github.login }, identitySource: grant.kind === "agent" ? "dedicated" as const : "personal" as const, connectionId: grant.connectionId, grantId: grant.id, }, }; }; let failure: { configured: boolean; identitySource?: "personal" | "dedicated"; error?: string }; try { const result = await acquire(selection); if (result.credential) return result; failure = result; } catch { failure = { configured: true, identitySource: selection.identitySource, error: "GitHub credentials are temporarily unavailable" }; } // Retry credential acquisition, never the GitHub operation. An alternate // authorization must still belong to this exact principal and account. const alternate = await resolveManagedGitHubIdentitySelection(db, companyId, { ...context, excludeGrantId: selection.grant.id, }); const accountId = selection.grant.providerTenant?.github?.userId; if (!accountId || !alternate.grant || alternate.identitySource !== selection.identitySource || alternate.grant.providerTenant?.github?.userId !== accountId || alternate.grant.subjectUserId !== selection.grant.subjectUserId || alternate.grant.subjectAgentId !== selection.grant.subjectAgentId) return failure; try { return await acquire(alternate); } catch { return failure; } }