feat: agent roleUniversalIdentifier for manifest-driven role assignment (#23206)
## Summary - Adds optional `roleUniversalIdentifier` on `AgentManifest` / `defineAgent` so apps can declaratively assign a role to an agent (same config shape as `defaultRoleUniversalIdentifier`). - Wires `agentUniversalIdentifier` as a sync many-to-one FK on `roleTarget`, and emits a deterministic `roleTarget` from the agent during app sync (create / update / delete). - Enables app agents (e.g. Slack assistant) to get a role on install without postInstall hooks or manual admin assignment. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23206?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
This commit is contained in:
+1
@@ -77,6 +77,7 @@ export const fromCreateAgentInputToFlatAgent = ({
|
||||
roleUniversalIdentifier,
|
||||
userWorkspaceId: null,
|
||||
agentId,
|
||||
agentUniversalIdentifier: flatAgentToCreate.universalIdentifier,
|
||||
apiKeyId: null,
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
|
||||
+2
@@ -66,6 +66,7 @@ const computeAgentFlatRoleTargetToUpdate = ({
|
||||
...existingRoleTarget,
|
||||
roleId,
|
||||
roleUniversalIdentifier: flatRole.universalIdentifier,
|
||||
agentUniversalIdentifier: flatAgent.universalIdentifier,
|
||||
updatedAt,
|
||||
},
|
||||
};
|
||||
@@ -78,6 +79,7 @@ const computeAgentFlatRoleTargetToUpdate = ({
|
||||
roleUniversalIdentifier: flatRole.universalIdentifier,
|
||||
userWorkspaceId: null,
|
||||
agentId: flatAgent.id,
|
||||
agentUniversalIdentifier: flatAgent.universalIdentifier,
|
||||
apiKeyId: null,
|
||||
createdAt: updatedAt,
|
||||
updatedAt,
|
||||
|
||||
+29
-17
@@ -7,6 +7,7 @@ import { IsNull, Not, Repository } from 'typeorm';
|
||||
import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service';
|
||||
|
||||
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
|
||||
import { FlatRoleTargetByAgentIdMaps } from 'src/engine/metadata-modules/flat-agent/types/flat-role-target-by-agent-id-maps.type';
|
||||
import { fromRoleTargetEntityToFlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/utils/from-role-target-entity-to-flat-role-target.util';
|
||||
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
|
||||
@@ -26,6 +27,8 @@ export class WorkspaceFlatRoleTargetByAgentIdService extends WorkspaceCacheProvi
|
||||
private readonly applicationRepository: Repository<ApplicationEntity>,
|
||||
@InjectWorkspaceScopedRepository(RoleEntity)
|
||||
private readonly roleRepository: WorkspaceScopedRepository<RoleEntity>,
|
||||
@InjectWorkspaceScopedRepository(AgentEntity)
|
||||
private readonly agentRepository: WorkspaceScopedRepository<AgentEntity>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@@ -33,28 +36,36 @@ export class WorkspaceFlatRoleTargetByAgentIdService extends WorkspaceCacheProvi
|
||||
async computeForCache(
|
||||
workspaceId: string,
|
||||
): Promise<FlatRoleTargetByAgentIdMaps> {
|
||||
const [roleTargetEntities, applications, roles] = await Promise.all([
|
||||
this.roleTargetRepository.find(workspaceId, {
|
||||
where: {
|
||||
agentId: Not(IsNull()),
|
||||
},
|
||||
withDeleted: true,
|
||||
}),
|
||||
this.applicationRepository.find({
|
||||
where: { workspaceId },
|
||||
select: ['id', 'universalIdentifier'],
|
||||
withDeleted: true,
|
||||
}),
|
||||
this.roleRepository.find(workspaceId, {
|
||||
select: ['id', 'universalIdentifier'],
|
||||
withDeleted: true,
|
||||
}),
|
||||
]);
|
||||
const [roleTargetEntities, applications, roles, agents] = await Promise.all(
|
||||
[
|
||||
this.roleTargetRepository.find(workspaceId, {
|
||||
where: {
|
||||
agentId: Not(IsNull()),
|
||||
},
|
||||
withDeleted: true,
|
||||
}),
|
||||
this.applicationRepository.find({
|
||||
where: { workspaceId },
|
||||
select: ['id', 'universalIdentifier'],
|
||||
withDeleted: true,
|
||||
}),
|
||||
this.roleRepository.find(workspaceId, {
|
||||
select: ['id', 'universalIdentifier'],
|
||||
withDeleted: true,
|
||||
}),
|
||||
this.agentRepository.find(workspaceId, {
|
||||
select: ['id', 'universalIdentifier'],
|
||||
withDeleted: true,
|
||||
}),
|
||||
],
|
||||
);
|
||||
|
||||
const applicationIdToUniversalIdentifierMap =
|
||||
createIdToUniversalIdentifierMap(applications);
|
||||
const roleIdToUniversalIdentifierMap =
|
||||
createIdToUniversalIdentifierMap(roles);
|
||||
const agentIdToUniversalIdentifierMap =
|
||||
createIdToUniversalIdentifierMap(agents);
|
||||
|
||||
const flatRoleTargetByAgentIdMaps: FlatRoleTargetByAgentIdMaps = {};
|
||||
|
||||
@@ -66,6 +77,7 @@ export class WorkspaceFlatRoleTargetByAgentIdService extends WorkspaceCacheProvi
|
||||
entity: roleTargetEntity,
|
||||
applicationIdToUniversalIdentifierMap,
|
||||
roleIdToUniversalIdentifierMap,
|
||||
agentIdToUniversalIdentifierMap,
|
||||
});
|
||||
|
||||
flatRoleTargetByAgentIdMaps[roleTargetEntity.agentId] = flatRoleTarget;
|
||||
|
||||
+1
-1
@@ -282,7 +282,7 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma
|
||||
"roleUniversalIdentifier",
|
||||
"userWorkspaceId",
|
||||
"apiKeyId",
|
||||
"agentId",
|
||||
"agentUniversalIdentifier",
|
||||
],
|
||||
"propertiesToStringify": [],
|
||||
},
|
||||
|
||||
+1
-1
@@ -885,7 +885,7 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
|
||||
agentId: {
|
||||
toCompare: true,
|
||||
toStringify: false,
|
||||
universalProperty: undefined,
|
||||
universalProperty: 'agentUniversalIdentifier',
|
||||
},
|
||||
createdAt: {
|
||||
toCompare: false,
|
||||
|
||||
+3
@@ -170,6 +170,9 @@ export const ALL_MANY_TO_ONE_METADATA_FOREIGN_KEY = {
|
||||
role: {
|
||||
foreignKey: 'roleId',
|
||||
},
|
||||
agent: {
|
||||
foreignKey: 'agentId',
|
||||
},
|
||||
apiKey: null,
|
||||
workspace: null,
|
||||
application: null,
|
||||
|
||||
+7
@@ -303,6 +303,13 @@ export const ALL_MANY_TO_ONE_METADATA_RELATIONS = {
|
||||
isNullable: false,
|
||||
universalForeignKey: 'roleUniversalIdentifier',
|
||||
},
|
||||
agent: {
|
||||
metadataName: 'agent',
|
||||
foreignKey: 'agentId',
|
||||
inverseOneToManyProperty: null,
|
||||
isNullable: true,
|
||||
universalForeignKey: 'agentUniversalIdentifier',
|
||||
},
|
||||
apiKey: null,
|
||||
workspace: null,
|
||||
application: null,
|
||||
|
||||
+1
@@ -120,6 +120,7 @@ exports[`getMetadataRelatedMetadataNames should return related metadata names fo
|
||||
exports[`getMetadataRelatedMetadataNames should return related metadata names for roleTarget 1`] = `
|
||||
[
|
||||
"role",
|
||||
"agent",
|
||||
]
|
||||
`;
|
||||
|
||||
|
||||
+1
-1
@@ -12,10 +12,10 @@ exports[`sortMetadataNamesChildrenFirst should return metadata names sorted with
|
||||
"objectPermission",
|
||||
"pageLayoutWidget",
|
||||
"rolePermissionFlag",
|
||||
"roleTarget",
|
||||
"viewSort",
|
||||
"index",
|
||||
"pageLayout",
|
||||
"roleTarget",
|
||||
"rowLevelPermissionPredicateGroup",
|
||||
"viewGroup",
|
||||
"agent",
|
||||
|
||||
+11
-1
@@ -6,6 +6,7 @@ import { Repository } from 'typeorm';
|
||||
import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service';
|
||||
|
||||
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
|
||||
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
|
||||
import { type FlatRoleTargetMaps } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target-maps.type';
|
||||
import { fromRoleTargetEntityToFlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/utils/from-role-target-entity-to-flat-role-target.util';
|
||||
@@ -27,12 +28,14 @@ export class WorkspaceFlatRoleTargetMapCacheService extends WorkspaceCacheProvid
|
||||
private readonly applicationRepository: Repository<ApplicationEntity>,
|
||||
@InjectWorkspaceScopedRepository(RoleEntity)
|
||||
private readonly roleRepository: WorkspaceScopedRepository<RoleEntity>,
|
||||
@InjectWorkspaceScopedRepository(AgentEntity)
|
||||
private readonly agentRepository: WorkspaceScopedRepository<AgentEntity>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async computeForCache(workspaceId: string): Promise<FlatRoleTargetMaps> {
|
||||
const [roleTargets, applications, roles] = await Promise.all([
|
||||
const [roleTargets, applications, roles, agents] = await Promise.all([
|
||||
this.roleTargetRepository.find(workspaceId, {
|
||||
withDeleted: true,
|
||||
}),
|
||||
@@ -45,12 +48,18 @@ export class WorkspaceFlatRoleTargetMapCacheService extends WorkspaceCacheProvid
|
||||
select: ['id', 'universalIdentifier'],
|
||||
withDeleted: true,
|
||||
}),
|
||||
this.agentRepository.find(workspaceId, {
|
||||
select: ['id', 'universalIdentifier'],
|
||||
withDeleted: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
const applicationIdToUniversalIdentifierMap =
|
||||
createIdToUniversalIdentifierMap(applications);
|
||||
const roleIdToUniversalIdentifierMap =
|
||||
createIdToUniversalIdentifierMap(roles);
|
||||
const agentIdToUniversalIdentifierMap =
|
||||
createIdToUniversalIdentifierMap(agents);
|
||||
|
||||
const flatRoleTargetMaps = createEmptyFlatEntityMaps();
|
||||
|
||||
@@ -59,6 +68,7 @@ export class WorkspaceFlatRoleTargetMapCacheService extends WorkspaceCacheProvid
|
||||
entity: roleTargetEntity,
|
||||
applicationIdToUniversalIdentifierMap,
|
||||
roleIdToUniversalIdentifierMap,
|
||||
agentIdToUniversalIdentifierMap,
|
||||
});
|
||||
|
||||
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from 'typeorm';
|
||||
|
||||
import { ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity';
|
||||
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
|
||||
|
||||
@@ -50,6 +51,13 @@ export class RoleTargetEntity extends SyncableEntity {
|
||||
@Column({ nullable: true, type: 'uuid' })
|
||||
agentId: string | null;
|
||||
|
||||
@ManyToOne(() => AgentEntity, {
|
||||
onDelete: 'CASCADE',
|
||||
nullable: true,
|
||||
})
|
||||
@JoinColumn({ name: 'agentId' })
|
||||
agent: Relation<AgentEntity> | null;
|
||||
|
||||
@Column({ nullable: true, type: 'uuid' })
|
||||
apiKeyId: string | null;
|
||||
|
||||
|
||||
+8
-1
@@ -56,7 +56,12 @@ export class RoleTargetService {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { flatRoleTargetMaps, flatApplicationMaps, flatRoleMaps } =
|
||||
const {
|
||||
flatRoleTargetMaps,
|
||||
flatApplicationMaps,
|
||||
flatRoleMaps,
|
||||
flatAgentMaps,
|
||||
} =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
@@ -64,6 +69,7 @@ export class RoleTargetService {
|
||||
'flatRoleTargetMaps',
|
||||
'flatApplicationMaps',
|
||||
'flatRoleMaps',
|
||||
'flatAgentMaps',
|
||||
],
|
||||
},
|
||||
);
|
||||
@@ -88,6 +94,7 @@ export class RoleTargetService {
|
||||
createRoleTargetInput,
|
||||
flatRoleTargetMaps,
|
||||
flatRoleMaps,
|
||||
flatAgentMaps,
|
||||
workspaceId,
|
||||
flatApplication: flatApplication ?? workspaceCustomFlatApplication,
|
||||
});
|
||||
|
||||
+15
-1
@@ -3,6 +3,7 @@ import { v4 } from 'uuid';
|
||||
|
||||
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
|
||||
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { resolveEntityRelationUniversalIdentifiers } from 'src/engine/metadata-modules/flat-entity/utils/resolve-entity-relation-universal-identifiers.util';
|
||||
import { type FlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target.type';
|
||||
import { findFlatRoleTargetFromForeignKey } from 'src/engine/metadata-modules/flat-role-target/utils/find-flat-role-target-from-foreign-key.util';
|
||||
@@ -13,12 +14,16 @@ export const fromCreateRoleTargetInputToFlatRoleTargetToCreate = ({
|
||||
workspaceId,
|
||||
flatRoleTargetMaps,
|
||||
flatRoleMaps,
|
||||
flatAgentMaps,
|
||||
flatApplication,
|
||||
}: {
|
||||
createRoleTargetInput: CreateRoleTargetInput;
|
||||
workspaceId: string;
|
||||
flatApplication: FlatApplication;
|
||||
} & Pick<AllFlatEntityMaps, 'flatRoleTargetMaps' | 'flatRoleMaps'>): {
|
||||
} & Pick<
|
||||
AllFlatEntityMaps,
|
||||
'flatRoleTargetMaps' | 'flatRoleMaps' | 'flatAgentMaps'
|
||||
>): {
|
||||
flatRoleTargetToCreate: FlatRoleTarget;
|
||||
flatRoleTargetsToDelete: FlatRoleTarget[];
|
||||
} => {
|
||||
@@ -34,12 +39,21 @@ export const fromCreateRoleTargetInputToFlatRoleTargetToCreate = ({
|
||||
},
|
||||
);
|
||||
|
||||
const agentUniversalIdentifier =
|
||||
targetMetadataForeignKey === 'agentId'
|
||||
? findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityMaps: flatAgentMaps,
|
||||
flatEntityId: targetId,
|
||||
}).universalIdentifier
|
||||
: null;
|
||||
|
||||
const flatRoleTargetToCreate: FlatRoleTarget = {
|
||||
id: v4(),
|
||||
roleId,
|
||||
roleUniversalIdentifier,
|
||||
userWorkspaceId: null,
|
||||
agentId: null,
|
||||
agentUniversalIdentifier,
|
||||
apiKeyId: null,
|
||||
createdAt: now.toISOString(),
|
||||
updatedAt: now.toISOString(),
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ApplicationModule } from 'src/engine/core-modules/application/applicati
|
||||
import { FileModule } from 'src/engine/core-modules/file/file.module';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
|
||||
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
|
||||
import { AiAgentRoleModule } from 'src/engine/metadata-modules/ai/ai-agent-role/ai-agent-role.module';
|
||||
import { FlatAgentModule } from 'src/engine/metadata-modules/flat-agent/flat-agent.module';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
|
||||
@@ -37,6 +38,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
ApplicationEntity,
|
||||
AgentEntity,
|
||||
RoleEntity,
|
||||
RoleTargetEntity,
|
||||
ObjectPermissionEntity,
|
||||
@@ -72,6 +74,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
|
||||
WorkspaceRolesPermissionsCacheService,
|
||||
provideWorkspaceScopedRepository(RoleEntity),
|
||||
provideWorkspaceScopedRepository(RoleTargetEntity),
|
||||
provideWorkspaceScopedRepository(AgentEntity),
|
||||
provideWorkspaceScopedRepository(ObjectPermissionEntity),
|
||||
provideWorkspaceScopedRepository(FieldPermissionEntity),
|
||||
provideWorkspaceScopedRepository(RowLevelPermissionPredicateEntity),
|
||||
|
||||
Reference in New Issue
Block a user