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-29 22:18:14 +02:00
committed by GitHub
co-authored by Antoine Moreaux Lucas Bordeau Charles Bochet Guillim Charles Bochet Weiko github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> github-actions Félix Malfait Marie martmull Thomas Trompette Etienne Baptiste Devessier nitin Paul Rastoin prastoin Vicky Wang Vicky Wang Raphaël Bosi
parent 317336ab71
commit 74b6466a57
53 changed files with 4804 additions and 478 deletions
@@ -12,9 +12,9 @@ import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { RoleService } from 'src/engine/metadata-modules/role/role.service';
import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
import { WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service';
@@ -29,7 +29,7 @@ describe('WorkspaceManagerService', () => {
let dataSourceRepository: Repository<DataSourceEntity>;
let workspaceFieldMetadataRepository: Repository<FieldMetadataEntity>;
let workspaceDataSourceService: WorkspaceDataSourceService;
let userWorkspaceRoleRepository: Repository<UserWorkspaceRoleEntity>;
let roleTargetsRepository: Repository<RoleTargetsEntity>;
let roleRepository: Repository<RoleEntity>;
beforeEach(async () => {
@@ -71,7 +71,7 @@ describe('WorkspaceManagerService', () => {
},
},
{
provide: getRepositoryToken(UserWorkspaceRoleEntity, 'core'),
provide: getRepositoryToken(RoleTargetsEntity, 'core'),
useValue: {
delete: jest.fn(),
},
@@ -134,9 +134,9 @@ describe('WorkspaceManagerService', () => {
workspaceDataSourceService = module.get<WorkspaceDataSourceService>(
WorkspaceDataSourceService,
);
userWorkspaceRoleRepository = module.get<
Repository<UserWorkspaceRoleEntity>
>(getRepositoryToken(UserWorkspaceRoleEntity, 'core'));
roleTargetsRepository = module.get<Repository<RoleTargetsEntity>>(
getRepositoryToken(RoleTargetsEntity, 'core'),
);
roleRepository = module.get<Repository<RoleEntity>>(
getRepositoryToken(RoleEntity, 'core'),
);
@@ -159,7 +159,7 @@ describe('WorkspaceManagerService', () => {
expect(dataSourceRepository.delete).toHaveBeenCalledWith({
workspaceId: 'workspace-id',
});
expect(userWorkspaceRoleRepository.delete).toHaveBeenCalledWith({
expect(roleTargetsRepository.delete).toHaveBeenCalledWith({
workspaceId: 'workspace-id',
});
expect(roleRepository.delete).toHaveBeenCalledWith({
@@ -8,9 +8,9 @@ import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-s
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { RoleModule } from 'src/engine/metadata-modules/role/role.module';
import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity';
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
@@ -35,7 +35,7 @@ import { WorkspaceManagerService } from './workspace-manager.service';
RoleModule,
UserRoleModule,
TypeOrmModule.forFeature(
[FieldMetadataEntity, UserWorkspaceRoleEntity, RoleEntity],
[FieldMetadataEntity, RoleTargetsEntity, RoleEntity],
'core',
),
],
@@ -10,9 +10,9 @@ import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-s
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { RoleService } from 'src/engine/metadata-modules/role/role.service';
import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
@@ -38,10 +38,10 @@ export class WorkspaceManagerService {
private readonly featureFlagService: FeatureFlagService,
@InjectRepository(Workspace, 'core')
private readonly workspaceRepository: Repository<Workspace>,
@InjectRepository(UserWorkspaceRoleEntity, 'core')
private readonly userWorkspaceRoleRepository: Repository<UserWorkspaceRoleEntity>,
@InjectRepository(RoleEntity, 'core')
private readonly roleRepository: Repository<RoleEntity>,
@InjectRepository(RoleTargetsEntity, 'core')
private readonly roleTargetsRepository: Repository<RoleTargetsEntity>,
) {}
public async init({
@@ -139,10 +139,10 @@ export class WorkspaceManagerService {
});
this.logger.log(`workspace ${workspaceId} field metadata deleted`);
await this.userWorkspaceRoleRepository.delete({
await this.roleTargetsRepository.delete({
workspaceId,
});
this.logger.log(`workspace ${workspaceId} user workspace role deleted`);
this.logger.log(`workspace ${workspaceId} role targets deleted`);
await this.roleRepository.delete({
workspaceId,