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
@@ -62,6 +62,7 @@ export type Agent = {
name: Scalars['String'];
prompt: Scalars['String'];
responseFormat?: Maybe<Scalars['JSON']>;
roleId?: Maybe<Scalars['UUID']>;
updatedAt: Scalars['DateTime'];
};
@@ -952,6 +953,7 @@ export type Mutation = {
__typename?: 'Mutation';
activateWorkflowVersion: Scalars['Boolean'];
activateWorkspace: Workspace;
assignRoleToAgent: Scalars['Boolean'];
authorizeApp: AuthorizeApp;
checkCustomDomainValidRecords?: Maybe<CustomDomainValidRecords>;
checkoutSession: BillingSessionOutput;
@@ -996,6 +998,7 @@ export type Mutation = {
getLoginTokenFromEmailVerificationToken: GetLoginTokenFromEmailVerificationTokenOutput;
impersonate: ImpersonateOutput;
publishServerlessFunction: ServerlessFunction;
removeRoleFromAgent: Scalars['Boolean'];
renewToken: AuthTokens;
resendEmailVerificationToken: ResendEmailVerificationTokenOutput;
resendWorkspaceInvitation: SendInvitationsOutput;
@@ -1046,6 +1049,12 @@ export type MutationActivateWorkspaceArgs = {
};
export type MutationAssignRoleToAgentArgs = {
agentId: Scalars['UUID'];
roleId: Scalars['UUID'];
};
export type MutationAuthorizeAppArgs = {
clientId: Scalars['String'];
codeChallenge?: InputMaybe<Scalars['String']>;
@@ -1240,6 +1249,11 @@ export type MutationPublishServerlessFunctionArgs = {
};
export type MutationRemoveRoleFromAgentArgs = {
agentId: Scalars['UUID'];
};
export type MutationRenewTokenArgs = {
appToken: Scalars['String'];
};
@@ -3180,6 +3194,21 @@ export type UpdateWorkflowVersionStepMutationVariables = Exact<{
export type UpdateWorkflowVersionStepMutation = { __typename?: 'Mutation', updateWorkflowVersionStep: { __typename?: 'WorkflowAction', id: any, name: string, type: string, settings: any, valid: boolean, nextStepIds?: Array<any> | null } };
export type AssignRoleToAgentMutationVariables = Exact<{
agentId: Scalars['UUID'];
roleId: Scalars['UUID'];
}>;
export type AssignRoleToAgentMutation = { __typename?: 'Mutation', assignRoleToAgent: boolean };
export type RemoveRoleFromAgentMutationVariables = Exact<{
agentId: Scalars['UUID'];
}>;
export type RemoveRoleFromAgentMutation = { __typename?: 'Mutation', removeRoleFromAgent: boolean };
export type UpdateOneAgentMutationVariables = Exact<{
input: UpdateAgentInput;
}>;
@@ -3192,7 +3221,7 @@ export type FindOneAgentQueryVariables = Exact<{
}>;
export type FindOneAgentQuery = { __typename?: 'Query', findOneAgent: { __typename?: 'Agent', id: any, name: string, description?: string | null, prompt: string, modelId: string, responseFormat?: any | null } };
export type FindOneAgentQuery = { __typename?: 'Query', findOneAgent: { __typename?: 'Agent', id: any, name: string, description?: string | null, prompt: string, modelId: string, responseFormat?: any | null, roleId?: any | null } };
export type SubmitFormStepMutationVariables = Exact<{
input: SubmitFormStepInput;
@@ -6569,6 +6598,69 @@ export function useUpdateWorkflowVersionStepMutation(baseOptions?: Apollo.Mutati
export type UpdateWorkflowVersionStepMutationHookResult = ReturnType<typeof useUpdateWorkflowVersionStepMutation>;
export type UpdateWorkflowVersionStepMutationResult = Apollo.MutationResult<UpdateWorkflowVersionStepMutation>;
export type UpdateWorkflowVersionStepMutationOptions = Apollo.BaseMutationOptions<UpdateWorkflowVersionStepMutation, UpdateWorkflowVersionStepMutationVariables>;
export const AssignRoleToAgentDocument = gql`
mutation AssignRoleToAgent($agentId: UUID!, $roleId: UUID!) {
assignRoleToAgent(agentId: $agentId, roleId: $roleId)
}
`;
export type AssignRoleToAgentMutationFn = Apollo.MutationFunction<AssignRoleToAgentMutation, AssignRoleToAgentMutationVariables>;
/**
* __useAssignRoleToAgentMutation__
*
* To run a mutation, you first call `useAssignRoleToAgentMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useAssignRoleToAgentMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [assignRoleToAgentMutation, { data, loading, error }] = useAssignRoleToAgentMutation({
* variables: {
* agentId: // value for 'agentId'
* roleId: // value for 'roleId'
* },
* });
*/
export function useAssignRoleToAgentMutation(baseOptions?: Apollo.MutationHookOptions<AssignRoleToAgentMutation, AssignRoleToAgentMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<AssignRoleToAgentMutation, AssignRoleToAgentMutationVariables>(AssignRoleToAgentDocument, options);
}
export type AssignRoleToAgentMutationHookResult = ReturnType<typeof useAssignRoleToAgentMutation>;
export type AssignRoleToAgentMutationResult = Apollo.MutationResult<AssignRoleToAgentMutation>;
export type AssignRoleToAgentMutationOptions = Apollo.BaseMutationOptions<AssignRoleToAgentMutation, AssignRoleToAgentMutationVariables>;
export const RemoveRoleFromAgentDocument = gql`
mutation RemoveRoleFromAgent($agentId: UUID!) {
removeRoleFromAgent(agentId: $agentId)
}
`;
export type RemoveRoleFromAgentMutationFn = Apollo.MutationFunction<RemoveRoleFromAgentMutation, RemoveRoleFromAgentMutationVariables>;
/**
* __useRemoveRoleFromAgentMutation__
*
* To run a mutation, you first call `useRemoveRoleFromAgentMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useRemoveRoleFromAgentMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [removeRoleFromAgentMutation, { data, loading, error }] = useRemoveRoleFromAgentMutation({
* variables: {
* agentId: // value for 'agentId'
* },
* });
*/
export function useRemoveRoleFromAgentMutation(baseOptions?: Apollo.MutationHookOptions<RemoveRoleFromAgentMutation, RemoveRoleFromAgentMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<RemoveRoleFromAgentMutation, RemoveRoleFromAgentMutationVariables>(RemoveRoleFromAgentDocument, options);
}
export type RemoveRoleFromAgentMutationHookResult = ReturnType<typeof useRemoveRoleFromAgentMutation>;
export type RemoveRoleFromAgentMutationResult = Apollo.MutationResult<RemoveRoleFromAgentMutation>;
export type RemoveRoleFromAgentMutationOptions = Apollo.BaseMutationOptions<RemoveRoleFromAgentMutation, RemoveRoleFromAgentMutationVariables>;
export const UpdateOneAgentDocument = gql`
mutation UpdateOneAgent($input: UpdateAgentInput!) {
updateOneAgent(input: $input) {
@@ -6616,6 +6708,7 @@ export const FindOneAgentDocument = gql`
prompt
modelId
responseFormat
roleId
}
}
`;