paperclipai/paperclip · error · ToolGatewayHttpError
grant_owner_missing
grant_owner_missing
Error message
Personal authorization has no owner
What it means
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.
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
Example fix
// before
if (!grant.subjectUserId) {
throw new ToolGatewayHttpError(422, "Personal authorization has no owner", "grant_owner_missing", { connectionId: connection.id, grantId: grant.id });
}
// after (data repair)
await db.execute(sql`UPDATE connection_grants SET subject_user_id = ${ownerUserId} WHERE id = ${grant.id} AND kind = 'personal' AND subject_user_id IS NULL`); Defensive patterns
Strategy: validation
Validate before calling
if (!grant.subjectUserId) {
await repairOrRecreateGrant(grant.id); // set owner or delete grant and re-auth
return;
} Type guard
function hasOwner(grant) { return typeof grant.subjectUserId === 'string' && grant.subjectUserId.length > 0; } Try / catch
try {
return await resolveSecretForGrant(grant);
} catch (e) {
if (e?.code === 'grant_owner_missing') {
await reauthorizeConnection(e.details.connectionId);
return resolveSecretForGrant(await reloadGrant(e.details.grantId));
}
throw e;
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- github_identity_unavailable
- grant_credential_invalid
- oauth_refresh_failed
- user_authorization_required
- access.reasonCode
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18).
Data as JSON: /api/errors/c5b9780c25f34791.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/services/tool-gateway.ts:3494
consumerType: "tool_connection" as const,
consumerId: connection.id,
configPath,
actorType: "system" as const,
actorId: session.agentId,
responsibleUserId: grant.subjectUserId,
issueId: session.issueId,
heartbeatRunId: session.runId,
};
if (grant.kind !== "user") {
return secrets.resolveSecretValue(
connection.companyId,
ref.secretId,
ref.versionSelector ?? "latest",
{ accessContext },
);
}
if (!grant.subjectUserId) {
throw new ToolGatewayHttpError(
422,
"Personal authorization has no owner",
"grant_owner_missing",
{
connectionId: connection.id,
grantId: grant.id,
},
);
}
const [secret] = await db
.select({
scope: companySecrets.scope,
ownerUserId: companySecrets.ownerUserId,
userSecretDefinitionId: companySecrets.userSecretDefinitionId,
})
.from(companySecrets)
.where(
and(View on GitHub (pinned to 3f1d897a7c)