paperclipai/paperclip · error · ToolGatewayHttpError
grant_credential_invalid
grant_credential_invalid
Error message
Personal authorization has an invalid credential
What it means
A connection grant of kind 'user' references a credential secret, but the secret row looked up from companySecrets is either missing, not scoped to 'user', not owned by the grant's subjectUserId, or lacks a userSecretDefinitionId. The tool gateway throws this 422 before resolving the personal credential, because a personal authorization grant may only use a user-scoped secret owned by the subject user and tied to a user secret definition.
Solutions
- Re-create the personal authorization so the OAuth callback regenerates the user-scoped secret with the correct ownerUserId and userSecretDefinitionId.
- Inspect the companySecrets row for ref.secretId and fix scope ('user'), ownerUserId (must equal grant.subjectUserId), and userSecretDefinitionId.
- If the grant should not be personal, change it to a non-user kind (e.g. agent/company) so the generic resolveSecretValue path is used instead.
- Verify the user secret definition exists for the connection's credential; re-register the definition and relink the secret.
Example fix
// before: company-scoped secret attached to a personal grant
{ id: 'sec_123', scope: 'company', ownerUserId: null, userSecretDefinitionId: null }
// after: user-scoped secret owned by the grant subject
{ id: 'sec_123', scope: 'user', ownerUserId: 'usr_42', userSecretDefinitionId: 'usd_api_key' } Defensive patterns
Strategy: validation
Validate before calling
const [secret] = await db.select().from(companySecrets).where(and(eq(companySecrets.id, ref.secretId), eq(companySecrets.companyId, companyId)));
if (!secret || secret.scope !== 'user' || secret.ownerUserId !== grant.subjectUserId || !secret.userSecretDefinitionId) {
throw new Error(`Grant ${grant.id} references an invalid credential for ${ref.secretId}`);
} Type guard
function isValidUserSecret(s: typeof companySecrets.$inferSelect | undefined, subjectUserId: string): s is typeof companySecrets.$inferSelect & { scope: 'user'; ownerUserId: string; userSecretDefinitionId: string } {
return !!s && s.scope === 'user' && s.ownerUserId === subjectUserId && typeof s.userSecretDefinitionId === 'string';
} Try / catch
try {
const value = await resolveCredentialHeaders(session, connection, grant);
} catch (e) {
if (e instanceof ToolGatewayHttpError && e.code === 'grant_credential_invalid') {
// surface 're-authorize personal connection' to the user
}
throw e;
} Prevention
- Always create personal-grant secrets through the OAuth callback flow, never by manually editing companySecrets.
- Assert scope === 'user' and ownerUserId === subjectUserId at grant creation time.
- Re-run secret-linkage checks after company clones or restores.
When it happens
Trigger: resolveGrantSecretValue is called for a user-kind grant whose grant.credentialSecretRefs point to a secret that (a) does not exist in companySecrets for the company, (b) has scope 'company' instead of 'user', (c) has ownerUserId different from grant.subjectUserId, or (d) was created without a userSecretDefinitionId linkage.
Common situations: An operator manually copied a company-level secret into a personal grant's credential refs; a user's OAuth callback created the secret but the definition linkage migration was skipped; secrets were re-created after a company clone so the grant references a stale secret id; a grant was reassigned to a different user without updating the secret owner.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- grant_owner_missing
- user_secret_missing
- access.reasonCode
- agent_authorization_required
- agent_not_assigned
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18).
Data as JSON: /api/errors/2e82f8a4d0d6e5b9.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/services/tool-gateway.ts:3524
scope: companySecrets.scope,
ownerUserId: companySecrets.ownerUserId,
userSecretDefinitionId: companySecrets.userSecretDefinitionId,
})
.from(companySecrets)
.where(
and(
eq(companySecrets.id, ref.secretId),
eq(companySecrets.companyId, connection.companyId),
),
)
.limit(1);
if (
!secret ||
secret.scope !== "user" ||
secret.ownerUserId !== grant.subjectUserId ||
!secret.userSecretDefinitionId
) {
throw new ToolGatewayHttpError(
422,
"Personal authorization has an invalid credential",
"grant_credential_invalid",
{
connectionId: connection.id,
grantId: grant.id,
credential: configPath,
},
);
}
const resolved = await secrets.resolveUserSecretValue(
connection.companyId,
{
definitionId: secret.userSecretDefinitionId,
responsibleUserId: grant.subjectUserId,
version: ref.versionSelector ?? "latest",
required: ref.required ?? true,
},View on GitHub (pinned to 3f1d897a7c)