{"record":{"id":"0d40b8502ec3d0e5","repo":"paperclipai/paperclip","slug":"device-login-promotion-the-account-identifier-can","errorCode":null,"errorMessage":"device-login promotion: the account identifier cannot form a valid account handle","messagePattern":"device-login promotion: the account identifier cannot form a valid account handle","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/adapters/codex-local/src/server/adapter-auth-promotion.ts","lineNumber":226,"sourceCode":"  }\n\n  // 2. Validate the credential with the export rules. This rejects an empty, an\n  //    oversized, an API-key, a non-subscription, and a malformed payload.\n  assertUsableSubscriptionShape(authBytes);\n  const accountId = readSubscriptionAccountId(authBytes);\n  if (!accountId) {\n    // The shape gate above already guarantees a subscription identity; this guard\n    // keeps the account_id non-null for the handle conversion without a non-null\n    // cast.\n    throw new Error(\"device-login promotion: the credential has no subscription identity\");\n  }\n\n  // 2b. Convert the identity into a safe account handle. The handle names both\n  //     this account's own home directory and its company secret, so a login\n  //     whose identity cannot form one must fail before any write.\n  const accountHandle = toAccountHandle(accountId);\n  if (!accountHandle) {\n    throw new Error(\"device-login promotion: the account identifier cannot form a valid account handle\");\n  }\n\n  // 3. Decision C: only a user-initiated login seeds a home.\n  if (!userInitiated) {\n    await log(\"[paperclip] Codex device-login promotion: skipped (an automatic background login never seeds a home).\");\n    return { outcome: \"background_skipped\", accountId, accountHomeDir: null, accountHomeCreated: false };\n  }\n\n  // 4. Decision H: write only while the session still owns the active slot.\n  const soleOwner = await isSoleActiveOwner();\n  if (!soleOwner) {\n    await log(\"[paperclip] Codex device-login promotion: skipped (the session no longer holds the sole active claim on the slot).\");\n    return { outcome: \"not_sole_owner\", accountId, accountHomeDir: null, accountHomeCreated: false };\n  }\n\n  // 5a. This account's own home is the durable result of a login: each account\n  //     handle addresses exactly one home, so this write can never collide with\n  //     a different identity, and a write failure here fails the whole","sourceCodeStart":208,"sourceCodeEnd":244,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/packages/adapters/codex-local/src/server/adapter-auth-promotion.ts#L208-L244","documentation":"During device-login promotion, the adapter converts the Codex account identity into a sanitized account handle via toAccountHandle(). That handle names the account's home directory and the company secret, so an identity that cannot produce a safe handle is rejected before any filesystem or secret-store write occurs. This is a fail-fast invariant guarding against unsafe or malformed identifiers.","triggerScenarios":"Calling promoteDeviceLoginCredential() with an accountId whose shape cannot be sanitized by toAccountHandle() — e.g. an empty string, a string of only unsafe characters, or an identifier containing path separators/segments that would normalize away.","commonSituations":"Auth JSON from a Codex install missing or corrupting the account id field; parsing an auth payload where account_id was never set (e.g. a fresh/anonymous login); a version change upstream altering the id format; programmatic promotion with a placeholder or empty identifier.","solutions":["Inspect the Codex auth source the accountId came from and confirm the account_id field is populated and well-formed.","Re-run the Codex device login so a fresh, valid account identity is produced, then retry promotion.","Sanitize or normalize the identifier before calling promoteDeviceLoginCredential() so toAccountHandle() succeeds.","If the id is structurally valid but still rejected, check the toAccountHandle implementation for the exact accepted character set and align your id."],"exampleFix":"// before\nawait promoteDeviceLoginCredential({ accountId: auth.lastRefreshAccountId ?? \"\", ... });\n// after\nconst accountId = auth.lastRefreshAccountId;\nif (!accountId || toAccountHandle(accountId) === null) {\n  throw new Error(`codex auth: unusable account identifier: ${JSON.stringify(auth.lastRefreshAccountId)}`);\n}\nawait promoteDeviceLoginCredential({ accountId, ... });","handlingStrategy":"validation","validationCode":"function canFormAccountHandle(accountId: unknown): accountId is string {\n  return typeof accountId === \"string\" && accountId.trim().length > 0 && toAccountHandle(accountId) !== null;\n}","typeGuard":"function isValidAccountId(v: unknown): v is string {\n  return typeof v === \"string\" && v.length > 0 && toAccountHandle(v) !== null;\n}","tryCatchPattern":"try {\n  await promoteDeviceLoginCredential({ accountId, ... });\n} catch (err) {\n  if (err instanceof Error && err.message.includes(\"cannot form a valid account handle\")) {\n    log(\"codex account id unusable; re-run device login\");\n    return { outcome: \"invalid_account_id\" };\n  }\n  throw err;\n}","preventionTips":["Always read accountId directly from the Codex auth payload field, never a derived label or email.","Check auth.lastRefreshAccountId for non-empty before initiating promotion.","Add a startup assertion that toAccountHandle(id) is non-null before any write path.","Re-run Codex device login whenever auth state looks stale or incomplete."],"tags":["authentication","validation","identifier","codex"],"backgroundTag":"invalid-identifier-format","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"}