paperclipai/paperclip · error

Provider credential is not ready

Error message

Provider credential is not ready

What it means

This server throws when an agent-adapter login promotion attempts to save staged provider credentials but the staged auth bytes have not passed the provider-specific readiness check (checkStagedCredentialReadiness). Promotion binds staged credentials into an AI connection, and the code refuses to persist credentials that are not yet verifiably usable, while holding the per-company adapter promotion lock to keep the critical section safe. It protects the aiConnection from being overwritten with incomplete or unusable auth material.

Solutions

  1. Wait for the staged credential readiness check to pass, then retry promotion.
  2. Re-run the adapter login flow to re-stage fresh credentials, then promote again.
  3. Check whether a concurrent login for the same adapter/company failed and removed the staged secret directory; serialize logins rather than running them in parallel.
  4. Inspect server logs around checkStagedCredentialReadiness to see why the staged bytes were not ready.

Example fix

// before
await adapterLoginStore.promote(authBytes, context); // throws if not ready
// after
const staged = await adapterLoginStore.get(context.sessionId);
if (!staged) throw new Error("Login session expired; start the login again");
try {
  await adapterLoginStore.promote(authBytes, context);
} catch (e) {
  if (e.message === "Provider credential is not ready") {
    // guide the user to re-run the login flow
    return res.status(409).json({ error: "Credential not ready; redo the provider login" });
  }
  throw e;
}
Defensive patterns

Strategy: validation

Validate before calling

const staged = await adapterLoginStore.get(sessionId);
if (!staged) throw new Error("Session expired; redo login");
// promote only after login flow reports credentials staged/validated

Type guard

function isPromotable(s) { return Boolean(s && s.aiConnection && s.stagedAt && !s.stagingError); }

Try / catch

try { await adapterLoginStore.promote(authBytes, ctx); }
catch (e) {
  if (e.message === "Provider credential is not ready") return redoLoginFlow();
  throw e;
}

Prevention

When it happens

Trigger: Calling the agent login promote endpoint while the staged auth bytes for the adapter have not completed provider validation — e.g. the first-login credential check has not finished, the staged secret directory was removed by a concurrent login's secret-write failure, or the staged payload was corrupted or truncated.

Common situations: Two logins for the same adapter racing where the first's later secret-write failure removes the shared staged directory; a user clicking 'finish login' before the provider handshake completes; stale staged sessions being promoted after a server restart or provider-side credential rotation.

Related errors


AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18). Data as JSON: /api/errors/e5351bb75a4ea00f. Report an issue: GitHub.

Appendix: source

Thrown at server/src/routes/agents.ts:803

    promotionByAdapterType: {
      codex_local: {
        // Hold one lock across the whole promotion sequence below: the
        // credential write, the existing-secret check, the secret create, and
        // the cleanup a create failure can trigger. Two different logins for
        // the SAME Codex account run this whole sequence one at a time, so a
        // login can never decide to delete the shared account-home directory
        // while another login's own sequence is still mid-way through writing
        // its credential or binding its own secret to that same directory. A
        // lock around only the directory-creation step is not enough: that
        // lock is already released by the time a login reaches the secret
        // bind, so a second login can write its credential and be about to
        // bind its own secret while the first login's later, unrelated
        // secret-write failure removes the directory both logins now share.
        async promote(authBytes, context) {
          const managedSession = await adapterLoginStore.get(context.sessionId);
          if (managedSession?.aiConnection) {
            await adapterLoginStore.withCompanyAdapterPromotionLock(context.companyId, context.startedByUserId, context.adapterType, async () => {
              if (!(await checkStagedCredentialReadiness(authBytes)).ready) throw new Error("Provider credential is not ready");
              await aiConnectionService(db).save(context.companyId, context.startedByUserId, managedSession.aiConnection!, authBytes.toString("utf8"), context.sessionId);
            });
            return;
          }

          return withCodexAccountHomePromotionLock(undefined, context.companyId, async () => {
            // Hold the promotion critical-section lock across the ownership check
            // and the credential write. The reaper takes the same lock before it
            // reclaims a stale `promoting` row. So a reclaim never interleaves with
            // a live write: the reaper either wins the lock first and the
            // ownership check then reads a reclaimed row and writes nothing, or
            // the write finishes first under the lock and the reaper reclaims only
            // after it completes. A read-only fence is not enough, because the
            // filesystem write can start after the fence; the lock spans the whole
            // section.
            const result = await adapterLoginStore.withCompanyAdapterPromotionLock(
              context.companyId,
              context.startedByUserId,

View on GitHub (pinned to 3f1d897a7c)