{"record":{"id":"9a1519ff7659a7c3","repo":"paperclipai/paperclip","slug":"device-login-credential-promotion-rejected-the-pr","errorCode":null,"errorMessage":"device-login credential promotion rejected: the promotion carried no account home","messagePattern":"device-login credential promotion rejected: the promotion carried no account home","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"server/src/routes/agents.ts","lineNumber":832,"sourceCode":"                    logger.info({ sessionId: context.sessionId }, line);\n                  },\n                }),\n            );\n            // A resolved promotion is not necessarily an accepted promotion. In\n            // particular, a reaper/expiry race can revoke this session's sole\n            // ownership between the service transition and Decision H. Fail closed:\n            // only a credential write or a deliberate safe keep can authenticate.\n            if (result.outcome !== \"promoted\" && result.outcome !== \"kept\") {\n              throw new Error(`device-login credential promotion rejected: ${result.outcome}`);\n            }\n            // The account's own home is durable at this point (the promotion above\n            // wrote it fail-loud). Name it with a company secret, so any agent can\n            // bind to it. Reading the secret by name first keeps a repeat login for\n            // the same account idempotent: `create` throws a conflict when the name\n            // already exists.\n            const handle = result.accountId ? toAccountHandle(result.accountId) : null;\n            if (!handle || !result.accountHomeDir) {\n              throw new Error(\n                \"device-login credential promotion rejected: the promotion carried no account home\",\n              );\n            }\n            const secretName = `CODEX_HOME_${handle}`;\n            const accountHomeDir = result.accountHomeDir;\n            const existingSecret = await secretsSvc.getByName(context.companyId, secretName);\n            if (existingSecret) {\n              // A same-name secret already exists. Confirm it still names this\n              // account's own home before treating a repeat login as a success:\n              // the name alone is not proof of a match.\n              //\n              // Run the check inside the same lock a `local_encrypted` secret\n              // rotate holds for its whole write, the same lock a rotate\n              // takes. This is an early fail-fast only: the lock is fully\n              // released once this call returns, well before the login\n              // service commits its terminal state, so queue the same check\n              // for `runTerminalCommit` to run again right before that\n              // commit, under a fresh lock acquisition it holds across the","sourceCodeStart":814,"sourceCodeEnd":850,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/server/src/routes/agents.ts#L814-L850","documentation":"After an accepted promotion ('promoted'/'kept'), promote() derives the account handle from result.accountId and requires result.accountHomeDir to build the CODEX_HOME_<handle> company secret. If the promotion result carries no usable accountId or no accountHomeDir, the login cannot bind agents to a durable home, so it fails closed with this error. This guards an adapter-internal invariant: a successful promotion must always report where the account home was written.","triggerScenarios":"promoteDeviceLoginCredential returns outcome 'promoted' or 'kept' but result.accountId is null/undefined (handle can't be derived) or result.accountHomeDir is empty/null — i.e. the adapter's promotion service resolved without propagating the account-home path it was supposed to write.","commonSituations":"Adapter (Codex) version change altering the promotion result shape; an adapter bug where the home write is skipped for a 'kept' outcome; custom/mock adapter in tests returning a partial result object.","solutions":["Inspect the promotion result from promoteDeviceLoginCredential for this adapter and fix the adapter/service to always populate accountId and accountHomeDir on promoted/kept outcomes.","Update or pin the adapter package version if a recent adapter upgrade changed the result shape.","Check server logs for the promotion lines (logger.info with sessionId) to see which field was missing.","If the account exists on disk but the dir wasn't reported, locate the Codex home and verify the adapter's home-derivation logic."],"exampleFix":"// before (adapter returns partial result)\nreturn { outcome: 'promoted', accountId: null, accountHomeDir: null };\n// after\nreturn { outcome: 'promoted', accountId: accountId, accountHomeDir: homeDir }; // always set on promoted/kept","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"function promotionCarriedAccountHome(\n  r: { outcome: string; accountId?: string | null; accountHomeDir?: string | null },\n): r is { outcome: 'promoted' | 'kept'; accountId: string; accountHomeDir: string } {\n  return (r.outcome === 'promoted' || r.outcome === 'kept')\n    && typeof r.accountId === 'string' && r.accountId.length > 0\n    && typeof r.accountHomeDir === 'string' && r.accountHomeDir.length > 0;\n}","tryCatchPattern":"try {\n  await promoteDeviceLogin(...);\n} catch (e) {\n  if (e instanceof Error && e.message.includes('the promotion carried no account home')) {\n    console.error('Adapter promotion result missing accountId/accountHomeDir — check adapter version/bug');\n  } else throw e;\n}","preventionTips":["Keep the adapter package at a version known to return full promotion results","Add an adapter-level unit test asserting accountId and accountHomeDir are set for promoted/kept outcomes","Validate the promotion result shape immediately after promoteDeviceLoginCredential returns","Pin adapter versions in deployment configs to avoid silent result-shape drift"],"tags":["device-login","internal-invariant","adapter","codex"],"backgroundTag":"internal-invariant-violation","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}