diff --git a/src/management-system-v2/lib/data/roles.ts b/src/management-system-v2/lib/data/roles.ts index fdc496ca6..99462501d 100644 --- a/src/management-system-v2/lib/data/roles.ts +++ b/src/management-system-v2/lib/data/roles.ts @@ -11,14 +11,13 @@ import { getUserRoles as _getUserRoles, getRoleWithMembersById, } from '@/lib/data/db/iam/roles'; -import { UnauthorizedError } from '../ability/abilityHelper'; +import Ability, { UnauthorizedError } from '../ability/abilityHelper'; import { Role } from './role-schema'; import db from '@/lib/data/db'; -import { asyncForEach } from '../helpers/javascriptHelpers'; import { addRoleMappings } from './db/iam/role-mappings'; -export async function deleteRoles(envitonmentId: string, roleIds: string[]) { - const { ability } = await getCurrentEnvironment(envitonmentId); +export async function deleteRoles(environmentId: string, roleIds: string[]) { + const { ability } = await getCurrentEnvironment(environmentId); try { for (const roleId of roleIds) { @@ -117,9 +116,9 @@ export async function handleFolderRoleChanges( } } -export async function getRoles(environmentId: string) { +export async function getRoles(environmentId: string, ability?: Ability) { try { - const { ability } = await getCurrentEnvironment(environmentId); + if (!ability) ({ ability } = await getCurrentEnvironment(environmentId)); return await _getRoles(environmentId, ability); } catch (_) { diff --git a/src/management-system-v2/lib/engines/deployment.ts b/src/management-system-v2/lib/engines/deployment.ts index d85a0f394..b36a7d973 100644 --- a/src/management-system-v2/lib/engines/deployment.ts +++ b/src/management-system-v2/lib/engines/deployment.ts @@ -288,6 +288,11 @@ export type InstanceInfo = { milestones: { [name: string]: number }; priority?: number; costsRealSetByOwner?: string; + performers?: { + user: string[]; + roles: string[]; + }; + actualOwner?: string[]; }[]; variables: { [key: string]: { @@ -314,6 +319,11 @@ export type InstanceInfo = { executionWasInterrupted?: true; priority?: number; costsRealSetByOwner?: string; + performers?: { + user: string[]; + roles: string[]; + }; + actualOwner?: string[]; variableChanges?: Record; }[]; adaptationLog: any[]; diff --git a/src/management-system-v2/lib/engines/server-actions.ts b/src/management-system-v2/lib/engines/server-actions.ts index 606b7c59d..acb4033f2 100644 --- a/src/management-system-v2/lib/engines/server-actions.ts +++ b/src/management-system-v2/lib/engines/server-actions.ts @@ -765,8 +765,8 @@ export async function getAvailableSpaceEngines(spaceId: string) { } } -export async function getDeployment(spaceId: string, definitionId: string) { - const engines = await getCorrectTargetEngines(spaceId); +export async function getDeployment(spaceId: string, definitionId: string, ability?: Ability) { + const engines = await getCorrectTargetEngines(spaceId, undefined, undefined, ability); const deployments = await fetchDeployments(engines); diff --git a/src/management-system-v2/lib/helpers/javascriptHelpers.ts b/src/management-system-v2/lib/helpers/javascriptHelpers.ts index b95d12f8f..a912253a4 100644 --- a/src/management-system-v2/lib/helpers/javascriptHelpers.ts +++ b/src/management-system-v2/lib/helpers/javascriptHelpers.ts @@ -1,3 +1,5 @@ +import { Prettify } from '../typescript-utils'; + export async function asyncMap( array: Array, cb: (entry: Type, index: number) => Promise, @@ -26,6 +28,24 @@ export async function asyncFilter(array: Array, cb: (entry: Type) => ).filter((entry) => entry) as Array; } +export function pick( + obj: T, + keys: PickKeys, +): Prettify> { + return Object.fromEntries( + Object.entries(obj).filter(([key]) => keys.includes(key as keyof T)), + ) as Prettify>; +} + +export function omit( + obj: T, + keys: OmitKeys, +): Prettify> { + return Object.fromEntries( + Object.entries(obj).filter(([key]) => !keys.includes(key as keyof T)), + ) as Prettify>; +} + export interface DiffResult { path: string; valueA: any; diff --git a/src/management-system-v2/tools/getAccessibleTools.ts b/src/management-system-v2/tools/getAccessibleTools.ts index cd9a73d94..8e71affa4 100644 --- a/src/management-system-v2/tools/getAccessibleTools.ts +++ b/src/management-system-v2/tools/getAccessibleTools.ts @@ -60,11 +60,13 @@ export default async function getAvailableTools({ userCode }: InferSchema) { + try { + const verification = await verifyCode(userCode); + if (isUserErrorResponse(verification)) return `Error: ${verification.error.message}`; + + const { userId, environmentId, ability } = verification; + + let accessible = await isAccessible( + userId, + environmentId, + ['PROCEED_PUBLIC_PROCESS_AUTOMATION_ACTIVE'], + ['process-automation.executions'], + [ + ['view', 'Execution'], + ['view', 'Machine'], + ], + ); + + if (!accessible) + return 'Error: The user cannot access execution information in this space. This might be due to a space wide setting or due to the user not having the permission to view execution information.'; + + const engines = await getCorrectTargetEngines(environmentId, undefined, undefined, ability); + + const deployments = await getDeployments(engines, 'instances'); + + const instanceIds = new Set(); + + deployments.forEach((d) => d.instances.forEach((i) => instanceIds.add(i.processInstanceId))); + + return { + content: [{ type: 'text', text: JSON.stringify([...instanceIds]) }], + }; + } catch (err) { + if (err instanceof Error) return err.message; + else return 'Error: Something went wrong'; + } +} diff --git a/src/management-system-v2/tools/getExecutionInfo.ts b/src/management-system-v2/tools/getExecutionInfo.ts new file mode 100644 index 000000000..c74c18ee4 --- /dev/null +++ b/src/management-system-v2/tools/getExecutionInfo.ts @@ -0,0 +1,197 @@ +import { z } from 'zod'; +import { type InferSchema } from 'xmcp'; +import { isAccessible, toAuthorizationSchema, verifyCode } from '@/lib/mcp-utils'; +import { isUserErrorResponse } from '@/lib/user-error'; +import { getDeployment } from '@/lib/engines/server-actions'; +import { omit, pick } from '@/lib/helpers/javascriptHelpers'; +import { getRoles } from '@/lib/data/roles'; +import { truthyFilter } from '@/lib/typescript-utils'; +import { getFullMembersWithRoles } from '@/lib/data/db/iam/memberships'; +import { getElementById, toBpmnObject } from '@proceed/bpmn-helper'; +import { InstanceInfo } from '@/lib/engines/deployment'; + +// Define the schema for tool parameters +export const schema = toAuthorizationSchema({ + instanceId: z.string().describe('The id of the process execution to inspect.'), +}); + +// Define tool metadata +export const metadata = { + name: 'get-execution-info', + description: + "Returns information about the current state of a process' execution in the form of a json file.", + annotations: { + title: 'Get execution info', + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + }, +}; + +// Tool implementation +export default async function getExecutionInfo({ + userCode, + instanceId, +}: InferSchema) { + try { + const verification = await verifyCode(userCode); + if (isUserErrorResponse(verification)) return `Error: ${verification.error.message}`; + + const { userId, environmentId, ability } = verification; + + let accessible = await isAccessible( + userId, + environmentId, + ['PROCEED_PUBLIC_PROCESS_AUTOMATION_ACTIVE'], + ['process-automation.executions'], + [ + ['view', 'Execution'], + ['view', 'Machine'], + ], + ); + + if (!accessible) + return 'Error: The user cannot access execution information in this space. This might be due to a space wide setting or due to the user not having the permission to view execution information.'; + + const [definitionId] = instanceId.split('-_'); + + const deployment = await getDeployment(environmentId, definitionId, ability); + + if (!deployment) return 'Could not find an execution with the given id.'; + + const instance = deployment.instances.find((i) => i.processInstanceId === instanceId); + + if (!instance) return 'Could not find an execution with the given id.'; + + const usersWithRoles = await getFullMembersWithRoles(environmentId, ability); + + let roles = await getRoles(environmentId, ability); + if (isUserErrorResponse(roles)) roles = []; + + const users = Object.fromEntries( + usersWithRoles.map((user) => [ + user.id, + { + ...pick(user, ['id', 'username', 'firstName', 'lastName', 'email']), + roles: user.roles.map((r) => pick(r, ['id', 'name', 'description'])), + }, + ]), + ); + const roleMap = Object.fromEntries( + roles.map((role) => [role.id, pick(role, ['id', 'name', 'description'])]), + ); + + const idToUser = (id: string) => { + if (id in users) return { type: 'user', ...users[id] }; + }; + + const idToRole = (id: string) => { + if (id in roleMap) return { type: 'role', ...roleMap[id] }; + }; + + const version = deployment.versions.find((v) => v.versionId === instance.processVersion); + + const bpmnObj = version ? await toBpmnObject(version.bpmn) : undefined; + const idToName = (id: string) => { + if (bpmnObj) { + return (getElementById(bpmnObj, id) as any)?.name; + } + }; + + const transformPerformerInfo = < + T extends { + actualOwner?: string[]; + performers?: InstanceInfo['tokens'][number]['performers']; + }, + >( + input: T, + ) => { + return { + ...omit(input, ['actualOwner', 'performers']), + actualPerformers: input.actualOwner + ? input.actualOwner.map(idToUser).filter(truthyFilter) + : undefined, + potentialPerformers: input.performers + ? { + user: input.performers.user.map(idToUser).filter(truthyFilter), + roles: input.performers.roles.map(idToRole).filter(truthyFilter), + } + : undefined, + }; + }; + + console.log(JSON.stringify(instance, null, 2)); + + // extend the instance information object with data that might be useful to the user and the LLM + // the most significant changes are mapping from user/role ids to actual user/role information + // insertions of process element names alongside process element ids + const mappedInstance = { + // this information is not needed by/already known to the LLM + ...omit(instance, ['managementSystemLocation', 'spaceIdOfProcessInitiator']), + processInitiator: instance.processInitiator ? idToUser(instance.processInitiator) : undefined, + tokens: instance.tokens.map((t) => ({ + ...transformPerformerInfo(t), + // extend with user readable information + currentFlowElementName: idToName(t.currentFlowElementId), + })), + variables: Object.fromEntries( + Object.entries(instance.variables).map(([key, info]) => [ + key, + { + ...info, + // add the name so the llm can show it instead of the id + log: info.log.map((l) => ({ + ...l, + changedByElementName: l.changedBy && idToName(l.changedBy), + })), + }, + ]), + ), + log: instance.log.map((l) => ({ + ...transformPerformerInfo(l), + // add the name so the llm can show it instead of the id + flowElementName: idToName(l.flowElementId), + actualPerformers: l.actualOwner ? l.actualOwner.map(idToUser) : undefined, + potentialPerformers: l.performers + ? { + user: l.performers.user.map(idToUser), + roles: l.performers.roles.map(idToRole), + } + : undefined, + })), + // remove execution information needed by the engine + processVersion: version ? omit(version, ['bpmn', 'needs']) : instance.processVersion, + userTasks: instance.userTasks?.map((uT) => ({ + // remove execution information needed by the engine + ...omit(transformPerformerInfo(uT), [ + 'processInstance', + 'definitionVersion', + '$type', + 'implementation', + 'attrs', + 'resources', + ]), + // unwrap the name of the file in which the html for the user task is saved + fileName: uT.attrs?.['proceed:fileName'], + // map engine internal information about potential owners to user readable information + potentialPerformers: uT.resources + ?.flatMap((r: any) => { + try { + const { user, roles } = JSON.parse(r.resourceAssignmentExpression.expression.body); + return [...user.map(idToUser), ...roles.map(idToRole)]; + } catch (err) {} + + return undefined; + }) + .filter(truthyFilter), + })), + }; + + return { + content: [{ type: 'text', text: JSON.stringify(mappedInstance) }], + }; + } catch (err) { + if (err instanceof Error) return err.message; + else return 'Error: Something went wrong'; + } +} diff --git a/src/management-system-v2/tools/startProcess.ts b/src/management-system-v2/tools/startProcess.ts index 98453dacd..a6394968d 100644 --- a/src/management-system-v2/tools/startProcess.ts +++ b/src/management-system-v2/tools/startProcess.ts @@ -94,6 +94,10 @@ export default async function startProcess({ engine, startParameters && Object.fromEntries(Object.entries(startParameters).map(([key, value]) => [key, { value }])), + { + processInitiator: userId, + spaceIdOfProcessInitiator: environmentId, + }, ); if (isUserErrorResponse(instanceId)) {