@elevasis/core 0.33.0 → 0.34.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,51 +1,55 @@
1
- /**
2
- * Registry Validation Utilities
3
- *
4
- * Centralized validation logic for ResourceRegistry.
5
- * All validation runs at API startup - fails fast in development.
6
- */
7
-
8
- import type { z } from 'zod'
9
- import type { ModelConfig } from '../../execution/engine/llm/model-info'
10
- import { validateModelConfig, ModelConfigError } from '../../execution/engine/llm/errors'
11
- import type { DeploymentSpec } from './resource-registry'
12
- import type { ResourceEntry } from '../../organization-model/domains/resources'
1
+ /**
2
+ * Registry Validation Utilities
3
+ *
4
+ * Centralized validation logic for ResourceRegistry.
5
+ * All validation runs at API startup - fails fast in development.
6
+ */
7
+
8
+ import type { z } from 'zod'
9
+ import type { ModelConfig } from '../../execution/engine/llm/model-info'
10
+ import { validateModelConfig, ModelConfigError } from '../../execution/engine/llm/errors'
11
+ import type { DeploymentSpec } from './resource-registry'
12
+ import type { ResourceEntry } from '../../organization-model/domains/resources'
13
13
  import type { SystemEntry } from '../../organization-model/domains/systems'
14
14
  import type { OrganizationModel } from '../../organization-model/types'
15
15
  import { listAllSystems } from '../../organization-model/helpers'
16
- import { compileOrganizationOntology, type OntologyKind, type ResolvedOntologyIndex } from '../../organization-model/ontology'
16
+ import {
17
+ compileOrganizationOntology,
18
+ type OntologyKind,
19
+ type ResolvedOntologyIndex
20
+ } from '../../organization-model/ontology'
17
21
  import type { OmTopologyNodeRef } from '../../organization-model/domains/topology'
18
- import type {
19
- TriggerDefinition,
20
- ResourceRelationships,
21
- ExternalResourceDefinition,
22
- HumanCheckpointDefinition,
23
- ResourceType
24
- } from './types'
25
-
26
- // ============================================================================
27
- // Validation Error Types
28
- // ============================================================================
29
-
30
- export class RegistryValidationError extends Error {
31
- constructor(
32
- public readonly orgName: string,
33
- public readonly resourceId: string | null,
34
- public readonly field: string | null,
35
- message: string
36
- ) {
37
- super(message)
38
- this.name = 'RegistryValidationError'
39
- }
40
- }
41
-
42
- export type ResourceValidatorMode = 'strict' | 'warn-only'
43
-
44
- export type ResourceGovernanceValidationIssueType =
45
- | 'missing-code-resource'
46
- | 'missing-om-resource'
47
- | 'type-mismatch'
48
- | 'system-mismatch'
22
+ import type {
23
+ TriggerDefinition,
24
+ ResourceRelationships,
25
+ ExternalResourceDefinition,
26
+ HumanCheckpointDefinition,
27
+ ResourceType
28
+ } from './types'
29
+
30
+ // ============================================================================
31
+ // Validation Error Types
32
+ // ============================================================================
33
+
34
+ export class RegistryValidationError extends Error {
35
+ constructor(
36
+ public readonly orgName: string,
37
+ public readonly resourceId: string | null,
38
+ public readonly field: string | null,
39
+ message: string
40
+ ) {
41
+ super(message)
42
+ this.name = 'RegistryValidationError'
43
+ }
44
+ }
45
+
46
+ export type ResourceValidatorMode = 'strict' | 'warn-only'
47
+
48
+ export type ResourceGovernanceValidationIssueType =
49
+ | 'missing-code-resource'
50
+ | 'missing-om-resource'
51
+ | 'type-mismatch'
52
+ | 'system-mismatch'
49
53
  | 'missing-om-system'
50
54
  | 'raw-resource-id'
51
55
  | 'descriptor-mismatch'
@@ -64,85 +68,85 @@ export interface ResourceGovernanceModel {
64
68
  entities?: OrganizationModel['entities']
65
69
  actions?: OrganizationModel['actions']
66
70
  }
67
-
68
- export interface ResourceGovernanceValidationIssue {
69
- type: ResourceGovernanceValidationIssueType
70
- orgName: string
71
- resourceId: string
72
- message: string
73
- }
74
-
75
- export interface ResourceGovernanceValidationResult {
76
- valid: boolean
77
- mode: ResourceValidatorMode
78
- issues: ResourceGovernanceValidationIssue[]
79
- }
80
-
81
- export interface ResourceGovernanceValidationOptions {
82
- mode?: ResourceValidatorMode
83
- onWarning?: (issue: ResourceGovernanceValidationIssue) => void
84
- }
85
-
86
- function getResourceValidatorMode(explicitMode?: ResourceValidatorMode): ResourceValidatorMode {
87
- if (explicitMode) return explicitMode
88
- const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env
89
- return env?.ELEVASIS_RESOURCE_VALIDATOR === 'warn-only' ? 'warn-only' : 'strict'
90
- }
91
-
92
- function addGovernanceIssue(
93
- issues: ResourceGovernanceValidationIssue[],
94
- type: ResourceGovernanceValidationIssueType,
95
- orgName: string,
96
- resourceId: string,
97
- message: string
98
- ): void {
99
- issues.push({
100
- type,
101
- orgName,
102
- resourceId,
103
- message
104
- })
105
- }
106
-
107
- function emitGovernanceIssues(
108
- issues: ResourceGovernanceValidationIssue[],
109
- mode: ResourceValidatorMode,
110
- onWarning?: (issue: ResourceGovernanceValidationIssue) => void
111
- ): void {
112
- if (issues.length === 0) return
113
-
114
- if (mode === 'strict') {
115
- const first = issues[0]
116
- throw new RegistryValidationError(first.orgName, first.resourceId, 'organizationModel.resources', first.message)
117
- }
118
-
119
- const warn = onWarning ?? ((issue: ResourceGovernanceValidationIssue) => console.warn(issue.message))
120
- for (const issue of issues) {
121
- warn(issue)
122
- }
123
- }
124
-
71
+
72
+ export interface ResourceGovernanceValidationIssue {
73
+ type: ResourceGovernanceValidationIssueType
74
+ orgName: string
75
+ resourceId: string
76
+ message: string
77
+ }
78
+
79
+ export interface ResourceGovernanceValidationResult {
80
+ valid: boolean
81
+ mode: ResourceValidatorMode
82
+ issues: ResourceGovernanceValidationIssue[]
83
+ }
84
+
85
+ export interface ResourceGovernanceValidationOptions {
86
+ mode?: ResourceValidatorMode
87
+ onWarning?: (issue: ResourceGovernanceValidationIssue) => void
88
+ }
89
+
90
+ function getResourceValidatorMode(explicitMode?: ResourceValidatorMode): ResourceValidatorMode {
91
+ if (explicitMode) return explicitMode
92
+ const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env
93
+ return env?.ELEVASIS_RESOURCE_VALIDATOR === 'warn-only' ? 'warn-only' : 'strict'
94
+ }
95
+
96
+ function addGovernanceIssue(
97
+ issues: ResourceGovernanceValidationIssue[],
98
+ type: ResourceGovernanceValidationIssueType,
99
+ orgName: string,
100
+ resourceId: string,
101
+ message: string
102
+ ): void {
103
+ issues.push({
104
+ type,
105
+ orgName,
106
+ resourceId,
107
+ message
108
+ })
109
+ }
110
+
111
+ function emitGovernanceIssues(
112
+ issues: ResourceGovernanceValidationIssue[],
113
+ mode: ResourceValidatorMode,
114
+ onWarning?: (issue: ResourceGovernanceValidationIssue) => void
115
+ ): void {
116
+ if (issues.length === 0) return
117
+
118
+ if (mode === 'strict') {
119
+ const first = issues[0]
120
+ throw new RegistryValidationError(first.orgName, first.resourceId, 'organizationModel.resources', first.message)
121
+ }
122
+
123
+ const warn = onWarning ?? ((issue: ResourceGovernanceValidationIssue) => console.warn(issue.message))
124
+ for (const issue of issues) {
125
+ warn(issue)
126
+ }
127
+ }
128
+
125
129
  function getRuntimeResources(resources: DeploymentSpec): Array<{
126
- resourceId: string
127
- type: ResourceType
128
- descriptor?: ResourceEntry
129
- }> {
130
- return [
131
- ...(resources.workflows ?? []).map((workflow) => ({
132
- resourceId: workflow.config.resourceId,
133
- type: workflow.config.type,
134
- descriptor: workflow.config.resource
135
- })),
136
- ...(resources.agents ?? []).map((agent) => ({
137
- resourceId: agent.config.resourceId,
138
- type: agent.config.type,
139
- descriptor: agent.config.resource
140
- })),
141
- ...(resources.integrations ?? []).map((integration) => ({
142
- resourceId: integration.resourceId,
143
- type: integration.type,
144
- descriptor: (integration as { resource?: ResourceEntry }).resource
145
- }))
130
+ resourceId: string
131
+ type: ResourceType
132
+ descriptor?: ResourceEntry
133
+ }> {
134
+ return [
135
+ ...(resources.workflows ?? []).map((workflow) => ({
136
+ resourceId: workflow.config.resourceId,
137
+ type: workflow.config.type,
138
+ descriptor: workflow.config.resource
139
+ })),
140
+ ...(resources.agents ?? []).map((agent) => ({
141
+ resourceId: agent.config.resourceId,
142
+ type: agent.config.type,
143
+ descriptor: agent.config.resource
144
+ })),
145
+ ...(resources.integrations ?? []).map((integration) => ({
146
+ resourceId: integration.resourceId,
147
+ type: integration.type,
148
+ descriptor: (integration as { resource?: ResourceEntry }).resource
149
+ }))
146
150
  ]
147
151
  }
148
152
 
@@ -307,34 +311,39 @@ function addTopologyIssues(
307
311
  })
