{"record":{"id":"a02aba2bb34a7ffa","repo":"paperclipai/paperclip","slug":"device-login-credential-promotion-rejected-failed","errorCode":null,"errorMessage":"device-login credential promotion rejected: failed to record the account home secret","messagePattern":"device-login credential promotion rejected: failed to record the account home secret","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"server/src/routes/agents.ts","lineNumber":940,"sourceCode":"              // either, because the claiming secret can carry any name.\n              //\n              // The check and the delete run inside `withAccountHomeSecretMutationLock`,\n              // the same lock the secrets service holds for the whole of a\n              // `local_encrypted` secret's create or rotate call. That closes the\n              // window `anySecretNamesAccountHome`'s own multi-pass scan cannot: a\n              // secret write that commits after this check's last pass but before\n              // the delete runs. Under the shared lock, a write either finishes\n              // (and becomes visible to the check) before this section acquires the\n              // lock, or it waits for this section to finish before it can commit.\n              if (result.accountHomeCreated) {\n                await withAccountHomeSecretMutationLock(undefined, context.companyId, async () => {\n                  const claimed = await anySecretNamesAccountHome(secretsSvc, context.companyId, accountHomeDir);\n                  if (!claimed) {\n                    await rm(accountHomeDir, { recursive: true, force: true }).catch(() => undefined);\n                  }\n                });\n              }\n              throw new Error(\n                \"device-login credential promotion rejected: failed to record the account home secret\",\n              );\n            }\n          });\n        },\n        // The login service calls this immediately before it commits its\n        // terminal `authenticated` write, wrapping that write in the\n        // callback it hands in as `commit`. `promote` above already\n        // validated the bound account-home secret once, early, but its own\n        // lock is fully released by the time `promote` returns — well before\n        // this runs. Re-run the same check here, and hold the SAME lock\n        // across both the check and `commit`, so a rotate cannot land in the\n        // gap between the validated value and the terminal write that\n        // reports it as authenticated: a rotate either finishes (and this\n        // check reads its new value, and rejects) before this section\n        // acquires the lock, or it waits for this section — including the\n        // terminal commit — to finish first.\n        async runTerminalCommit(commit, context) {","sourceCodeStart":922,"sourceCodeEnd":958,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/server/src/routes/agents.ts#L922-L958","documentation":"This error is thrown inside the device-login credential promotion callback in the agents route. During a device login, the login service calls `promote` to persist the account's home directory as a company secret (`local_encrypted`). If recording that secret fails for any reason other than a resolvable 409 naming conflict, the code cleans up any freshly-created account home directory (only when no other secret claims it) and throws this error so the login is rejected rather than committed without a recorded credential.","triggerScenarios":"A device-login flow calls the adapter login service which invokes the `promote` callback; the secret write to the secrets service fails with a non-409 error (secret service unavailable, encryption failure, DB write error, malformed secret payload), or a 409 conflict occurs but the winning secret cannot be found/verified against the account home directory.","commonSituations":"Database down or migrated mid-login; concurrent logins racing on the same account secret name; a manually created secret pointing at the same account home directory with a different name; disk/encryption key misconfiguration in the local_encrypted secret backend; secrets service version drift after an upgrade.","solutions":["Check the secrets service and database health at the time of login (the underlying cause is logged by the secret write that failed before this throw).","Retry the device login once concurrent activity settles; races on the same account secret name are the most common transient trigger.","Verify no orphaned/hand-created secret names the same account home directory; resolve or remove conflicting secrets for the company.","If the login created the account home dir and it was rolled back, remove stale directories and re-run the login cleanly.","Inspect the underlying error thrown by secretsSvc.create (logged before this throw) for encryption/backend-specific failures and fix that root cause."],"exampleFix":"// before: opaque error propagates from login service\nthrow new Error(\"device-login credential promotion rejected: failed to record the account home secret\");\n// after: preserve the underlying cause for debugging\nthrow new Error(\n  \"device-login credential promotion rejected: failed to record the account home secret\",\n  { cause: err },\n);","handlingStrategy":"try-catch","validationCode":"// before initiating device login\nconst healthy = await fetch('/api/health');\nif (!healthy.ok) throw new Error('server/secrets backend not healthy; defer device login');\n// ensure no conflicting secret exists for this account\nconst existing = await secretsSvc.getByName(companyId, secretName);\nif (existing) await verifySecretMatchesAccountHome(existing, accountHomeDir);","typeGuard":"function isPromotionRejection(err: unknown): err is Error & { message: string } {\n  return err instanceof Error && err.message.startsWith('device-login credential promotion rejected:');\n}","tryCatchPattern":"try {\n  await startDeviceLogin(adapter, accountHomeDir);\n} catch (err) {\n  if (isPromotionRejection(err)) {\n    // inspect cause, clean stale account home dir, retry once after settling\n    await retryWithBackoff(() => startDeviceLogin(adapter, accountHomeDir), { attempts: 2 });\n  } else throw err;\n}","preventionTips":["Check DB and secrets service health before starting device logins.","Avoid running concurrent logins for the same account in the same company.","Do not hand-create secrets that point into adapter-managed account home directories.","Always carry `{ cause: err }` so the underlying secret-write failure is inspectable."],"tags":["authentication","secrets","concurrency","device-login"],"backgroundTag":"database-write-failed","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}