danny-avila/LibreChat · error · Error

Role ${accessRoleId} is for ${role.resourceType} resources,

Error message

Role ${accessRoleId} is for ${role.resourceType} resources, not ${resourceType}

What it means

Thrown by PermissionService.grantPermission when the resolved role exists but role.resourceType differs from the requested resourceType. Each access role is scoped to one resource type (its permBits are meaningful only for that type), so granting an agent-scoped role against a prompt resource is rejected to prevent meaningless ACL entries.

Source

Thrown at api/server/services/PermissionService.js:113

      // User and Group IDs must be valid ObjectIds
      throw new Error(`Invalid principal ID: ${principalId}`);
    }

    if (!resourceId || !mongoose.Types.ObjectId.isValid(resourceId)) {
      throw new Error(`Invalid resource ID: ${resourceId}`);
    }

    validateResourceType(resourceType);

    // Get the role to determine permission bits
    const role = await db.findRoleByIdentifier(accessRoleId);
    if (!role) {
      throw new Error(`Role ${accessRoleId} not found`);
    }

    // Ensure the role is for the correct resource type
    if (role.resourceType !== resourceType) {
      throw new Error(
        `Role ${accessRoleId} is for ${role.resourceType} resources, not ${resourceType}`,
      );
    }
    return await db.grantPermission(
      principalType,
      principalId,
      resourceType,
      resourceId,
      role.permBits,
      grantedBy,
      session,
      role._id,
    );
  } catch (error) {
    logger.error(`[PermissionService.grantPermission] Error: ${error.message}`);
    throw error;
  }
};

View on GitHub (pinned to 5ff282f900)

Solutions

  1. Map each resourceType to its role identifier at the caller and pass the pair together (e.g. { 'agent': AccessRoleIds.AGENT_VIEWER, 'prompt': AccessRoleIds.PROMPT_VIEWER }).
  2. Inspect the resolved role (db.findRoleByIdentifier) in a REPL to confirm its resourceType before wiring the grant call.
  3. If the role genuinely should apply to the new resourceType, create and seed a new role document with that resourceType rather than reusing the existing one.

Example fix

// before
await grantPermission({ resourceType, resourceId, accessRoleId: AccessRoleIds.AGENT_VIEWER, ... });

// after
const roleByType = {
  agent: AccessRoleIds.AGENT_VIEWER,
  prompt: AccessRoleIds.PROMPT_VIEWER,
};
const accessRoleId = roleByType[resourceType];
if (!accessRoleId) throw new Error(`No role mapped for resourceType ${resourceType}`);
await grantPermission({ resourceType, resourceId, accessRoleId, ... });
Defensive patterns

Strategy: validation

Validate before calling

const role = await db.findRoleByIdentifier(accessRoleId);
if (role && role.resourceType !== resourceType) {
  throw new Error(`Role ${accessRoleId} is for ${role.resourceType} resources, not ${resourceType}`);
}

Type guard

const roleMatchesResource = (role, resourceType) => !!role && role.resourceType === resourceType;

Try / catch

try {
  await grantPermission({ ..., accessRoleId, resourceType });
} catch (err) {
  if (err.message.includes('resources, not')) return res.status(400).json({ message: err.message });
  throw err;
}

Prevention

When it happens

Trigger: Passing AccessRoleIds.AGENT_VIEWER with resourceType: 'prompt', or any combination where the role identifier and resourceType come from independent sources and disagree. Common when the caller hard-codes one role constant but varies resourceType dynamically.

Common situations: Reusing a single role constant across a generic 'share' handler that accepts multiple resource types; copy-pasting a grant call for a new resource type without switching the role; UI defaulting the role dropdown to the agent value.

Related errors


AI-assisted analysis of danny-avila/LibreChat@5ff282f900 (2026-08-12). Data as JSON: /api/errors/af4e17ac2e189612. Report an issue: GitHub.