@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.
- package/dist/test-utils/index.js +5 -5
- package/package.json +1 -1
- package/src/business/acquisition/ontology-validation.ts +1 -1
- package/src/organization-model/__tests__/schema.test.ts +428 -399
- package/src/platform/constants/versions.ts +1 -1
- package/src/platform/registry/__tests__/resource-registry.nested-systems.test.ts +257 -245
- package/src/platform/registry/resource-registry.ts +881 -888
- package/src/platform/registry/validation.ts +721 -712
- package/src/reference/_generated/contracts.md +25 -30
|
@@ -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 {
|
|
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
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
+
}
|