{"record":{"id":"e5351bb75a4ea00f","repo":"paperclipai/paperclip","slug":"provider-credential-is-not-ready","errorCode":null,"errorMessage":"Provider credential is not ready","messagePattern":"Provider credential is not ready","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/src/routes/agents.ts","lineNumber":803,"sourceCode":"    promotionByAdapterType: {\n      codex_local: {\n        // Hold one lock across the whole promotion sequence below: the\n        // credential write, the existing-secret check, the secret create, and\n        // the cleanup a create failure can trigger. Two different logins for\n        // the SAME Codex account run this whole sequence one at a time, so a\n        // login can never decide to delete the shared account-home directory\n        // while another login's own sequence is still mid-way through writing\n        // its credential or binding its own secret to that same directory. A\n        // lock around only the directory-creation step is not enough: that\n        // lock is already released by the time a login reaches the secret\n        // bind, so a second login can write its credential and be about to\n        // bind its own secret while the first login's later, unrelated\n        // secret-write failure removes the directory both logins now share.\n        async promote(authBytes, context) {\n          const managedSession = await adapterLoginStore.get(context.sessionId);\n          if (managedSession?.aiConnection) {\n            await adapterLoginStore.withCompanyAdapterPromotionLock(context.companyId, context.startedByUserId, context.adapterType, async () => {\n              if (!(await checkStagedCredentialReadiness(authBytes)).ready) throw new Error(\"Provider credential is not ready\");\n              await aiConnectionService(db).save(context.companyId, context.startedByUserId, managedSession.aiConnection!, authBytes.toString(\"utf8\"), context.sessionId);\n            });\n            return;\n          }\n\n          return withCodexAccountHomePromotionLock(undefined, context.companyId, async () => {\n            // Hold the promotion critical-section lock across the ownership check\n            // and the credential write. The reaper takes the same lock before it\n            // reclaims a stale `promoting` row. So a reclaim never interleaves with\n            // a live write: the reaper either wins the lock first and the\n            // ownership check then reads a reclaimed row and writes nothing, or\n            // the write finishes first under the lock and the reaper reclaims only\n            // after it completes. A read-only fence is not enough, because the\n            // filesystem write can start after the fence; the lock spans the whole\n            // section.\n            const result = await adapterLoginStore.withCompanyAdapterPromotionLock(\n              context.companyId,\n              context.startedByUserId,","sourceCodeStart":785,"sourceCodeEnd":821,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/routes/agents.ts#L785-L821","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Wait for the staged credential readiness check to pass, then retry promotion.","Re-run the adapter login flow to re-stage fresh credentials, then promote again.","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.","Inspect server logs around checkStagedCredentialReadiness to see why the staged bytes were not ready."],"exampleFix":"// before\nawait adapterLoginStore.promote(authBytes, context); // throws if not ready\n// after\nconst staged = await adapterLoginStore.get(context.sessionId);\nif (!staged) throw new Error(\"Login session expired; start the login again\");\ntry {\n  await adapterLoginStore.promote(authBytes, context);\n} catch (e) {\n  if (e.message === \"Provider credential is not ready\") {\n    // guide the user to re-run the login flow\n    return res.status(409).json({ error: \"Credential not ready; redo the provider login\" });\n  }\n  throw e;\n}","handlingStrategy":"validation","validationCode":"const staged = await adapterLoginStore.get(sessionId);\nif (!staged) throw new Error(\"Session expired; redo login\");\n// promote only after login flow reports credentials staged/validated","typeGuard":"function isPromotable(s) { return Boolean(s && s.aiConnection && s.stagedAt && !s.stagingError); }","tryCatchPattern":"try { await adapterLoginStore.promote(authBytes, ctx); }\ncatch (e) {\n  if (e.message === \"Provider credential is not ready\") return redoLoginFlow();\n  throw e;\n}","preventionTips":["Only call promote after the login flow signals completion","Never run two logins for the same adapter/company in parallel","Re-run login rather than retrying promotion when this error appears","Monitor staged-session age; stale sessions often fail readiness"],"tags":["credentials","adapter-login","race-condition"],"backgroundTag":"authentication-required","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}