@@ -13,12 +13,17 @@ import {
1313 UseGuards ,
1414} from '@nestjs/common' ;
1515import type { Prisma } from '@prisma/client' ;
16+ import { db } from '@db' ;
1617import { InternalTokenGuard } from '../../auth/internal-token.guard' ;
1718import { DynamicIntegrationRepository } from '../repositories/dynamic-integration.repository' ;
1819import { DynamicCheckRepository } from '../repositories/dynamic-check.repository' ;
1920import { ProviderRepository } from '../repositories/provider.repository' ;
21+ import { CheckRunRepository } from '../repositories/check-run.repository' ;
2022import { DynamicManifestLoaderService } from '../services/dynamic-manifest-loader.service' ;
21- import { validateIntegrationDefinition } from '@trycompai/integration-platform' ;
23+ import {
24+ validateIntegrationDefinition ,
25+ SyncDefinitionSchema ,
26+ } from '@trycompai/integration-platform' ;
2227
2328@Controller ( { path : 'internal/dynamic-integrations' , version : '1' } )
2429@UseGuards ( InternalTokenGuard )
@@ -29,6 +34,7 @@ export class DynamicIntegrationsController {
2934 private readonly dynamicIntegrationRepo : DynamicIntegrationRepository ,
3035 private readonly dynamicCheckRepo : DynamicCheckRepository ,
3136 private readonly providerRepo : ProviderRepository ,
37+ private readonly checkRunRepo : CheckRunRepository ,
3238 private readonly loaderService : DynamicManifestLoaderService ,
3339 ) { }
3440
@@ -48,6 +54,12 @@ export class DynamicIntegrationsController {
4854
4955 const def = validation . data ! ;
5056
57+ // Validate and store syncDefinition through Zod to apply defaults (e.g., employeesPath)
58+ const rawSyncDef = ( body as Record < string , unknown > ) . syncDefinition ;
59+ const validatedSyncDef = rawSyncDef
60+ ? SyncDefinitionSchema . parse ( rawSyncDef )
61+ : undefined ;
62+
5163 // Upsert the integration
5264 const integration = await this . dynamicIntegrationRepo . upsertBySlug ( {
5365 slug : def . slug ,
@@ -61,6 +73,9 @@ export class DynamicIntegrationsController {
6173 authConfig : def . authConfig as unknown as Prisma . InputJsonValue ,
6274 capabilities : def . capabilities as unknown as Prisma . InputJsonValue ,
6375 supportsMultipleConnections : def . supportsMultipleConnections ,
76+ syncDefinition : validatedSyncDef
77+ ? ( JSON . parse ( JSON . stringify ( validatedSyncDef ) ) as Prisma . InputJsonValue )
78+ : null ,
6479 } ) ;
6580
6681 // Delete checks not in the new definition, then upsert the rest
@@ -132,6 +147,10 @@ export class DynamicIntegrationsController {
132147 ) ;
133148 }
134149
150+ const rawSyncDefCreate = ( body as Record < string , unknown > ) . syncDefinition ;
151+ const validatedSyncDefCreate = rawSyncDefCreate
152+ ? SyncDefinitionSchema . parse ( rawSyncDefCreate )
153+ : undefined ;
135154 const integration = await this . dynamicIntegrationRepo . create ( {
136155 slug : def . slug ,
137156 name : def . name ,
@@ -144,6 +163,9 @@ export class DynamicIntegrationsController {
144163 authConfig : def . authConfig as unknown as Prisma . InputJsonValue ,
145164 capabilities : def . capabilities as unknown as Prisma . InputJsonValue ,
146165 supportsMultipleConnections : def . supportsMultipleConnections ,
166+ syncDefinition : validatedSyncDefCreate
167+ ? ( JSON . parse ( JSON . stringify ( validatedSyncDefCreate ) ) as Prisma . InputJsonValue )
168+ : undefined ,
147169 } ) ;
148170
149171 for ( const [ index , check ] of def . checks . entries ( ) ) {
@@ -365,4 +387,139 @@ export class DynamicIntegrationsController {
365387 this . logger . log ( `Deactivated dynamic integration: ${ integration . slug } ` ) ;
366388 return { success : true } ;
367389 }
390+
391+ // ==================== Agent Debugging Endpoints ====================
392+
393+ /**
394+ * Validate a definition without saving.
395+ * Agents use this to check syntax/structure before committing.
396+ */
397+ @Post ( 'validate' )
398+ async validate ( @Body ( ) body : Record < string , unknown > ) {
399+ const result = validateIntegrationDefinition ( body ) ;
400+
401+ if ( ! result . success ) {
402+ return {
403+ valid : false ,
404+ errors : result . errors ,
405+ } ;
406+ }
407+
408+ // validateIntegrationDefinition validates everything via Zod:
409+ // the manifest fields, all check definitions, and syncDefinition.
410+ // If we got here, the entire definition is valid.
411+ const definition = result . data ! ;
412+
413+ return {
414+ valid : true ,
415+ summary : {
416+ slug : definition . slug ,
417+ name : definition . name ,
418+ category : definition . category ,
419+ capabilities : definition . capabilities ,
420+ checksCount : definition . checks . length ,
421+ checkSlugs : definition . checks . map ( ( c ) => c . checkSlug ) ,
422+ hasSyncDefinition : ! ! ( body as Record < string , unknown > ) . syncDefinition ,
423+ } ,
424+ } ;
425+ }
426+
427+ /**
428+ * Get recent check run history for a dynamic integration.
429+ * Agents use this to debug failing checks — includes full logs and results.
430+ */
431+ @Get ( ':id/check-runs' )
432+ async getCheckRuns ( @Param ( 'id' ) id : string ) {
433+ const integration = await this . dynamicIntegrationRepo . findById ( id ) ;
434+ if ( ! integration ) {
435+ throw new HttpException ( 'Dynamic integration not found' , HttpStatus . NOT_FOUND ) ;
436+ }
437+
438+ // Find all connections for this provider
439+ const connections = await db . integrationConnection . findMany ( {
440+ where : {
441+ provider : { slug : integration . slug } ,
442+ status : 'active' ,
443+ } ,
444+ select : { id : true , organizationId : true } ,
445+ } ) ;
446+
447+ if ( connections . length === 0 ) {
448+ return { runs : [ ] , total : 0 } ;
449+ }
450+
451+ // Get recent runs across all connections
452+ const runs = await db . integrationCheckRun . findMany ( {
453+ where : {
454+ connectionId : { in : connections . map ( ( c ) => c . id ) } ,
455+ } ,
456+ include : {
457+ results : {
458+ select : {
459+ id : true ,
460+ passed : true ,
461+ title : true ,
462+ resourceType : true ,
463+ resourceId : true ,
464+ severity : true ,
465+ remediation : true ,
466+ } ,
467+ } ,
468+ } ,
469+ orderBy : { createdAt : 'desc' } ,
470+ take : 20 ,
471+ } ) ;
472+
473+ return {
474+ runs : runs . map ( ( run ) => ( {
475+ id : run . id ,
476+ checkId : run . checkId ,
477+ checkName : run . checkName ,
478+ connectionId : run . connectionId ,
479+ status : run . status ,
480+ startedAt : run . startedAt ,
481+ completedAt : run . completedAt ,
482+ durationMs : run . durationMs ,
483+ totalChecked : run . totalChecked ,
484+ passedCount : run . passedCount ,
485+ failedCount : run . failedCount ,
486+ errorMessage : run . errorMessage ,
487+ logs : run . logs ,
488+ results : run . results ,
489+ } ) ) ,
490+ total : runs . length ,
491+ } ;
492+ }
493+
494+ /**
495+ * Get a single check run with full details (logs, results, error info).
496+ * Agents use this to debug a specific failed run.
497+ */
498+ @Get ( 'check-runs/:runId' )
499+ async getCheckRunById ( @Param ( 'runId' ) runId : string ) {
500+ const run = await this . checkRunRepo . findById ( runId ) ;
501+ if ( ! run ) {
502+ throw new HttpException ( 'Check run not found' , HttpStatus . NOT_FOUND ) ;
503+ }
504+
505+ return {
506+ id : run . id ,
507+ checkId : run . checkId ,
508+ checkName : run . checkName ,
509+ connectionId : run . connectionId ,
510+ status : run . status ,
511+ startedAt : run . startedAt ,
512+ completedAt : run . completedAt ,
513+ durationMs : run . durationMs ,
514+ totalChecked : run . totalChecked ,
515+ passedCount : run . passedCount ,
516+ failedCount : run . failedCount ,
517+ errorMessage : run . errorMessage ,
518+ logs : run . logs ,
519+ results : run . results ,
520+ provider : run . connection ?. provider
521+ ? { slug : run . connection . provider . slug , name : run . connection . provider . name }
522+ : null ,
523+ } ;
524+ }
368525}
0 commit comments