implement "acting on behalf of user" for workflows and agents (#15103)
## Summary **Step 1 of 2:** Implements the "acting on behalf of user" concept for workflows and agents to prevent permission escalation and maintain proper audit trails. ## Problem Previously, workflows and agents would bypass permissions regardless of who initiated them, allowing users to escalate their privileges by triggering workflows that performed actions they couldn't do directly. ## Solution ### For Workflows Introduced `WorkflowExecutionContext` service that determines execution mode: - **Manual triggers/test button**: Uses user's roleId for permissions, user's identity for `createdBy` - **Automated triggers** (cron, database events, webhooks): Bypasses permissions, uses workflow identity ### For Agents **In Chat:** - Always act on behalf of the user - Use user's roleId for permission checks - Use user's identity for `createdBy` # Step 1 vs Step 2 ### ✅ Step 1 (This PR): Acting on Behalf Concept - Introduced `isActingOnBehalfOfUser` boolean concept - Single roleId used for permission checks (user's OR system bypass) - `createdBy` field properly attributes actions to initiator - Prevents permission escalation in user-initiated flows ### 🔜 Step 2 (Future): Multi-Role Permission Support - Support role intersection: `{ intersection: ['roleA', 'roleB'] }` - Support role union: `{ union: ['roleA', 'roleB', 'roleC'] }` - Enable user+agent collaboration scenarios - Update `WorkspaceEntityManager` and `WorkspaceDatasource` to handle multiple roleIds --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
||||
getRecordInputSchema,
|
||||
} from 'src/engine/metadata-modules/agent/utils/agent-tool-schema.utils';
|
||||
import { isWorkflowRunObject } from 'src/engine/metadata-modules/agent/utils/is-workflow-run-object.util';
|
||||
import { type ActorMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
||||
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
@@ -30,7 +31,11 @@ export class ToolService {
|
||||
private readonly findRecordsService: FindRecordsService,
|
||||
) {}
|
||||
|
||||
async listTools(roleId: string, workspaceId: string): Promise<ToolSet> {
|
||||
async listTools(
|
||||
roleId: string,
|
||||
workspaceId: string,
|
||||
actorContext?: ActorMetadata,
|
||||
): Promise<ToolSet> {
|
||||
const tools: ToolSet = {};
|
||||
|
||||
const { data: rolesPermissions } =
|
||||
@@ -70,6 +75,7 @@ export class ToolService {
|
||||
objectRecord: parameters.input,
|
||||
workspaceId,
|
||||
roleId,
|
||||
createdBy: actorContext,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
+3
-3
@@ -85,9 +85,9 @@ export class CreateRecordService {
|
||||
const insertResult = await repository.insert({
|
||||
...transformedObjectRecord,
|
||||
position,
|
||||
createdBy: {
|
||||
source: roleId ? FieldActorSource.AGENT : FieldActorSource.WORKFLOW,
|
||||
name: roleId ? 'Agent' : 'Workflow',
|
||||
createdBy: params.createdBy ?? {
|
||||
source: FieldActorSource.WORKFLOW,
|
||||
name: 'Workflow',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
+2
@@ -1,8 +1,10 @@
|
||||
import { type ObjectRecordProperties } from 'src/engine/core-modules/record-crud/types/object-record-properties.type';
|
||||
import { type ActorMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||
|
||||
export type CreateRecordParams = {
|
||||
objectName: string;
|
||||
objectRecord: ObjectRecordProperties;
|
||||
workspaceId: string;
|
||||
roleId?: string;
|
||||
createdBy?: ActorMetadata;
|
||||
};
|
||||
|
||||
@@ -24,7 +24,9 @@ import { type Workspace } from 'src/engine/core-modules/workspace/workspace.enti
|
||||
import { AgentHandoffToolService } from 'src/engine/metadata-modules/agent/agent-handoff-tool.service';
|
||||
import { AGENT_CONFIG } from 'src/engine/metadata-modules/agent/constants/agent-config.const';
|
||||
import { AGENT_SYSTEM_PROMPTS } from 'src/engine/metadata-modules/agent/constants/agent-system-prompts.const';
|
||||
import { AgentActorContextService } from 'src/engine/metadata-modules/agent/services/agent-actor-context.service';
|
||||
import { type RecordIdsByObjectMetadataNameSingularType } from 'src/engine/metadata-modules/agent/types/recordIdsByObjectMetadataNameSingular.type';
|
||||
import { type ActorMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||
import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util';
|
||||
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
@@ -54,6 +56,7 @@ export class AgentExecutionService implements AgentExecutionContext {
|
||||
private readonly agentToolGeneratorService: AgentToolGeneratorService,
|
||||
private readonly agentModelConfigService: AgentModelConfigService,
|
||||
private readonly aiBillingService: AIBillingService,
|
||||
private readonly agentActorContextService: AgentActorContextService,
|
||||
@InjectRepository(AgentEntity)
|
||||
private readonly agentRepository: Repository<AgentEntity>,
|
||||
) {}
|
||||
@@ -62,11 +65,15 @@ export class AgentExecutionService implements AgentExecutionContext {
|
||||
messages,
|
||||
system,
|
||||
agent,
|
||||
actorContext,
|
||||
roleIdOverride,
|
||||
excludeHandoffTools = false,
|
||||
}: {
|
||||
system: string;
|
||||
agent: AgentEntity | null;
|
||||
messages: UIMessage<unknown, UIDataTypes, UITools>[];
|
||||
actorContext?: ActorMetadata;
|
||||
roleIdOverride?: string;
|
||||
excludeHandoffTools?: boolean;
|
||||
}) {
|
||||
try {
|
||||
@@ -87,6 +94,8 @@ export class AgentExecutionService implements AgentExecutionContext {
|
||||
await this.agentToolGeneratorService.generateToolsForAgent(
|
||||
agent.id,
|
||||
agent.workspaceId,
|
||||
actorContext,
|
||||
roleIdOverride,
|
||||
);
|
||||
|
||||
let handoffTools = {};
|
||||
@@ -261,10 +270,18 @@ export class AgentExecutionService implements AgentExecutionContext {
|
||||
contextString = `\n\nCONTEXT:\n${contextPart}`;
|
||||
}
|
||||
|
||||
const { actorContext, roleId } =
|
||||
await this.agentActorContextService.buildUserActorContext(
|
||||
userWorkspaceId,
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
const aiRequestConfig = await this.prepareAIRequestConfig({
|
||||
system: `${AGENT_SYSTEM_PROMPTS.AGENT_CHAT}\n\n${agent.prompt}${contextString}`,
|
||||
agent,
|
||||
messages,
|
||||
actorContext,
|
||||
roleIdOverride: roleId,
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
|
||||
+10
-6
@@ -7,6 +7,7 @@ import { Repository } from 'typeorm';
|
||||
import { ToolAdapterService } from 'src/engine/core-modules/ai/services/tool-adapter.service';
|
||||
import { ToolService } from 'src/engine/core-modules/ai/services/tool.service';
|
||||
import { AgentService } from 'src/engine/metadata-modules/agent/agent.service';
|
||||
import { type ActorMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||
import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants';
|
||||
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
@@ -29,6 +30,8 @@ export class AgentToolGeneratorService {
|
||||
async generateToolsForAgent(
|
||||
agentId: string,
|
||||
workspaceId: string,
|
||||
actorContext?: ActorMetadata,
|
||||
roleIdOverride?: string,
|
||||
): Promise<ToolSet> {
|
||||
let tools: ToolSet = {};
|
||||
|
||||
@@ -38,15 +41,15 @@ export class AgentToolGeneratorService {
|
||||
|
||||
tools = { ...actionTools };
|
||||
|
||||
const roleId = agent.roleId;
|
||||
const effectiveRoleId = roleIdOverride || agent.roleId;
|
||||
|
||||
if (!roleId) {
|
||||
if (!effectiveRoleId) {
|
||||
return tools;
|
||||
}
|
||||
|
||||
const role = await this.roleRepository.findOne({
|
||||
where: {
|
||||
id: roleId,
|
||||
id: effectiveRoleId,
|
||||
workspaceId,
|
||||
},
|
||||
relations: ['permissionFlags'],
|
||||
@@ -65,21 +68,22 @@ export class AgentToolGeneratorService {
|
||||
if (hasWorkflowPermission) {
|
||||
const workflowTools = this.workflowToolService.generateWorkflowTools(
|
||||
workspaceId,
|
||||
roleId,
|
||||
effectiveRoleId,
|
||||
);
|
||||
|
||||
tools = { ...tools, ...workflowTools };
|
||||
}
|
||||
|
||||
const databaseTools = await this.toolService.listTools(
|
||||
roleId,
|
||||
effectiveRoleId,
|
||||
workspaceId,
|
||||
actorContext,
|
||||
);
|
||||
|
||||
tools = { ...tools, ...databaseTools };
|
||||
|
||||
const roleActionTools = await this.toolAdapterService.getTools(
|
||||
roleId,
|
||||
effectiveRoleId,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
|
||||
@@ -11,12 +11,14 @@ import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-
|
||||
import { FileModule } from 'src/engine/core-modules/file/file.module';
|
||||
import { ThrottlerModule } from 'src/engine/core-modules/throttler/throttler.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 { AgentRoleModule } from 'src/engine/metadata-modules/agent-role/agent-role.module';
|
||||
import { AgentChatController } from 'src/engine/metadata-modules/agent/agent-chat.controller';
|
||||
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 { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
|
||||
import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module';
|
||||
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
||||
import { WorkflowToolsModule } from 'src/modules/workflow/workflow-tools/workflow-tools.module';
|
||||
@@ -39,6 +41,8 @@ import { AgentEntity } from './agent.entity';
|
||||
import { AgentResolver } from './agent.resolver';
|
||||
import { AgentService } from './agent.service';
|
||||
|
||||
import { AgentActorContextService } from './services/agent-actor-context.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
@@ -66,6 +70,8 @@ import { AgentService } from './agent.service';
|
||||
TokenModule,
|
||||
DomainManagerModule,
|
||||
WorkflowToolsModule,
|
||||
UserWorkspaceModule,
|
||||
UserRoleModule,
|
||||
],
|
||||
controllers: [AgentChatController],
|
||||
providers: [
|
||||
@@ -81,6 +87,7 @@ import { AgentService } from './agent.service';
|
||||
AgentTitleGenerationService,
|
||||
AgentHandoffExecutorService,
|
||||
AgentHandoffService,
|
||||
AgentActorContextService,
|
||||
],
|
||||
exports: [
|
||||
AgentService,
|
||||
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { buildCreatedByFromFullNameMetadata } from 'src/engine/core-modules/actor/utils/build-created-by-from-full-name-metadata.util';
|
||||
import { UserWorkspaceService as UserService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||
import {
|
||||
AgentException,
|
||||
AgentExceptionCode,
|
||||
} from 'src/engine/metadata-modules/agent/agent.exception';
|
||||
import { type ActorMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
|
||||
export type AgentActorContext = {
|
||||
actorContext: ActorMetadata;
|
||||
roleId: string | undefined;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class AgentActorContextService {
|
||||
constructor(
|
||||
private readonly userService: UserService,
|
||||
private readonly userRoleService: UserRoleService,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {}
|
||||
|
||||
async buildUserActorContext(
|
||||
userWorkspaceId: string,
|
||||
workspaceId: string,
|
||||
): Promise<AgentActorContext> {
|
||||
const userWorkspace = await this.userService.findById(userWorkspaceId);
|
||||
|
||||
if (!userWorkspace) {
|
||||
throw new AgentException(
|
||||
'User workspace not found',
|
||||
AgentExceptionCode.AGENT_EXECUTION_FAILED,
|
||||
);
|
||||
}
|
||||
|
||||
const workspaceMemberRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
'workspaceMember',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const workspaceMember = await workspaceMemberRepository.findOne({
|
||||
where: {
|
||||
userId: userWorkspace.userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!workspaceMember) {
|
||||
throw new AgentException(
|
||||
'Workspace member not found for user',
|
||||
AgentExceptionCode.AGENT_EXECUTION_FAILED,
|
||||
);
|
||||
}
|
||||
|
||||
const roleId = await this.userRoleService.getRoleIdForUserWorkspace({
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
const actorContext = buildCreatedByFromFullNameMetadata({
|
||||
fullNameMetadata: workspaceMember.name,
|
||||
workspaceMemberId: workspaceMember.id,
|
||||
});
|
||||
|
||||
return { actorContext, roleId };
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { UserWorkspaceService as UserService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||
import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
||||
import { type WorkflowExecutionContext } from 'src/modules/workflow/workflow-executor/types/workflow-execution-context.type';
|
||||
import { WorkflowRunWorkspaceService as WorkflowRunService } from 'src/modules/workflow/workflow-runner/workflow-run/workflow-run.workspace-service';
|
||||
|
||||
@Injectable()
|
||||
export class WorkflowExecutionContextService {
|
||||
constructor(
|
||||
private readonly workflowRunService: WorkflowRunService,
|
||||
private readonly userService: UserService,
|
||||
private readonly userRoleService: UserRoleService,
|
||||
) {}
|
||||
|
||||
async getExecutionContext(runInfo: {
|
||||
workflowRunId: string;
|
||||
workspaceId: string;
|
||||
}): Promise<WorkflowExecutionContext> {
|
||||
const workflowRun = await this.workflowRunService.getWorkflowRunOrFail({
|
||||
workflowRunId: runInfo.workflowRunId,
|
||||
workspaceId: runInfo.workspaceId,
|
||||
});
|
||||
|
||||
const isActingOnBehalfOfUser =
|
||||
workflowRun.createdBy.source === FieldActorSource.MANUAL &&
|
||||
isDefined(workflowRun.createdBy.workspaceMemberId);
|
||||
|
||||
let roleId: string | undefined;
|
||||
|
||||
if (isActingOnBehalfOfUser) {
|
||||
const workspaceMember = await this.userService.getWorkspaceMemberOrThrow({
|
||||
workspaceMemberId: workflowRun.createdBy.workspaceMemberId!,
|
||||
workspaceId: runInfo.workspaceId,
|
||||
});
|
||||
|
||||
const userWorkspace =
|
||||
await this.userService.getUserWorkspaceForUserOrThrow({
|
||||
userId: workspaceMember.userId,
|
||||
workspaceId: runInfo.workspaceId,
|
||||
});
|
||||
|
||||
roleId = await this.userRoleService.getRoleIdForUserWorkspace({
|
||||
userWorkspaceId: userWorkspace.id,
|
||||
workspaceId: runInfo.workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
isActingOnBehalfOfUser,
|
||||
initiator: workflowRun.createdBy,
|
||||
roleId,
|
||||
};
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import { type ActorMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||
|
||||
export type WorkflowExecutionContext = {
|
||||
isActingOnBehalfOfUser: boolean;
|
||||
initiator: ActorMetadata;
|
||||
roleId?: string;
|
||||
};
|
||||
+9
-2
@@ -2,21 +2,28 @@ import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { AiModule } from 'src/engine/core-modules/ai/ai.module';
|
||||
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
|
||||
import { AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity';
|
||||
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
|
||||
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
|
||||
import { WorkflowExecutionContextService } from 'src/modules/workflow/workflow-executor/services/workflow-execution-context.service';
|
||||
import { AiAgentExecutorService } from 'src/modules/workflow/workflow-executor/workflow-actions/ai-agent/services/ai-agent-executor.service';
|
||||
import { WorkflowRunModule } from 'src/modules/workflow/workflow-runner/workflow-run/workflow-run.module';
|
||||
|
||||
import { AiAgentWorkflowAction } from './ai-agent.workflow-action';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
AiModule,
|
||||
TypeOrmModule.forFeature([AgentEntity, RoleTargetsEntity, RoleEntity]),
|
||||
TypeOrmModule.forFeature([AgentEntity, RoleTargetsEntity]),
|
||||
WorkflowRunModule,
|
||||
UserWorkspaceModule,
|
||||
UserRoleModule,
|
||||
],
|
||||
providers: [
|
||||
ScopedWorkspaceContextFactory,
|
||||
WorkflowExecutionContextService,
|
||||
AiAgentWorkflowAction,
|
||||
AiAgentExecutorService,
|
||||
],
|
||||
|
||||
+10
@@ -16,6 +16,7 @@ import {
|
||||
WorkflowStepExecutorException,
|
||||
WorkflowStepExecutorExceptionCode,
|
||||
} from 'src/modules/workflow/workflow-executor/exceptions/workflow-step-executor.exception';
|
||||
import { WorkflowExecutionContextService } from 'src/modules/workflow/workflow-executor/services/workflow-execution-context.service';
|
||||
import { type WorkflowActionInput } from 'src/modules/workflow/workflow-executor/types/workflow-action-input';
|
||||
import { type WorkflowActionOutput } from 'src/modules/workflow/workflow-executor/types/workflow-action-output.type';
|
||||
import { findStepOrThrow } from 'src/modules/workflow/workflow-executor/utils/find-step-or-throw.util';
|
||||
@@ -28,6 +29,7 @@ export class AiAgentWorkflowAction implements WorkflowAction {
|
||||
constructor(
|
||||
private readonly aiAgentExecutionService: AiAgentExecutorService,
|
||||
private readonly aiBillingService: AIBillingService,
|
||||
private readonly workflowExecutionContextService: WorkflowExecutionContextService,
|
||||
@InjectRepository(AgentEntity)
|
||||
private readonly agentRepository: Repository<AgentEntity>,
|
||||
) {}
|
||||
@@ -36,6 +38,7 @@ export class AiAgentWorkflowAction implements WorkflowAction {
|
||||
currentStepId,
|
||||
steps,
|
||||
context,
|
||||
runInfo,
|
||||
}: WorkflowActionInput): Promise<WorkflowActionOutput> {
|
||||
const step = findStepOrThrow({
|
||||
stepId: currentStepId,
|
||||
@@ -71,11 +74,18 @@ export class AiAgentWorkflowAction implements WorkflowAction {
|
||||
);
|
||||
}
|
||||
|
||||
const executionContext =
|
||||
await this.workflowExecutionContextService.getExecutionContext(runInfo);
|
||||
|
||||
const { result, usage } = await this.aiAgentExecutionService.executeAgent(
|
||||
{
|
||||
agent,
|
||||
schema: step.settings.outputSchema,
|
||||
userPrompt: resolveInput(prompt, context) as string,
|
||||
actorContext: executionContext.isActingOnBehalfOfUser
|
||||
? executionContext.initiator
|
||||
: undefined,
|
||||
roleId: executionContext.roleId,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
+13
-13
@@ -16,8 +16,8 @@ import {
|
||||
import { AGENT_CONFIG } from 'src/engine/metadata-modules/agent/constants/agent-config.const';
|
||||
import { AGENT_SYSTEM_PROMPTS } from 'src/engine/metadata-modules/agent/constants/agent-system-prompts.const';
|
||||
import { convertOutputSchemaToZod } from 'src/engine/metadata-modules/agent/utils/convert-output-schema-to-zod';
|
||||
import { type ActorMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { OutputSchema } from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type';
|
||||
|
||||
@Injectable()
|
||||
@@ -28,14 +28,14 @@ export class AiAgentExecutorService {
|
||||
private readonly toolAdapterService: ToolAdapterService,
|
||||
@InjectRepository(RoleTargetsEntity)
|
||||
private readonly roleTargetsRepository: Repository<RoleTargetsEntity>,
|
||||
@InjectRepository(RoleEntity)
|
||||
private readonly roleRepository: Repository<RoleEntity>,
|
||||
private readonly toolService: ToolService,
|
||||
) {}
|
||||
|
||||
private async getTools(
|
||||
agentId: string,
|
||||
workspaceId: string,
|
||||
actorContext?: ActorMetadata,
|
||||
roleIdOverride?: string,
|
||||
): Promise<ToolSet> {
|
||||
const roleTarget = await this.roleTargetsRepository.findOne({
|
||||
where: {
|
||||
@@ -45,27 +45,23 @@ export class AiAgentExecutorService {
|
||||
select: ['roleId'],
|
||||
});
|
||||
|
||||
const role = await this.roleRepository.findOne({
|
||||
where: {
|
||||
id: roleTarget?.roleId,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
const effectiveRoleId = roleIdOverride || roleTarget?.roleId;
|
||||
|
||||
if (!roleTarget?.roleId || !role) {
|
||||
if (!effectiveRoleId) {
|
||||
const actionTools = await this.toolAdapterService.getTools();
|
||||
|
||||
return { ...actionTools };
|
||||
}
|
||||
|
||||
const actionTools = await this.toolAdapterService.getTools(
|
||||
role.id,
|
||||
effectiveRoleId,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const databaseTools = await this.toolService.listTools(
|
||||
role.id,
|
||||
effectiveRoleId,
|
||||
workspaceId,
|
||||
actorContext,
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -78,17 +74,21 @@ export class AiAgentExecutorService {
|
||||
agent,
|
||||
schema,
|
||||
userPrompt,
|
||||
actorContext,
|
||||
roleId,
|
||||
}: {
|
||||
agent: AgentEntity | null;
|
||||
schema: OutputSchema;
|
||||
userPrompt: string;
|
||||
actorContext?: ActorMetadata;
|
||||
roleId?: string;
|
||||
}): Promise<AgentExecutionResult> {
|
||||
try {
|
||||
const registeredModel =
|
||||
await this.aiModelRegistryService.resolveModelForAgent(agent);
|
||||
|
||||
const tools = agent
|
||||
? await this.getTools(agent.id, agent.workspaceId)
|
||||
? await this.getTools(agent.id, agent.workspaceId, actorContext, roleId)
|
||||
: {};
|
||||
|
||||
this.logger.log(`Generated ${Object.keys(tools).length} tools for agent`);
|
||||
|
||||
+30
@@ -9,9 +9,15 @@ import {
|
||||
RecordCrudExceptionCode,
|
||||
} from 'src/engine/core-modules/record-crud/exceptions/record-crud.exception';
|
||||
import { CreateRecordService } from 'src/engine/core-modules/record-crud/services/create-record.service';
|
||||
import {
|
||||
type ActorMetadata,
|
||||
FieldActorSource,
|
||||
} from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
|
||||
import { WorkflowExecutionContextService } from 'src/modules/workflow/workflow-executor/services/workflow-execution-context.service';
|
||||
import { type WorkflowActionInput } from 'src/modules/workflow/workflow-executor/types/workflow-action-input';
|
||||
import { type WorkflowActionOutput } from 'src/modules/workflow/workflow-executor/types/workflow-action-output.type';
|
||||
import { type WorkflowExecutionContext } from 'src/modules/workflow/workflow-executor/types/workflow-execution-context.type';
|
||||
import { findStepOrThrow } from 'src/modules/workflow/workflow-executor/utils/find-step-or-throw.util';
|
||||
import { type WorkflowCreateRecordActionInput } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/types/workflow-record-crud-action-input.type';
|
||||
|
||||
@@ -20,12 +26,14 @@ export class CreateRecordWorkflowAction implements WorkflowAction {
|
||||
constructor(
|
||||
private readonly createRecordService: CreateRecordService,
|
||||
private readonly scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory,
|
||||
private readonly workflowExecutionContextService: WorkflowExecutionContextService,
|
||||
) {}
|
||||
|
||||
async execute({
|
||||
currentStepId,
|
||||
steps,
|
||||
context,
|
||||
runInfo,
|
||||
}: WorkflowActionInput): Promise<WorkflowActionOutput> {
|
||||
const step = findStepOrThrow({
|
||||
steps,
|
||||
@@ -46,10 +54,17 @@ export class CreateRecordWorkflowAction implements WorkflowAction {
|
||||
context,
|
||||
) as WorkflowCreateRecordActionInput;
|
||||
|
||||
const executionContext =
|
||||
await this.workflowExecutionContextService.getExecutionContext(runInfo);
|
||||
|
||||
const createdBy = this.buildCreatedByActor(executionContext);
|
||||
|
||||
const toolOutput = await this.createRecordService.execute({
|
||||
objectName: workflowActionInput.objectName,
|
||||
objectRecord: workflowActionInput.objectRecord,
|
||||
workspaceId,
|
||||
createdBy,
|
||||
roleId: executionContext.roleId,
|
||||
});
|
||||
|
||||
if (!toolOutput.success) {
|
||||
@@ -63,4 +78,19 @@ export class CreateRecordWorkflowAction implements WorkflowAction {
|
||||
result: toolOutput.result,
|
||||
};
|
||||
}
|
||||
|
||||
private buildCreatedByActor(
|
||||
executionContext: WorkflowExecutionContext,
|
||||
): ActorMetadata {
|
||||
if (executionContext.isActingOnBehalfOfUser) {
|
||||
return executionContext.initiator;
|
||||
}
|
||||
|
||||
return {
|
||||
source: FieldActorSource.WORKFLOW,
|
||||
name: 'Workflow',
|
||||
workspaceMemberId: null,
|
||||
context: {},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+7
@@ -14,6 +14,7 @@ import {
|
||||
WorkflowStepExecutorException,
|
||||
WorkflowStepExecutorExceptionCode,
|
||||
} from 'src/modules/workflow/workflow-executor/exceptions/workflow-step-executor.exception';
|
||||
import { WorkflowExecutionContextService } from 'src/modules/workflow/workflow-executor/services/workflow-execution-context.service';
|
||||
import { type WorkflowActionInput } from 'src/modules/workflow/workflow-executor/types/workflow-action-input';
|
||||
import { type WorkflowActionOutput } from 'src/modules/workflow/workflow-executor/types/workflow-action-output.type';
|
||||
import { findStepOrThrow } from 'src/modules/workflow/workflow-executor/utils/find-step-or-throw.util';
|
||||
@@ -25,12 +26,14 @@ export class DeleteRecordWorkflowAction implements WorkflowAction {
|
||||
constructor(
|
||||
private readonly deleteRecordService: DeleteRecordService,
|
||||
private readonly scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory,
|
||||
private readonly workflowExecutionContextService: WorkflowExecutionContextService,
|
||||
) {}
|
||||
|
||||
async execute({
|
||||
currentStepId,
|
||||
steps,
|
||||
context,
|
||||
runInfo,
|
||||
}: WorkflowActionInput): Promise<WorkflowActionOutput> {
|
||||
const step = findStepOrThrow({
|
||||
steps,
|
||||
@@ -69,10 +72,14 @@ export class DeleteRecordWorkflowAction implements WorkflowAction {
|
||||
);
|
||||
}
|
||||
|
||||
const executionContext =
|
||||
await this.workflowExecutionContextService.getExecutionContext(runInfo);
|
||||
|
||||
const toolOutput = await this.deleteRecordService.execute({
|
||||
objectName: workflowActionInput.objectName,
|
||||
objectRecordId: workflowActionInput.objectRecordId,
|
||||
workspaceId,
|
||||
roleId: executionContext.roleId,
|
||||
soft: true,
|
||||
});
|
||||
|
||||
|
||||
+7
@@ -14,6 +14,7 @@ import {
|
||||
WorkflowStepExecutorException,
|
||||
WorkflowStepExecutorExceptionCode,
|
||||
} from 'src/modules/workflow/workflow-executor/exceptions/workflow-step-executor.exception';
|
||||
import { WorkflowExecutionContextService } from 'src/modules/workflow/workflow-executor/services/workflow-execution-context.service';
|
||||
import { type WorkflowActionInput } from 'src/modules/workflow/workflow-executor/types/workflow-action-input';
|
||||
import { type WorkflowActionOutput } from 'src/modules/workflow/workflow-executor/types/workflow-action-output.type';
|
||||
import { findStepOrThrow } from 'src/modules/workflow/workflow-executor/utils/find-step-or-throw.util';
|
||||
@@ -25,12 +26,14 @@ export class FindRecordsWorkflowAction implements WorkflowAction {
|
||||
constructor(
|
||||
private readonly findRecordsService: FindRecordsService,
|
||||
private readonly scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory,
|
||||
private readonly workflowExecutionContextService: WorkflowExecutionContextService,
|
||||
) {}
|
||||
|
||||
async execute({
|
||||
currentStepId,
|
||||
steps,
|
||||
context,
|
||||
runInfo,
|
||||
}: WorkflowActionInput): Promise<WorkflowActionOutput> {
|
||||
const step = findStepOrThrow({
|
||||
steps,
|
||||
@@ -58,12 +61,16 @@ export class FindRecordsWorkflowAction implements WorkflowAction {
|
||||
);
|
||||
}
|
||||
|
||||
const executionContext =
|
||||
await this.workflowExecutionContextService.getExecutionContext(runInfo);
|
||||
|
||||
const toolOutput = await this.findRecordsService.execute({
|
||||
objectName: workflowActionInput.objectName,
|
||||
filter: workflowActionInput.filter?.gqlOperationFilter,
|
||||
orderBy: workflowActionInput.orderBy,
|
||||
limit: workflowActionInput.limit,
|
||||
workspaceId,
|
||||
roleId: executionContext.roleId,
|
||||
});
|
||||
|
||||
if (!toolOutput.success) {
|
||||
|
||||
+11
-1
@@ -1,16 +1,26 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { RecordCrudModule } from 'src/engine/core-modules/record-crud/record-crud.module';
|
||||
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
|
||||
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
|
||||
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
|
||||
import { WorkflowExecutionContextService } from 'src/modules/workflow/workflow-executor/services/workflow-execution-context.service';
|
||||
import { CreateRecordWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/create-record.workflow-action';
|
||||
import { DeleteRecordWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/delete-record.workflow-action';
|
||||
import { FindRecordsWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/find-records.workflow-action';
|
||||
import { UpdateRecordWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/update-record.workflow-action';
|
||||
import { WorkflowRunModule } from 'src/modules/workflow/workflow-runner/workflow-run/workflow-run.module';
|
||||
|
||||
@Module({
|
||||
imports: [RecordCrudModule],
|
||||
imports: [
|
||||
RecordCrudModule,
|
||||
WorkflowRunModule,
|
||||
UserWorkspaceModule,
|
||||
UserRoleModule,
|
||||
],
|
||||
providers: [
|
||||
ScopedWorkspaceContextFactory,
|
||||
WorkflowExecutionContextService,
|
||||
CreateRecordWorkflowAction,
|
||||
UpdateRecordWorkflowAction,
|
||||
DeleteRecordWorkflowAction,
|
||||
|
||||
+7
@@ -14,6 +14,7 @@ import {
|
||||
WorkflowStepExecutorException,
|
||||
WorkflowStepExecutorExceptionCode,
|
||||
} from 'src/modules/workflow/workflow-executor/exceptions/workflow-step-executor.exception';
|
||||
import { WorkflowExecutionContextService } from 'src/modules/workflow/workflow-executor/services/workflow-execution-context.service';
|
||||
import { type WorkflowActionInput } from 'src/modules/workflow/workflow-executor/types/workflow-action-input';
|
||||
import { type WorkflowActionOutput } from 'src/modules/workflow/workflow-executor/types/workflow-action-output.type';
|
||||
import { findStepOrThrow } from 'src/modules/workflow/workflow-executor/utils/find-step-or-throw.util';
|
||||
@@ -25,12 +26,14 @@ export class UpdateRecordWorkflowAction implements WorkflowAction {
|
||||
constructor(
|
||||
private readonly updateRecordService: UpdateRecordService,
|
||||
private readonly scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory,
|
||||
private readonly workflowExecutionContextService: WorkflowExecutionContextService,
|
||||
) {}
|
||||
|
||||
async execute({
|
||||
currentStepId,
|
||||
steps,
|
||||
context,
|
||||
runInfo,
|
||||
}: WorkflowActionInput): Promise<WorkflowActionOutput> {
|
||||
const step = findStepOrThrow({
|
||||
steps,
|
||||
@@ -69,12 +72,16 @@ export class UpdateRecordWorkflowAction implements WorkflowAction {
|
||||
);
|
||||
}
|
||||
|
||||
const executionContext =
|
||||
await this.workflowExecutionContextService.getExecutionContext(runInfo);
|
||||
|
||||
const toolOutput = await this.updateRecordService.execute({
|
||||
objectName: workflowActionInput.objectName,
|
||||
objectRecordId: workflowActionInput.objectRecordId,
|
||||
objectRecord: workflowActionInput.objectRecord,
|
||||
fieldsToUpdate: workflowActionInput.fieldsToUpdate,
|
||||
workspaceId,
|
||||
roleId: executionContext.roleId,
|
||||
});
|
||||
|
||||
if (!toolOutput.success) {
|
||||
|
||||
Reference in New Issue
Block a user