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

  1. Reconnect/re-authorize the connection for the intended user so a fresh personal grant with subjectUserId is created
  2. 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
  3. If the owner user was deleted, delete the grant and re-link the connection under a surviving user
  4. 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

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


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)