{"record":{"id":"c5b9780c25f34791","repo":"paperclipai/paperclip","slug":"grant-owner-missing","errorCode":"grant_owner_missing","errorMessage":"Personal authorization has no owner","messagePattern":"Personal authorization has no owner","errorType":"http","errorClass":"ToolGatewayHttpError","httpStatus":422,"severity":"error","filePath":"server/src/services/tool-gateway.ts","lineNumber":3494,"sourceCode":"      consumerType: \"tool_connection\" as const,\n      consumerId: connection.id,\n      configPath,\n      actorType: \"system\" as const,\n      actorId: session.agentId,\n      responsibleUserId: grant.subjectUserId,\n      issueId: session.issueId,\n      heartbeatRunId: session.runId,\n    };\n    if (grant.kind !== \"user\") {\n      return secrets.resolveSecretValue(\n        connection.companyId,\n        ref.secretId,\n        ref.versionSelector ?? \"latest\",\n        { accessContext },\n      );\n    }\n    if (!grant.subjectUserId) {\n      throw new ToolGatewayHttpError(\n        422,\n        \"Personal authorization has no owner\",\n        \"grant_owner_missing\",\n        {\n          connectionId: connection.id,\n          grantId: grant.id,\n        },\n      );\n    }\n    const [secret] = await db\n      .select({\n        scope: companySecrets.scope,\n        ownerUserId: companySecrets.ownerUserId,\n        userSecretDefinitionId: companySecrets.userSecretDefinitionId,\n      })\n      .from(companySecrets)\n      .where(\n        and(","sourceCodeStart":3476,"sourceCodeEnd":3512,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/tool-gateway.ts#L3476-L3512","documentation":"ToolGatewayHttpError (HTTP 422, code grant_owner_missing) thrown when resolving a personal (user-scoped) authorization grant whose subjectUserId is null/undefined. Personal grants must be owned by a specific user; a grant without an owner cannot be used to authorize secret access on behalf of anyone, so the gateway rejects the request with the owning connectionId and grantId in the details.","triggerScenarios":"Using a connection whose active authorization grant is of the personal kind but has no subjectUserId set — e.g. the grant row was created by an import/migration without an owner, a user was deleted and the column nulled, or the OAuth flow completed without binding the authenticated user.","commonSituations":"Database rows hand-edited or bulk-imported leaving personal grants ownerless; user deletion cascade nulling subject_user_id while the grant survives; a partially completed connect flow persisting the grant before user binding; company vs personal grant kind mixups.","solutions":["Reconnect/re-authorize the connection for the intended user so a fresh personal grant with subjectUserId is created","Fix the data: set subject_user_id on the orphaned grant (grantId in error details) or delete it so the gateway falls back to a valid grant","If the owner user was deleted, delete the grant and re-link the connection under a surviving user","Audit grant creation code paths (imports, migrations, OAuth callbacks) to ensure subjectUserId is always set for personal grants"],"exampleFix":"// before\nif (!grant.subjectUserId) {\n  throw new ToolGatewayHttpError(422, \"Personal authorization has no owner\", \"grant_owner_missing\", { connectionId: connection.id, grantId: grant.id });\n}\n// after (data repair)\nawait db.execute(sql`UPDATE connection_grants SET subject_user_id = ${ownerUserId} WHERE id = ${grant.id} AND kind = 'personal' AND subject_user_id IS NULL`);","handlingStrategy":"validation","validationCode":"if (!grant.subjectUserId) {\n  await repairOrRecreateGrant(grant.id); // set owner or delete grant and re-auth\n  return;\n}","typeGuard":"function hasOwner(grant) { return typeof grant.subjectUserId === 'string' && grant.subjectUserId.length > 0; }","tryCatchPattern":"try {\n  return await resolveSecretForGrant(grant);\n} catch (e) {\n  if (e?.code === 'grant_owner_missing') {\n    await reauthorizeConnection(e.details.connectionId);\n    return resolveSecretForGrant(await reloadGrant(e.details.grantId));\n  }\n  throw e;\n}","preventionTips":["Set subjectUserId atomically with grant creation","Add a DB constraint or trigger rejecting personal grants with null owner","Clean up grants on user deletion in the same transaction","Surface re-auth prompts to users instead of failing secret resolution"],"tags":["oauth","grants","authorization","data-integrity"],"backgroundTag":"missing-credentials","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"}