{"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":859,"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            // Whether the company default home ended on a DIFFERENT account\n            // than this login. The promotion's own company-home write already\n            // ran (a seed or same-identity refresh landed this login there; a\n            // different account's claim was kept), so this read observes the\n            // post-promotion state. The flag rides to the owner status read,\n            // where the client offers binding the agent to this account — the\n            // only way the login can take effect while another account holds\n            // the company slot. Any read failure degrades to `false`: the\n            // client then simply offers nothing, never a wrong bind.\n            const companyIdentityDiffers = await (async () => {\n              try {\n                const companyAuthBytes = await readFile(\n                  path.join(resolveManagedCodexHomeDir(process.env, context.companyId), \"auth.json\"),","sourceCodeStart":841,"sourceCodeEnd":877,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/routes/agents.ts#L841-L877","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":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}