Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
f1415d8
feat(agents): add managed-runtime mode with Claude Platform integration
djabarovgeorge May 10, 2026
3fd2dc1
fix(agents): propagate AGENT_RUNTIME channel type across dashboard lo…
djabarovgeorge May 10, 2026
105936d
feat(agents): enhance agent creation with runtime and skills support
djabarovgeorge May 11, 2026
5b7daee
fix(agents): resolve issues with agent runtime configuration and dash…
djabarovgeorge May 11, 2026
1f9fa26
feat(agents): implement adoption of existing managed agents
djabarovgeorge May 11, 2026
953ae5b
feat(agents): enhance agent provisioning with apiKey support
djabarovgeorge May 11, 2026
7c38fe8
feat(agents): enhance agent deletion process with provider option
djabarovgeorge May 11, 2026
489d540
feat(agents): add skills support to agent runtime configuration
djabarovgeorge May 11, 2026
1dfea32
feat(agents): add creationSource field to agent DTOs and commands
djabarovgeorge May 11, 2026
3ee1d69
refactor(agents): remove runtime providers endpoint and related DTOs
djabarovgeorge May 11, 2026
4b863c8
feat(agents): enhance agent runtime configuration with skills and cap…
djabarovgeorge May 11, 2026
ea6786b
feat(agents): add externalEnvironmentId to integration schema and enh…
djabarovgeorge May 11, 2026
25168f9
Merge remote-tracking branch 'origin/next' into cursor/managed-agents…
djabarovgeorge May 11, 2026
8c502f8
fix(agents): address PR review comments
djabarovgeorge May 11, 2026
a102ad1
feat(dependencies): add @anthropic-ai/sdk and standardwebhooks to pnp…
djabarovgeorge May 11, 2026
a0a5488
Merge branch 'next' into cursor/managed-agents-claude-platform
LetItRock May 12, 2026
134c793
feat(agents): refactor managed runtime integration handling
djabarovgeorge May 12, 2026
8b6d628
Merge branch 'cursor/managed-agents-claude-platform' of https://githu…
djabarovgeorge May 12, 2026
6d12930
feat(integrations): introduce integration kind distinction and update…
djabarovgeorge May 12, 2026
40980db
feat(agents): enhance managed agent provisioning and runtime configur…
djabarovgeorge May 12, 2026
8796d4e
feat(agents): update agent creation DTO and use case validation
djabarovgeorge May 12, 2026
276e794
refactor(workflow): replace NotificationChannelTypeEnum with ChannelT…
djabarovgeorge May 12, 2026
18d9a90
refactor(tests): update managed agent E2E tests to stub AgentRuntimeF…
djabarovgeorge May 12, 2026
7ddbc16
fix(tests): update managed agent E2E test to correct MCP server URL
djabarovgeorge May 12, 2026
f1186b8
chore: update hash
djabarovgeorge May 12, 2026
b8c3968
Merge branch 'next' into cursor/managed-agents-claude-platform
djabarovgeorge May 12, 2026
29b5c99
fix(api-service): enforce MCP catalog on agent runtime config PATCH (…
djabarovgeorge May 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat(integrations): introduce integration kind distinction and update…
… related DTOs

- Added `kind` property to integration-related DTOs and entities to differentiate between delivery and agent-runtime integrations.
- Updated existing integration handling to accommodate the new `kind` property, ensuring proper validation and processing.
- Modified API documentation to clarify the requirements for agent-kind integrations, including optional channel specifications.
- Enhanced error handling and validation logic across various use cases to support the new integration structure.

These changes improve the clarity and functionality of the integration system, facilitating better management of different integration types.
  • Loading branch information
djabarovgeorge committed May 12, 2026
commit 6d12930bdc55666b7ccf1b8d46b571a75dbc1a9e
2 changes: 1 addition & 1 deletion .source
2 changes: 1 addition & 1 deletion apps/api/src/app/agents/agent-analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export function trackAgentIntegrationConnected(
integrationId: string;
integrationIdentifier: string;
providerId: string;
channel: string;
channel?: string;
connectionSource: 'existing_integration' | 'novu_email_provisioned';
}
): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@ export class AgentIntegrationResponseIntegrationDto {
@ApiProperty()
providerId: string;

@ApiProperty({ enum: ChannelTypeEnum, enumName: 'ChannelTypeEnum' })
channel: ChannelTypeEnum;
@ApiPropertyOptional({
description: 'Delivery channel; not set for agent-runtime integrations.',
enum: ChannelTypeEnum,
enumName: 'ChannelTypeEnum',
})
channel?: ChannelTypeEnum;

@ApiProperty()
active: boolean;
Expand Down
10 changes: 7 additions & 3 deletions apps/api/src/app/agents/dtos/agent-integration-summary.dto.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ApiProperty } from '@nestjs/swagger';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { ChannelTypeEnum } from '@novu/shared';

