@elevasis/core 0.23.0 → 0.24.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/index.d.ts +4343 -2690
- package/dist/index.js +1101 -156
- package/dist/knowledge/index.d.ts +574 -210
- package/dist/knowledge/index.js +104 -1
- package/dist/organization-model/index.d.ts +4343 -2690
- package/dist/organization-model/index.js +1101 -156
- package/dist/test-utils/index.d.ts +483 -109
- package/dist/test-utils/index.js +904 -144
- package/package.json +3 -3
- package/src/README.md +14 -14
- package/src/__tests__/publish.test.ts +24 -24
- package/src/__tests__/template-core-compatibility.test.ts +9 -12
- package/src/_gen/__tests__/__snapshots__/contracts.md.snap +2137 -2093
- package/src/_gen/__tests__/scaffold-contracts.test.ts +30 -30
- package/src/auth/multi-tenancy/credentials/__tests__/encryption.test.ts +217 -217
- package/src/auth/multi-tenancy/credentials/server/encryption.ts +69 -69
- package/src/auth/multi-tenancy/credentials/server/kek-loader.ts +37 -37
- package/src/auth/multi-tenancy/index.ts +26 -26
- package/src/auth/multi-tenancy/invitations/api-schemas.ts +104 -104
- package/src/auth/multi-tenancy/memberships/api-schemas.ts +143 -143
- package/src/auth/multi-tenancy/memberships/index.ts +26 -26
- package/src/auth/multi-tenancy/memberships/membership.ts +130 -130
- package/src/auth/multi-tenancy/organizations/__tests__/api-schemas.test.ts +194 -194
- package/src/auth/multi-tenancy/organizations/api-schemas.ts +136 -136
- package/src/auth/multi-tenancy/permissions.test.ts +42 -42
- package/src/auth/multi-tenancy/permissions.ts +123 -123
- package/src/auth/multi-tenancy/role-management/api-schemas.ts +78 -78
- package/src/auth/multi-tenancy/role-management/index.ts +16 -16
- package/src/auth/multi-tenancy/theme-presets.ts +45 -45
- package/src/auth/multi-tenancy/types.ts +57 -57
- package/src/auth/multi-tenancy/users/api-schemas.ts +165 -165
- package/src/business/README.md +2 -2
- package/src/business/acquisition/activity-events.test.ts +250 -250
- package/src/business/acquisition/activity-events.ts +93 -93
- package/src/business/acquisition/api-schemas.test.ts +1883 -1843
- package/src/business/acquisition/api-schemas.ts +1492 -1497
- package/src/business/acquisition/build-templates.test.ts +240 -240
- package/src/business/acquisition/build-templates.ts +98 -98
- package/src/business/acquisition/crm-next-action.test.ts +262 -262
- package/src/business/acquisition/crm-next-action.ts +220 -220
- package/src/business/acquisition/crm-priority.test.ts +216 -216
- package/src/business/acquisition/crm-priority.ts +349 -349
- package/src/business/acquisition/crm-state-actions.test.ts +153 -153
- package/src/business/acquisition/deal-ownership.test.ts +351 -351
- package/src/business/acquisition/deal-ownership.ts +120 -120
- package/src/business/acquisition/derive-actions.test.ts +129 -104
- package/src/business/acquisition/derive-actions.ts +74 -84
- package/src/business/acquisition/index.ts +171 -170
- package/src/business/acquisition/ontology-validation.ts +309 -0
- package/src/business/acquisition/stateful.ts +30 -30
- package/src/business/acquisition/types.ts +396 -396
- package/src/business/clients/api-schemas.test.ts +115 -115
- package/src/business/clients/api-schemas.ts +158 -158
- package/src/business/clients/index.ts +1 -1
- package/src/business/crm/api-schemas.ts +40 -40
- package/src/business/crm/index.ts +1 -1
- package/src/business/deals/api-schemas.ts +87 -87
- package/src/business/deals/index.ts +1 -1
- package/src/business/index.ts +5 -5
- package/src/business/projects/types.ts +144 -144
- package/src/commands/queue/types/task.ts +15 -15
- package/src/execution/core/runner-types.ts +61 -61
- package/src/execution/core/sse-executions.ts +7 -7
- package/src/execution/engine/__tests__/fixtures/test-agents.ts +10 -10
- package/src/execution/engine/agent/core/__tests__/agent.test.ts +16 -16
- package/src/execution/engine/agent/core/__tests__/error-passthrough.test.ts +4 -4
- package/src/execution/engine/agent/core/types.ts +25 -25
- package/src/execution/engine/agent/index.ts +6 -6
- package/src/execution/engine/agent/reasoning/__tests__/request-builder.test.ts +24 -24
- package/src/execution/engine/index.ts +443 -443
- package/src/execution/engine/tools/integration/server/adapters/apify/__tests__/apify-run-actor.integration.test.ts +298 -298
- package/src/execution/engine/tools/integration/server/adapters/apify/apify-adapter.test.ts +55 -55
- package/src/execution/engine/tools/integration/server/adapters/apify/apify-adapter.ts +107 -107
- package/src/execution/engine/tools/integration/server/adapters/apollo/apollo-adapter.test.ts +48 -48
- package/src/execution/engine/tools/integration/server/adapters/apollo/apollo-adapter.ts +99 -99
- package/src/execution/engine/tools/integration/server/adapters/apollo/index.ts +1 -1
- package/src/execution/engine/tools/integration/server/adapters/attio/__tests__/attio-crud.integration.test.ts +363 -363
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/get-record/index.test.ts +162 -162
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/list-records/index.test.ts +316 -316
- package/src/execution/engine/tools/integration/server/adapters/clickup/clickup-adapter.test.ts +18 -18
- package/src/execution/engine/tools/integration/server/adapters/clickup/clickup-adapter.ts +194 -194
- package/src/execution/engine/tools/integration/server/adapters/clickup/index.ts +7 -7
- package/src/execution/engine/tools/integration/server/adapters/gmail/gmail-adapter.ts +204 -204
- package/src/execution/engine/tools/integration/server/adapters/gmail/gmail-tools.ts +105 -105
- package/src/execution/engine/tools/integration/server/adapters/google-calendar/google-calendar-adapter.ts +428 -428
- package/src/execution/engine/tools/integration/server/adapters/google-calendar/index.ts +2 -2
- package/src/execution/engine/tools/integration/server/adapters/google-sheets/__tests__/google-sheets.integration.test.ts +261 -261
- package/src/execution/engine/tools/integration/server/adapters/instantly/instantly-tools.ts +1474 -1474
- package/src/execution/engine/tools/integration/server/adapters/millionverifier/millionverifier-tools.ts +103 -103
- package/src/execution/engine/tools/integration/server/adapters/resend/fetch/send-email/index.test.ts +88 -88
- package/src/execution/engine/tools/integration/server/adapters/resend/fetch/send-email/index.ts +141 -141
- package/src/execution/engine/tools/integration/server/adapters/resend/fetch/utils/types.ts +76 -76
- package/src/execution/engine/tools/integration/server/adapters/signature-api/signature-api-tools.ts +182 -182
- package/src/execution/engine/tools/integration/server/adapters/stripe/stripe-tools.ts +310 -310
- package/src/execution/engine/tools/integration/service.test.ts +239 -239
- package/src/execution/engine/tools/integration/service.ts +172 -172
- package/src/execution/engine/tools/integration/tool.ts +255 -255
- package/src/execution/engine/tools/lead-service-types.ts +1005 -1005
- package/src/execution/engine/tools/messages.ts +43 -43
- package/src/execution/engine/tools/platform/acquisition/company-tools.ts +7 -7
- package/src/execution/engine/tools/platform/acquisition/contact-tools.ts +6 -6
- package/src/execution/engine/tools/platform/acquisition/list-tools.ts +6 -6
- package/src/execution/engine/tools/platform/acquisition/types.ts +280 -280
- package/src/execution/engine/tools/platform/email/types.ts +97 -97
- package/src/execution/engine/tools/registry.ts +704 -704
- package/src/execution/engine/tools/tool-maps.ts +831 -831
- package/src/execution/engine/tools/types.ts +234 -234
- package/src/execution/engine/workflow/types.ts +195 -197
- package/src/execution/external/__tests__/api-schemas.test.ts +127 -127
- package/src/execution/external/api-schemas.ts +40 -40
- package/src/execution/external/index.ts +1 -1
- package/src/index.ts +18 -18
- package/src/integrations/credentials/__tests__/api-schemas.test.ts +420 -420
- package/src/integrations/credentials/api-schemas.ts +146 -146
- package/src/integrations/credentials/schemas.ts +200 -200
- package/src/integrations/oauth/__tests__/provider-registry.test.ts +7 -7
- package/src/integrations/oauth/provider-registry.ts +74 -74
- package/src/integrations/oauth/server/credentials.ts +43 -43
- package/src/integrations/webhook-endpoints/__tests__/api-schemas.test.ts +327 -327
- package/src/integrations/webhook-endpoints/api-schemas.ts +103 -103
- package/src/integrations/webhook-endpoints/types.ts +58 -58
- package/src/knowledge/README.md +32 -32
- package/src/knowledge/__tests__/queries.test.ts +626 -535
- package/src/knowledge/format.ts +99 -99
- package/src/knowledge/index.ts +5 -5
- package/src/knowledge/published.ts +5 -5
- package/src/knowledge/queries.ts +269 -218
- package/src/operations/activities/api-schemas.ts +80 -80
- package/src/operations/activities/types.ts +64 -64
- package/src/organization-model/README.md +149 -149
- package/src/organization-model/__tests__/content-kinds-registry.test.ts +210 -210
- package/src/organization-model/__tests__/defaults.test.ts +168 -168
- package/src/organization-model/__tests__/domains/actions.test.ts +78 -56
- package/src/organization-model/__tests__/domains/customers.test.ts +299 -299
- package/src/organization-model/__tests__/domains/entities.test.ts +56 -56
- package/src/organization-model/__tests__/domains/goals.test.ts +493 -493
- package/src/organization-model/__tests__/domains/identity.test.ts +280 -280
- package/src/organization-model/__tests__/domains/navigation.test.ts +268 -268
- package/src/organization-model/__tests__/domains/offerings.test.ts +414 -414
- package/src/organization-model/__tests__/domains/policies.test.ts +323 -323
- package/src/organization-model/__tests__/domains/resource-mappings.test.ts +293 -293
- package/src/organization-model/__tests__/domains/resources.test.ts +387 -277
- package/src/organization-model/__tests__/domains/roles.test.ts +463 -463
- package/src/organization-model/__tests__/domains/statuses.test.ts +246 -246
- package/src/organization-model/__tests__/domains/systems.test.ts +209 -209
- package/src/organization-model/__tests__/domains/topology.test.ts +188 -0
- package/src/organization-model/__tests__/flatten-additive-merge.test.ts +362 -361
- package/src/organization-model/__tests__/foundation.test.ts +77 -77
- package/src/organization-model/__tests__/get-resources-for-system.test.ts +144 -144
- package/src/organization-model/__tests__/graph.test.ts +1312 -862
- package/src/organization-model/__tests__/icons.test.ts +10 -1
- package/src/organization-model/__tests__/knowledge.test.ts +251 -15
- package/src/organization-model/__tests__/lookup-helpers.test.ts +438 -438
- package/src/organization-model/__tests__/migration-helpers.test.ts +591 -591
- package/src/organization-model/__tests__/prospecting-ssot.test.ts +103 -103
- package/src/organization-model/__tests__/recursive-system-schema.test.ts +535 -506
- package/src/organization-model/__tests__/resolve.test.ts +274 -164
- package/src/organization-model/__tests__/schema.test.ts +844 -301
- package/src/organization-model/__tests__/surface-projection.test.ts +284 -284
- package/src/organization-model/catalogs/lead-gen.ts +144 -144
- package/src/organization-model/content-kinds/config.ts +36 -36
- package/src/organization-model/content-kinds/index.ts +76 -72
- package/src/organization-model/content-kinds/pipeline.ts +68 -68
- package/src/organization-model/content-kinds/registry.ts +44 -44
- package/src/organization-model/content-kinds/status.ts +71 -71
- package/src/organization-model/content-kinds/template.ts +83 -83
- package/src/organization-model/content-kinds/types.ts +117 -117
- package/src/organization-model/contracts.ts +27 -27
- package/src/organization-model/defaults.ts +42 -50
- package/src/organization-model/domains/actions.ts +333 -239
- package/src/organization-model/domains/customers.ts +78 -78
- package/src/organization-model/domains/entities.ts +144 -144
- package/src/organization-model/domains/goals.ts +83 -83
- package/src/organization-model/domains/knowledge.ts +117 -101
- package/src/organization-model/domains/navigation.ts +139 -139
- package/src/organization-model/domains/offerings.ts +71 -71
- package/src/organization-model/domains/policies.ts +102 -102
- package/src/organization-model/domains/projects.ts +14 -14
- package/src/organization-model/domains/prospecting.ts +395 -395
- package/src/organization-model/domains/resources.ts +202 -124
- package/src/organization-model/domains/roles.ts +96 -96
- package/src/organization-model/domains/sales.test.ts +218 -218
- package/src/organization-model/domains/sales.ts +380 -380
- package/src/organization-model/domains/shared.ts +63 -63
- package/src/organization-model/domains/statuses.ts +339 -339
- package/src/organization-model/domains/systems.ts +217 -172
- package/src/organization-model/domains/topology.ts +261 -0
- package/src/organization-model/foundation.ts +75 -75
- package/src/organization-model/graph/build.ts +1043 -867
- package/src/organization-model/graph/index.ts +4 -4
- package/src/organization-model/graph/link.ts +10 -10
- package/src/organization-model/graph/schema.ts +75 -68
- package/src/organization-model/graph/types.ts +71 -64
- package/src/organization-model/helpers.ts +289 -241
- package/src/organization-model/icons.ts +78 -66
- package/src/organization-model/index.ts +128 -125
- package/src/organization-model/migration-helpers.ts +247 -244
- package/src/organization-model/ontology.ts +658 -0
- package/src/organization-model/organization-graph.mdx +110 -90
- package/src/organization-model/organization-model.mdx +225 -213
- package/src/organization-model/published.ts +299 -222
- package/src/organization-model/resolve.ts +146 -91
- package/src/organization-model/schema.ts +818 -659
- package/src/organization-model/surface-projection.ts +212 -212
- package/src/organization-model/types.ts +179 -155
- package/src/platform/api/types.ts +38 -38
- package/src/platform/constants/versions.ts +3 -3
- package/src/platform/index.ts +23 -23
- package/src/platform/registry/__tests__/command-view.test.ts +10 -10
- package/src/platform/registry/__tests__/resource-link.test.ts +35 -35
- package/src/platform/registry/__tests__/resource-registry.integration.test.ts +20 -20
- package/src/platform/registry/__tests__/resource-registry.nested-systems.test.ts +245 -245
- package/src/platform/registry/__tests__/resource-registry.test.ts +2053 -2053
- package/src/platform/registry/__tests__/validation.test.ts +1444 -1259
- package/src/platform/registry/command-view.ts +10 -10
- package/src/platform/registry/index.ts +103 -103
- package/src/platform/registry/resource-link.ts +32 -32
- package/src/platform/registry/resource-registry.ts +886 -886
- package/src/platform/registry/serialization.ts +295 -295
- package/src/platform/registry/serialized-types.ts +166 -166
- package/src/platform/registry/stats-types.ts +68 -68
- package/src/platform/registry/types.ts +425 -425
- package/src/platform/registry/validation.ts +876 -684
- package/src/platform/utils/__tests__/validation.test.ts +1084 -1084
- package/src/platform/utils/validation.ts +425 -425
- package/src/projects/api-schemas.test.ts +39 -39
- package/src/projects/api-schemas.ts +291 -291
- package/src/reference/_generated/contracts.md +2136 -2093
- package/src/reference/glossary.md +76 -76
- package/src/scaffold-registry/__tests__/index.test.ts +206 -206
- package/src/scaffold-registry/__tests__/schema.test.ts +166 -166
- package/src/scaffold-registry/index.ts +392 -392
- package/src/scaffold-registry/schema.ts +243 -243
- package/src/server.ts +289 -289
- package/src/supabase/database.types.ts +3 -0
- package/src/test-utils/README.md +37 -37
- package/src/test-utils/entities.ts +108 -108
- package/src/test-utils/fixtures/memberships.ts +82 -82
- package/src/test-utils/index.ts +12 -12
- package/src/test-utils/organization-model.ts +65 -65
- package/src/test-utils/published.ts +6 -6
- package/src/test-utils/rls/RLSTestContext.ts +588 -588
- package/src/test-utils/test-utils.test.ts +44 -44
|
@@ -1,745 +1,937 @@
|
|
|
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 type
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
// ============================================================================
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
public readonly
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
export type
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
| '
|
|
46
|
-
| '
|
|
16
|
+
import { compileOrganizationOntology, type OntologyKind, type ResolvedOntologyIndex } from '../../organization-model/ontology'
|
|
17
|
+
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'
|
|
47
49
|
| 'missing-om-system'
|
|
48
50
|
| 'raw-resource-id'
|
|
51
|
+
| 'descriptor-mismatch'
|
|
52
|
+
| 'missing-ontology-actions'
|
|
53
|
+
| 'ontology-reference-missing'
|
|
54
|
+
| 'primary-action-mismatch'
|
|
55
|
+
| 'topology-reference-missing'
|
|
49
56
|
|
|
50
57
|
export interface ResourceGovernanceModel {
|
|
51
58
|
systems?: Record<string, SystemEntry>
|
|
52
59
|
resources?: Record<string, ResourceEntry>
|
|
60
|
+
ontology?: OrganizationModel['ontology']
|
|
61
|
+
topology?: OrganizationModel['topology']
|
|
62
|
+
roles?: OrganizationModel['roles']
|
|
63
|
+
policies?: OrganizationModel['policies']
|
|
64
|
+
entities?: OrganizationModel['entities']
|
|
65
|
+
actions?: OrganizationModel['actions']
|
|
53
66
|
}
|
|
54
|
-
|
|
55
|
-
export interface ResourceGovernanceValidationIssue {
|
|
56
|
-
type: ResourceGovernanceValidationIssueType
|
|
57
|
-
orgName: string
|
|
58
|
-
resourceId: string
|
|
59
|
-
message: string
|
|
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
|
+
|
|
125
|
+
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
|
+
}))
|
|
146
|
+
]
|
|
60
147
|
}
|
|
61
148
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
149
|
+
function hasOntologySources(organizationModel: ResourceGovernanceModel): boolean {
|
|
150
|
+
return (
|
|
151
|
+
organizationModel.ontology !== undefined ||
|
|
152
|
+
organizationModel.entities !== undefined ||
|
|
153
|
+
organizationModel.actions !== undefined ||
|
|
154
|
+
Object.values(organizationModel.systems ?? {}).some(systemHasOntologySource)
|
|
155
|
+
)
|
|
66
156
|
}
|
|
67
157
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
158
|
+
function systemHasOntologySource(system: SystemEntry): boolean {
|
|
159
|
+
if (system.ontology !== undefined || (system.content !== undefined && Object.keys(system.content).length > 0)) return true
|
|
160
|
+
return Object.values(system.systems ?? system.subsystems ?? {}).some(systemHasOntologySource)
|
|
71
161
|
}
|
|
72
162
|
|
|
73
|
-
function
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
163
|
+
function ontologyIndexForKind(index: ResolvedOntologyIndex, kind: OntologyKind): Record<string, unknown> {
|
|
164
|
+
switch (kind) {
|
|
165
|
+
case 'object':
|
|
166
|
+
return index.objectTypes
|
|
167
|
+
case 'link':
|
|
168
|
+
return index.linkTypes
|
|
169
|
+
case 'action':
|
|
170
|
+
return index.actionTypes
|
|
171
|
+
case 'catalog':
|
|
172
|
+
return index.catalogTypes
|
|
173
|
+
case 'event':
|
|
174
|
+
return index.eventTypes
|
|
175
|
+
case 'interface':
|
|
176
|
+
return index.interfaceTypes
|
|
177
|
+
case 'value-type':
|
|
178
|
+
return index.valueTypes
|
|
179
|
+
case 'property':
|
|
180
|
+
return index.sharedProperties
|
|
181
|
+
case 'group':
|
|
182
|
+
return index.groups
|
|
183
|
+
case 'surface':
|
|
184
|
+
return index.surfaces
|
|
185
|
+
}
|
|
77
186
|
}
|
|
78
187
|
|
|
79
|
-
function
|
|
80
|
-
|
|
81
|
-
type: ResourceGovernanceValidationIssueType,
|
|
82
|
-
orgName: string,
|
|
83
|
-
resourceId: string,
|
|
84
|
-
message: string
|
|
85
|
-
): void {
|
|
86
|
-
issues.push({
|
|
87
|
-
type,
|
|
88
|
-
orgName,
|
|
89
|
-
resourceId,
|
|
90
|
-
message
|
|
91
|
-
})
|
|
188
|
+
function sameJson(left: unknown, right: unknown): boolean {
|
|
189
|
+
return JSON.stringify(left ?? null) === JSON.stringify(right ?? null)
|
|
92
190
|
}
|
|
93
191
|
|
|
94
|
-
function
|
|
192
|
+
function addOntologyBindingIssues(
|
|
95
193
|
issues: ResourceGovernanceValidationIssue[],
|
|
96
|
-
|
|
97
|
-
|
|
194
|
+
orgName: string,
|
|
195
|
+
resource: ResourceEntry,
|
|
196
|
+
ontologyIndex: ResolvedOntologyIndex | undefined
|
|
98
197
|
): void {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
198
|
+
const binding = resource.ontology
|
|
199
|
+
if (binding === undefined) return
|
|
200
|
+
|
|
201
|
+
if ((resource.kind === 'workflow' || resource.kind === 'agent') && (binding.actions?.length ?? 0) === 0) {
|
|
202
|
+
addGovernanceIssue(
|
|
203
|
+
issues,
|
|
204
|
+
'missing-ontology-actions',
|
|
205
|
+
orgName,
|
|
206
|
+
resource.id,
|
|
207
|
+
`[${orgName}] Resource '${resource.id}' declares ontology bindings but no ontology actions.`
|
|
208
|
+
)
|
|
104
209
|
}
|
|
105
210
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
type: ResourceType
|
|
115
|
-
descriptor?: ResourceEntry
|
|
116
|
-
}> {
|
|
117
|
-
return [
|
|
118
|
-
...(resources.workflows ?? []).map((workflow) => ({
|
|
119
|
-
resourceId: workflow.config.resourceId,
|
|
120
|
-
type: workflow.config.type,
|
|
121
|
-
descriptor: workflow.config.resource
|
|
122
|
-
})),
|
|
123
|
-
...(resources.agents ?? []).map((agent) => ({
|
|
124
|
-
resourceId: agent.config.resourceId,
|
|
125
|
-
type: agent.config.type,
|
|
126
|
-
descriptor: agent.config.resource
|
|
127
|
-
})),
|
|
128
|
-
...(resources.integrations ?? []).map((integration) => ({
|
|
129
|
-
resourceId: integration.resourceId,
|
|
130
|
-
type: integration.type,
|
|
131
|
-
descriptor: (integration as { resource?: ResourceEntry }).resource
|
|
132
|
-
}))
|
|
133
|
-
]
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Validates runtime resource definitions against OM Resources and Systems.
|
|
138
|
-
*
|
|
139
|
-
* This is the shared core entry point for SDK, CI, deploy, and ResourceRegistry.
|
|
140
|
-
* Default mode is strict. ELEVASIS_RESOURCE_VALIDATOR=warn-only remains a
|
|
141
|
-
* permanent emergency escape hatch unless an explicit mode is passed.
|
|
142
|
-
*/
|
|
143
|
-
export function validateResourceGovernance(
|
|
144
|
-
orgName: string,
|
|
145
|
-
deployment: DeploymentSpec,
|
|
146
|
-
organizationModel: ResourceGovernanceModel | undefined = deployment.organizationModel,
|
|
147
|
-
options: ResourceGovernanceValidationOptions = {}
|
|
148
|
-
): ResourceGovernanceValidationResult {
|
|
149
|
-
const mode = getResourceValidatorMode(options.mode)
|
|
150
|
-
const omResourcesMap = organizationModel?.resources
|
|
151
|
-
const omSystemsMap = organizationModel?.systems
|
|
152
|
-
const issues: ResourceGovernanceValidationIssue[] = []
|
|
153
|
-
|
|
154
|
-
if (!omResourcesMap || !omSystemsMap) {
|
|
155
|
-
return { valid: true, mode, issues }
|
|
211
|
+
if (binding.primaryAction !== undefined && !binding.actions?.includes(binding.primaryAction)) {
|
|
212
|
+
addGovernanceIssue(
|
|
213
|
+
issues,
|
|
214
|
+
'primary-action-mismatch',
|
|
215
|
+
orgName,
|
|
216
|
+
resource.id,
|
|
217
|
+
`[${orgName}] Resource '${resource.id}' primaryAction '${binding.primaryAction}' must be included in ontology.actions.`
|
|
218
|
+
)
|
|
156
219
|
}
|
|
157
220
|
|
|
158
|
-
|
|
159
|
-
// The lookup key is the dot-joined path, which equals resource.systemPath.
|
|
160
|
-
const systemsById = new Map(
|
|
161
|
-
listAllSystems({ systems: omSystemsMap } as OrganizationModel).map(({ path, system }) => [path, system])
|
|
162
|
-
)
|
|
163
|
-
const activeOmResources = Object.values(omResourcesMap).filter((resource) => resource.status === 'active')
|
|
164
|
-
const omResourcesById = new Map(activeOmResources.map((resource) => [resource.id, resource]))
|
|
165
|
-
const runtimeResources = getRuntimeResources(deployment)
|
|
166
|
-
const runtimeResourcesById = new Map(runtimeResources.map((resource) => [resource.resourceId, resource]))
|
|
167
|
-
|
|
168
|
-
for (const resource of activeOmResources) {
|
|
169
|
-
if (!systemsById.has(resource.systemPath)) {
|
|
170
|
-
addGovernanceIssue(
|
|
171
|
-
issues,
|
|
172
|
-
'missing-om-system',
|
|
173
|
-
orgName,
|
|
174
|
-
resource.id,
|
|
175
|
-
`[${orgName}] OM resource '${resource.id}' references missing system path '${resource.systemPath}'.`
|
|
176
|
-
)
|
|
177
|
-
}
|
|
221
|
+
if (ontologyIndex === undefined) return
|
|
178
222
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
`[${orgName}] OM resource '${resource.id}' has no matching code-side resource.`
|
|
187
|
-
)
|
|
188
|
-
continue
|
|
189
|
-
}
|
|
223
|
+
const validateRefs = (
|
|
224
|
+
bindingKey: 'actions' | 'primaryAction' | 'reads' | 'writes' | 'usesCatalogs' | 'emits',
|
|
225
|
+
expectedKind: OntologyKind,
|
|
226
|
+
refs: string[] | string | undefined
|
|
227
|
+
) => {
|
|
228
|
+
const values = refs === undefined ? [] : Array.isArray(refs) ? refs : [refs]
|
|
229
|
+
const index = ontologyIndexForKind(ontologyIndex, expectedKind)
|
|
190
230
|
|
|
191
|
-
|
|
231
|
+
for (const ref of values) {
|
|
232
|
+
if (index[ref] !== undefined) continue
|
|
192
233
|
addGovernanceIssue(
|
|
193
234
|
issues,
|
|
194
|
-
'
|
|
235
|
+
'ontology-reference-missing',
|
|
195
236
|
orgName,
|
|
196
237
|
resource.id,
|
|
197
|
-
`[${orgName}] Resource '${resource.id}'
|
|
198
|
-
)
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
if (runtimeResource.descriptor && runtimeResource.descriptor.systemPath !== resource.systemPath) {
|
|
202
|
-
addGovernanceIssue(
|
|
203
|
-
issues,
|
|
204
|
-
'system-mismatch',
|
|
205
|
-
orgName,
|
|
206
|
-
resource.id,
|
|
207
|
-
`[${orgName}] Resource '${resource.id}' system mismatch: code descriptor has '${runtimeResource.descriptor.systemPath}', OM has '${resource.systemPath}'.`
|
|
238
|
+
`[${orgName}] Resource '${resource.id}' ontology.${bindingKey} references missing ${expectedKind} ontology record '${ref}'.`
|
|
208
239
|
)
|
|
209
240
|
}
|
|
210
241
|
}
|
|
211
242
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
orgName,
|
|
219
|
-
runtimeResource.resourceId,
|
|
220
|
-
`[${orgName}] Code-side resource '${runtimeResource.resourceId}' has no active OM Resource descriptor.`
|
|
221
|
-
)
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
if (!runtimeResource.descriptor) {
|
|
225
|
-
addGovernanceIssue(
|
|
226
|
-
issues,
|
|
227
|
-
'raw-resource-id',
|
|
228
|
-
orgName,
|
|
229
|
-
runtimeResource.resourceId,
|
|
230
|
-
`[${orgName}] Code-side resource '${runtimeResource.resourceId}' authors raw resourceId/type values. Use an OM Resource descriptor and bindResourceDescriptor().`
|
|
231
|
-
)
|
|
232
|
-
continue
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
if (runtimeResource.descriptor.id !== runtimeResource.resourceId) {
|
|
236
|
-
addGovernanceIssue(
|
|
237
|
-
issues,
|
|
238
|
-
'raw-resource-id',
|
|
239
|
-
orgName,
|
|
240
|
-
runtimeResource.resourceId,
|
|
241
|
-
`[${orgName}] Code-side resource '${runtimeResource.resourceId}' does not derive identity from its OM descriptor '${runtimeResource.descriptor.id}'.`
|
|
242
|
-
)
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (runtimeResource.descriptor.kind !== runtimeResource.type) {
|
|
246
|
-
addGovernanceIssue(
|
|
247
|
-
issues,
|
|
248
|
-
'type-mismatch',
|
|
249
|
-
orgName,
|
|
250
|
-
runtimeResource.resourceId,
|
|
251
|
-
`[${orgName}] Code-side resource '${runtimeResource.resourceId}' descriptor kind '${runtimeResource.descriptor.kind}' does not match runtime type '${runtimeResource.type}'.`
|
|
252
|
-
)
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
emitGovernanceIssues(issues, mode, options.onWarning)
|
|
257
|
-
|
|
258
|
-
return {
|
|
259
|
-
valid: issues.length === 0,
|
|
260
|
-
mode,
|
|
261
|
-
issues
|
|
262
|
-
}
|
|
243
|
+
validateRefs('actions', 'action', binding.actions)
|
|
244
|
+
validateRefs('primaryAction', 'action', binding.primaryAction)
|
|
245
|
+
validateRefs('reads', 'object', binding.reads)
|
|
246
|
+
validateRefs('writes', 'object', binding.writes)
|
|
247
|
+
validateRefs('usesCatalogs', 'catalog', binding.usesCatalogs)
|
|
248
|
+
validateRefs('emits', 'event', binding.emits)
|
|
263
249
|
}
|
|
264
250
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
// ============================================================================
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* Validates resources for a single organization
|
|
271
|
-
* - Duplicate resourceId check
|
|
272
|
-
* - Model configuration validation
|
|
273
|
-
* - ExecutionInterface-to-inputSchema validation
|
|
274
|
-
* @throws RegistryValidationError if validation fails
|
|
275
|
-
*/
|
|
276
|
-
export function validateDeploymentSpec(orgName: string, resources: DeploymentSpec): void {
|
|
277
|
-
const seenIds = new Set<string>()
|
|
278
|
-
|
|
279
|
-
// Validate workflows
|
|
280
|
-
resources.workflows?.forEach((workflow) => {
|
|
281
|
-
const id = workflow.config.resourceId
|
|
282
|
-
|
|
283
|
-
// Check for duplicate IDs
|
|
284
|
-
if (seenIds.has(id)) {
|
|
285
|
-
throw new RegistryValidationError(
|
|
286
|
-
orgName,
|
|
287
|
-
id,
|
|
288
|
-
null,
|
|
289
|
-
`Duplicate resourceId "${id}" in organization "${orgName}". ` +
|
|
290
|
-
`Workflows and agents must have unique IDs within an organization.`
|
|
291
|
-
)
|
|
292
|
-
}
|
|
293
|
-
seenIds.add(id)
|
|
294
|
-
// Validate model config if present (workflows may optionally have modelConfig)
|
|
295
|
-
if ('modelConfig' in workflow && workflow.modelConfig) {
|
|
296
|
-
validateResourceModelConfig(orgName, id, workflow.modelConfig as ModelConfig)
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// Validate ExecutionInterface matches inputSchema
|
|
300
|
-
if (workflow.interface) {
|
|
301
|
-
validateExecutionInterface(orgName, id, workflow.interface, workflow.contract.inputSchema)
|
|
302
|
-
}
|
|
303
|
-
})
|
|
304
|
-
|
|
305
|
-
// Validate agents
|
|
306
|
-
resources.agents?.forEach((agent) => {
|
|
307
|
-
const id = agent.config.resourceId
|
|
308
|
-
|
|
309
|
-
// Check for duplicate IDs
|
|
310
|
-
if (seenIds.has(id)) {
|
|
311
|
-
throw new RegistryValidationError(
|
|
312
|
-
orgName,
|
|
313
|
-
id,
|
|
314
|
-
null,
|
|
315
|
-
`Duplicate resourceId "${id}" in organization "${orgName}". ` +
|
|
316
|
-
`Workflows and agents must have unique IDs within an organization.`
|
|
317
|
-
)
|
|
318
|
-
}
|
|
319
|
-
seenIds.add(id)
|
|
320
|
-
|
|
321
|
-
// Validate model config
|
|
322
|
-
validateResourceModelConfig(orgName, id, agent.modelConfig)
|
|
323
|
-
|
|
324
|
-
// Validate ExecutionInterface matches inputSchema
|
|
325
|
-
if (agent.interface) {
|
|
326
|
-
validateExecutionInterface(orgName, id, agent.interface, agent.contract.inputSchema)
|
|
327
|
-
}
|
|
328
|
-
})
|
|
329
|
-
|
|
330
|
-
validateResourceGovernance(orgName, resources)
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
/**
|
|
334
|
-
* Validates model configuration for a resource
|
|
335
|
-
*/
|
|
336
|
-
function validateResourceModelConfig(orgName: string, resourceId: string, modelConfig: ModelConfig): void {
|
|
337
|
-
try {
|
|
338
|
-
validateModelConfig(modelConfig)
|
|
339
|
-
} catch (error) {
|
|
340
|
-
if (error instanceof ModelConfigError) {
|
|
341
|
-
throw new RegistryValidationError(
|
|
342
|
-
orgName,
|
|
343
|
-
resourceId,
|
|
344
|
-
error.field,
|
|
345
|
-
`Invalid model config in ${orgName}/${resourceId}: ${error.message} (field: ${error.field})`
|
|
346
|
-
)
|
|
347
|
-
}
|
|
348
|
-
throw error
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// ============================================================================
|
|
353
|
-
// ExecutionInterface Validation
|
|
354
|
-
// ============================================================================
|
|
355
|
-
|
|
356
|
-
/**
|
|
357
|
-
* Validates that ExecutionInterface form fields match the inputSchema
|
|
358
|
-
*
|
|
359
|
-
* Checks:
|
|
360
|
-
* 1. All required fields in inputSchema have corresponding form fields
|
|
361
|
-
* 2. Form field names match schema field names (or have valid fieldMappings)
|
|
362
|
-
* 3. Required/optional alignment between form and schema
|
|
363
|
-
*
|
|
364
|
-
* @throws RegistryValidationError if interface doesn't match schema
|
|
365
|
-
*/
|
|
366
|
-
export function validateExecutionInterface(
|
|
251
|
+
function addTopologyIssues(
|
|
252
|
+
issues: ResourceGovernanceValidationIssue[],
|
|
367
253
|
orgName: string,
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
254
|
+
deployment: DeploymentSpec,
|
|
255
|
+
organizationModel: ResourceGovernanceModel,
|
|
256
|
+
systemsById: Map<string, SystemEntry>,
|
|
257
|
+
omResourcesById: Map<string, ResourceEntry>,
|
|
258
|
+
ontologyIndex: ResolvedOntologyIndex | undefined
|
|
373
259
|
): void {
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
for (const schemaFieldName of schemaFieldNames) {
|
|
395
|
-
const schemaField = schemaShape[schemaFieldName]
|
|
396
|
-
const isRequired = !isZodOptional(schemaField)
|
|
397
|
-
|
|
398
|
-
// Find form field that maps to this schema field
|
|
399
|
-
let hasFormField = false
|
|
400
|
-
for (const [formFieldName, mappedSchemaName] of Array.from(formToSchemaMap.entries())) {
|
|
401
|
-
if (mappedSchemaName === schemaFieldName) {
|
|
402
|
-
hasFormField = true
|
|
403
|
-
|
|
404
|
-
// Check required alignment
|
|
405
|
-
const formField = form.fields.find((f) => f.name === formFieldName)
|
|
406
|
-
if (isRequired && !formField?.required) {
|
|
407
|
-
throw new RegistryValidationError(
|
|
408
|
-
orgName,
|
|
409
|
-
resourceId,
|
|
410
|
-
`interface.form.fields.${formFieldName}`,
|
|
411
|
-
`ExecutionInterface field "${formFieldName}" should be required to match inputSchema field "${schemaFieldName}" in ${orgName}/${resourceId}`
|
|
412
|
-
)
|
|
413
|
-
}
|
|
414
|
-
break
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
if (isRequired && !hasFormField) {
|
|
419
|
-
throw new RegistryValidationError(
|
|
420
|
-
orgName,
|
|
421
|
-
resourceId,
|
|
422
|
-
'interface.form.fields',
|
|
423
|
-
`ExecutionInterface missing required field "${schemaFieldName}" from inputSchema in ${orgName}/${resourceId}`
|
|
424
|
-
)
|
|
260
|
+
const relationships = organizationModel.topology?.relationships
|
|
261
|
+
if (relationships === undefined) return
|
|
262
|
+
|
|
263
|
+
const rolesById = new Set(Object.keys(organizationModel.roles ?? {}))
|
|
264
|
+
const policiesById = new Set(Object.keys(organizationModel.policies ?? {}))
|
|
265
|
+
const triggerIds = new Set(deployment.triggers?.map((trigger) => trigger.resourceId) ?? [])
|
|
266
|
+
const humanCheckpointIds = new Set(deployment.humanCheckpoints?.map((checkpoint) => checkpoint.resourceId) ?? [])
|
|
267
|
+
const externalResourceIds = new Set(deployment.externalResources?.map((external) => external.resourceId) ?? [])
|
|
268
|
+
|
|
269
|
+
const topologyRefExists = (ref: OmTopologyNodeRef): boolean => {
|
|
270
|
+
if (ref.kind === 'system') return systemsById.has(ref.id)
|
|
271
|
+
if (ref.kind === 'resource') return omResourcesById.has(ref.id)
|
|
272
|
+
if (ref.kind === 'policy') return policiesById.has(ref.id)
|
|
273
|
+
if (ref.kind === 'role') return rolesById.has(ref.id)
|
|
274
|
+
if (ref.kind === 'trigger') return triggerIds.has(ref.id)
|
|
275
|
+
if (ref.kind === 'humanCheckpoint') return humanCheckpointIds.has(ref.id)
|
|
276
|
+
if (ref.kind === 'externalResource') return externalResourceIds.has(ref.id)
|
|
277
|
+
if (ref.kind === 'ontology') {
|
|
278
|
+
if (ontologyIndex === undefined) return true
|
|
279
|
+
return Object.values(ontologyIndex).some((records) => records[ref.id] !== undefined)
|
|
425
280
|
}
|
|
281
|
+
return false
|
|
426
282
|
}
|
|
427
283
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
const topLevelField = schemaFieldName.split('.')[0]
|
|
433
|
-
throw new RegistryValidationError(
|
|
434
|
-
orgName,
|
|
435
|
-
resourceId,
|
|
436
|
-
`interface.form.fields.${formFieldName}`,
|
|
437
|
-
`ExecutionInterface field "${formFieldName}" uses nested notation. ` +
|
|
438
|
-
`Flatten the inputSchema by moving nested fields to top-level ` +
|
|
439
|
-
`(e.g., "${topLevelField}.x" → "x") in ${orgName}/${resourceId}`
|
|
440
|
-
)
|
|
441
|
-
}
|
|
284
|
+
for (const [relationshipId, relationship] of Object.entries(relationships)) {
|
|
285
|
+
;(['from', 'to'] as const).forEach((side) => {
|
|
286
|
+
const ref = relationship[side]
|
|
287
|
+
if (topologyRefExists(ref)) return
|
|
442
288
|
|
|
443
|
-
|
|
444
|
-
|
|
289
|
+
addGovernanceIssue(
|
|
290
|
+
issues,
|
|
291
|
+
'topology-reference-missing',
|
|
445
292
|
orgName,
|
|
446
|
-
|
|
447
|
-
`
|
|
448
|
-
`ExecutionInterface field "${formFieldName}" maps to non-existent schema field "${schemaFieldName}" in ${orgName}/${resourceId}`
|
|
293
|
+
ref.id,
|
|
294
|
+
`[${orgName}] Topology relationship '${relationshipId}' ${side} references missing ${ref.kind} '${ref.id}'.`
|
|
449
295
|
)
|
|
450
|
-
}
|
|
296
|
+
})
|
|
451
297
|
}
|
|
452
298
|
}
|
|
453
|
-
|
|
454
|
-
/**
|
|
455
|
-
*
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Validates runtime resource definitions against OM Resources and Systems.
|
|
302
|
+
*
|
|
303
|
+
* This is the shared core entry point for SDK, CI, deploy, and ResourceRegistry.
|
|
304
|
+
* Default mode is strict. ELEVASIS_RESOURCE_VALIDATOR=warn-only remains a
|
|
305
|
+
* permanent emergency escape hatch unless an explicit mode is passed.
|
|
306
|
+
*/
|
|
307
|
+
export function validateResourceGovernance(
|
|
308
|
+
orgName: string,
|
|
309
|
+
deployment: DeploymentSpec,
|
|
310
|
+
organizationModel: ResourceGovernanceModel | undefined = deployment.organizationModel,
|
|
311
|
+
options: ResourceGovernanceValidationOptions = {}
|
|
312
|
+
): ResourceGovernanceValidationResult {
|
|
313
|
+
const mode = getResourceValidatorMode(options.mode)
|
|
314
|
+
const omResourcesMap = organizationModel?.resources
|
|
315
|
+
const omSystemsMap = organizationModel?.systems
|
|
316
|
+
const issues: ResourceGovernanceValidationIssue[] = []
|
|
317
|
+
|
|
318
|
+
if (!omResourcesMap || !omSystemsMap) {
|
|
319
|
+
return { valid: true, mode, issues }
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Use listAllSystems (DFS) so nested systems under .subsystems are included.
|
|
323
|
+
// The lookup key is the dot-joined path, which equals resource.systemPath.
|
|
324
|
+
const systemsById = new Map(
|
|
325
|
+
listAllSystems({ systems: omSystemsMap } as OrganizationModel).map(({ path, system }) => [path, system])
|
|
326
|
+
)
|
|
327
|
+
const activeOmResources = Object.values(omResourcesMap).filter((resource) => resource.status === 'active')
|
|
328
|
+
const omResourcesById = new Map(activeOmResources.map((resource) => [resource.id, resource]))
|
|
329
|
+
const runtimeResources = getRuntimeResources(deployment)
|
|
330
|
+
const runtimeResourcesById = new Map(runtimeResources.map((resource) => [resource.resourceId, resource]))
|
|
331
|
+
const ontologyCompilation = hasOntologySources(organizationModel)
|
|
332
|
+
? compileOrganizationOntology(organizationModel)
|
|
333
|
+
: undefined
|
|
334
|
+
const ontologyIndex = ontologyCompilation?.ontology
|
|
335
|
+
|
|
336
|
+
for (const diagnostic of ontologyCompilation?.diagnostics ?? []) {
|
|
337
|
+
addGovernanceIssue(
|
|
338
|
+
issues,
|
|
339
|
+
'ontology-reference-missing',
|
|
340
|
+
orgName,
|
|
341
|
+
diagnostic.id,
|
|
342
|
+
`[${orgName}] ${diagnostic.message}`
|
|
343
|
+
)
|
|
472
344
|
}
|
|
473
345
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
validateTriggers(orgName, resources.triggers ?? [], validAgentIds, validWorkflowIds)
|
|
515
|
-
|
|
516
|
-
// Validate resource relationships
|
|
517
|
-
validateResourceRelationships(
|
|
518
|
-
orgName,
|
|
519
|
-
resources.relationships ?? {},
|
|
520
|
-
validAgentIds,
|
|
521
|
-
validWorkflowIds,
|
|
522
|
-
validIntegrationIds,
|
|
523
|
-
validTriggerIds
|
|
524
|
-
)
|
|
525
|
-
|
|
526
|
-
// Validate external resources
|
|
527
|
-
validateExternalResources(
|
|
528
|
-
orgName,
|
|
529
|
-
resources.externalResources ?? [],
|
|
530
|
-
allInternalIds,
|
|
531
|
-
validAgentIds,
|
|
532
|
-
validWorkflowIds,
|
|
533
|
-
validIntegrationIds
|
|
534
|
-
)
|
|
535
|
-
|
|
536
|
-
// Validate human checkpoints
|
|
537
|
-
validateHumanCheckpoints(orgName, resources.humanCheckpoints ?? [], allInternalIds, validAgentIds, validWorkflowIds)
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
/**
|
|
541
|
-
* Validates trigger invocations
|
|
542
|
-
* NOTE: Trigger relationships are declared in ResourceRelationships, not on TriggerDefinition
|
|
543
|
-
* This validation is now handled by validateResourceRelationships()
|
|
544
|
-
*/
|
|
545
|
-
function validateTriggers(
|
|
546
|
-
_orgName: string,
|
|
547
|
-
_triggers: TriggerDefinition[],
|
|
548
|
-
_validAgentIds: Set<string>,
|
|
549
|
-
_validWorkflowIds: Set<string>
|
|
550
|
-
): void {
|
|
551
|
-
// No validation needed here - triggers declare their relationships in ResourceRelationships
|
|
552
|
-
// This prevents duplication and keeps all relationship declarations in one place
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
/**
|
|
556
|
-
* Validates resource relationship declarations
|
|
557
|
-
*/
|
|
558
|
-
function validateResourceRelationships(
|
|
559
|
-
orgName: string,
|
|
560
|
-
relationships: ResourceRelationships,
|
|
561
|
-
validAgentIds: Set<string>,
|
|
562
|
-
validWorkflowIds: Set<string>,
|
|
563
|
-
validIntegrationIds: Set<string>,
|
|
564
|
-
validTriggerIds: Set<string>
|
|
565
|
-
): void {
|
|
566
|
-
for (const [resourceId, declaration] of Object.entries(relationships)) {
|
|
567
|
-
// Validate declaring resource exists (agents, workflows, or triggers can declare relationships)
|
|
568
|
-
const resourceExists =
|
|
569
|
-
validAgentIds.has(resourceId) || validWorkflowIds.has(resourceId) || validTriggerIds.has(resourceId)
|
|
570
|
-
if (!resourceExists) {
|
|
571
|
-
throw new RegistryValidationError(
|
|
572
|
-
orgName,
|
|
573
|
-
resourceId,
|
|
574
|
-
null,
|
|
575
|
-
`[${orgName}] Relationship declared for non-existent resource: ${resourceId}`
|
|
346
|
+
for (const resource of activeOmResources) {
|
|
347
|
+
if (!systemsById.has(resource.systemPath)) {
|
|
348
|
+
addGovernanceIssue(
|
|
349
|
+
issues,
|
|
350
|
+
'missing-om-system',
|
|
351
|
+
orgName,
|
|
352
|
+
resource.id,
|
|
353
|
+
`[${orgName}] OM resource '${resource.id}' references missing system path '${resource.systemPath}'.`
|
|
354
|
+
)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const runtimeResource = runtimeResourcesById.get(resource.id)
|
|
358
|
+
if (!runtimeResource) {
|
|
359
|
+
addGovernanceIssue(
|
|
360
|
+
issues,
|
|
361
|
+
'missing-code-resource',
|
|
362
|
+
orgName,
|
|
363
|
+
resource.id,
|
|
364
|
+
`[${orgName}] OM resource '${resource.id}' has no matching code-side resource.`
|
|
365
|
+
)
|
|
366
|
+
continue
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (runtimeResource.type !== resource.kind) {
|
|
370
|
+
addGovernanceIssue(
|
|
371
|
+
issues,
|
|
372
|
+
'type-mismatch',
|
|
373
|
+
orgName,
|
|
374
|
+
resource.id,
|
|
375
|
+
`[${orgName}] Resource '${resource.id}' type mismatch: code has '${runtimeResource.type}', OM has '${resource.kind}'.`
|
|
376
|
+
)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (runtimeResource.descriptor && runtimeResource.descriptor.systemPath !== resource.systemPath) {
|
|
380
|
+
addGovernanceIssue(
|
|
381
|
+
issues,
|
|
382
|
+
'system-mismatch',
|
|
383
|
+
orgName,
|
|
384
|
+
resource.id,
|
|
385
|
+
`[${orgName}] Resource '${resource.id}' system mismatch: code descriptor has '${runtimeResource.descriptor.systemPath}', OM has '${resource.systemPath}'.`
|
|
576
386
|
)
|
|
577
387
|
}
|
|
578
388
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
orgName,
|
|
584
|
-
resourceId,
|
|
585
|
-
'triggers.agents',
|
|
586
|
-
`[${orgName}] Resource '${resourceId}' triggers non-existent agent: ${agentId}`
|
|
587
|
-
)
|
|
588
|
-
}
|
|
589
|
-
})
|
|
590
|
-
|
|
591
|
-
// Validate triggers.workflows
|
|
592
|
-
declaration.triggers?.workflows?.forEach((workflowId) => {
|
|
593
|
-
if (!validWorkflowIds.has(workflowId)) {
|
|
594
|
-
throw new RegistryValidationError(
|
|
595
|
-
orgName,
|
|
596
|
-
resourceId,
|
|
597
|
-
'triggers.workflows',
|
|
598
|
-
`[${orgName}] Resource '${resourceId}' triggers non-existent workflow: ${workflowId}`
|
|
599
|
-
)
|
|
600
|
-
}
|
|
601
|
-
})
|
|
602
|
-
|
|
603
|
-
// Validate uses.integrations
|
|
604
|
-
declaration.uses?.integrations?.forEach((integrationId) => {
|
|
605
|
-
if (!validIntegrationIds.has(integrationId)) {
|
|
606
|
-
throw new RegistryValidationError(
|
|
607
|
-
orgName,
|
|
608
|
-
resourceId,
|
|
609
|
-
'uses.integrations',
|
|
610
|
-
`[${orgName}] Resource '${resourceId}' uses non-existent integration: ${integrationId}`
|
|
611
|
-
)
|
|
612
|
-
}
|
|
613
|
-
})
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
/**
|
|
618
|
-
* Validates external resource definitions
|
|
619
|
-
*/
|
|
620
|
-
function validateExternalResources(
|
|
621
|
-
orgName: string,
|
|
622
|
-
externalResources: ExternalResourceDefinition[],
|
|
623
|
-
allInternalIds: Set<string>,
|
|
624
|
-
validAgentIds: Set<string>,
|
|
625
|
-
validWorkflowIds: Set<string>,
|
|
626
|
-
validIntegrationIds: Set<string>
|
|
627
|
-
): void {
|
|
628
|
-
externalResources.forEach((external) => {
|
|
629
|
-
// Validate unique ID (no conflict with internal resources)
|
|
630
|
-
if (allInternalIds.has(external.resourceId)) {
|
|
631
|
-
throw new RegistryValidationError(
|
|
389
|
+
if (runtimeResource.descriptor && !sameJson(runtimeResource.descriptor.ontology, resource.ontology)) {
|
|
390
|
+
addGovernanceIssue(
|
|
391
|
+
issues,
|
|
392
|
+
'descriptor-mismatch',
|
|
632
393
|
orgName,
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
`[${orgName}] External resource ID '${external.resourceId}' conflicts with internal resource ID`
|
|
394
|
+
resource.id,
|
|
395
|
+
`[${orgName}] Resource '${resource.id}' ontology descriptor mismatch between code and OM.`
|
|
636
396
|
)
|
|
637
397
|
}
|
|
638
398
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
allInternalIds: Set<string>,
|
|
683
|
-
validAgentIds: Set<string>,
|
|
684
|
-
validWorkflowIds: Set<string>
|
|
685
|
-
): void {
|
|
686
|
-
humanCheckpoints.forEach((humanCheckpoint) => {
|
|
687
|
-
// Check for ID conflicts with internal resources
|
|
688
|
-
if (allInternalIds.has(humanCheckpoint.resourceId)) {
|
|
689
|
-
throw new RegistryValidationError(
|
|
690
|
-
orgName,
|
|
691
|
-
humanCheckpoint.resourceId,
|
|
692
|
-
null,
|
|
693
|
-
`[${orgName}] Human checkpoint ID '${humanCheckpoint.resourceId}' conflicts with internal resource ID`
|
|
399
|
+
addOntologyBindingIssues(issues, orgName, resource, ontologyIndex)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
for (const runtimeResource of runtimeResources) {
|
|
403
|
+
const omResource = omResourcesById.get(runtimeResource.resourceId)
|
|
404
|
+
if (!omResource) {
|
|
405
|
+
addGovernanceIssue(
|
|
406
|
+
issues,
|
|
407
|
+
'missing-om-resource',
|
|
408
|
+
orgName,
|
|
409
|
+
runtimeResource.resourceId,
|
|
410
|
+
`[${orgName}] Code-side resource '${runtimeResource.resourceId}' has no active OM Resource descriptor.`
|
|
411
|
+
)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (!runtimeResource.descriptor) {
|
|
415
|
+
addGovernanceIssue(
|
|
416
|
+
issues,
|
|
417
|
+
'raw-resource-id',
|
|
418
|
+
orgName,
|
|
419
|
+
runtimeResource.resourceId,
|
|
420
|
+
`[${orgName}] Code-side resource '${runtimeResource.resourceId}' authors raw resourceId/type values. Use an OM Resource descriptor and bindResourceDescriptor().`
|
|
421
|
+
)
|
|
422
|
+
continue
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (runtimeResource.descriptor.id !== runtimeResource.resourceId) {
|
|
426
|
+
addGovernanceIssue(
|
|
427
|
+
issues,
|
|
428
|
+
'raw-resource-id',
|
|
429
|
+
orgName,
|
|
430
|
+
runtimeResource.resourceId,
|
|
431
|
+
`[${orgName}] Code-side resource '${runtimeResource.resourceId}' does not derive identity from its OM descriptor '${runtimeResource.descriptor.id}'.`
|
|
432
|
+
)
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (runtimeResource.descriptor.kind !== runtimeResource.type) {
|
|
436
|
+
addGovernanceIssue(
|
|
437
|
+
issues,
|
|
438
|
+
'type-mismatch',
|
|
439
|
+
orgName,
|
|
440
|
+
runtimeResource.resourceId,
|
|
441
|
+
`[${orgName}] Code-side resource '${runtimeResource.resourceId}' descriptor kind '${runtimeResource.descriptor.kind}' does not match runtime type '${runtimeResource.type}'.`
|
|
694
442
|
)
|
|
695
443
|
}
|
|
444
|
+
}
|
|
696
445
|
|
|
697
|
-
|
|
698
|
-
humanCheckpoint.requestedBy?.agents?.forEach((agentId) => {
|
|
699
|
-
if (!validAgentIds.has(agentId)) {
|
|
700
|
-
throw new RegistryValidationError(
|
|
701
|
-
orgName,
|
|
702
|
-
humanCheckpoint.resourceId,
|
|
703
|
-
'requestedBy.agents',
|
|
704
|
-
`[${orgName}] Human checkpoint '${humanCheckpoint.resourceId}' requestedBy non-existent agent: ${agentId}`
|
|
705
|
-
)
|
|
706
|
-
}
|
|
707
|
-
})
|
|
708
|
-
|
|
709
|
-
// Validate requestedBy.workflows exist
|
|
710
|
-
humanCheckpoint.requestedBy?.workflows?.forEach((workflowId) => {
|
|
711
|
-
if (!validWorkflowIds.has(workflowId)) {
|
|
712
|
-
throw new RegistryValidationError(
|
|
713
|
-
orgName,
|
|
714
|
-
humanCheckpoint.resourceId,
|
|
715
|
-
'requestedBy.workflows',
|
|
716
|
-
`[${orgName}] Human checkpoint '${humanCheckpoint.resourceId}' requestedBy non-existent workflow: ${workflowId}`
|
|
717
|
-
)
|
|
718
|
-
}
|
|
719
|
-
})
|
|
720
|
-
|
|
721
|
-
// Validate routesTo.agents exist
|
|
722
|
-
humanCheckpoint.routesTo?.agents?.forEach((agentId) => {
|
|
723
|
-
if (!validAgentIds.has(agentId)) {
|
|
724
|
-
throw new RegistryValidationError(
|
|
725
|
-
orgName,
|
|
726
|
-
humanCheckpoint.resourceId,
|
|
727
|
-
'routesTo.agents',
|
|
728
|
-
`[${orgName}] Human checkpoint '${humanCheckpoint.resourceId}' routesTo non-existent agent: ${agentId}`
|
|
729
|
-
)
|
|
730
|
-
}
|
|
731
|
-
})
|
|
446
|
+
addTopologyIssues(issues, orgName, deployment, organizationModel, systemsById, omResourcesById, ontologyIndex)
|
|
732
447
|
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
448
|
+
emitGovernanceIssues(issues, mode, options.onWarning)
|
|
449
|
+
|
|
450
|
+
return {
|
|
451
|
+
valid: issues.length === 0,
|
|
452
|
+
mode,
|
|
453
|
+
issues
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// ============================================================================
|
|
458
|
+
// Resource Validation
|
|
459
|
+
// ============================================================================
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Validates resources for a single organization
|
|
463
|
+
* - Duplicate resourceId check
|
|
464
|
+
* - Model configuration validation
|
|
465
|
+
* - ExecutionInterface-to-inputSchema validation
|
|
466
|
+
* @throws RegistryValidationError if validation fails
|
|
467
|
+
*/
|
|
468
|
+
export function validateDeploymentSpec(orgName: string, resources: DeploymentSpec): void {
|
|
469
|
+
const seenIds = new Set<string>()
|
|
470
|
+
|
|
471
|
+
// Validate workflows
|
|
472
|
+
resources.workflows?.forEach((workflow) => {
|
|
473
|
+
const id = workflow.config.resourceId
|
|
474
|
+
|
|
475
|
+
// Check for duplicate IDs
|
|
476
|
+
if (seenIds.has(id)) {
|
|
477
|
+
throw new RegistryValidationError(
|
|
478
|
+
orgName,
|
|
479
|
+
id,
|
|
480
|
+
null,
|
|
481
|
+
`Duplicate resourceId "${id}" in organization "${orgName}". ` +
|
|
482
|
+
`Workflows and agents must have unique IDs within an organization.`
|
|
483
|
+
)
|
|
484
|
+
}
|
|
485
|
+
seenIds.add(id)
|
|
486
|
+
// Validate model config if present (workflows may optionally have modelConfig)
|
|
487
|
+
if ('modelConfig' in workflow && workflow.modelConfig) {
|
|
488
|
+
validateResourceModelConfig(orgName, id, workflow.modelConfig as ModelConfig)
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Validate ExecutionInterface matches inputSchema
|
|
492
|
+
if (workflow.interface) {
|
|
493
|
+
validateExecutionInterface(orgName, id, workflow.interface, workflow.contract.inputSchema)
|
|
494
|
+
}
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
// Validate agents
|
|
498
|
+
resources.agents?.forEach((agent) => {
|
|
499
|
+
const id = agent.config.resourceId
|
|
500
|
+
|
|
501
|
+
// Check for duplicate IDs
|
|
502
|
+
if (seenIds.has(id)) {
|
|
503
|
+
throw new RegistryValidationError(
|
|
504
|
+
orgName,
|
|
505
|
+
id,
|
|
506
|
+
null,
|
|
507
|
+
`Duplicate resourceId "${id}" in organization "${orgName}". ` +
|
|
508
|
+
`Workflows and agents must have unique IDs within an organization.`
|
|
509
|
+
)
|
|
510
|
+
}
|
|
511
|
+
seenIds.add(id)
|
|
512
|
+
|
|
513
|
+
// Validate model config
|
|
514
|
+
validateResourceModelConfig(orgName, id, agent.modelConfig)
|
|
515
|
+
|
|
516
|
+
// Validate ExecutionInterface matches inputSchema
|
|
517
|
+
if (agent.interface) {
|
|
518
|
+
validateExecutionInterface(orgName, id, agent.interface, agent.contract.inputSchema)
|
|
519
|
+
}
|
|
520
|
+
})
|
|
521
|
+
|
|
522
|
+
validateResourceGovernance(orgName, resources)
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Validates model configuration for a resource
|
|
527
|
+
*/
|
|
528
|
+
function validateResourceModelConfig(orgName: string, resourceId: string, modelConfig: ModelConfig): void {
|
|
529
|
+
try {
|
|
530
|
+
validateModelConfig(modelConfig)
|
|
531
|
+
} catch (error) {
|
|
532
|
+
if (error instanceof ModelConfigError) {
|
|
533
|
+
throw new RegistryValidationError(
|
|
534
|
+
orgName,
|
|
535
|
+
resourceId,
|
|
536
|
+
error.field,
|
|
537
|
+
`Invalid model config in ${orgName}/${resourceId}: ${error.message} (field: ${error.field})`
|
|
538
|
+
)
|
|
539
|
+
}
|
|
540
|
+
throw error
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// ============================================================================
|
|
545
|
+
// ExecutionInterface Validation
|
|
546
|
+
// ============================================================================
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Validates that ExecutionInterface form fields match the inputSchema
|
|
550
|
+
*
|
|
551
|
+
* Checks:
|
|
552
|
+
* 1. All required fields in inputSchema have corresponding form fields
|
|
553
|
+
* 2. Form field names match schema field names (or have valid fieldMappings)
|
|
554
|
+
* 3. Required/optional alignment between form and schema
|
|
555
|
+
*
|
|
556
|
+
* @throws RegistryValidationError if interface doesn't match schema
|
|
557
|
+
*/
|
|
558
|
+
export function validateExecutionInterface(
|
|
559
|
+
orgName: string,
|
|
560
|
+
resourceId: string,
|
|
561
|
+
executionInterface: {
|
|
562
|
+
form: { fields: Array<{ name: string; required?: boolean }>; fieldMappings?: Record<string, string> }
|
|
563
|
+
},
|
|
564
|
+
inputSchema: z.ZodType
|
|
565
|
+
): void {
|
|
566
|
+
const form = executionInterface.form
|
|
567
|
+
const fieldMappings = form.fieldMappings ?? {}
|
|
568
|
+
|
|
569
|
+
// Extract schema shape (only works for ZodObject)
|
|
570
|
+
const schemaShape = extractZodShape(inputSchema)
|
|
571
|
+
if (!schemaShape) {
|
|
572
|
+
// Can't validate non-object schemas - skip validation
|
|
573
|
+
return
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const schemaFieldNames = Object.keys(schemaShape)
|
|
577
|
+
|
|
578
|
+
// Build effective mapping: form field name -> schema field name
|
|
579
|
+
const formToSchemaMap = new Map<string, string>()
|
|
580
|
+
for (const field of form.fields) {
|
|
581
|
+
const schemaKey = fieldMappings[field.name] ?? field.name
|
|
582
|
+
formToSchemaMap.set(field.name, schemaKey)
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Check 1: All required schema fields have form fields
|
|
586
|
+
for (const schemaFieldName of schemaFieldNames) {
|
|
587
|
+
const schemaField = schemaShape[schemaFieldName]
|
|
588
|
+
const isRequired = !isZodOptional(schemaField)
|
|
589
|
+
|
|
590
|
+
// Find form field that maps to this schema field
|
|
591
|
+
let hasFormField = false
|
|
592
|
+
for (const [formFieldName, mappedSchemaName] of Array.from(formToSchemaMap.entries())) {
|
|
593
|
+
if (mappedSchemaName === schemaFieldName) {
|
|
594
|
+
hasFormField = true
|
|
595
|
+
|
|
596
|
+
// Check required alignment
|
|
597
|
+
const formField = form.fields.find((f) => f.name === formFieldName)
|
|
598
|
+
if (isRequired && !formField?.required) {
|
|
599
|
+
throw new RegistryValidationError(
|
|
600
|
+
orgName,
|
|
601
|
+
resourceId,
|
|
602
|
+
`interface.form.fields.${formFieldName}`,
|
|
603
|
+
`ExecutionInterface field "${formFieldName}" should be required to match inputSchema field "${schemaFieldName}" in ${orgName}/${resourceId}`
|
|
604
|
+
)
|
|
605
|
+
}
|
|
606
|
+
break
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (isRequired && !hasFormField) {
|
|
611
|
+
throw new RegistryValidationError(
|
|
612
|
+
orgName,
|
|
613
|
+
resourceId,
|
|
614
|
+
'interface.form.fields',
|
|
615
|
+
`ExecutionInterface missing required field "${schemaFieldName}" from inputSchema in ${orgName}/${resourceId}`
|
|
616
|
+
)
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Check 2: All form fields map to valid schema fields
|
|
621
|
+
for (const [formFieldName, schemaFieldName] of Array.from(formToSchemaMap.entries())) {
|
|
622
|
+
// Detect nested field notation (e.g., "criteria.targetTitles") and suggest flattening
|
|
623
|
+
if (schemaFieldName.includes('.')) {
|
|
624
|
+
const topLevelField = schemaFieldName.split('.')[0]
|
|
625
|
+
throw new RegistryValidationError(
|
|
626
|
+
orgName,
|
|
627
|
+
resourceId,
|
|
628
|
+
`interface.form.fields.${formFieldName}`,
|
|
629
|
+
`ExecutionInterface field "${formFieldName}" uses nested notation. ` +
|
|
630
|
+
`Flatten the inputSchema by moving nested fields to top-level ` +
|
|
631
|
+
`(e.g., "${topLevelField}.x" → "x") in ${orgName}/${resourceId}`
|
|
632
|
+
)
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (!schemaFieldNames.includes(schemaFieldName)) {
|
|
636
|
+
throw new RegistryValidationError(
|
|
637
|
+
orgName,
|
|
638
|
+
resourceId,
|
|
639
|
+
`interface.form.fields.${formFieldName}`,
|
|
640
|
+
`ExecutionInterface field "${formFieldName}" maps to non-existent schema field "${schemaFieldName}" in ${orgName}/${resourceId}`
|
|
641
|
+
)
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Extract shape from a Zod schema (handles ZodObject and wrapped types)
|
|
648
|
+
*/
|
|
649
|
+
function extractZodShape(schema: z.ZodType): Record<string, z.ZodType> | null {
|
|
650
|
+
// Handle ZodObject directly
|
|
651
|
+
if ('shape' in schema && typeof schema.shape === 'object') {
|
|
652
|
+
return schema.shape as Record<string, z.ZodType>
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Handle wrapped types (ZodEffects, ZodDefault, etc.)
|
|
656
|
+
if ('_def' in schema) {
|
|
657
|
+
const def = schema._def as { innerType?: z.ZodType; schema?: z.ZodType }
|
|
658
|
+
if (def.innerType) {
|
|
659
|
+
return extractZodShape(def.innerType)
|
|
660
|
+
}
|
|
661
|
+
if (def.schema) {
|
|
662
|
+
return extractZodShape(def.schema)
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
return null
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Check if a Zod type is optional (or has a default)
|
|
671
|
+
* Uses isOptional() method which handles ZodOptional, ZodDefault, ZodNullable
|
|
672
|
+
*/
|
|
673
|
+
function isZodOptional(schema: z.ZodType): boolean {
|
|
674
|
+
// Zod provides isOptional() which returns true for optional, default, and nullable types
|
|
675
|
+
return schema.isOptional()
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// ============================================================================
|
|
679
|
+
// Relationship Validation
|
|
680
|
+
// ============================================================================
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Validates relationship declarations reference valid resources
|
|
684
|
+
* @throws RegistryValidationError if validation fails
|
|
685
|
+
*/
|
|
686
|
+
export function validateRelationships(orgName: string, resources: DeploymentSpec): void {
|
|
687
|
+
// Skip if no manifest data
|
|
688
|
+
if (!resources.relationships && !resources.triggers && !resources.externalResources && !resources.humanCheckpoints)
|
|
689
|
+
return
|
|
690
|
+
|
|
691
|
+
// Build resource ID sets for validation
|
|
692
|
+
const validAgentIds = new Set(resources.agents?.map((a) => a.config.resourceId) ?? [])
|
|
693
|
+
const validWorkflowIds = new Set(resources.workflows?.map((w) => w.config.resourceId) ?? [])
|
|
694
|
+
const validIntegrationIds = new Set(resources.integrations?.map((i) => i.resourceId) ?? [])
|
|
695
|
+
const validTriggerIds = new Set(resources.triggers?.map((t) => t.resourceId) ?? [])
|
|
696
|
+
|
|
697
|
+
// Collect all internal resource IDs for uniqueness check
|
|
698
|
+
const allInternalIds = new Set([
|
|
699
|
+
...Array.from(validAgentIds),
|
|
700
|
+
...Array.from(validWorkflowIds),
|
|
701
|
+
...Array.from(validTriggerIds),
|
|
702
|
+
...Array.from(validIntegrationIds)
|
|
703
|
+
])
|
|
704
|
+
|
|
705
|
+
// Validate triggers
|
|
706
|
+
validateTriggers(orgName, resources.triggers ?? [], validAgentIds, validWorkflowIds)
|
|
707
|
+
|
|
708
|
+
// Validate resource relationships
|
|
709
|
+
validateResourceRelationships(
|
|
710
|
+
orgName,
|
|
711
|
+
resources.relationships ?? {},
|
|
712
|
+
validAgentIds,
|
|
713
|
+
validWorkflowIds,
|
|
714
|
+
validIntegrationIds,
|
|
715
|
+
validTriggerIds
|
|
716
|
+
)
|
|
717
|
+
|
|
718
|
+
// Validate external resources
|
|
719
|
+
validateExternalResources(
|
|
720
|
+
orgName,
|
|
721
|
+
resources.externalResources ?? [],
|
|
722
|
+
allInternalIds,
|
|
723
|
+
validAgentIds,
|
|
724
|
+
validWorkflowIds,
|
|
725
|
+
validIntegrationIds
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
// Validate human checkpoints
|
|
729
|
+
validateHumanCheckpoints(orgName, resources.humanCheckpoints ?? [], allInternalIds, validAgentIds, validWorkflowIds)
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* Validates trigger invocations
|
|
734
|
+
* NOTE: Trigger relationships are declared in ResourceRelationships, not on TriggerDefinition
|
|
735
|
+
* This validation is now handled by validateResourceRelationships()
|
|
736
|
+
*/
|
|
737
|
+
function validateTriggers(
|
|
738
|
+
_orgName: string,
|
|
739
|
+
_triggers: TriggerDefinition[],
|
|
740
|
+
_validAgentIds: Set<string>,
|
|
741
|
+
_validWorkflowIds: Set<string>
|
|
742
|
+
): void {
|
|
743
|
+
// No validation needed here - triggers declare their relationships in ResourceRelationships
|
|
744
|
+
// This prevents duplication and keeps all relationship declarations in one place
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Validates resource relationship declarations
|
|
749
|
+
*/
|
|
750
|
+
function validateResourceRelationships(
|
|
751
|
+
orgName: string,
|
|
752
|
+
relationships: ResourceRelationships,
|
|
753
|
+
validAgentIds: Set<string>,
|
|
754
|
+
validWorkflowIds: Set<string>,
|
|
755
|
+
validIntegrationIds: Set<string>,
|
|
756
|
+
validTriggerIds: Set<string>
|
|
757
|
+
): void {
|
|
758
|
+
for (const [resourceId, declaration] of Object.entries(relationships)) {
|
|
759
|
+
// Validate declaring resource exists (agents, workflows, or triggers can declare relationships)
|
|
760
|
+
const resourceExists =
|
|
761
|
+
validAgentIds.has(resourceId) || validWorkflowIds.has(resourceId) || validTriggerIds.has(resourceId)
|
|
762
|
+
if (!resourceExists) {
|
|
763
|
+
throw new RegistryValidationError(
|
|
764
|
+
orgName,
|
|
765
|
+
resourceId,
|
|
766
|
+
null,
|
|
767
|
+
`[${orgName}] Relationship declared for non-existent resource: ${resourceId}`
|
|
768
|
+
)
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Validate triggers.agents
|
|
772
|
+
declaration.triggers?.agents?.forEach((agentId) => {
|
|
773
|
+
if (!validAgentIds.has(agentId)) {
|
|
774
|
+
throw new RegistryValidationError(
|
|
775
|
+
orgName,
|
|
776
|
+
resourceId,
|
|
777
|
+
'triggers.agents',
|
|
778
|
+
`[${orgName}] Resource '${resourceId}' triggers non-existent agent: ${agentId}`
|
|
779
|
+
)
|
|
780
|
+
}
|
|
781
|
+
})
|
|
782
|
+
|
|
783
|
+
// Validate triggers.workflows
|
|
784
|
+
declaration.triggers?.workflows?.forEach((workflowId) => {
|
|
785
|
+
if (!validWorkflowIds.has(workflowId)) {
|
|
786
|
+
throw new RegistryValidationError(
|
|
787
|
+
orgName,
|
|
788
|
+
resourceId,
|
|
789
|
+
'triggers.workflows',
|
|
790
|
+
`[${orgName}] Resource '${resourceId}' triggers non-existent workflow: ${workflowId}`
|
|
791
|
+
)
|
|
792
|
+
}
|
|
793
|
+
})
|
|
794
|
+
|
|
795
|
+
// Validate uses.integrations
|
|
796
|
+
declaration.uses?.integrations?.forEach((integrationId) => {
|
|
797
|
+
if (!validIntegrationIds.has(integrationId)) {
|
|
798
|
+
throw new RegistryValidationError(
|
|
799
|
+
orgName,
|
|
800
|
+
resourceId,
|
|
801
|
+
'uses.integrations',
|
|
802
|
+
`[${orgName}] Resource '${resourceId}' uses non-existent integration: ${integrationId}`
|
|
803
|
+
)
|
|
804
|
+
}
|
|
805
|
+
})
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* Validates external resource definitions
|
|
811
|
+
*/
|
|
812
|
+
function validateExternalResources(
|
|
813
|
+
orgName: string,
|
|
814
|
+
externalResources: ExternalResourceDefinition[],
|
|
815
|
+
allInternalIds: Set<string>,
|
|
816
|
+
validAgentIds: Set<string>,
|
|
817
|
+
validWorkflowIds: Set<string>,
|
|
818
|
+
validIntegrationIds: Set<string>
|
|
819
|
+
): void {
|
|
820
|
+
externalResources.forEach((external) => {
|
|
821
|
+
// Validate unique ID (no conflict with internal resources)
|
|
822
|
+
if (allInternalIds.has(external.resourceId)) {
|
|
823
|
+
throw new RegistryValidationError(
|
|
824
|
+
orgName,
|
|
825
|
+
external.resourceId,
|
|
826
|
+
null,
|
|
827
|
+
`[${orgName}] External resource ID '${external.resourceId}' conflicts with internal resource ID`
|
|
828
|
+
)
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// Validate triggers (external -> internal)
|
|
832
|
+
external.triggers?.agents?.forEach((agentId) => {
|
|
833
|
+
if (!validAgentIds.has(agentId)) {
|
|
834
|
+
throw new RegistryValidationError(
|
|
835
|
+
orgName,
|
|
836
|
+
external.resourceId,
|
|
837
|
+
'triggers.agents',
|
|
838
|
+
`[${orgName}] External resource '${external.resourceId}' triggers non-existent agent: ${agentId}`
|
|
839
|
+
)
|
|
840
|
+
}
|
|
841
|
+
})
|
|
842
|
+
|
|
843
|
+
external.triggers?.workflows?.forEach((workflowId) => {
|
|
844
|
+
if (!validWorkflowIds.has(workflowId)) {
|
|
845
|
+
throw new RegistryValidationError(
|
|
846
|
+
orgName,
|
|
847
|
+
external.resourceId,
|
|
848
|
+
'triggers.workflows',
|
|
849
|
+
`[${orgName}] External resource '${external.resourceId}' triggers non-existent workflow: ${workflowId}`
|
|
850
|
+
)
|
|
851
|
+
}
|
|
852
|
+
})
|
|
853
|
+
|
|
854
|
+
// Validate uses integrations
|
|
855
|
+
external.uses?.integrations?.forEach((integrationId) => {
|
|
856
|
+
if (!validIntegrationIds.has(integrationId)) {
|
|
857
|
+
throw new RegistryValidationError(
|
|
858
|
+
orgName,
|
|
859
|
+
external.resourceId,
|
|
860
|
+
'uses.integrations',
|
|
861
|
+
`[${orgName}] External resource '${external.resourceId}' uses non-existent integration: ${integrationId}`
|
|
862
|
+
)
|
|
863
|
+
}
|
|
864
|
+
})
|
|
865
|
+
})
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* Validates human checkpoint definitions
|
|
870
|
+
*/
|
|
871
|
+
function validateHumanCheckpoints(
|
|
872
|
+
orgName: string,
|
|
873
|
+
humanCheckpoints: HumanCheckpointDefinition[],
|
|
874
|
+
allInternalIds: Set<string>,
|
|
875
|
+
validAgentIds: Set<string>,
|
|
876
|
+
validWorkflowIds: Set<string>
|
|
877
|
+
): void {
|
|
878
|
+
humanCheckpoints.forEach((humanCheckpoint) => {
|
|
879
|
+
// Check for ID conflicts with internal resources
|
|
880
|
+
if (allInternalIds.has(humanCheckpoint.resourceId)) {
|
|
881
|
+
throw new RegistryValidationError(
|
|
882
|
+
orgName,
|
|
883
|
+
humanCheckpoint.resourceId,
|
|
884
|
+
null,
|
|
885
|
+
`[${orgName}] Human checkpoint ID '${humanCheckpoint.resourceId}' conflicts with internal resource ID`
|
|
886
|
+
)
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// Validate requestedBy.agents exist
|
|
890
|
+
humanCheckpoint.requestedBy?.agents?.forEach((agentId) => {
|
|
891
|
+
if (!validAgentIds.has(agentId)) {
|
|
892
|
+
throw new RegistryValidationError(
|
|
893
|
+
orgName,
|
|
894
|
+
humanCheckpoint.resourceId,
|
|
895
|
+
'requestedBy.agents',
|
|
896
|
+
`[${orgName}] Human checkpoint '${humanCheckpoint.resourceId}' requestedBy non-existent agent: ${agentId}`
|
|
897
|
+
)
|
|
898
|
+
}
|
|
899
|
+
})
|
|
900
|
+
|
|
901
|
+
// Validate requestedBy.workflows exist
|
|
902
|
+
humanCheckpoint.requestedBy?.workflows?.forEach((workflowId) => {
|
|
903
|
+
if (!validWorkflowIds.has(workflowId)) {
|
|
904
|
+
throw new RegistryValidationError(
|
|
905
|
+
orgName,
|
|
906
|
+
humanCheckpoint.resourceId,
|
|
907
|
+
'requestedBy.workflows',
|
|
908
|
+
`[${orgName}] Human checkpoint '${humanCheckpoint.resourceId}' requestedBy non-existent workflow: ${workflowId}`
|
|
909
|
+
)
|
|
910
|
+
}
|
|
911
|
+
})
|
|
912
|
+
|
|
913
|
+
// Validate routesTo.agents exist
|
|
914
|
+
humanCheckpoint.routesTo?.agents?.forEach((agentId) => {
|
|
915
|
+
if (!validAgentIds.has(agentId)) {
|
|
916
|
+
throw new RegistryValidationError(
|
|
917
|
+
orgName,
|
|
918
|
+
humanCheckpoint.resourceId,
|
|
919
|
+
'routesTo.agents',
|
|
920
|
+
`[${orgName}] Human checkpoint '${humanCheckpoint.resourceId}' routesTo non-existent agent: ${agentId}`
|
|
921
|
+
)
|
|
922
|
+
}
|
|
923
|
+
})
|
|
924
|
+
|
|
925
|
+
// Validate routesTo.workflows exist
|
|
926
|
+
humanCheckpoint.routesTo?.workflows?.forEach((workflowId) => {
|
|
927
|
+
if (!validWorkflowIds.has(workflowId)) {
|
|
928
|
+
throw new RegistryValidationError(
|
|
929
|
+
orgName,
|
|
930
|
+
humanCheckpoint.resourceId,
|
|
931
|
+
'routesTo.workflows',
|
|
932
|
+
`[${orgName}] Human checkpoint '${humanCheckpoint.resourceId}' routesTo non-existent workflow: ${workflowId}`
|
|
933
|
+
)
|
|
934
|
+
}
|
|
935
|
+
})
|
|
936
|
+
})
|
|
937
|
+
}
|