feat: Add agent role assignment and database CRUD tools for AI agent nodes (#12888)

This PR introduces a significant enhancement to the role-based
permission system by extending it to support AI agents, enabling them to
perform database operations based on assigned permissions.

## Key Changes

### 1. Database Schema Migration
- **Table Rename**: `userWorkspaceRole` → `roleTargets` to better
reflect its expanded purpose
- **New Column**: Added `agentId` (UUID, nullable) to support AI agent
role assignments
- **Constraint Updates**: 
- Made `userWorkspaceId` nullable to accommodate agent-only role
assignments
- Added check constraint `CHK_role_targets_either_agent_or_user`
ensuring either `agentId` OR `userWorkspaceId` is set (not both)

### 2. Entity & Service Layer Updates
- **RoleTargetsEntity**: Updated with new `agentId` field and constraint
validation
- **AgentRoleService**: New service for managing agent role assignments
with validation
- **AgentService**: Enhanced to include role information when retrieving
agents
- **RoleResolver**: Added GraphQL mutations for `assignRoleToAgent` and
`removeRoleFromAgent`

### 3. AI Agent CRUD Operations
- **Permission-Based Tool Generation**: AI agents now receive database
tools based on their assigned role permissions
- **Dynamic Tool Creation**: The `AgentToolService` generates CRUD tools
(`create_*`, `find_*`, `update_*`, `soft_delete_*`, `destroy_*`) for
each object based on role permissions
- **Granular Permissions**: Supports both global role permissions
(`canReadAllObjectRecords`) and object-specific permissions
(`canReadObjectRecords`)

### 4. Frontend Integration
- **Role Assignment UI**: Added hooks and components for
assigning/removing roles from agents

## Demo


https://github.com/user-attachments/assets/41732267-742e-416c-b423-b687c2614c82

---------

Co-authored-by: Antoine Moreaux <moreaux.antoine@gmail.com>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
Co-authored-by: Guillim <guillim@users.noreply.github.com>
Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
Co-authored-by: Weiko <corentin@twenty.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@twenty.com>
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Marie <51697796+ijreilly@users.noreply.github.com>
Co-authored-by: martmull <martmull@hotmail.fr>
Co-authored-by: Thomas Trompette <thomas.trompette@sfr.fr>
Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
Co-authored-by: Baptiste Devessier <baptiste@devessier.fr>
Co-authored-by: nitin <142569587+ehconitin@users.noreply.github.com>
Co-authored-by: Paul Rastoin <45004772+prastoin@users.noreply.github.com>
Co-authored-by: prastoin <paul@twenty.com>
Co-authored-by: Vicky Wang <157669812+vickywxng@users.noreply.github.com>
Co-authored-by: Vicky Wang <vw92@cornell.edu>
Co-authored-by: Raphaël Bosi <71827178+bosiraphael@users.noreply.github.com>
This commit is contained in:
Abdul Rahman
2025-06-30 01:48:14 +05:30
committed by GitHub
parent 317336ab71
commit 74b6466a57
53 changed files with 4804 additions and 478 deletions

View File

@ -17,6 +17,7 @@ export class TwentyORMGlobalManager {
workspaceEntity: Type<T>,
options?: {
shouldBypassPermissionChecks?: boolean;
roleId?: string;
},
): Promise<WorkspaceRepository<T>>;
@ -25,6 +26,7 @@ export class TwentyORMGlobalManager {
objectMetadataName: string,
options?: {
shouldBypassPermissionChecks?: boolean;
roleId?: string;
},
): Promise<WorkspaceRepository<T>>;
@ -33,6 +35,7 @@ export class TwentyORMGlobalManager {
workspaceEntityOrObjectMetadataName: Type<T> | string,
options: {
shouldBypassPermissionChecks?: boolean;
roleId?: string;
} = {
shouldBypassPermissionChecks: false,
},
@ -53,6 +56,7 @@ export class TwentyORMGlobalManager {
const repository = workspaceDataSource.getRepository<T>(
objectMetadataName,
options.shouldBypassPermissionChecks,
options.roleId,
);
return repository;

View File

@ -4,7 +4,7 @@ import { InjectRepository } from '@nestjs/typeorm';
import { isDefined } from 'twenty-shared/utils';
import { ObjectLiteral, Repository } from 'typeorm';
import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
import { WorkspaceDatasourceFactory } from 'src/engine/twenty-orm/factories/workspace-datasource.factory';
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
@ -13,8 +13,8 @@ import { convertClassNameToObjectMetadataName } from 'src/engine/workspace-manag
@Injectable()
export class TwentyORMManager {
constructor(
@InjectRepository(UserWorkspaceRoleEntity, 'core')
private readonly userWorkspaceRoleRepository: Repository<UserWorkspaceRoleEntity>,
@InjectRepository(RoleTargetsEntity, 'core')
private readonly roleTargetsRepository: Repository<RoleTargetsEntity>,
private readonly workspaceDataSourceFactory: WorkspaceDatasourceFactory,
private readonly scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory,
) {}
@ -53,14 +53,14 @@ export class TwentyORMManager {
let roleId: string | undefined;
if (isDefined(userWorkspaceId)) {
const userWorkspaceRole = await this.userWorkspaceRoleRepository.findOne({
const roleTarget = await this.roleTargetsRepository.findOne({
where: {
userWorkspaceId,
workspaceId: workspaceId,
workspaceId,
},
});
roleId = userWorkspaceRole?.roleId;
roleId = roleTarget?.roleId;
}
const shouldBypassPermissionChecks = !!isExecutedByApiKey;

View File

@ -7,7 +7,7 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { WorkspaceFeatureFlagsMapCacheModule } from 'src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.module';
import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module';
import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module';
@ -23,7 +23,7 @@ import { PgPoolSharedModule } from './pg-shared-pool/pg-shared-pool.module';
@Module({
imports: [
TypeOrmModule.forFeature(
[ObjectMetadataEntity, UserWorkspaceRoleEntity, Workspace],
[ObjectMetadataEntity, RoleTargetsEntity, Workspace],
'core',
),
DataSourceModule,