export class AgentIntegrationSummaryDto {
Expand All @@ -14,8 +14,12 @@ export class AgentIntegrationSummaryDto {
@ApiProperty()
identifier: string;

@ApiProperty({ enum: ChannelTypeEnum, enumName: 'ChannelTypeEnum' })
channel: ChannelTypeEnum;
@ApiPropertyOptional({
description: 'Delivery channel; not set for agent-runtime integrations.',
enum: ChannelTypeEnum,
enumName: 'ChannelTypeEnum',
})
channel?: ChannelTypeEnum;

@ApiProperty()
active: boolean;
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/app/agents/dtos/agent-runtime-config.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ export class ManagedRuntimeDto {

@ApiProperty({
description:
'ID of an existing Novu integration (channel: AGENT_RUNTIME) that holds the provider API key and ' +
'ID of an existing Novu integration (kind: "agent") that holds the provider API key and ' +
'provisioned environment. Create the integration first via POST /integrations.',
})
@IsNotEmpty()
Expand Down
24 changes: 12 additions & 12 deletions apps/api/src/app/agents/e2e/managed-agent.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
encryptCredentials,
} from '@novu/application-generic';
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
import { AgentRepository, IntegrationRepository } from '@novu/dal';
import { AgentRuntimeProviderIdEnum, ChannelTypeEnum } from '@novu/shared';
import { AgentRuntimeProviderIdEnum, IntegrationKindEnum } from '@novu/shared';
import { UserSession } from '@novu/testing';
import { expect } from 'chai';
import sinon from 'sinon';
Expand Down Expand Up @@ -82,12 +82,12 @@ describe('Managed Agents API #novu-v2', () => {
createdIntegrationIds.length = 0;
});

// ─── Helper: create an AGENT_RUNTIME integration via POST /v1/integrations ───
// ─── Helper: create an agent-kind integration via POST /v1/integrations ───

async function createAgentRuntimeIntegration(overrides: Record<string, unknown> = {}): Promise<string> {
const res = await session.testAgent.post('/v1/integrations').send({
providerId: AgentRuntimeProviderIdEnum.Anthropic,
channel: ChannelTypeEnum.AGENT_RUNTIME,
kind: IntegrationKindEnum.AGENT,
credentials: { apiKey: FAKE_API_KEY },
active: true,
name: `anthropic-runtime-e2e-${Date.now()}`,
Expand All @@ -114,13 +114,13 @@ describe('Managed Agents API #novu-v2', () => {
};
}

// ─── POST /v1/integrations — AGENT_RUNTIME provisioning ──────────────────────
// ─── POST /v1/integrations — agent-kind provisioning ─────────────────────────

describe('POST /v1/integrations — AGENT_RUNTIME channel provisioning', () => {
describe('POST /v1/integrations — agent kind provisioning', () => {
it('should create an integration and call provisionIntegration on the provider', async () => {
const res = await session.testAgent.post('/v1/integrations').send({
providerId: AgentRuntimeProviderIdEnum.Anthropic,
channel: ChannelTypeEnum.AGENT_RUNTIME,
kind: IntegrationKindEnum.AGENT,
credentials: { apiKey: FAKE_API_KEY },
active: true,
name: `anthropic-provision-test-${Date.now()}`,
Expand Down Expand Up @@ -158,28 +158,28 @@ describe('Managed Agents API #novu-v2', () => {

const res = await session.testAgent.post('/v1/integrations').send({
providerId: AgentRuntimeProviderIdEnum.Anthropic,
channel: ChannelTypeEnum.AGENT_RUNTIME,
kind: IntegrationKindEnum.AGENT,
credentials: { apiKey: FAKE_API_KEY },
active: true,
name: `anthropic-rollback-test-${Date.now()}`,
});

expect(res.status).to.be.oneOf([400, 422, 500, 503]);

// No leftover integration records for this environment
// No leftover agent-kind integration records for this environment
const integrations = await integrationRepository.find({
_environmentId: session.environment._id,
_organizationId: session.organization._id,
channel: ChannelTypeEnum.AGENT_RUNTIME,
kind: IntegrationKindEnum.AGENT,
});

expect(integrations.length, 'no AGENT_RUNTIME integrations should remain after rollback').to.equal(0);
expect(integrations.length, 'no agent-kind integrations should remain after rollback').to.equal(0);
});

it('should NOT call provisionIntegration for non-AGENT_RUNTIME channels', async () => {
it('should NOT call provisionIntegration for delivery-kind integrations', async () => {
await session.testAgent.post('/v1/integrations').send({
providerId: 'sendgrid',
channel: ChannelTypeEnum.EMAIL,
channel: 'email',
credentials: { apiKey: FAKE_API_KEY },
active: false,
name: `email-non-agent-${Date.now()}`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export class SyncAgentToEnvironment {
stubIntegration = await this.integrationRepository.create({
providerId: sourceIntegration.providerId,
channel: sourceIntegration.channel,
kind: sourceIntegration.kind,
name: sourceIntegration.name,
identifier: sourceIntegration.identifier,
credentials: {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export class SendTestEmail {

if (integration.providerId === EmailProviderIdEnum.Novu) {
integration.credentials = await this.getNovuProviderCredentials.execute({
channelType: integration.channel,
channelType: ChannelTypeEnum.EMAIL,
providerId: integration.providerId,
environmentId: integration._environmentId,
organizationId: integration._organizationId,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { ApiPropertyOptional } from '@nestjs/swagger';
import { CredentialsDto, StepFilterDto } from '@novu/application-generic';
import { ChannelTypeEnum, ICreateIntegrationBodyDto } from '@novu/shared';
import { ChannelTypeEnum, ICreateIntegrationBodyDto, IntegrationKindEnum } from '@novu/shared';
import { Type } from 'class-transformer';
import {
IsArray,
Expand Down Expand Up @@ -30,18 +30,27 @@ export class CreateIntegrationRequestDto implements ICreateIntegrationBodyDto {
@IsMongoId()
_environmentId?: string;

@ApiProperty({ type: String, description: 'The provider ID for the integration' })
@ApiPropertyOptional({ type: String, description: 'The provider ID for the integration' })
@IsDefined()
@IsString()
providerId: string;

@ApiProperty({
@ApiPropertyOptional({
enum: ChannelTypeEnum,
description: 'The channel type for the integration',
description: 'The channel type for the integration. Not required for agent-kind integrations.',
})
@IsDefined()
@IsOptional()
@IsEnum(ChannelTypeEnum)
channel: ChannelTypeEnum;
channel?: ChannelTypeEnum;

@ApiPropertyOptional({
enum: IntegrationKindEnum,
description:
'Distinguishes delivery integrations from agent-runtime integrations. Defaults to "delivery". Agent integrations do not require a channel.',
})
@IsOptional()
@IsEnum(IntegrationKindEnum)
kind?: IntegrationKindEnum;

@ApiPropertyOptional({
type: CredentialsDto,
Expand Down
1 change: 1 addition & 0 deletions apps/api/src/app/integrations/integrations.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ export class IntegrationsController {
organizationId: user.organizationId,
providerId: body.providerId,
channel: body.channel,
kind: body.kind,
credentials: body.credentials,
active: body.active ?? false,
check: body.check ?? false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ export class SlackOauthCallback {
private async getDemoNovuSlackCredentials(integration: IntegrationEntity): Promise<ICredentialsEntity> {
return await this.getNovuProviderCredentials.execute(
GetNovuProviderCredentialsCommand.create({
channelType: integration.channel,
channelType: integration.channel ?? ChannelTypeEnum.CHAT,
providerId: integration.providerId,
environmentId: integration._environmentId,
organizationId: integration._organizationId,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { ChannelTypeEnum, ICredentials } from '@novu/shared';
import { IsDefined, IsString } from 'class-validator';
import { IsDefined, IsOptional, IsString } from 'class-validator';
import { EnvironmentCommand } from '../../../shared/commands/project.command';

export class CheckIntegrationCommand extends EnvironmentCommand {
@IsDefined()
@IsString()
providerId: string;

@IsDefined()
channel: ChannelTypeEnum;
/** Optional because agent-kind integrations do not have a delivery channel. */
@IsOptional()
channel?: ChannelTypeEnum;

@IsDefined()
credentials?: ICredentials;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { MessageFilter } from '@novu/application-generic';
import { ChannelTypeEnum, ICredentialsDto } from '@novu/shared';
import { ChannelTypeEnum, ICredentialsDto, IntegrationKindEnum } from '@novu/shared';
import { IsArray, IsDefined, IsEnum, IsObject, IsOptional, IsString, ValidateNested } from 'class-validator';

import { EnvironmentCommand } from '../../../shared/commands/project.command';
Expand All @@ -17,9 +17,14 @@ export class CreateIntegrationCommand extends EnvironmentCommand {
@IsString()
providerId: string;

@IsDefined()
@IsOptional()
@IsEnum(ChannelTypeEnum)
channel: ChannelTypeEnum;
channel?: ChannelTypeEnum;

/** Distinguishes delivery integrations from agent-runtime integrations. Defaults to 'delivery'. */
@IsOptional()
@IsEnum(IntegrationKindEnum)
kind?: IntegrationKindEnum;

@IsOptional()
credentials?: ICredentialsDto;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
ChatProviderIdEnum,
EmailProviderIdEnum,
InAppProviderIdEnum,
IntegrationKindEnum,
providers,
SmsProviderIdEnum,
slugify,
Expand Down Expand Up @@ -81,39 +82,43 @@ export class CreateIntegration {
}

private async validate(command: CreateIntegrationCommand): Promise<void> {
const existingIntegration = await this.integrationRepository.findOne({
_environmentId: command.environmentId,
providerId: command.providerId,
channel: command.channel,
});

if (
existingIntegration &&
command.providerId === InAppProviderIdEnum.Novu &&
command.channel === ChannelTypeEnum.IN_APP
) {
throw new BadRequestException('One environment can only have one In app provider');
}
const isAgentKind = command.kind === IntegrationKindEnum.AGENT;

if (
(command.providerId === SmsProviderIdEnum.Novu && !areNovuSmsCredentialsSet()) ||
(command.providerId === EmailProviderIdEnum.Novu && !areNovuEmailCredentialsSet()) ||
(command.providerId === ChatProviderIdEnum.Novu && !areNovuSlackCredentialsSet())
) {
throw new BadRequestException(`Creating Novu integration for ${command.providerId} provider is not allowed`);
}

if (command.providerId === SmsProviderIdEnum.Novu || command.providerId === EmailProviderIdEnum.Novu) {
const count = await this.integrationRepository.count({
if (!isAgentKind) {
const existingIntegration = await this.integrationRepository.findOne({
_environmentId: command.environmentId,
providerId: command.providerId,
channel: command.channel,
});

if (count > 0) {
throw new ConflictException(
`Integration with novu provider for ${command.channel.toLowerCase()} channel already exists`
);
if (
existingIntegration &&
command.providerId === InAppProviderIdEnum.Novu &&
command.channel === ChannelTypeEnum.IN_APP
) {
throw new BadRequestException('One environment can only have one In app provider');
}

if (
(command.providerId === SmsProviderIdEnum.Novu && !areNovuSmsCredentialsSet()) ||
(command.providerId === EmailProviderIdEnum.Novu && !areNovuEmailCredentialsSet()) ||
(command.providerId === ChatProviderIdEnum.Novu && !areNovuSlackCredentialsSet())
) {
throw new BadRequestException(`Creating Novu integration for ${command.providerId} provider is not allowed`);
}

if (command.providerId === SmsProviderIdEnum.Novu || command.providerId === EmailProviderIdEnum.Novu) {
const count = await this.integrationRepository.count({
_environmentId: command.environmentId,
providerId: command.providerId,
channel: command.channel,
});

if (count > 0) {
throw new ConflictException(
`Integration with novu provider for ${command.channel?.toLowerCase()} channel already exists`
);
}
}
}

Expand Down Expand Up @@ -144,11 +149,14 @@ export class CreateIntegration {
this.analyticsService.track('Create Integration - [Integrations]', command.userId, {
providerId: command.providerId,
channel: command.channel,
kind: command.kind,
_organization: command.organizationId,
});

try {
if (command.check) {
const isAgentKind = command.kind === IntegrationKindEnum.AGENT;

if (command.check && !isAgentKind) {
await this.checkIntegration.execute(
CheckIntegrationCommand.create({
environmentId: command.environmentId,
Expand Down Expand Up @@ -177,14 +185,19 @@ export class CreateIntegration {
_environmentId: command.environmentId,
_organizationId: command.organizationId,
providerId: command.providerId,
channel: command.channel,
credentials: encryptCredentials(managedCredentials),
active: command.active,
conditions: command.conditions,
configurations: command.configurations,
kind: command.kind ?? IntegrationKindEnum.DELIVERY,
};

const isActiveAndChannelSupportsPrimary = command.active && CHANNELS_WITH_PRIMARY.includes(command.channel);
if (!isAgentKind && command.channel) {
query.channel = command.channel;
}

const isActiveAndChannelSupportsPrimary =
!isAgentKind && command.active && command.channel && CHANNELS_WITH_PRIMARY.includes(command.channel);

if (isActiveAndChannelSupportsPrimary) {
const { primary, priority } = await this.calculatePriorityAndPrimary(command);
Expand All @@ -195,7 +208,7 @@ export class CreateIntegration {

const integrationEntity = await this.integrationRepository.create(query);

if (command.channel === ChannelTypeEnum.AGENT_RUNTIME) {
if (isAgentKind) {
await this.provisionAgentRuntimeIntegration(integrationEntity._id, identifier, command);
}

Expand Down
Loading
Loading