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

@ -4,7 +4,7 @@ import { Relation } from 'typeorm';
import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto';
import { ObjectPermissionDTO } from 'src/engine/metadata-modules/object-permission/dtos/object-permission.dto';
import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { SettingPermissionDTO } from 'src/engine/metadata-modules/setting-permission/dtos/setting-permission.dto';
@ObjectType('Role')
@ -25,7 +25,7 @@ export class RoleDTO {
isEditable: boolean;
@HideField()
userWorkspaceRoles: Relation<UserWorkspaceRoleEntity[]>;
roleTargets: Relation<RoleTargetsEntity[]>;
@Field(() => [WorkspaceMember], { nullable: true })
workspaceMembers?: WorkspaceMember[];

View File

@ -1,4 +1,5 @@
import {
Check,
Column,
CreateDateColumn,
Entity,
@ -13,16 +14,15 @@ import {
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
@Entity('userWorkspaceRole')
@Unique('IDX_USER_WORKSPACE_ROLE_USER_WORKSPACE_ID_ROLE_ID_UNIQUE', [
'userWorkspaceId',
'roleId',
])
@Index('IDX_USER_WORKSPACE_ROLE_USER_WORKSPACE_ID_WORKSPACE_ID', [
'userWorkspaceId',
'workspaceId',
])
export class UserWorkspaceRoleEntity {
@Entity('roleTargets')
@Unique('IDX_ROLE_TARGETS_UNIQUE', ['userWorkspaceId', 'roleId', 'agentId'])
@Index('IDX_ROLE_TARGETS_WORKSPACE_ID', ['userWorkspaceId', 'workspaceId'])
@Index('IDX_ROLE_TARGETS_AGENT_ID', ['agentId'])
@Check(
'CHK_role_targets_either_agent_or_user',
'("agentId" IS NOT NULL AND "userWorkspaceId" IS NULL) OR ("agentId" IS NULL AND "userWorkspaceId" IS NOT NULL)',
)
export class RoleTargetsEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@ -32,15 +32,18 @@ export class UserWorkspaceRoleEntity {
@Column({ nullable: false, type: 'uuid' })
roleId: string;
@ManyToOne(() => RoleEntity, (role) => role.userWorkspaceRoles, {
@ManyToOne(() => RoleEntity, (role) => role.roleTargets, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'roleId' })
role: Relation<RoleEntity>;
@Column({ nullable: false, type: 'uuid' })
@Column({ nullable: true, type: 'uuid' })
userWorkspaceId: string;
@Column({ nullable: true, type: 'uuid' })
agentId: string;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;

View File

@ -10,7 +10,7 @@ import {
} from 'typeorm';
import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity';
import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { SettingPermissionEntity } from 'src/engine/metadata-modules/setting-permission/setting-permission.entity';
@Entity('role')
@ -56,10 +56,10 @@ export class RoleEntity {
isEditable: boolean;
@OneToMany(
() => UserWorkspaceRoleEntity,
(userWorkspaceRole: UserWorkspaceRoleEntity) => userWorkspaceRole.role,
() => RoleTargetsEntity,
(roleTargets: RoleTargetsEntity) => roleTargets.role,
)
userWorkspaceRoles: Relation<UserWorkspaceRoleEntity[]>;
roleTargets: Relation<RoleTargetsEntity[]>;
@OneToMany(
() => ObjectPermissionEntity,

View File

@ -5,8 +5,10 @@ import { FileModule } from 'src/engine/core-modules/file/file.module';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AgentRoleModule } from 'src/engine/metadata-modules/agent-role/agent-role.module';
import { ObjectPermissionModule } from 'src/engine/metadata-modules/object-permission/object-permission.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 { RoleResolver } from 'src/engine/metadata-modules/role/role.resolver';
import { RoleService } from 'src/engine/metadata-modules/role/role.service';
@ -16,9 +18,10 @@ import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/wor
@Module({
imports: [
TypeOrmModule.forFeature([RoleEntity], 'core'),
TypeOrmModule.forFeature([RoleEntity, RoleTargetsEntity], 'core'),
TypeOrmModule.forFeature([UserWorkspace, Workspace], 'core'),
UserRoleModule,
AgentRoleModule,
PermissionsModule,
UserWorkspaceModule,
ObjectPermissionModule,

View File

@ -8,6 +8,8 @@ import {
Resolver,
} from '@nestjs/graphql';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
@ -15,9 +17,11 @@ import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-mem
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspaceMemberId } from 'src/engine/decorators/auth/auth-workspace-member-id.decorator';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { RequireFeatureFlag } from 'src/engine/guards/feature-flag.guard';
import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard';
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { AgentRoleService } from 'src/engine/metadata-modules/agent-role/agent-role.service';
import { ObjectPermissionDTO } from 'src/engine/metadata-modules/object-permission/dtos/object-permission.dto';
import { UpsertObjectPermissionsInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-object-permissions.input';
import { ObjectPermissionService } from 'src/engine/metadata-modules/object-permission/object-permission.service';
@ -55,6 +59,7 @@ export class RoleResolver {
private readonly userWorkspaceService: UserWorkspaceService,
private readonly objectPermissionService: ObjectPermissionService,
private readonly settingPermissionService: SettingPermissionService,
private readonly agentRoleService: AgentRoleService,
) {}
@Query(() => [RoleDTO])
@ -174,6 +179,36 @@ export class RoleResolver {
});
}
@Mutation(() => Boolean)
@RequireFeatureFlag(FeatureFlagKey.IS_AI_ENABLED)
async assignRoleToAgent(
@Args('agentId', { type: () => UUIDScalarType }) agentId: string,
@Args('roleId', { type: () => UUIDScalarType }) roleId: string,
@AuthWorkspace() { id: workspaceId }: Workspace,
) {
await this.agentRoleService.assignRoleToAgent({
agentId,
roleId,
workspaceId,
});
return true;
}
@Mutation(() => Boolean)
@RequireFeatureFlag(FeatureFlagKey.IS_AI_ENABLED)
async removeRoleFromAgent(
@Args('agentId', { type: () => UUIDScalarType }) agentId: string,
@AuthWorkspace() { id: workspaceId }: Workspace,
) {
await this.agentRoleService.removeRoleFromAgent({
agentId,
workspaceId,
});
return true;
}
@ResolveField('workspaceMembers', () => [WorkspaceMember])
async getWorkspaceMembersAssignedToRole(
@Parent() role: RoleDTO,

View File

@ -16,6 +16,7 @@ import {
UpdateRoleInput,
UpdateRolePayload,
} from 'src/engine/metadata-modules/role/dtos/update-role-input.dto';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
import { isArgDefinedIfProvidedOrThrow } from 'src/engine/metadata-modules/utils/is-arg-defined-if-provided-or-throw.util';
@ -27,6 +28,8 @@ export class RoleService {
private readonly workspaceRepository: Repository<Workspace>,
@InjectRepository(RoleEntity, 'core')
private readonly roleRepository: Repository<RoleEntity>,
@InjectRepository(RoleTargetsEntity, 'core')
private readonly roleTargetsRepository: Repository<RoleTargetsEntity>,
private readonly userRoleService: UserRoleService,
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
) {}
@ -36,11 +39,7 @@ export class RoleService {
where: {
workspaceId,
},
relations: [
'userWorkspaceRoles',
'settingPermissions',
'objectPermissions',
],
relations: ['roleTargets', 'settingPermissions', 'objectPermissions'],
});
}
@ -53,7 +52,7 @@ export class RoleService {
id,
workspaceId,
},
relations: ['userWorkspaceRoles', 'settingPermissions'],
relations: ['roleTargets', 'settingPermissions'],
});
}

View File

@ -1,31 +1,19 @@
import { RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
export const fromRoleEntityToRoleDto = ({
id,
label,
canUpdateAllSettings,
description,
icon,
isEditable,
userWorkspaceRoles,
canReadAllObjectRecords,
canUpdateAllObjectRecords,
canSoftDeleteAllObjectRecords,
canDestroyAllObjectRecords,
}: RoleEntity): RoleDTO => {
export const fromRoleEntityToRoleDto = (role: RoleEntity): RoleDTO => {
return {
id,
label,
canUpdateAllSettings,
description,
icon,
isEditable,
userWorkspaceRoles,
canReadAllObjectRecords,
canUpdateAllObjectRecords,
canSoftDeleteAllObjectRecords,
canDestroyAllObjectRecords,
id: role.id,
label: role.label,
canUpdateAllSettings: role.canUpdateAllSettings,
description: role.description,
icon: role.icon,
isEditable: role.isEditable,
canReadAllObjectRecords: role.canReadAllObjectRecords,
canUpdateAllObjectRecords: role.canUpdateAllObjectRecords,
canSoftDeleteAllObjectRecords: role.canSoftDeleteAllObjectRecords,
canDestroyAllObjectRecords: role.canDestroyAllObjectRecords,
roleTargets: role.roleTargets,
};
};