308
312
  }
309
313
  }
310
-
311
- /**
312
- * Validates runtime resource definitions against OM Resources and Systems.
313
- *
314
- * This is the shared core entry point for SDK, CI, deploy, and ResourceRegistry.
315
- * Default mode is strict. ELEVASIS_RESOURCE_VALIDATOR=warn-only remains a
316
- * permanent emergency escape hatch unless an explicit mode is passed.
317
- */
318
- export function validateResourceGovernance(
319
- orgName: string,
320
- deployment: DeploymentSpec,
321
- organizationModel: ResourceGovernanceModel | undefined = deployment.organizationModel,
322
- options: ResourceGovernanceValidationOptions = {}
323
- ): ResourceGovernanceValidationResult {
324
- const mode = getResourceValidatorMode(options.mode)
314
+
315
+ /**
316
+ * Validates runtime resource definitions against OM Resources and Systems.
317
+ *
318
+ * This is an overlay validator for code/resource bindings, ontology bindings,
319
+ * and topology refs that need runtime manifest context. It does not validate
320
+ * Organization Model shape; deploy/runtime structural gates use
321
+ * OrganizationModelSchema.
322
+ *
323
+ * This is the shared core entry point for SDK, CI, deploy, and ResourceRegistry.
324
+ * Default mode is strict. ELEVASIS_RESOURCE_VALIDATOR=warn-only remains a
325
+ * permanent emergency escape hatch unless an explicit mode is passed.
326
+ */
327
+ export function validateResourceGovernance(
328
+ orgName: string,
329
+ deployment: DeploymentSpec,
330
+ organizationModel: ResourceGovernanceModel | undefined = deployment.organizationModel,
331
+ options: ResourceGovernanceValidationOptions = {}
332
+ ): ResourceGovernanceValidationResult {
333
+ const mode = getResourceValidatorMode(options.mode)
325
334
  const omResourcesMap = organizationModel?.resources
326
335
  const omSystemsMap = organizationModel?.systems
327
336
  const issues: ResourceGovernanceValidationIssue[] = []
328
-
329
- if (!omResourcesMap || !omSystemsMap) {
330
- return { valid: true, mode, issues }
331
- }
332
-
333
- // Use listAllSystems (DFS) so nested systems under .subsystems are included.
334
- // The lookup key is the dot-joined path, which equals resource.systemPath.
335
- const systemsById = new Map(
336
- listAllSystems({ systems: omSystemsMap } as OrganizationModel).map(({ path, system }) => [path, system])
337
- )
337
+
338
+ if (!omResourcesMap || !omSystemsMap) {
339
+ return { valid: true, mode, issues }
340
+ }
341
+
342
+ // Use listAllSystems (DFS) so nested systems under .subsystems are included.
343
+ // The lookup key is the dot-joined path, which equals resource.systemPath.
344
+ const systemsById = new Map(
345
+ listAllSystems({ systems: omSystemsMap } as OrganizationModel).map(({ path, system }) => [path, system])
346
+ )
338
347
  const activeOmResources = Object.values(omResourcesMap).filter((resource) => resource.status === 'active')
339
348
  const omResourcesById = new Map(activeOmResources.map((resource) => [resource.id, resource]))
340
349
  const runtimeResources = getRuntimeResources(deployment)
@@ -355,45 +364,45 @@ export function validateResourceGovernance(
355
364
  }
356
365
 
357
366
  for (const resource of activeOmResources) {
358
- if (!systemsById.has(resource.systemPath)) {
359
- addGovernanceIssue(
360
- issues,
361
- 'missing-om-system',
362
- orgName,
363
- resource.id,
364
- `[${orgName}] OM resource '${resource.id}' references missing system path '${resource.systemPath}'.`
365
- )
366
- }
367
-
368
- const runtimeResource = runtimeResourcesById.get(resource.id)
369
- if (!runtimeResource) {
370
- addGovernanceIssue(
371
- issues,
372
- 'missing-code-resource',
373
- orgName,
374
- resource.id,
375
- `[${orgName}] OM resource '${resource.id}' has no matching code-side resource.`
376
- )
377
- continue
378
- }
379
-
380
- if (runtimeResource.type !== resource.kind) {
381
- addGovernanceIssue(
382
- issues,
383
- 'type-mismatch',
384
- orgName,
385
- resource.id,
386
- `[${orgName}] Resource '${resource.id}' type mismatch: code has '${runtimeResource.type}', OM has '${resource.kind}'.`
387
- )
388
- }
389
-
367
+ if (!systemsById.has(resource.systemPath)) {
368
+ addGovernanceIssue(
369
+ issues,
370
+ 'missing-om-system',
371
+ orgName,
372
+ resource.id,
373
+ `[${orgName}] OM resource '${resource.id}' references missing system path '${resource.systemPath}'.`
374
+ )
375
+ }
376
+
377
+ const runtimeResource = runtimeResourcesById.get(resource.id)
378
+ if (!runtimeResource) {
379
+ addGovernanceIssue(
380
+ issues,
381
+ 'missing-code-resource',
382
+ orgName,
383
+ resource.id,
384
+ `[${orgName}] OM resource '${resource.id}' has no matching code-side resource.`
385
+ )
386
+ continue
387
+ }
388
+
389
+ if (runtimeResource.type !== resource.kind) {
390
+ addGovernanceIssue(
391
+ issues,
392
+ 'type-mismatch',
393
+ orgName,
394
+ resource.id,
395
+ `[${orgName}] Resource '${resource.id}' type mismatch: code has '${runtimeResource.type}', OM has '${resource.kind}'.`
396
+ )
397
+ }
398
+
390
399
  if (runtimeResource.descriptor && runtimeResource.descriptor.systemPath !== resource.systemPath) {
391
400
  addGovernanceIssue(
392
- issues,
393
- 'system-mismatch',
394
- orgName,
395
- resource.id,
396
- `[${orgName}] Resource '${resource.id}' system mismatch: code descriptor has '${runtimeResource.descriptor.systemPath}', OM has '${resource.systemPath}'.`
401
+ issues,
402
+ 'system-mismatch',
403
+ orgName,
404
+ resource.id,
405
+ `[${orgName}] Resource '${resource.id}' system mismatch: code descriptor has '${runtimeResource.descriptor.systemPath}', OM has '${resource.systemPath}'.`
397
406
  )
398
407
  }
399
408
 
@@ -409,47 +418,47 @@ export function validateResourceGovernance(
409
418
 
410
419
  addOntologyBindingIssues(issues, orgName, resource, ontologyIndex)
411
420
  }
412
-
413
- for (const runtimeResource of runtimeResources) {
414
- const omResource = omResourcesById.get(runtimeResource.resourceId)
415
- if (!omResource) {
416
- addGovernanceIssue(
417
- issues,
418
- 'missing-om-resource',
419
- orgName,
420
- runtimeResource.resourceId,
421
- `[${orgName}] Code-side resource '${runtimeResource.resourceId}' has no active OM Resource descriptor.`
422
- )
423
- }
424
-
425
- if (!runtimeResource.descriptor) {
426
- addGovernanceIssue(
427
- issues,
428
- 'raw-resource-id',
429
- orgName,
430
- runtimeResource.resourceId,
431
- `[${orgName}] Code-side resource '${runtimeResource.resourceId}' authors raw resourceId/type values. Use an OM Resource descriptor and bindResourceDescriptor().`
432
- )
433
- continue
434
- }
435
-
436
- if (runtimeResource.descriptor.id !== runtimeResource.resourceId) {
437
- addGovernanceIssue(
438
- issues,
439
- 'raw-resource-id',
440
- orgName,
441
- runtimeResource.resourceId,
442
- `[${orgName}] Code-side resource '${runtimeResource.resourceId}' does not derive identity from its OM descriptor '${runtimeResource.descriptor.id}'.`
443
- )
444
- }
445
-
421
+
422
+ for (const runtimeResource of runtimeResources) {
423
+ const omResource = omResourcesById.get(runtimeResource.resourceId)
424
+ if (!omResource) {
425
+ addGovernanceIssue(
426
+ issues,
427
+ 'missing-om-resource',
428
+ orgName,
429
+ runtimeResource.resourceId,
430
+ `[${orgName}] Code-side resource '${runtimeResource.resourceId}' has no active OM Resource descriptor.`
431
+ )
432
+ }
433
+
434
+ if (!runtimeResource.descriptor) {
435
+ addGovernanceIssue(
436
+ issues,
437
+ 'raw-resource-id',
438
+ orgName,
439
+ runtimeResource.resourceId,
440
+ `[${orgName}] Code-side resource '${runtimeResource.resourceId}' authors raw resourceId/type values. Use an OM Resource descriptor and bindResourceDescriptor().`
441
+ )
442
+ continue
443
+ }
444
+
445
+ if (runtimeResource.descriptor.id !== runtimeResource.resourceId) {
446
+ addGovernanceIssue(
447
+ issues,
448
+ 'raw-resource-id',
449
+ orgName,
450
+ runtimeResource.resourceId,
451
+ `[${orgName}] Code-side resource '${runtimeResource.resourceId}' does not derive identity from its OM descriptor '${runtimeResource.descriptor.id}'.`
452
+ )
453
+ }
454
+
446
455
  if (runtimeResource.descriptor.kind !== runtimeResource.type) {
447
456
  addGovernanceIssue(
448
- issues,
449
- 'type-mismatch',
450
- orgName,
451
- runtimeResource.resourceId,
452
- `[${orgName}] Code-side resource '${runtimeResource.resourceId}' descriptor kind '${runtimeResource.descriptor.kind}' does not match runtime type '${runtimeResource.type}'.`
457
+ issues,
458
+ 'type-mismatch',
459
+ orgName,
460
+ runtimeResource.resourceId,
461
+ `[${orgName}] Code-side resource '${runtimeResource.resourceId}' descriptor kind '${runtimeResource.descriptor.kind}' does not match runtime type '${runtimeResource.type}'.`
453
462
  )
454
463
  }
455
464
  }
@@ -457,492 +466,492 @@ export function validateResourceGovernance(
457
466
  addTopologyIssues(issues, orgName, deployment, organizationModel, systemsById, omResourcesById, ontologyIndex)
458
467
 
459
468
  emitGovernanceIssues(issues, mode, options.onWarning)
460
-
461
- return {
462
- valid: issues.length === 0,
463
- mode,
464
- issues
465
- }
466
- }
467
-
468
- // ============================================================================
469
- // Resource Validation
470
- // ============================================================================
471
-
472
- /**
473
- * Validates resources for a single organization
474
- * - Duplicate resourceId check
475
- * - Model configuration validation
476
- * - ExecutionInterface-to-inputSchema validation
477
- * @throws RegistryValidationError if validation fails
478
- */
479
- export function validateDeploymentSpec(orgName: string, resources: DeploymentSpec): void {
480
- const seenIds = new Set<string>()
481
-
482
- // Validate workflows
483
- resources.workflows?.forEach((workflow) => {
484
- const id = workflow.config.resourceId
485
-
486
- // Check for duplicate IDs
487
- if (seenIds.has(id)) {
488
- throw new RegistryValidationError(
489
- orgName,
490
- id,
491
- null,
492
- `Duplicate resourceId "${id}" in organization "${orgName}". ` +
493
- `Workflows and agents must have unique IDs within an organization.`
494
- )
495
- }
496
- seenIds.add(id)
497
- // Validate model config if present (workflows may optionally have modelConfig)
498
- if ('modelConfig' in workflow && workflow.modelConfig) {
499
- validateResourceModelConfig(orgName, id, workflow.modelConfig as ModelConfig)
500
- }
501
-
502
- // Validate ExecutionInterface matches inputSchema
503
- if (workflow.interface) {
504
- validateExecutionInterface(orgName, id, workflow.interface, workflow.contract.inputSchema)
505
- }
506
- })
507
-
508
- // Validate agents
509
- resources.agents?.forEach((agent) => {
510
- const id = agent.config.resourceId
511
-
512
- // Check for duplicate IDs
513
- if (seenIds.has(id)) {
514
- throw new RegistryValidationError(
515
- orgName,
516
- id,
517
- null,
518
- `Duplicate resourceId "${id}" in organization "${orgName}". ` +
519
- `Workflows and agents must have unique IDs within an organization.`
520
- )
521
- }
522
- seenIds.add(id)
523
-
524
- // Validate model config
525
- validateResourceModelConfig(orgName, id, agent.modelConfig)
526
-
527
- // Validate ExecutionInterface matches inputSchema
528
- if (agent.interface) {
529
- validateExecutionInterface(orgName, id, agent.interface, agent.contract.inputSchema)
530
- }
531
- })
532
-
533
- validateResourceGovernance(orgName, resources)
534
- }
535
-
536
- /**
537
- * Validates model configuration for a resource
538
- */
539
- function validateResourceModelConfig(orgName: string, resourceId: string, modelConfig: ModelConfig): void {
540
- try {
541
- validateModelConfig(modelConfig)
542
- } catch (error) {
543
- if (error instanceof ModelConfigError) {
544
- throw new RegistryValidationError(
545
- orgName,
546
- resourceId,
547
- error.field,
548
- `Invalid model config in ${orgName}/${resourceId}: ${error.message} (field: ${error.field})`
549
- )
550
- }
551
- throw error
552
- }
553
- }
554
-
555
- // ============================================================================
556
- // ExecutionInterface Validation
557
- // ============================================================================
558
-
559
- /**
560
- * Validates that ExecutionInterface form fields match the inputSchema
561
- *
562
- * Checks:
563
- * 1. All required fields in inputSchema have corresponding form fields
564
- * 2. Form field names match schema field names (or have valid fieldMappings)
565
- * 3. Required/optional alignment between form and schema
566
- *
567
- * @throws RegistryValidationError if interface doesn't match schema
568
- */
569
- export function validateExecutionInterface(
570
- orgName: string,
571
- resourceId: string,
572
- executionInterface: {
573
- form: { fields: Array<{ name: string; required?: boolean }>; fieldMappings?: Record<string, string> }
574
- },
575
- inputSchema: z.ZodType
576
- ): void {
577
- const form = executionInterface.form
578
- const fieldMappings = form.fieldMappings ?? {}
579
-
580
- // Extract schema shape (only works for ZodObject)
581
- const schemaShape = extractZodShape(inputSchema)
582
- if (!schemaShape) {
583
- // Can't validate non-object schemas - skip validation
584
- return
585
- }
586
-
587
- const schemaFieldNames = Object.keys(schemaShape)
588
-
589
- // Build effective mapping: form field name -> schema field name
590
- const formToSchemaMap = new Map<string, string>()
591
- for (const field of form.fields) {
592
- const schemaKey = fieldMappings[field.name] ?? field.name
593
- formToSchemaMap.set(field.name, schemaKey)
594
- }
595
-
596
- // Check 1: All required schema fields have form fields
597
- for (const schemaFieldName of schemaFieldNames) {
598
- const schemaField = schemaShape[schemaFieldName]
599
- const isRequired = !isZodOptional(schemaField)
600
-
601
- // Find form field that maps to this schema field
602
- let hasFormField = false
603
- for (const [formFieldName, mappedSchemaName] of Array.from(formToSchemaMap.entries())) {
604
- if (mappedSchemaName === schemaFieldName) {
605
- hasFormField = true
606
-
607
- // Check required alignment
608
- const formField = form.fields.find((f) => f.name === formFieldName)
609
- if (isRequired && !formField?.required) {
610
- throw new RegistryValidationError(
611
- orgName,
612
- resourceId,
613
- `interface.form.fields.${formFieldName}`,
614
- `ExecutionInterface field "${formFieldName}" should be required to match inputSchema field "${schemaFieldName}" in ${orgName}/${resourceId}`
615
- )
616
- }
617
- break
618
- }
619
- }
620
-
621
- if (isRequired && !hasFormField) {
622
- throw new RegistryValidationError(
623
- orgName,
624
- resourceId,
625
- 'interface.form.fields',
626
- `ExecutionInterface missing required field "${schemaFieldName}" from inputSchema in ${orgName}/${resourceId}`
627
- )
628
- }
629
- }
630
-
631
- // Check 2: All form fields map to valid schema fields
632
- for (const [formFieldName, schemaFieldName] of Array.from(formToSchemaMap.entries())) {
633
- // Detect nested field notation (e.g., "criteria.targetTitles") and suggest flattening
634
- if (schemaFieldName.includes('.')) {
635
- const topLevelField = schemaFieldName.split('.')[0]
636
- throw new RegistryValidationError(
637
- orgName,
638
- resourceId,
639
- `interface.form.fields.${formFieldName}`,
640
- `ExecutionInterface field "${formFieldName}" uses nested notation. ` +
641
- `Flatten the inputSchema by moving nested fields to top-level ` +
642
- `(e.g., "${topLevelField}.x" → "x") in ${orgName}/${resourceId}`
643
- )
644
- }
645
-
646
- if (!schemaFieldNames.includes(schemaFieldName)) {
647
- throw new RegistryValidationError(
648
- orgName,
649
- resourceId,
650
- `interface.form.fields.${formFieldName}`,
651
- `ExecutionInterface field "${formFieldName}" maps to non-existent schema field "${schemaFieldName}" in ${orgName}/${resourceId}`
652
- )
653
- }
654
- }
655
- }
656
-
657
- /**
658
- * Extract shape from a Zod schema (handles ZodObject and wrapped types)
659
- */
660
- function extractZodShape(schema: z.ZodType): Record<string, z.ZodType> | null {
661
- // Handle ZodObject directly
662
- if ('shape' in schema && typeof schema.shape === 'object') {
663
- return schema.shape as Record<string, z.ZodType>
664
- }
665
-
666
- // Handle wrapped types (ZodEffects, ZodDefault, etc.)
667
- if ('_def' in schema) {
668
- const def = schema._def as { innerType?: z.ZodType; schema?: z.ZodType }
669
- if (def.innerType) {
670
- return extractZodShape(def.innerType)
671
- }
672
- if (def.schema) {
673
- return extractZodShape(def.schema)
674
- }
675
- }
676
-
677
- return null
678
- }
679
-
680
- /**
681
- * Check if a Zod type is optional (or has a default)
682
- * Uses isOptional() method which handles ZodOptional, ZodDefault, ZodNullable
683
- */
684
- function isZodOptional(schema: z.ZodType): boolean {
685
- // Zod provides isOptional() which returns true for optional, default, and nullable types
686
- return schema.isOptional()
687
- }
688
-
689
- // ============================================================================
690
- // Relationship Validation
691
- // ============================================================================
692
-
693
- /**
694
- * Validates relationship declarations reference valid resources
695
- * @throws RegistryValidationError if validation fails
696
- */
697
- export function validateRelationships(orgName: string, resources: DeploymentSpec): void {
698
- // Skip if no manifest data
699
- if (!resources.relationships && !resources.triggers && !resources.externalResources && !resources.humanCheckpoints)
700
- return
701
-
702
- // Build resource ID sets for validation
703
- const validAgentIds = new Set(resources.agents?.map((a) => a.config.resourceId) ?? [])
704
- const validWorkflowIds = new Set(resources.workflows?.map((w) => w.config.resourceId) ?? [])
705
- const validIntegrationIds = new Set(resources.integrations?.map((i) => i.resourceId) ?? [])
706
- const validTriggerIds = new Set(resources.triggers?.map((t) => t.resourceId) ?? [])
707
-
708
- // Collect all internal resource IDs for uniqueness check
709
- const allInternalIds = new Set([
710
- ...Array.from(validAgentIds),
711
- ...Array.from(validWorkflowIds),
712
- ...Array.from(validTriggerIds),
713
- ...Array.from(validIntegrationIds)
714
- ])
715
-
716
- // Validate triggers
717
- validateTriggers(orgName, resources.triggers ?? [], validAgentIds, validWorkflowIds)
718
-
719
- // Validate resource relationships
720
- validateResourceRelationships(
721
- orgName,
722
- resources.relationships ?? {},
723
- validAgentIds,
724
- validWorkflowIds,
725
- validIntegrationIds,
726
- validTriggerIds
727
- )
728
-
729
- // Validate external resources
730
- validateExternalResources(
731
- orgName,
732
- resources.externalResources ?? [],
733
- allInternalIds,
734
- validAgentIds,
735
- validWorkflowIds,
736
- validIntegrationIds
737
- )
738
-
739
- // Validate human checkpoints
740
- validateHumanCheckpoints(orgName, resources.humanCheckpoints ?? [], allInternalIds, validAgentIds, validWorkflowIds)
741
- }
742
-
743
- /**
744
- * Validates trigger invocations
745
- * NOTE: Trigger relationships are declared in ResourceRelationships, not on TriggerDefinition
746
- * This validation is now handled by validateResourceRelationships()
747
- */
748
- function validateTriggers(
749
- _orgName: string,
750
- _triggers: TriggerDefinition[],
751
- _validAgentIds: Set<string>,
752
- _validWorkflowIds: Set<string>
753
- ): void {
754
- // No validation needed here - triggers declare their relationships in ResourceRelationships
755
- // This prevents duplication and keeps all relationship declarations in one place
756
- }
757
-
758
- /**
759
- * Validates resource relationship declarations
760
- */
761
- function validateResourceRelationships(
762
- orgName: string,
763
- relationships: ResourceRelationships,
764
- validAgentIds: Set<string>,
765
- validWorkflowIds: Set<string>,
766
- validIntegrationIds: Set<string>,
767
- validTriggerIds: Set<string>
768
- ): void {
769
- for (const [resourceId, declaration] of Object.entries(relationships)) {
770
- // Validate declaring resource exists (agents, workflows, or triggers can declare relationships)
771
- const resourceExists =
772
- validAgentIds.has(resourceId) || validWorkflowIds.has(resourceId) || validTriggerIds.has(resourceId)
773
- if (!resourceExists) {
774
- throw new RegistryValidationError(
775
- orgName,
776
- resourceId,
777
- null,
778
- `[${orgName}] Relationship declared for non-existent resource: ${resourceId}`
779
- )
780
- }
781
-
782
- // Validate triggers.agents
783
- declaration.triggers?.agents?.forEach((agentId) => {
784
- if (!validAgentIds.has(agentId)) {
785
- throw new RegistryValidationError(
786
- orgName,
787
- resourceId,
788
- 'triggers.agents',
789
- `[${orgName}] Resource '${resourceId}' triggers non-existent agent: ${agentId}`
790
- )
791
- }
792
- })
793
-
794
- // Validate triggers.workflows
795
- declaration.triggers?.workflows?.forEach((workflowId) => {
796
- if (!validWorkflowIds.has(workflowId)) {
797
- throw new RegistryValidationError(
798
- orgName,
799
- resourceId,
800
- 'triggers.workflows',
801
- `[${orgName}] Resource '${resourceId}' triggers non-existent workflow: ${workflowId}`
802
- )
803
- }
804
- })
805
-
806
- // Validate uses.integrations
807
- declaration.uses?.integrations?.forEach((integrationId) => {
808
- if (!validIntegrationIds.has(integrationId)) {
809
- throw new RegistryValidationError(
810
- orgName,
811
- resourceId,
812
- 'uses.integrations',
813
- `[${orgName}] Resource '${resourceId}' uses non-existent integration: ${integrationId}`
814
- )
815
- }
816
- })
817
- }
818
- }
819
-
820
- /**
821
- * Validates external resource definitions
822
- */
823
- function validateExternalResources(
824
- orgName: string,
825
- externalResources: ExternalResourceDefinition[],
826
- allInternalIds: Set<string>,
827
- validAgentIds: Set<string>,
828
- validWorkflowIds: Set<string>,
829
- validIntegrationIds: Set<string>
830
- ): void {
831
- externalResources.forEach((external) => {
832
- // Validate unique ID (no conflict with internal resources)
833
- if (allInternalIds.has(external.resourceId)) {
834
- throw new RegistryValidationError(
835
- orgName,
836
- external.resourceId,
837
- null,
838
- `[${orgName}] External resource ID '${external.resourceId}' conflicts with internal resource ID`
839
- )
840
- }
841
-
842
- // Validate triggers (external -> internal)
843
- external.triggers?.agents?.forEach((agentId) => {
844
- if (!validAgentIds.has(agentId)) {
845
- throw new RegistryValidationError(
846
- orgName,
847
- external.resourceId,
848
- 'triggers.agents',
849
- `[${orgName}] External resource '${external.resourceId}' triggers non-existent agent: ${agentId}`
850
- )
851
- }
852
- })
853
-
854
- external.triggers?.workflows?.forEach((workflowId) => {
855
- if (!validWorkflowIds.has(workflowId)) {
856
- throw new RegistryValidationError(
857
- orgName,
858
- external.resourceId,
859
- 'triggers.workflows',
860
- `[${orgName}] External resource '${external.resourceId}' triggers non-existent workflow: ${workflowId}`
861
- )
862
- }
863
- })
864
-
865
- // Validate uses integrations
866
- external.uses?.integrations?.forEach((integrationId) => {
867
- if (!validIntegrationIds.has(integrationId)) {
868
- throw new RegistryValidationError(
869
- orgName,
870
- external.resourceId,
871
- 'uses.integrations',
872
- `[${orgName}] External resource '${external.resourceId}' uses non-existent integration: ${integrationId}`
873
- )
874
- }
875
- })
876
- })
877
- }
878
-
879
- /**
880
- * Validates human checkpoint definitions
881
- */
882
- function validateHumanCheckpoints(
883
- orgName: string,
884
- humanCheckpoints: HumanCheckpointDefinition[],
885
- allInternalIds: Set<string>,
886
- validAgentIds: Set<string>,
887
- validWorkflowIds: Set<string>
888
- ): void {
889
- humanCheckpoints.forEach((humanCheckpoint) => {
890
- // Check for ID conflicts with internal resources
891
- if (allInternalIds.has(humanCheckpoint.resourceId)) {
892
- throw new RegistryValidationError(
893
- orgName,
894
- humanCheckpoint.resourceId,
895
- null,
896
- `[${orgName}] Human checkpoint ID '${humanCheckpoint.resourceId}' conflicts with internal resource ID`
897
- )
898
- }
899
-
900
- // Validate requestedBy.agents exist
901
- humanCheckpoint.requestedBy?.agents?.forEach((agentId) => {
902
- if (!validAgentIds.has(agentId)) {
903
- throw new RegistryValidationError(
904
- orgName,
905
- humanCheckpoint.resourceId,
906
- 'requestedBy.agents',
907
- `[${orgName}] Human checkpoint '${humanCheckpoint.resourceId}' requestedBy non-existent agent: ${agentId}`
908
- )
909
- }
910
- })
911
-
912
- // Validate requestedBy.workflows exist
913
- humanCheckpoint.requestedBy?.workflows?.forEach((workflowId) => {
914
- if (!validWorkflowIds.has(workflowId)) {
915
- throw new RegistryValidationError(
916
- orgName,
917
- humanCheckpoint.resourceId,
918
- 'requestedBy.workflows',
919
- `[${orgName}] Human checkpoint '${humanCheckpoint.resourceId}' requestedBy non-existent workflow: ${workflowId}`
920
- )
921
- }
922
- })
923
-
924
- // Validate routesTo.agents exist
925
- humanCheckpoint.routesTo?.agents?.forEach((agentId) => {
926
- if (!validAgentIds.has(agentId)) {
927
- throw new RegistryValidationError(
928
- orgName,
929
- humanCheckpoint.resourceId,
930
- 'routesTo.agents',
931
- `[${orgName}] Human checkpoint '${humanCheckpoint.resourceId}' routesTo non-existent agent: ${agentId}`
932
- )
933
- }
934
- })
935
-
936
- // Validate routesTo.workflows exist
937
- humanCheckpoint.routesTo?.workflows?.forEach((workflowId) => {
938
- if (!validWorkflowIds.has(workflowId)) {
939
- throw new RegistryValidationError(
940
- orgName,
941
- humanCheckpoint.resourceId,
942
- 'routesTo.workflows',
943
- `[${orgName}] Human checkpoint '${humanCheckpoint.resourceId}' routesTo non-existent workflow: ${workflowId}`
944
- )
945
- }
946
- })
947
- })
948
- }
469
+
470
+ return {
471
+ valid: issues.length === 0,
472
+ mode,
473
+ issues
474
+ }
475
+ }
476
+
477
+ // ============================================================================
478
+ // Resource Validation
479
+ // ============================================================================
480
+
481
+ /**
482
+ * Validates resources for a single organization
483
+ * - Duplicate resourceId check
484
+ * - Model configuration validation
485
+ * - ExecutionInterface-to-inputSchema validation
486
+ * @throws RegistryValidationError if validation fails
487
+ */
488
+ export function validateDeploymentSpec(orgName: string, resources: DeploymentSpec): void {
489
+ const seenIds = new Set<string>()
490
+
491
+ // Validate workflows
492
+ resources.workflows?.forEach((workflow) => {
493
+ const id = workflow.config.resourceId
494
+
495
+ // Check for duplicate IDs
496
+ if (seenIds.has(id)) {
497
+ throw new RegistryValidationError(
498
+ orgName,
499
+ id,
500
+ null,
501
+ `Duplicate resourceId "${id}" in organization "${orgName}". ` +
502
+ `Workflows and agents must have unique IDs within an organization.`
503
+ )
504
+ }
505
+ seenIds.add(id)
506
+ // Validate model config if present (workflows may optionally have modelConfig)
507
+ if ('modelConfig' in workflow && workflow.modelConfig) {
508
+ validateResourceModelConfig(orgName, id, workflow.modelConfig as ModelConfig)
509
+ }
510
+
511
+ // Validate ExecutionInterface matches inputSchema
512
+ if (workflow.interface) {
513
+ validateExecutionInterface(orgName, id, workflow.interface, workflow.contract.inputSchema)
514
+ }
515
+ })
516
+
517
+ // Validate agents
518
+ resources.agents?.forEach((agent) => {
519
+ const id = agent.config.resourceId
520
+
521
+ // Check for duplicate IDs
522
+ if (seenIds.has(id)) {
523
+ throw new RegistryValidationError(
524
+ orgName,
525
+ id,
526
+ null,
527
+ `Duplicate resourceId "${id}" in organization "${orgName}". ` +
528
+ `Workflows and agents must have unique IDs within an organization.`
529
+ )
530
+ }
531
+ seenIds.add(id)
532
+
533
+ // Validate model config
534
+ validateResourceModelConfig(orgName, id, agent.modelConfig)
535
+
536
+ // Validate ExecutionInterface matches inputSchema
537
+ if (agent.interface) {
538
+ validateExecutionInterface(orgName, id, agent.interface, agent.contract.inputSchema)
539
+ }
540
+ })
541
+
542
+ validateResourceGovernance(orgName, resources)
543
+ }
544
+
545
+ /**
546
+ * Validates model configuration for a resource
547
+ */
548
+ function validateResourceModelConfig(orgName: string, resourceId: string, modelConfig: ModelConfig): void {
549
+ try {
550
+ validateModelConfig(modelConfig)
551
+ } catch (error) {
552
+ if (error instanceof ModelConfigError) {
553
+ throw new RegistryValidationError(
554
+ orgName,
555
+ resourceId,
556
+ error.field,
557
+ `Invalid model config in ${orgName}/${resourceId}: ${error.message} (field: ${error.field})`
558
+ )
559
+ }
560
+ throw error
561
+ }
562
+ }
563
+
564
+ // ============================================================================
565
+ // ExecutionInterface Validation
566
+ // ============================================================================
567
+
568
+ /**
569
+ * Validates that ExecutionInterface form fields match the inputSchema
570
+ *
571
+ * Checks:
572
+ * 1. All required fields in inputSchema have corresponding form fields
573
+ * 2. Form field names match schema field names (or have valid fieldMappings)
574
+ * 3. Required/optional alignment between form and schema
575
+ *
576
+ * @throws RegistryValidationError if interface doesn't match schema
577
+ */
578
+ export function validateExecutionInterface(
579
+ orgName: string,
580
+ resourceId: string,
581
+ executionInterface: {
582
+ form: { fields: Array<{ name: string; required?: boolean }>; fieldMappings?: Record<string, string> }
583
+ },
584
+ inputSchema: z.ZodType
585
+ ): void {
586
+ const form = executionInterface.form
587
+ const fieldMappings = form.fieldMappings ?? {}
588
+
589
+ // Extract schema shape (only works for ZodObject)
590
+ const schemaShape = extractZodShape(inputSchema)
591
+ if (!schemaShape) {
592
+ // Can't validate non-object schemas - skip validation
593
+ return
594
+ }
595
+
596
+ const schemaFieldNames = Object.keys(schemaShape)
597
+
598
+ // Build effective mapping: form field name -> schema field name
599
+ const formToSchemaMap = new Map<string, string>()
600
+ for (const field of form.fields) {
601
+ const schemaKey = fieldMappings[field.name] ?? field.name
602
+ formToSchemaMap.set(field.name, schemaKey)
603
+ }
604
+
605
+ // Check 1: All required schema fields have form fields
606
+ for (const schemaFieldName of schemaFieldNames) {
607
+ const schemaField = schemaShape[schemaFieldName]
608
+ const isRequired = !isZodOptional(schemaField)
609
+
610
+ // Find form field that maps to this schema field
611
+ let hasFormField = false
612
+ for (const [formFieldName, mappedSchemaName] of Array.from(formToSchemaMap.entries())) {
613
+ if (mappedSchemaName === schemaFieldName) {
614
+ hasFormField = true
615
+
616
+ // Check required alignment
617
+ const formField = form.fields.find((f) => f.name === formFieldName)
618
+ if (isRequired && !formField?.required) {
619
+ throw new RegistryValidationError(
620
+ orgName,
621
+ resourceId,
622
+ `interface.form.fields.${formFieldName}`,
623
+ `ExecutionInterface field "${formFieldName}" should be required to match inputSchema field "${schemaFieldName}" in ${orgName}/${resourceId}`
624
+ )
625
+ }
626
+ break
627
+ }
628
+ }
629
+
630
+ if (isRequired && !hasFormField) {
631
+ throw new RegistryValidationError(
632
+ orgName,
633
+ resourceId,
634
+ 'interface.form.fields',
635
+ `ExecutionInterface missing required field "${schemaFieldName}" from inputSchema in ${orgName}/${resourceId}`
636
+ )
637
+ }
638
+ }
639
+
640
+ // Check 2: All form fields map to valid schema fields
641
+ for (const [formFieldName, schemaFieldName] of Array.from(formToSchemaMap.entries())) {
642
+ // Detect nested field notation (e.g., "criteria.targetTitles") and suggest flattening
643
+ if (schemaFieldName.includes('.')) {
644
+ const topLevelField = schemaFieldName.split('.')[0]
645
+ throw new RegistryValidationError(
646
+ orgName,
647
+ resourceId,
648
+ `interface.form.fields.${formFieldName}`,
649
+ `ExecutionInterface field "${formFieldName}" uses nested notation. ` +
650
+ `Flatten the inputSchema by moving nested fields to top-level ` +
651
+ `(e.g., "${topLevelField}.x" → "x") in ${orgName}/${resourceId}`
652
+ )
653
+ }
654
+
655
+ if (!schemaFieldNames.includes(schemaFieldName)) {
656
+ throw new RegistryValidationError(
657
+ orgName,
658
+ resourceId,
659
+ `interface.form.fields.${formFieldName}`,
660
+ `ExecutionInterface field "${formFieldName}" maps to non-existent schema field "${schemaFieldName}" in ${orgName}/${resourceId}`
661
+ )
662
+ }
663
+ }
664
+ }
665
+
666
+ /**
667
+ * Extract shape from a Zod schema (handles ZodObject and wrapped types)
668
+ */
669
+ function extractZodShape(schema: z.ZodType): Record<string, z.ZodType> | null {
670
+ // Handle ZodObject directly
671
+ if ('shape' in schema && typeof schema.shape === 'object') {
672
+ return schema.shape as Record<string, z.ZodType>
673
+ }
674
+
675
+ // Handle wrapped types (ZodEffects, ZodDefault, etc.)
676
+ if ('_def' in schema) {
677
+ const def = schema._def as { innerType?: z.ZodType; schema?: z.ZodType }
678
+ if (def.innerType) {
679
+ return extractZodShape(def.innerType)
680
+ }
681
+ if (def.schema) {
682
+ return extractZodShape(def.schema)
683
+ }
684
+ }
685
+
686
+ return null
687
+ }
688
+
689
+ /**
690
+ * Check if a Zod type is optional (or has a default)
691
+ * Uses isOptional() method which handles ZodOptional, ZodDefault, ZodNullable
692
+ */
693
+ function isZodOptional(schema: z.ZodType): boolean {
694
+ // Zod provides isOptional() which returns true for optional, default, and nullable types
695
+ return schema.isOptional()
696
+ }
697
+
698
+ // ============================================================================
699
+ // Relationship Validation
700
+ // ============================================================================
701
+
702
+ /**
703
+ * Validates relationship declarations reference valid resources
704
+ * @throws RegistryValidationError if validation fails
705
+ */
706
+ export function validateRelationships(orgName: string, resources: DeploymentSpec): void {
707
+ // Skip if no manifest data
708
+ if (!resources.relationships && !resources.triggers && !resources.externalResources && !resources.humanCheckpoints)
709
+ return
710
+
711
+ // Build resource ID sets for validation
712
+ const validAgentIds = new Set(resources.agents?.map((a) => a.config.resourceId) ?? [])
713
+ const validWorkflowIds = new Set(resources.workflows?.map((w) => w.config.resourceId) ?? [])
714
+ const validIntegrationIds = new Set(resources.integrations?.map((i) => i.resourceId) ?? [])
715
+ const validTriggerIds = new Set(resources.triggers?.map((t) => t.resourceId) ?? [])
716
+
717
+ // Collect all internal resource IDs for uniqueness check
718
+ const allInternalIds = new Set([
719
+ ...Array.from(validAgentIds),
720
+ ...Array.from(validWorkflowIds),
721
+ ...Array.from(validTriggerIds),
722
+ ...Array.from(validIntegrationIds)
723
+ ])
724
+
725
+ // Validate triggers
726
+ validateTriggers(orgName, resources.triggers ?? [], validAgentIds, validWorkflowIds)
727
+
728
+ // Validate resource relationships
729
+ validateResourceRelationships(
730
+ orgName,
731
+ resources.relationships ?? {},
732
+ validAgentIds,
733
+ validWorkflowIds,
734
+ validIntegrationIds,
735
+ validTriggerIds
736
+ )
737
+
738
+ // Validate external resources
739
+ validateExternalResources(
740
+ orgName,
741
+ resources.externalResources ?? [],
742
+ allInternalIds,
743
+ validAgentIds,
744
+ validWorkflowIds,
745
+ validIntegrationIds
746
+ )
747
+
748
+ // Validate human checkpoints
749
+ validateHumanCheckpoints(orgName, resources.humanCheckpoints ?? [], allInternalIds, validAgentIds, validWorkflowIds)
750
+ }
751
+
752
+ /**
753
+ * Validates trigger invocations
754
+ * NOTE: Trigger relationships are declared in ResourceRelationships, not on TriggerDefinition
755
+ * This validation is now handled by validateResourceRelationships()
756
+ */
757
+ function validateTriggers(
758
+ _orgName: string,
759
+ _triggers: TriggerDefinition[],
760
+ _validAgentIds: Set<string>,
761
+ _validWorkflowIds: Set<string>
762
+ ): void {
763
+ // No validation needed here - triggers declare their relationships in ResourceRelationships
764
+ // This prevents duplication and keeps all relationship declarations in one place
765
+ }
766
+
767
+ /**
768
+ * Validates resource relationship declarations
769
+ */
770
+ function validateResourceRelationships(
771
+ orgName: string,
772
+ relationships: ResourceRelationships,
773
+ validAgentIds: Set<string>,
774
+ validWorkflowIds: Set<string>,
775
+ validIntegrationIds: Set<string>,
776
+ validTriggerIds: Set<string>
777
+ ): void {
778
+ for (const [resourceId, declaration] of Object.entries(relationships)) {
779
+ // Validate declaring resource exists (agents, workflows, or triggers can declare relationships)
780
+ const resourceExists =
781
+ validAgentIds.has(resourceId) || validWorkflowIds.has(resourceId) || validTriggerIds.has(resourceId)
782
+ if (!resourceExists) {
783
+ throw new RegistryValidationError(
784
+ orgName,
785
+ resourceId,
786
+ null,
787
+ `[${orgName}] Relationship declared for non-existent resource: ${resourceId}`
788
+ )
789
+ }
790
+
791
+ // Validate triggers.agents
792
+ declaration.triggers?.agents?.forEach((agentId) => {
793
+ if (!validAgentIds.has(agentId)) {
794
+ throw new RegistryValidationError(
795
+ orgName,
796
+ resourceId,
797
+ 'triggers.agents',
798
+ `[${orgName}] Resource '${resourceId}' triggers non-existent agent: ${agentId}`
799
+ )
800
+ }
801
+ })
802
+
803
+ // Validate triggers.workflows
804
+ declaration.triggers?.workflows?.forEach((workflowId) => {
805
+ if (!validWorkflowIds.has(workflowId)) {
806
+ throw new RegistryValidationError(
807
+ orgName,
808
+ resourceId,
809
+ 'triggers.workflows',
810
+ `[${orgName}] Resource '${resourceId}' triggers non-existent workflow: ${workflowId}`
811
+ )
812
+ }
813
+ })
814
+
815
+ // Validate uses.integrations
816
+ declaration.uses?.integrations?.forEach((integrationId) => {
817
+ if (!validIntegrationIds.has(integrationId)) {
818
+ throw new RegistryValidationError(
819
+ orgName,
820
+ resourceId,
821
+ 'uses.integrations',
822
+ `[${orgName}] Resource '${resourceId}' uses non-existent integration: ${integrationId}`
823
+ )
824
+ }
825
+ })
826
+ }
827
+ }
828
+
829
+ /**
830
+ * Validates external resource definitions
831
+ */
832
+ function validateExternalResources(
833
+ orgName: string,
834
+ externalResources: ExternalResourceDefinition[],
835
+ allInternalIds: Set<string>,
836
+ validAgentIds: Set<string>,
837
+ validWorkflowIds: Set<string>,
838
+ validIntegrationIds: Set<string>
839
+ ): void {
840
+ externalResources.forEach((external) => {
841
+ // Validate unique ID (no conflict with internal resources)
842
+ if (allInternalIds.has(external.resourceId)) {
843
+ throw new RegistryValidationError(
844
+ orgName,
845
+ external.resourceId,
846
+ null,
847
+ `[${orgName}] External resource ID '${external.resourceId}' conflicts with internal resource ID`
848
+ )
849
+ }
850
+
851
+ // Validate triggers (external -> internal)
852
+ external.triggers?.agents?.forEach((agentId) => {
853
+ if (!validAgentIds.has(agentId)) {
854
+ throw new RegistryValidationError(
855
+ orgName,
856
+ external.resourceId,
857
+ 'triggers.agents',
858
+ `[${orgName}] External resource '${external.resourceId}' triggers non-existent agent: ${agentId}`
859
+ )
860
+ }
861
+ })
862
+
863
+ external.triggers?.workflows?.forEach((workflowId) => {
864
+ if (!validWorkflowIds.has(workflowId)) {
865
+ throw new RegistryValidationError(
866
+ orgName,
867
+ external.resourceId,
868
+ 'triggers.workflows',
869
+ `[${orgName}] External resource '${external.resourceId}' triggers non-existent workflow: ${workflowId}`
870
+ )
871
+ }
872
+ })
873
+
874
+ // Validate uses integrations
875
+ external.uses?.integrations?.forEach((integrationId) => {
876
+ if (!validIntegrationIds.has(integrationId)) {
877
+ throw new RegistryValidationError(
878
+ orgName,
879
+ external.resourceId,
880
+ 'uses.integrations',
881
+ `[${orgName}] External resource '${external.resourceId}' uses non-existent integration: ${integrationId}`
882
+ )
883
+ }
884
+ })
885
+ })
886
+ }
887
+
888
+ /**
889
+ * Validates human checkpoint definitions
890
+ */
891
+ function validateHumanCheckpoints(
892
+ orgName: string,
893
+ humanCheckpoints: HumanCheckpointDefinition[],
894
+ allInternalIds: Set<string>,
895
+ validAgentIds: Set<string>,
896
+ validWorkflowIds: Set<string>
897
+ ): void {
898
+ humanCheckpoints.forEach((humanCheckpoint) => {
899
+ // Check for ID conflicts with internal resources
900
+ if (allInternalIds.has(humanCheckpoint.resourceId)) {
901
+ throw new RegistryValidationError(
902
+ orgName,
903
+ humanCheckpoint.resourceId,
904
+ null,
905
+ `[${orgName}] Human checkpoint ID '${humanCheckpoint.resourceId}' conflicts with internal resource ID`
906
+ )
907
+ }
908
+
909
+ // Validate requestedBy.agents exist
910
+ humanCheckpoint.requestedBy?.agents?.forEach((agentId) => {
911
+ if (!validAgentIds.has(agentId)) {
912
+ throw new RegistryValidationError(
913
+ orgName,
914
+ humanCheckpoint.resourceId,
915
+ 'requestedBy.agents',
916
+ `[${orgName}] Human checkpoint '${humanCheckpoint.resourceId}' requestedBy non-existent agent: ${agentId}`
917
+ )
918
+ }
919
+ })
920
+
921
+ // Validate requestedBy.workflows exist
922
+ humanCheckpoint.requestedBy?.workflows?.forEach((workflowId) => {
923
+ if (!validWorkflowIds.has(workflowId)) {
924
+ throw new RegistryValidationError(
925
+ orgName,
926
+ humanCheckpoint.resourceId,
927
+ 'requestedBy.workflows',
928
+ `[${orgName}] Human checkpoint '${humanCheckpoint.resourceId}' requestedBy non-existent workflow: ${workflowId}`
929
+ )
930
+ }
931
+ })
932
+
933
+ // Validate routesTo.agents exist
934
+ humanCheckpoint.routesTo?.agents?.forEach((agentId) => {
935
+ if (!validAgentIds.has(agentId)) {
936
+ throw new RegistryValidationError(
937
+ orgName,
938
+ humanCheckpoint.resourceId,
939
+ 'routesTo.agents',
940
+ `[${orgName}] Human checkpoint '${humanCheckpoint.resourceId}' routesTo non-existent agent: ${agentId}`
941
+ )
942
+ }
943
+ })
944
+
945
+ // Validate routesTo.workflows exist
946
+ humanCheckpoint.routesTo?.workflows?.forEach((workflowId) => {
947
+ if (!validWorkflowIds.has(workflowId)) {
948
+ throw new RegistryValidationError(
949
+ orgName,
950
+ humanCheckpoint.resourceId,
951
+ 'routesTo.workflows',
952
+ `[${orgName}] Human checkpoint '${humanCheckpoint.resourceId}' routesTo non-existent workflow: ${workflowId}`
953
+ )
954
+ }
955
+ })
956
+ })
957
+ }