@elevasis/core 0.22.0 → 0.24.0

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.
Files changed (244) hide show
  1. package/dist/index.d.ts +3214 -2501
  2. package/dist/index.js +3112 -1222
  3. package/dist/knowledge/index.d.ts +1108 -1264
  4. package/dist/knowledge/index.js +112 -9
  5. package/dist/organization-model/index.d.ts +3214 -2501
  6. package/dist/organization-model/index.js +3112 -1222
  7. package/dist/test-utils/index.d.ts +985 -1103
  8. package/dist/test-utils/index.js +2464 -1165
  9. package/package.json +5 -5
  10. package/src/README.md +14 -14
  11. package/src/__tests__/publish.test.ts +24 -24
  12. package/src/__tests__/template-core-compatibility.test.ts +9 -80
  13. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +2389 -2121
  14. package/src/_gen/__tests__/scaffold-contracts.test.ts +30 -30
  15. package/src/auth/multi-tenancy/credentials/__tests__/encryption.test.ts +217 -217
  16. package/src/auth/multi-tenancy/credentials/server/encryption.ts +69 -69
  17. package/src/auth/multi-tenancy/credentials/server/kek-loader.ts +37 -37
  18. package/src/auth/multi-tenancy/index.ts +26 -26
  19. package/src/auth/multi-tenancy/invitations/api-schemas.ts +104 -104
  20. package/src/auth/multi-tenancy/memberships/api-schemas.ts +143 -143
  21. package/src/auth/multi-tenancy/memberships/index.ts +26 -26
  22. package/src/auth/multi-tenancy/memberships/membership.ts +130 -130
  23. package/src/auth/multi-tenancy/organizations/__tests__/api-schemas.test.ts +194 -194
  24. package/src/auth/multi-tenancy/organizations/api-schemas.ts +136 -136
  25. package/src/auth/multi-tenancy/permissions.test.ts +42 -42
  26. package/src/auth/multi-tenancy/permissions.ts +123 -123
  27. package/src/auth/multi-tenancy/role-management/api-schemas.ts +78 -78
  28. package/src/auth/multi-tenancy/role-management/index.ts +16 -16
  29. package/src/auth/multi-tenancy/theme-presets.ts +45 -45
  30. package/src/auth/multi-tenancy/types.ts +57 -57
  31. package/src/auth/multi-tenancy/users/api-schemas.ts +165 -165
  32. package/src/business/README.md +2 -2
  33. package/src/business/acquisition/activity-events.test.ts +250 -250
  34. package/src/business/acquisition/activity-events.ts +93 -93
  35. package/src/business/acquisition/api-schemas.test.ts +1883 -1843
  36. package/src/business/acquisition/api-schemas.ts +1493 -1500
  37. package/src/business/acquisition/build-templates.test.ts +240 -240
  38. package/src/business/acquisition/build-templates.ts +83 -41
  39. package/src/business/acquisition/crm-next-action.test.ts +262 -262
  40. package/src/business/acquisition/crm-next-action.ts +220 -220
  41. package/src/business/acquisition/crm-priority.test.ts +216 -216
  42. package/src/business/acquisition/crm-priority.ts +349 -349
  43. package/src/business/acquisition/crm-state-actions.test.ts +153 -151
  44. package/src/business/acquisition/deal-ownership.test.ts +351 -351
  45. package/src/business/acquisition/deal-ownership.ts +120 -120
  46. package/src/business/acquisition/derive-actions.test.ts +129 -104
  47. package/src/business/acquisition/derive-actions.ts +74 -84
  48. package/src/business/acquisition/index.ts +171 -170
  49. package/src/business/acquisition/ontology-validation.ts +309 -0
  50. package/src/business/acquisition/stateful.ts +30 -30
  51. package/src/business/acquisition/types.ts +396 -392
  52. package/src/business/clients/api-schemas.test.ts +115 -115
  53. package/src/business/clients/api-schemas.ts +158 -158
  54. package/src/business/clients/index.ts +1 -1
  55. package/src/business/crm/api-schemas.ts +40 -40
  56. package/src/business/crm/index.ts +1 -1
  57. package/src/business/deals/api-schemas.ts +87 -87
  58. package/src/business/deals/index.ts +1 -1
  59. package/src/business/index.ts +5 -5
  60. package/src/business/projects/types.ts +144 -144
  61. package/src/commands/queue/types/task.ts +15 -15
  62. package/src/execution/core/runner-types.ts +61 -61
  63. package/src/execution/core/sse-executions.ts +7 -7
  64. package/src/execution/engine/__tests__/fixtures/test-agents.ts +10 -10
  65. package/src/execution/engine/agent/core/__tests__/agent.test.ts +16 -16
  66. package/src/execution/engine/agent/core/__tests__/error-passthrough.test.ts +4 -4
  67. package/src/execution/engine/agent/core/types.ts +25 -25
  68. package/src/execution/engine/agent/index.ts +6 -6
  69. package/src/execution/engine/agent/reasoning/__tests__/request-builder.test.ts +24 -24
  70. package/src/execution/engine/index.ts +443 -443
  71. package/src/execution/engine/tools/integration/server/adapters/apify/__tests__/apify-run-actor.integration.test.ts +298 -298
  72. package/src/execution/engine/tools/integration/server/adapters/apify/apify-adapter.test.ts +55 -55
  73. package/src/execution/engine/tools/integration/server/adapters/apify/apify-adapter.ts +107 -107
  74. package/src/execution/engine/tools/integration/server/adapters/apollo/apollo-adapter.test.ts +48 -48
  75. package/src/execution/engine/tools/integration/server/adapters/apollo/apollo-adapter.ts +99 -99
  76. package/src/execution/engine/tools/integration/server/adapters/apollo/index.ts +1 -1
  77. package/src/execution/engine/tools/integration/server/adapters/attio/__tests__/attio-crud.integration.test.ts +363 -363
  78. package/src/execution/engine/tools/integration/server/adapters/attio/fetch/get-record/index.test.ts +162 -162
  79. package/src/execution/engine/tools/integration/server/adapters/attio/fetch/list-records/index.test.ts +316 -316
  80. package/src/execution/engine/tools/integration/server/adapters/clickup/clickup-adapter.test.ts +18 -18
  81. package/src/execution/engine/tools/integration/server/adapters/clickup/clickup-adapter.ts +194 -194
  82. package/src/execution/engine/tools/integration/server/adapters/clickup/index.ts +7 -7
  83. package/src/execution/engine/tools/integration/server/adapters/gmail/gmail-adapter.ts +204 -204
  84. package/src/execution/engine/tools/integration/server/adapters/gmail/gmail-tools.ts +105 -105
  85. package/src/execution/engine/tools/integration/server/adapters/google-calendar/google-calendar-adapter.ts +428 -428
  86. package/src/execution/engine/tools/integration/server/adapters/google-calendar/index.ts +2 -2
  87. package/src/execution/engine/tools/integration/server/adapters/google-sheets/__tests__/google-sheets.integration.test.ts +261 -261
  88. package/src/execution/engine/tools/integration/server/adapters/instantly/instantly-tools.ts +1474 -1474
  89. package/src/execution/engine/tools/integration/server/adapters/millionverifier/millionverifier-tools.ts +103 -103
  90. package/src/execution/engine/tools/integration/server/adapters/resend/fetch/send-email/index.test.ts +88 -88
  91. package/src/execution/engine/tools/integration/server/adapters/resend/fetch/send-email/index.ts +141 -141
  92. package/src/execution/engine/tools/integration/server/adapters/resend/fetch/utils/types.ts +76 -76
  93. package/src/execution/engine/tools/integration/server/adapters/signature-api/signature-api-tools.ts +182 -182
  94. package/src/execution/engine/tools/integration/server/adapters/stripe/stripe-tools.ts +310 -310
  95. package/src/execution/engine/tools/integration/service.test.ts +239 -239
  96. package/src/execution/engine/tools/integration/service.ts +172 -172
  97. package/src/execution/engine/tools/integration/tool.ts +255 -255
  98. package/src/execution/engine/tools/lead-service-types.ts +1005 -1005
  99. package/src/execution/engine/tools/messages.ts +43 -43
  100. package/src/execution/engine/tools/platform/acquisition/company-tools.ts +7 -7
  101. package/src/execution/engine/tools/platform/acquisition/contact-tools.ts +6 -6
  102. package/src/execution/engine/tools/platform/acquisition/list-tools.ts +6 -6
  103. package/src/execution/engine/tools/platform/acquisition/types.ts +280 -280
  104. package/src/execution/engine/tools/platform/email/types.ts +97 -97
  105. package/src/execution/engine/tools/registry.ts +704 -704
  106. package/src/execution/engine/tools/tool-maps.ts +831 -831
  107. package/src/execution/engine/tools/types.ts +234 -234
  108. package/src/execution/engine/workflow/types.ts +202 -202
  109. package/src/execution/external/__tests__/api-schemas.test.ts +127 -127
  110. package/src/execution/external/api-schemas.ts +40 -40
  111. package/src/execution/external/index.ts +1 -1
  112. package/src/index.ts +18 -18
  113. package/src/integrations/credentials/__tests__/api-schemas.test.ts +420 -420
  114. package/src/integrations/credentials/api-schemas.ts +146 -146
  115. package/src/integrations/credentials/schemas.ts +200 -200
  116. package/src/integrations/oauth/__tests__/provider-registry.test.ts +7 -7
  117. package/src/integrations/oauth/provider-registry.ts +74 -74
  118. package/src/integrations/oauth/server/credentials.ts +43 -43
  119. package/src/integrations/webhook-endpoints/__tests__/api-schemas.test.ts +327 -327
  120. package/src/integrations/webhook-endpoints/api-schemas.ts +103 -103
  121. package/src/integrations/webhook-endpoints/types.ts +58 -58
  122. package/src/knowledge/README.md +33 -32
  123. package/src/knowledge/__tests__/queries.test.ts +633 -541
  124. package/src/knowledge/format.ts +100 -99
  125. package/src/knowledge/index.ts +5 -5
  126. package/src/knowledge/published.ts +5 -5
  127. package/src/knowledge/queries.ts +274 -222
  128. package/src/operations/activities/api-schemas.ts +80 -80
  129. package/src/operations/activities/types.ts +64 -64
  130. package/src/organization-model/README.md +149 -109
  131. package/src/organization-model/__tests__/content-kinds-registry.test.ts +210 -0
  132. package/src/organization-model/__tests__/defaults.test.ts +168 -194
  133. package/src/organization-model/__tests__/domains/actions.test.ts +78 -0
  134. package/src/organization-model/__tests__/domains/customers.test.ts +48 -44
  135. package/src/organization-model/__tests__/domains/entities.test.ts +56 -0
  136. package/src/organization-model/__tests__/domains/goals.test.ts +110 -96
  137. package/src/organization-model/__tests__/domains/identity.test.ts +4 -3
  138. package/src/organization-model/__tests__/domains/navigation.test.ts +222 -166
  139. package/src/organization-model/__tests__/domains/offerings.test.ts +83 -88
  140. package/src/organization-model/__tests__/domains/policies.test.ts +323 -0
  141. package/src/organization-model/__tests__/domains/resource-mappings.test.ts +30 -30
  142. package/src/organization-model/__tests__/domains/resources.test.ts +396 -175
  143. package/src/organization-model/__tests__/domains/roles.test.ts +463 -402
  144. package/src/organization-model/__tests__/domains/statuses.test.ts +13 -10
  145. package/src/organization-model/__tests__/domains/systems.test.ts +209 -193
  146. package/src/organization-model/__tests__/flatten-additive-merge.test.ts +362 -0
  147. package/src/organization-model/__tests__/foundation.test.ts +47 -75
  148. package/src/organization-model/__tests__/get-resources-for-system.test.ts +144 -0
  149. package/src/organization-model/__tests__/graph.test.ts +1336 -149
  150. package/src/organization-model/__tests__/icons.test.ts +10 -1
  151. package/src/organization-model/__tests__/knowledge.test.ts +418 -61
  152. package/src/organization-model/__tests__/lookup-helpers.test.ts +438 -0
  153. package/src/organization-model/__tests__/migration-helpers.test.ts +591 -0
  154. package/src/organization-model/__tests__/prospecting-ssot.test.ts +103 -94
  155. package/src/organization-model/__tests__/recursive-system-schema.test.ts +549 -0
  156. package/src/organization-model/__tests__/resolve.test.ts +303 -42
  157. package/src/organization-model/__tests__/schema.test.ts +863 -153
  158. package/src/organization-model/__tests__/surface-projection.test.ts +284 -174
  159. package/src/organization-model/catalogs/lead-gen.ts +144 -0
  160. package/src/organization-model/content-kinds/config.ts +36 -0
  161. package/src/organization-model/content-kinds/index.ts +78 -0
  162. package/src/organization-model/content-kinds/pipeline.ts +68 -0
  163. package/src/organization-model/content-kinds/registry.ts +44 -0
  164. package/src/organization-model/content-kinds/status.ts +71 -0
  165. package/src/organization-model/content-kinds/template.ts +83 -0
  166. package/src/organization-model/content-kinds/types.ts +117 -0
  167. package/src/organization-model/contracts.ts +27 -17
  168. package/src/organization-model/defaults.ts +489 -107
  169. package/src/organization-model/domains/actions.ts +333 -0
  170. package/src/organization-model/domains/customers.ts +10 -7
  171. package/src/organization-model/domains/entities.ts +144 -0
  172. package/src/organization-model/domains/goals.ts +9 -6
  173. package/src/organization-model/domains/knowledge.ts +128 -54
  174. package/src/organization-model/domains/navigation.ts +139 -416
  175. package/src/organization-model/domains/offerings.ts +15 -10
  176. package/src/organization-model/domains/policies.ts +102 -0
  177. package/src/organization-model/domains/projects.ts +6 -40
  178. package/src/organization-model/domains/prospecting.ts +395 -514
  179. package/src/organization-model/domains/resources.ts +173 -81
  180. package/src/organization-model/domains/roles.ts +96 -93
  181. package/src/organization-model/domains/sales.test.ts +218 -218
  182. package/src/organization-model/domains/sales.ts +380 -589
  183. package/src/organization-model/domains/shared.ts +8 -8
  184. package/src/organization-model/domains/statuses.ts +298 -89
  185. package/src/organization-model/domains/systems.ts +240 -38
  186. package/src/organization-model/foundation.ts +35 -48
  187. package/src/organization-model/graph/build.ts +1035 -279
  188. package/src/organization-model/graph/index.ts +4 -4
  189. package/src/organization-model/graph/link.ts +10 -10
  190. package/src/organization-model/graph/schema.ts +77 -56
  191. package/src/organization-model/graph/types.ts +75 -56
  192. package/src/organization-model/helpers.ts +312 -59
  193. package/src/organization-model/icons.ts +78 -66
  194. package/src/organization-model/index.ts +129 -16
  195. package/src/organization-model/migration-helpers.ts +252 -0
  196. package/src/organization-model/ontology.ts +661 -0
  197. package/src/organization-model/organization-graph.mdx +110 -89
  198. package/src/organization-model/organization-model.mdx +226 -171
  199. package/src/organization-model/published.ts +295 -139
  200. package/src/organization-model/resolve.ts +139 -21
  201. package/src/organization-model/schema.ts +841 -301
  202. package/src/organization-model/surface-projection.ts +212 -218
  203. package/src/organization-model/types.ts +181 -90
  204. package/src/platform/api/types.ts +38 -38
  205. package/src/platform/constants/versions.ts +3 -3
  206. package/src/platform/index.ts +23 -23
  207. package/src/platform/registry/__tests__/command-view.test.ts +5 -7
  208. package/src/platform/registry/__tests__/resource-link.test.ts +35 -30
  209. package/src/platform/registry/__tests__/resource-registry.integration.test.ts +17 -32
  210. package/src/platform/registry/__tests__/resource-registry.nested-systems.test.ts +245 -0
  211. package/src/platform/registry/__tests__/resource-registry.test.ts +2053 -2051
  212. package/src/platform/registry/__tests__/validation.test.ts +1347 -1343
  213. package/src/platform/registry/command-view.ts +10 -10
  214. package/src/platform/registry/index.ts +103 -103
  215. package/src/platform/registry/resource-link.ts +32 -32
  216. package/src/platform/registry/resource-registry.ts +890 -878
  217. package/src/platform/registry/serialization.ts +295 -295
  218. package/src/platform/registry/serialized-types.ts +166 -166
  219. package/src/platform/registry/stats-types.ts +68 -68
  220. package/src/platform/registry/types.ts +425 -425
  221. package/src/platform/registry/validation.ts +745 -743
  222. package/src/platform/utils/__tests__/validation.test.ts +1084 -1084
  223. package/src/platform/utils/validation.ts +425 -425
  224. package/src/projects/api-schemas.test.ts +39 -39
  225. package/src/projects/api-schemas.ts +291 -291
  226. package/src/reference/_generated/contracts.md +2389 -2121
  227. package/src/reference/glossary.md +76 -76
  228. package/src/scaffold-registry/__tests__/index.test.ts +206 -206
  229. package/src/scaffold-registry/__tests__/schema.test.ts +166 -166
  230. package/src/scaffold-registry/index.ts +392 -392
  231. package/src/scaffold-registry/schema.ts +243 -243
  232. package/src/server.ts +289 -289
  233. package/src/supabase/database.types.ts +3153 -3093
  234. package/src/test-utils/README.md +37 -37
  235. package/src/test-utils/entities.ts +108 -108
  236. package/src/test-utils/fixtures/memberships.ts +82 -82
  237. package/src/test-utils/index.ts +12 -12
  238. package/src/test-utils/organization-model.ts +65 -65
  239. package/src/test-utils/published.ts +6 -6
  240. package/src/test-utils/rls/RLSTestContext.ts +588 -588
  241. package/src/test-utils/test-utils.test.ts +44 -49
  242. package/src/organization-model/__tests__/domains/operations.test.ts +0 -203
  243. package/src/organization-model/domains/features.ts +0 -31
  244. package/src/organization-model/domains/operations.ts +0 -85
@@ -1,878 +1,890 @@
1
- /**
2
- * ResourceRegistry - Resource discovery and lookup
3
- * Handles resource definitions from OrganizationRegistry
4
- *
5
- * Features:
6
- * - Resource discovery by organization
7
- * - Startup validation (duplicate IDs, model configs, relationships, interface-schema alignment)
8
- * - Pre-serialization cache for instant API responses
9
- * - Command View data generation
10
- */
11
-
12
- import type { WorkflowDefinition } from '../../execution/engine/workflow/types'
13
- import type { AgentDefinition } from '../../execution/engine/agent/core/types'
14
- import type { OrganizationModelResources, OrganizationModelSystems } from '../../organization-model/types'
15
- import type { ResourceEntry } from '../../organization-model/domains/resources'
16
- import type { SystemEntry } from '../../organization-model/domains/systems'
17
- import type {
18
- ResourceStatus,
19
- ResourceDefinition,
20
- ResourceList,
21
- TriggerDefinition,
22
- IntegrationDefinition,
23
- ResourceRelationships,
24
- ExternalResourceDefinition,
25
- HumanCheckpointDefinition
26
- } from './types'
27
- import type {
28
- SerializedOrganizationData,
29
- SerializedAgentDefinition,
30
- SerializedWorkflowDefinition,
31
- CommandViewData
32
- } from './serialized-types'
33
- import { validateDeploymentSpec, validateRelationships } from './validation'
34
- import { serializeAllOrganizations, serializeOrganization } from './serialization'
35
- import { isReservedResourceId } from './reserved'
36
-
37
- /** Filter out archived resources from an organization's resource collection */
38
- function filterArchived(org: DeploymentSpec): DeploymentSpec {
39
- return {
40
- ...org,
41
- workflows: org.workflows?.filter((w) => !w.config.archived),
42
- agents: org.agents?.filter((a) => !a.config.archived),
43
- triggers: org.triggers?.filter((t) => !t.archived),
44
- integrations: org.integrations?.filter((i) => !i.archived),
45
- externalResources: org.externalResources?.filter((e) => !e.archived),
46
- humanCheckpoints: org.humanCheckpoints?.filter((h) => !h.archived)
47
- }
48
- }
49
-
50
- function summarizeSystem(system: SystemEntry | undefined): ResourceDefinition['system'] {
51
- if (!system) return undefined
52
-
53
- return {
54
- id: system.id,
55
- title: system.title,
56
- description: system.description,
57
- kind: system.kind,
58
- status: system.status
59
- }
60
- }
61
-
62
- /**
63
- * Configuration for a remotely-deployed organization
64
- *
65
- * Stored alongside runtime-registered organizations to support
66
- * worker thread execution branching and credential management.
67
- */
68
- export interface RemoteOrgConfig {
69
- /** Supabase Storage path: "{orgId}/{deploymentId}/bundle.js" */
70
- storagePath: string
71
- /** Deployment record ID */
72
- deploymentId: string
73
- /** OS temp path to bundle -- set after first download, used by worker threads */
74
- cachedTempPath?: string
75
- /** Platform tool name -> credential name mapping */
76
- toolCredentials?: Record<string, string>
77
- /** SDK version used to deploy this bundle */
78
- sdkVersion?: string
79
- /** Deployment version (semver) of the deployed bundle */
80
- deploymentVersion?: string
81
- }
82
-
83
- /**
84
- * Configuration for a first-class System resource.
85
- *
86
- * System resources are owned by the platform, registered under the 'system' org,
87
- * and execute via the static-bundle loader mode in executeInWorker(). The moduleId
88
- * maps to an entry in the API's STATIC_MODULE_MAP.
89
- */
90
- export interface SystemConfig {
91
- kind: 'static'
92
- moduleId: string
93
- /** Always undefined for system resources; present for API compatibility with RemoteOrgConfig consumers */
94
- sdkVersion?: never
95
- }
96
-
97
- /**
98
- * Organization-specific resource collection
99
- *
100
- * Complete manifest of all automation resources for an organization.
101
- * Used by ResourceRegistry for discovery and Command View for visualization.
102
- */
103
- export interface DeploymentSpec {
104
- /** Deployment version (semver) */
105
- version: string
106
- /** Optional Organization Model governance catalog used for OM-code validation */
107
- organizationModel?: {
108
- systems?: OrganizationModelSystems
109
- resources?: OrganizationModelResources
110
- }
111
- /** Workflow definitions */
112
- workflows?: WorkflowDefinition[]
113
- /** Agent definitions */
114
- agents?: AgentDefinition[]
115
-
116
- // Resource Manifest fields (optional for backwards compatibility)
117
- /** Trigger definitions - entry points that initiate executions */
118
- triggers?: TriggerDefinition[]
119
- /** Integration definitions - external service connections */
120
- integrations?: IntegrationDefinition[]
121
- /** Explicit relationship declarations between resources */
122
- relationships?: ResourceRelationships
123
- /** External automation resources (n8n, Make, Zapier, etc.) */
124
- externalResources?: ExternalResourceDefinition[]
125
- /** Human checkpoint definitions - human decision points in automation */
126
- humanCheckpoints?: HumanCheckpointDefinition[]
127
- }
128
-
129
- /**
130
- * Organization Registry type
131
- */
132
- export type OrganizationRegistry = Record<string, DeploymentSpec>
133
-
134
- export class ResourceRegistry {
135
- /**
136
- * Pre-serialized organization data cache
137
- * Computed once at construction for static orgs, updated incrementally for runtime orgs
138
- */
139
- private serializedCache: Map<string, SerializedOrganizationData>
140
-
141
- /**
142
- * Per-resource remote configuration (external deployments)
143
- * Key: "orgName/resourceId", Value: RemoteOrgConfig for that resource.
144
- * Tracks which individual resources were added at runtime via deploy pipeline.
145
- * Static and remote resources coexist in the same org.
146
- */
147
- private remoteResources = new Map<string, RemoteOrgConfig>()
148
-
149
- /**
150
- * System configs for first-class platform resources.
151
- * Key: "orgName/resourceId", Value: SystemConfig.
152
- * Registered at startup alongside registerStaticResources().
153
- */
154
- private systemConfigs = new Map<string, SystemConfig>()
155
-
156
- constructor(private registry: OrganizationRegistry) {
157
- this.validateRegistry()
158
- this.validateRelationships()
159
- this.serializedCache = serializeAllOrganizations(registry)
160
- }
161
-
162
- /**
163
- * Validates registry on construction
164
- * - Checks for duplicate resourceIds within organizations
165
- * - Validates model configurations against constraints
166
- * - Validates ExecutionInterface matches inputSchema
167
- * @throws Error if validation fails
168
- */
169
- private validateRegistry(): void {
170
- for (const [orgName, resources] of Object.entries(this.registry)) {
171
- validateDeploymentSpec(orgName, resources)
172
- }
173
- }
174
-
175
- /**
176
- * Validates relationship declarations reference valid resources
177
- * Runs at API server startup - fails fast in development
178
- * @throws Error if validation fails
179
- */
180
- private validateRelationships(): void {
181
- for (const [orgName, resources] of Object.entries(this.registry)) {
182
- validateRelationships(orgName, resources)
183
- }
184
- }
185
-
186
- /**
187
- * Get the remote resource IDs currently registered for an organization.
188
- * Used to validate redeployments against the post-swap state before any
189
- * live registry mutation occurs.
190
- */
191
- private getRemoteResourceIds(orgName: string): Set<string> {
192
- const prefix = `${orgName}/`
193
- const remoteIds = new Set<string>()
194
- for (const key of this.remoteResources.keys()) {
195
- if (key.startsWith(prefix)) {
196
- remoteIds.add(key.slice(prefix.length))
197
- }
198
- }
199
- return remoteIds
200
- }
201
-
202
- /**
203
- * Build the "static + surviving" baseline for registration validation.
204
- * On redeploy, this strips the currently remote-owned resources and
205
- * deployment-owned metadata so validation reflects the state after swap.
206
- */
207
- private buildRegistrationBase(orgName: string): DeploymentSpec | undefined {
208
- const existingOrg = this.registry[orgName]
209
- if (!existingOrg) return undefined
210
-
211
- const remoteIds = this.getRemoteResourceIds(orgName)
212
- if (remoteIds.size === 0) return existingOrg
213
-
214
- const relationships = existingOrg.relationships
215
- ? Object.fromEntries(
216
- Object.entries(existingOrg.relationships).filter(([resourceId]) => !remoteIds.has(resourceId))
217
- )
218
- : undefined
219
-
220
- return {
221
- ...existingOrg,
222
- version: existingOrg.version ?? '0.0.0',
223
- workflows: (existingOrg.workflows ?? []).filter((w) => !remoteIds.has(w.config.resourceId)),
224
- agents: (existingOrg.agents ?? []).filter((a) => !remoteIds.has(a.config.resourceId)),
225
- triggers: undefined,
226
- integrations: undefined,
227
- humanCheckpoints: undefined,
228
- externalResources: undefined,
229
- relationships: relationships && Object.keys(relationships).length > 0 ? relationships : undefined
230
- }
231
- }
232
-
233
- /**
234
- * Validate the registry state that would exist after registration succeeds.
235
- * This runs before any live mutation so invalid redeploys preserve the
236
- * currently active remote resources.
237
- */
238
- private validateRegistrationCandidate(orgName: string, incoming: DeploymentSpec): void {
239
- const base = this.buildRegistrationBase(orgName)
240
-
241
- const candidate: DeploymentSpec = base
242
- ? {
243
- ...base,
244
- version: incoming.version ?? base.version ?? '0.0.0',
245
- workflows: [...(base.workflows ?? []), ...(incoming.workflows ?? [])],
246
- agents: [...(base.agents ?? []), ...(incoming.agents ?? [])],
247
- triggers: incoming.triggers,
248
- integrations: incoming.integrations,
249
- humanCheckpoints: incoming.humanCheckpoints,
250
- externalResources: incoming.externalResources,
251
- relationships: incoming.relationships
252
- ? {
253
- ...(base.relationships ?? {}),
254
- ...incoming.relationships
255
- }
256
- : base.relationships
257
- }
258
- : {
259
- ...incoming,
260
- version: incoming.version ?? '0.0.0'
261
- }
262
-
263
- validateDeploymentSpec(orgName, candidate)
264
- validateRelationships(orgName, candidate)
265
- }
266
-
267
- /**
268
- * Get a resource definition by ID
269
- * Returns full definition (WorkflowDefinition or AgentDefinition)
270
- * Check definition.config.type to determine if it's a workflow or agent
271
- */
272
- getResourceDefinition(organizationName: string, resourceId: string): WorkflowDefinition | AgentDefinition | null {
273
- const orgResources = this.registry[organizationName]
274
- if (!orgResources) return null
275
-
276
- // Check workflows first
277
- const workflow = orgResources.workflows?.find((w) => w.config.resourceId === resourceId)
278
- if (workflow) return workflow
279
-
280
- // Check agents
281
- const agent = orgResources.agents?.find((a) => a.config.resourceId === resourceId)
282
- if (agent) return agent
283
-
284
- return null
285
- }
286
-
287
- /**
288
- * List all resources for an organization
289
- * Returns ResourceDefinition metadata (not full definitions)
290
- *
291
- * All resources are returned regardless of server environment.
292
- * Pass an explicit `environment` filter to get only 'dev' or 'prod' resources.
293
- */
294
- listResourcesForOrganization(organizationName: string, environment?: ResourceStatus): ResourceList {
295
- const orgResources = this.registry[organizationName]
296
- if (!orgResources) {
297
- return {
298
- workflows: [],
299
- agents: [],
300
- total: 0,
301
- organizationName,
302
- environment
303
- }
304
- }
305
-
306
- const resourcesById = new Map((orgResources.organizationModel?.resources?.entries ?? []).map((r) => [r.id, r]))
307
- const systemsById = new Map((orgResources.organizationModel?.systems?.systems ?? []).map((s) => [s.id, s]))
308
- const getGovernanceMetadata = (
309
- resourceId: string,
310
- descriptor: ResourceEntry | undefined
311
- ): Pick<ResourceDefinition, 'systemId' | 'system' | 'governanceStatus'> => {
312
- const resource = descriptor ?? resourcesById.get(resourceId)
313
- if (!resource) return {}
314
-
315
- return {
316
- systemId: resource.systemId,
317
- system: summarizeSystem(systemsById.get(resource.systemId)),
318
- governanceStatus: resource.status
319
- }
320
- }
321
-
322
- // Map workflows to ResourceDefinition metadata and filter by environment
323
- const workflows: ResourceDefinition[] = (orgResources.workflows || [])
324
- .map((def) => ({
325
- resourceId: def.config.resourceId,
326
- name: def.config.name,
327
- description: def.config.description,
328
- version: def.config.version,
329
- type: def.config.type,
330
- status: def.config.status,
331
- links: def.config.links,
332
- category: def.config.category,
333
- origin: this.remoteResources.has(`${organizationName}/${def.config.resourceId}`)
334
- ? ('remote' as const)
335
- : ('local' as const),
336
- ...getGovernanceMetadata(def.config.resourceId, def.config.resource)
337
- }))
338
- .filter((resource) => !environment || resource.status === environment)
339
-
340
- // Map agents to ResourceDefinition metadata and filter by environment
341
- const agents: ResourceDefinition[] = (orgResources.agents || [])
342
- .map((def) => ({
343
- resourceId: def.config.resourceId,
344
- name: def.config.name,
345
- description: def.config.description,
346
- version: def.config.version,
347
- type: def.config.type,
348
- status: def.config.status,
349
- links: def.config.links,
350
- category: def.config.category,
351
- sessionCapable: def.config.sessionCapable ?? false,
352
- origin: this.remoteResources.has(`${organizationName}/${def.config.resourceId}`)
353
- ? ('remote' as const)
354
- : ('local' as const),
355
- ...getGovernanceMetadata(def.config.resourceId, def.config.resource)
356
- }))
357
- .filter((resource) => !environment || resource.status === environment)
358
-
359
- return {
360
- workflows,
361
- agents,
362
- total: workflows.length + agents.length,
363
- organizationName,
364
- environment
365
- }
366
- }
367
-
368
- /**
369
- * List all resources from all organizations
370
- * NOTE: For debugging only - returns raw registry data
371
- */
372
- listAllResources(): OrganizationRegistry {
373
- return this.registry
374
- }
375
-
376
- // ============================================================================
377
- // Runtime Organization Registration (External Deployments)
378
- // ============================================================================
379
-
380
- /**
381
- * Register external resources at runtime
382
- *
383
- * Called during deploy pipeline when an external developer deploys their bundle.
384
- * Merges the incoming stub definitions into the org's registry and stores
385
- * per-resource remote config for worker thread execution branching.
386
- *
387
- * Static and remote resources coexist in the same org. If the org already
388
- * has static resources, the incoming remote resources are merged alongside them.
389
- * If redeploying (some resources already registered as remote for this org),
390
- * the previous remote resources are unregistered first.
391
- *
392
- * @param orgName - Organization name (used as registry key)
393
- * @param org - Stub resource definitions (workflows/agents with placeholder handlers)
394
- * @param remote - Remote configuration (bundle path, deployment ID, env vars)
395
- * @throws Error if incoming resourceId conflicts with a static resource
396
- * @throws Error if incoming deployment contains duplicate resourceIds
397
- */
398
- registerOrganization(orgName: string, org: DeploymentSpec, remote: RemoteOrgConfig): void {
399
- // Filter out archived resources before any processing
400
- org = filterArchived(org)
401
-
402
- // Collect all incoming resource IDs for conflict checking
403
- const incomingWorkflowIds = (org.workflows ?? []).map((w) => w.config.resourceId)
404
- const incomingAgentIds = (org.agents ?? []).map((a) => a.config.resourceId)
405
- const incomingIds = [...incomingWorkflowIds, ...incomingAgentIds]
406
-
407
- // Check for intra-deployment duplicates
408
- const seen = new Set<string>()
409
- for (const id of incomingIds) {
410
- if (seen.has(id)) {
411
- throw new Error(`Duplicate resource ID '${id}' in deployment. Each resource must have a unique ID.`)
412
- }
413
- seen.add(id)
414
- }
415
-
416
- // Check for reserved resource IDs (cannot be claimed by external deployments)
417
- for (const id of incomingIds) {
418
- if (isReservedResourceId(id)) {
419
- throw new Error(
420
- `Resource ID '${id}' is reserved for platform use. External deployments cannot use reserved resource IDs.`
421
- )
422
- }
423
- }
424
-
425
- // Check for conflicts against the static/surviving org state.
426
- // On redeploy, current remote resources are excluded so a deployment can
427
- // legitimately replace its own resource IDs without colliding with itself.
428
- const validationBase = this.buildRegistrationBase(orgName)
429
- if (validationBase) {
430
- const staticWorkflowIds = new Set((validationBase.workflows ?? []).map((w) => w.config.resourceId))
431
- const staticAgentIds = new Set((validationBase.agents ?? []).map((a) => a.config.resourceId))
432
-
433
- for (const id of incomingIds) {
434
- if (staticWorkflowIds.has(id) || staticAgentIds.has(id)) {
435
- throw new Error(
436
- `Resource '${id}' already exists in '${orgName}' as an internal resource. External deployments cannot override internal resources.`
437
- )
438
- }
439
- }
440
- }
441
-
442
- // Validate the merged deployment shape before mutating the live registry.
443
- // This keeps the current deployment intact if a redeploy introduces
444
- // invalid relationships or other registry-level validation failures.
445
- this.validateRegistrationCandidate(orgName, org)
446
-
447
- // If redeploying (some resources already registered as remote for this org), clean up only
448
- // after validation succeeds so a failed redeploy preserves the current remote resources.
449
- if (this.isRemote(orgName)) {
450
- this.unregisterOrganization(orgName)
451
- }
452
-
453
- // Merge incoming resources into the org (or create new org entry)
454
- const existingOrg = this.registry[orgName]
455
- if (existingOrg) {
456
- existingOrg.workflows = [...(existingOrg.workflows ?? []), ...(org.workflows ?? [])]
457
- existingOrg.agents = [...(existingOrg.agents ?? []), ...(org.agents ?? [])]
458
- // Deployment-owned metadata: replace entirely (unregister cleared these)
459
- existingOrg.triggers = org.triggers
460
- existingOrg.integrations = org.integrations
461
- existingOrg.humanCheckpoints = org.humanCheckpoints
462
- existingOrg.externalResources = org.externalResources
463
- if (org.relationships) {
464
- existingOrg.relationships = {
465
- ...(existingOrg.relationships ?? {}),
466
- ...org.relationships
467
- }
468
- }
469
- } else {
470
- this.registry[orgName] = org
471
- }
472
-
473
- // Populate per-resource remote config entries
474
- for (const id of incomingIds) {
475
- this.remoteResources.set(`${orgName}/${id}`, remote)
476
- }
477
-
478
- // Rebuild serialized cache for the full merged org
479
- this.serializedCache.set(orgName, serializeOrganization(this.registry[orgName]))
480
- }
481
-
482
- /**
483
- * Patch serialized cache with pre-serialized schemas from an external manifest.
484
- *
485
- * External deployments use stub definitions with z.any() schemas (never called).
486
- * The manifest carries the real schemas as pre-serialized JSON Schema from the worker.
487
- * This method patches those into the serialized cache so describe/CLI display them.
488
- *
489
- * @param orgName - Organization name
490
- * @param manifestSchemas - Map of resourceId -> { contract, steps } with JSON Schema
491
- */
492
- patchManifestSchemas(
493
- orgName: string,
494
- manifestSchemas: Array<{
495
- resourceId: string
496
- type: 'workflow' | 'agent'
497
- contract?: { inputSchema?: object; outputSchema?: object }
498
- steps?: Array<{ id: string; inputSchema?: object; outputSchema?: object }>
499
- }>
500
- ): void {
501
- const cache = this.serializedCache.get(orgName)
502
- if (!cache) return
503
-
504
- for (const entry of manifestSchemas) {
505
- if (entry.type === 'workflow') {
506
- const def = cache.definitions.workflows.get(entry.resourceId)
507
- if (!def) continue
508
-
509
- // Patch contract schemas
510
- if (entry.contract?.inputSchema) def.contract.inputSchema = entry.contract.inputSchema
511
- if (entry.contract?.outputSchema) def.contract.outputSchema = entry.contract.outputSchema
512
-
513
- // Patch step schemas
514
- if (entry.steps && def.steps) {
515
- for (const stepPatch of entry.steps) {
516
- const step = def.steps.find((s) => s.id === stepPatch.id)
517
- if (!step) continue
518
- if (stepPatch.inputSchema) step.inputSchema = stepPatch.inputSchema
519
- if (stepPatch.outputSchema) step.outputSchema = stepPatch.outputSchema
520
- }
521
- }
522
- } else if (entry.type === 'agent') {
523
- const def = cache.definitions.agents.get(entry.resourceId)
524
- if (!def) continue
525
- if (entry.contract?.inputSchema) def.contract.inputSchema = entry.contract.inputSchema
526
- if (entry.contract?.outputSchema) def.contract.outputSchema = entry.contract.outputSchema
527
- }
528
- }
529
- }
530
-
531
- /**
532
- * Register built-in platform resources (static, local execution)
533
- *
534
- * Unlike registerOrganization(), these resources:
535
- * - Do NOT have remote config (execute in-process, not in worker threads)
536
- * - Are NOT removed by unregisterOrganization() (persist across redeployments)
537
- * - Use reserved resource IDs that external deployments cannot claim
538
- *
539
- * @param orgName - Organization name
540
- * @param org - Resource definitions with real handlers (not stubs)
541
- */
542
- registerStaticResources(orgName: string, org: DeploymentSpec): void {
543
- // Filter out archived resources before any processing
544
- org = filterArchived(org)
545
-
546
- const incomingWorkflowIds = (org.workflows ?? []).map((w) => w.config.resourceId)
547
- const incomingAgentIds = (org.agents ?? []).map((a) => a.config.resourceId)
548
- const incomingIds = [...incomingWorkflowIds, ...incomingAgentIds]
549
-
550
- // Check for duplicates within incoming resources
551
- const seen = new Set<string>()
552
- for (const id of incomingIds) {
553
- if (seen.has(id)) {
554
- throw new Error(`Duplicate resource ID '${id}' in static resources.`)
555
- }
556
- seen.add(id)
557
- }
558
-
559
- // Check for conflicts with existing resources in this org
560
- const existingOrg = this.registry[orgName]
561
- if (existingOrg) {
562
- const existingWorkflowIds = new Set((existingOrg.workflows ?? []).map((w) => w.config.resourceId))
563
- const existingAgentIds = new Set((existingOrg.agents ?? []).map((a) => a.config.resourceId))
564
-
565
- for (const id of incomingIds) {
566
- if (existingWorkflowIds.has(id) || existingAgentIds.has(id)) {
567
- throw new Error(`Static resource '${id}' conflicts with existing resource in '${orgName}'.`)
568
- }
569
- }
570
- }
571
-
572
- // Merge into registry (no remote config = local execution path)
573
- if (existingOrg) {
574
- existingOrg.workflows = [...(existingOrg.workflows ?? []), ...(org.workflows ?? [])]
575
- existingOrg.agents = [...(existingOrg.agents ?? []), ...(org.agents ?? [])]
576
- } else {
577
- this.registry[orgName] = org
578
- }
579
-
580
- // Rebuild serialized cache
581
- this.serializedCache.set(orgName, serializeOrganization(this.registry[orgName]))
582
- }
583
-
584
- /**
585
- * Unregister runtime-registered resources for an organization
586
- *
587
- * Removes only resources that were registered at runtime (via registerOrganization).
588
- * Static resources loaded at startup are preserved. If the org still has static
589
- * resources after removal, the serialization cache is rebuilt. If no resources
590
- * remain, the org is fully removed from the registry.
591
- * No-op if the org has no remote resources.
592
- *
593
- * @param orgName - Organization name to unregister remote resources from
594
- */
595
- unregisterOrganization(orgName: string): void {
596
- // Find all remote resource keys for this org
597
- const prefix = `${orgName}/`
598
- const remoteIds = new Set<string>()
599
- for (const key of this.remoteResources.keys()) {
600
- if (key.startsWith(prefix)) {
601
- remoteIds.add(key.slice(prefix.length))
602
- this.remoteResources.delete(key)
603
- }
604
- }
605
-
606
- // No-op if org had no remote resources
607
- if (remoteIds.size === 0) return
608
-
609
- const orgResources = this.registry[orgName]
610
- if (!orgResources) return
611
-
612
- // Remove remote resources from the org's arrays
613
- orgResources.workflows = (orgResources.workflows ?? []).filter((w) => !remoteIds.has(w.config.resourceId))
614
- orgResources.agents = (orgResources.agents ?? []).filter((a) => !remoteIds.has(a.config.resourceId))
615
-
616
- // Remove deployment-owned metadata (triggers, integrations, checkpoints, external resources)
617
- // These are always fully owned by the deployment — replaced on each deploy
618
- orgResources.triggers = undefined
619
- orgResources.integrations = undefined
620
- orgResources.humanCheckpoints = undefined
621
- orgResources.externalResources = undefined
622
-
623
- // Remove relationship entries for remote resource IDs
624
- if (orgResources.relationships) {
625
- for (const id of remoteIds) {
626
- delete orgResources.relationships[id]
627
- }
628
- if (Object.keys(orgResources.relationships).length === 0) {
629
- delete orgResources.relationships
630
- }
631
- }
632
-
633
- // If the org still has static resources, rebuild cache; otherwise fully remove
634
- // (triggers, integrations, checkpoints, externalResources were cleared above)
635
- const remaining = (orgResources.workflows?.length ?? 0) + (orgResources.agents?.length ?? 0)
636
-
637
- if (remaining > 0) {
638
- this.serializedCache.set(orgName, serializeOrganization(orgResources))
639
- } else {
640
- delete this.registry[orgName]
641
- this.serializedCache.delete(orgName)
642
- }
643
- }
644
-
645
- /**
646
- * Get remote configuration for a specific resource.
647
- *
648
- * Returns RemoteOrgConfig for externally-deployed resources, SystemConfig for
649
- * first-class platform resources, or null for static in-process resources.
650
- * Used by the execution coordinator to determine the execution path.
651
- *
652
- * @param orgName - Organization name
653
- * @param resourceId - Resource ID
654
- * @returns Remote or System config, or null
655
- */
656
- getRemoteConfig(orgName: string, resourceId: string): RemoteOrgConfig | SystemConfig | null {
657
- const key = `${orgName}/${resourceId}`
658
- return this.remoteResources.get(key) ?? this.systemConfigs.get(key) ?? null
659
- }
660
-
661
- /**
662
- * Register a System config for a first-class platform resource.
663
- *
664
- * Called at startup alongside registerStaticResources() so that
665
- * getRemoteConfig('system', resourceId) returns truthy and the execution
666
- * coordinator routes the resource through the worker-thread path.
667
- *
668
- * @param orgName - Organization name (typically 'system')
669
- * @param resourceId - Resource ID
670
- * @param config - SystemConfig with kind:'static' and moduleId
671
- */
672
- registerSystemConfig(orgName: string, resourceId: string, config: SystemConfig): void {
673
- this.systemConfigs.set(`${orgName}/${resourceId}`, config)
674
- }
675
-
676
- /**
677
- * Check if an organization has any remote (externally deployed) resources
678
- *
679
- * @param orgName - Organization name
680
- * @returns true if the org has at least one runtime-registered resource
681
- */
682
- isRemote(orgName: string): boolean {
683
- const prefix = `${orgName}/`
684
- for (const key of this.remoteResources.keys()) {
685
- if (key.startsWith(prefix)) return true
686
- }
687
- return false
688
- }
689
-
690
- /**
691
- * Get the remote config for any resource in an organization.
692
- * Used when the specific resource ID is unknown (e.g., to clean up a
693
- * temp file before unregistering an org -- all resources share one config).
694
- *
695
- * @param orgName - Organization name
696
- * @returns Remote config or null if org has no remote resources
697
- */
698
- getAnyRemoteConfig(orgName: string): RemoteOrgConfig | null {
699
- const prefix = `${orgName}/`
700
- for (const [key, config] of this.remoteResources) {
701
- if (key.startsWith(prefix)) return config
702
- }
703
- return null
704
- }
705
-
706
- /**
707
- * Get statistics about remotely-deployed resources
708
- * Used by the health endpoint for platform-wide deployment visibility.
709
- */
710
- getRemoteStats(): { activeOrgs: number; totalResources: number } {
711
- const orgs = new Set<string>()
712
- for (const key of this.remoteResources.keys()) {
713
- const orgName = key.split('/')[0]
714
- orgs.add(orgName)
715
- }
716
- return {
717
- activeOrgs: orgs.size,
718
- totalResources: this.remoteResources.size
719
- }
720
- }
721
-
722
- // ============================================================================
723
- // Resource Manifest Accessors
724
- // ============================================================================
725
-
726
- /**
727
- * Get triggers for an organization
728
- * @param organizationName - Organization name
729
- * @returns Array of trigger definitions (empty if none defined)
730
- */
731
- getTriggers(organizationName: string): TriggerDefinition[] {
732
- return this.registry[organizationName]?.triggers ?? []
733
- }
734
-
735
- /**
736
- * Get integrations for an organization
737
- * @param organizationName - Organization name
738
- * @returns Array of integration definitions (empty if none defined)
739
- */
740
- getIntegrations(organizationName: string): IntegrationDefinition[] {
741
- return this.registry[organizationName]?.integrations ?? []
742
- }
743
-
744
- /**
745
- * Get resource relationships for an organization
746
- * @param organizationName - Organization name
747
- * @returns Resource relationships map (undefined if none defined)
748
- */
749
- getRelationships(organizationName: string): ResourceRelationships | undefined {
750
- return this.registry[organizationName]?.relationships
751
- }
752
-
753
- /**
754
- * Get a specific trigger by ID
755
- * @param organizationName - Organization name
756
- * @param triggerId - Trigger ID
757
- * @returns Trigger definition or null if not found
758
- */
759
- getTrigger(organizationName: string, triggerId: string): TriggerDefinition | null {
760
- const triggers = this.getTriggers(organizationName)
761
- return triggers.find((t) => t.resourceId === triggerId) ?? null
762
- }
763
-
764
- /**
765
- * Get a specific integration by ID
766
- * @param organizationName - Organization name
767
- * @param integrationId - Integration ID
768
- * @returns Integration definition or null if not found
769
- */
770
- getIntegration(organizationName: string, integrationId: string): IntegrationDefinition | null {
771
- const integrations = this.getIntegrations(organizationName)
772
- return integrations.find((i) => i.resourceId === integrationId) ?? null
773
- }
774
-
775
- /**
776
- * Get external resources for an organization
777
- * @param organizationName - Organization name
778
- * @returns Array of external resource definitions (empty if none defined)
779
- */
780
- getExternalResources(organizationName: string): ExternalResourceDefinition[] {
781
- return this.registry[organizationName]?.externalResources ?? []
782
- }
783
-
784
- /**
785
- * Get a specific external resource by ID
786
- * @param organizationName - Organization name
787
- * @param externalId - External resource ID
788
- * @returns External resource definition or null if not found
789
- */
790
- getExternalResource(organizationName: string, externalId: string): ExternalResourceDefinition | null {
791
- const externalResources = this.getExternalResources(organizationName)
792
- return externalResources.find((e) => e.resourceId === externalId) ?? null
793
- }
794
-
795
- /**
796
- * Get human checkpoints for an organization
797
- * @param organizationName - Organization name
798
- * @returns Array of human checkpoint definitions (empty if none defined)
799
- */
800
- getHumanCheckpoints(organizationName: string): HumanCheckpointDefinition[] {
801
- return this.registry[organizationName]?.humanCheckpoints ?? []
802
- }
803
-
804
- /**
805
- * Get a specific human checkpoint by ID
806
- * @param organizationName - Organization name
807
- * @param humanCheckpointId - Human checkpoint ID
808
- * @returns Human checkpoint definition or null if not found
809
- */
810
- getHumanCheckpoint(organizationName: string, humanCheckpointId: string): HumanCheckpointDefinition | null {
811
- const humanCheckpoints = this.getHumanCheckpoints(organizationName)
812
- return humanCheckpoints.find((hc) => hc.resourceId === humanCheckpointId) ?? null
813
- }
814
-
815
- // ============================================================================
816
- // Serialized Data Access (Pre-computed at Startup)
817
- // ============================================================================
818
-
819
- /**
820
- * Get serialized resource definition (instant lookup)
821
- * Use for API responses - returns pre-computed JSON-safe structure
822
- *
823
- * @param organizationName - Organization name
824
- * @param resourceId - Resource ID
825
- * @returns Serialized definition or null if not found
826
- */
827
- getSerializedDefinition(
828
- organizationName: string,
829
- resourceId: string
830
- ): SerializedAgentDefinition | SerializedWorkflowDefinition | null {
831
- const cache = this.serializedCache.get(organizationName)
832
- if (!cache) return null
833
-
834
- return cache.definitions.agents.get(resourceId) ?? cache.definitions.workflows.get(resourceId) ?? null
835
- }
836
-
837
- /**
838
- * Get resource list for organization (instant lookup)
839
- * Use for /resources endpoint - returns pre-computed ResourceDefinition array
840
- *
841
- * @param organizationName - Organization name
842
- * @returns Resource list with workflows, agents, and total count
843
- */
844
- getResourceList(organizationName: string): {
845
- workflows: ResourceDefinition[]
846
- agents: ResourceDefinition[]
847
- total: number
848
- } {
849
- const cache = this.serializedCache.get(organizationName)
850
- if (!cache) {
851
- return { workflows: [], agents: [], total: 0 }
852
- }
853
- return cache.resources
854
- }
855
-
856
- /**
857
- * Get Command View data for organization (instant lookup)
858
- * Use for /command-view endpoint - returns complete graph data
859
- *
860
- * @param organizationName - Organization name
861
- * @returns Command View data with nodes and edges
862
- */
863
- getCommandViewData(organizationName: string): CommandViewData {
864
- const cache = this.serializedCache.get(organizationName)
865
- if (!cache) {
866
- return {
867
- workflows: [],
868
- agents: [],
869
- triggers: [],
870
- integrations: [],
871
- externalResources: [],
872
- humanCheckpoints: [],
873
- edges: []
874
- }
875
- }
876
- return cache.commandView
877
- }
878
- }
1
+ /**
2
+ * ResourceRegistry - Resource discovery and lookup
3
+ * Handles resource definitions from OrganizationRegistry
4
+ *
5
+ * Features:
6
+ * - Resource discovery by organization
7
+ * - Startup validation (duplicate IDs, model configs, relationships, interface-schema alignment)
8
+ * - Pre-serialization cache for instant API responses
9
+ * - Command View data generation
10
+ */
11
+
12
+ import type { WorkflowDefinition } from '../../execution/engine/workflow/types'
13
+ import type { AgentDefinition } from '../../execution/engine/agent/core/types'
14
+ import type {
15
+ OrganizationModel,
16
+ OrganizationModelResources,
17
+ OrganizationModelSystems
18
+ } from '../../organization-model/types'
19
+ import type { ResourceEntry } from '../../organization-model/domains/resources'
20
+ import type { SystemEntry } from '../../organization-model/domains/systems'
21
+ import { listAllSystems } from '../../organization-model/helpers'
22
+ import type {
23
+ ResourceStatus,
24
+ ResourceDefinition,
25
+ ResourceList,
26
+ TriggerDefinition,
27
+ IntegrationDefinition,
28
+ ResourceRelationships,
29
+ ExternalResourceDefinition,
30
+ HumanCheckpointDefinition
31
+ } from './types'
32
+ import type {
33
+ SerializedOrganizationData,
34
+ SerializedAgentDefinition,
35
+ SerializedWorkflowDefinition,
36
+ CommandViewData
37
+ } from './serialized-types'
38
+ import { validateDeploymentSpec, validateRelationships } from './validation'
39
+ import { serializeAllOrganizations, serializeOrganization } from './serialization'
40
+ import { isReservedResourceId } from './reserved'
41
+
42
+ /** Filter out archived resources from an organization's resource collection */
43
+ function filterArchived(org: DeploymentSpec): DeploymentSpec {
44
+ return {
45
+ ...org,
46
+ workflows: org.workflows?.filter((w) => !w.config.archived),
47
+ agents: org.agents?.filter((a) => !a.config.archived),
48
+ triggers: org.triggers?.filter((t) => !t.archived),
49
+ integrations: org.integrations?.filter((i) => !i.archived),
50
+ externalResources: org.externalResources?.filter((e) => !e.archived),
51
+ humanCheckpoints: org.humanCheckpoints?.filter((h) => !h.archived)
52
+ }
53
+ }
54
+
55
+ function summarizeSystem(system: SystemEntry | undefined): ResourceDefinition['system'] {
56
+ if (!system) return undefined
57
+
58
+ return {
59
+ id: system.id,
60
+ title: system.label ?? system.title,
61
+ description: system.description,
62
+ kind: system.kind,
63
+ lifecycle: system.lifecycle
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Configuration for a remotely-deployed organization
69
+ *
70
+ * Stored alongside runtime-registered organizations to support
71
+ * worker thread execution branching and credential management.
72
+ */
73
+ export interface RemoteOrgConfig {
74
+ /** Supabase Storage path: "{orgId}/{deploymentId}/bundle.js" */
75
+ storagePath: string
76
+ /** Deployment record ID */
77
+ deploymentId: string
78
+ /** OS temp path to bundle -- set after first download, used by worker threads */
79
+ cachedTempPath?: string
80
+ /** Platform tool name -> credential name mapping */
81
+ toolCredentials?: Record<string, string>
82
+ /** SDK version used to deploy this bundle */
83
+ sdkVersion?: string
84
+ /** Deployment version (semver) of the deployed bundle */
85
+ deploymentVersion?: string
86
+ }
87
+
88
+ /**
89
+ * Configuration for a first-class System resource.
90
+ *
91
+ * System resources are owned by the platform, registered under the 'system' org,
92
+ * and execute via the static-bundle loader mode in executeInWorker(). The moduleId
93
+ * maps to an entry in the API's STATIC_MODULE_MAP.
94
+ */
95
+ export interface SystemConfig {
96
+ kind: 'static'
97
+ moduleId: string
98
+ /** Always undefined for system resources; present for API compatibility with RemoteOrgConfig consumers */
99
+ sdkVersion?: never
100
+ }
101
+
102
+ /**
103
+ * Organization-specific resource collection
104
+ *
105
+ * Complete manifest of all automation resources for an organization.
106
+ * Used by ResourceRegistry for discovery and Command View for visualization.
107
+ */
108
+ export interface DeploymentSpec {
109
+ /** Deployment version (semver) */
110
+ version: string
111
+ /** Optional Organization Model governance catalog used for OM-code validation */
112
+ organizationModel?: {
113
+ systems?: OrganizationModelSystems
114
+ resources?: OrganizationModelResources
115
+ }
116
+ /** Workflow definitions */
117
+ workflows?: WorkflowDefinition[]
118
+ /** Agent definitions */
119
+ agents?: AgentDefinition[]
120
+
121
+ // Resource Manifest fields (optional for backwards compatibility)
122
+ /** Trigger definitions - entry points that initiate executions */
123
+ triggers?: TriggerDefinition[]
124
+ /** Integration definitions - external service connections */
125
+ integrations?: IntegrationDefinition[]
126
+ /** Explicit relationship declarations between resources */
127
+ relationships?: ResourceRelationships
128
+ /** External automation resources (n8n, Make, Zapier, etc.) */
129
+ externalResources?: ExternalResourceDefinition[]
130
+ /** Human checkpoint definitions - human decision points in automation */
131
+ humanCheckpoints?: HumanCheckpointDefinition[]
132
+ }
133
+
134
+ /**
135
+ * Organization Registry type
136
+ */
137
+ export type OrganizationRegistry = Record<string, DeploymentSpec>
138
+
139
+ export class ResourceRegistry {
140
+ /**
141
+ * Pre-serialized organization data cache
142
+ * Computed once at construction for static orgs, updated incrementally for runtime orgs
143
+ */
144
+ private serializedCache: Map<string, SerializedOrganizationData>
145
+
146
+ /**
147
+ * Per-resource remote configuration (external deployments)
148
+ * Key: "orgName/resourceId", Value: RemoteOrgConfig for that resource.
149
+ * Tracks which individual resources were added at runtime via deploy pipeline.
150
+ * Static and remote resources coexist in the same org.
151
+ */
152
+ private remoteResources = new Map<string, RemoteOrgConfig>()
153
+
154
+ /**
155
+ * System configs for first-class platform resources.
156
+ * Key: "orgName/resourceId", Value: SystemConfig.
157
+ * Registered at startup alongside registerStaticResources().
158
+ */
159
+ private systemConfigs = new Map<string, SystemConfig>()
160
+
161
+ constructor(private registry: OrganizationRegistry) {
162
+ this.validateRegistry()
163
+ this.validateRelationships()
164
+ this.serializedCache = serializeAllOrganizations(registry)
165
+ }
166
+
167
+ /**
168
+ * Validates registry on construction
169
+ * - Checks for duplicate resourceIds within organizations
170
+ * - Validates model configurations against constraints
171
+ * - Validates ExecutionInterface matches inputSchema
172
+ * @throws Error if validation fails
173
+ */
174
+ private validateRegistry(): void {
175
+ for (const [orgName, resources] of Object.entries(this.registry)) {
176
+ validateDeploymentSpec(orgName, resources)
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Validates relationship declarations reference valid resources
182
+ * Runs at API server startup - fails fast in development
183
+ * @throws Error if validation fails
184
+ */
185
+ private validateRelationships(): void {
186
+ for (const [orgName, resources] of Object.entries(this.registry)) {
187
+ validateRelationships(orgName, resources)
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Get the remote resource IDs currently registered for an organization.
193
+ * Used to validate redeployments against the post-swap state before any
194
+ * live registry mutation occurs.
195
+ */
196
+ private getRemoteResourceIds(orgName: string): Set<string> {
197
+ const prefix = `${orgName}/`
198
+ const remoteIds = new Set<string>()
199
+ for (const key of this.remoteResources.keys()) {
200
+ if (key.startsWith(prefix)) {
201
+ remoteIds.add(key.slice(prefix.length))
202
+ }
203
+ }
204
+ return remoteIds
205
+ }
206
+
207
+ /**
208
+ * Build the "static + surviving" baseline for registration validation.
209
+ * On redeploy, this strips the currently remote-owned resources and
210
+ * deployment-owned metadata so validation reflects the state after swap.
211
+ */
212
+ private buildRegistrationBase(orgName: string): DeploymentSpec | undefined {
213
+ const existingOrg = this.registry[orgName]
214
+ if (!existingOrg) return undefined
215
+
216
+ const remoteIds = this.getRemoteResourceIds(orgName)
217
+ if (remoteIds.size === 0) return existingOrg
218
+
219
+ const relationships = existingOrg.relationships
220
+ ? Object.fromEntries(
221
+ Object.entries(existingOrg.relationships).filter(([resourceId]) => !remoteIds.has(resourceId))
222
+ )
223
+ : undefined
224
+
225
+ return {
226
+ ...existingOrg,
227
+ version: existingOrg.version ?? '0.0.0',
228
+ workflows: (existingOrg.workflows ?? []).filter((w) => !remoteIds.has(w.config.resourceId)),
229
+ agents: (existingOrg.agents ?? []).filter((a) => !remoteIds.has(a.config.resourceId)),
230
+ triggers: undefined,
231
+ integrations: undefined,
232
+ humanCheckpoints: undefined,
233
+ externalResources: undefined,
234
+ relationships: relationships && Object.keys(relationships).length > 0 ? relationships : undefined
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Validate the registry state that would exist after registration succeeds.
240
+ * This runs before any live mutation so invalid redeploys preserve the
241
+ * currently active remote resources.
242
+ */
243
+ private validateRegistrationCandidate(orgName: string, incoming: DeploymentSpec): void {
244
+ const base = this.buildRegistrationBase(orgName)
245
+
246
+ const candidate: DeploymentSpec = base
247
+ ? {
248
+ ...base,
249
+ version: incoming.version ?? base.version ?? '0.0.0',
250
+ workflows: [...(base.workflows ?? []), ...(incoming.workflows ?? [])],
251
+ agents: [...(base.agents ?? []), ...(incoming.agents ?? [])],
252
+ triggers: incoming.triggers,
253
+ integrations: incoming.integrations,
254
+ humanCheckpoints: incoming.humanCheckpoints,
255
+ externalResources: incoming.externalResources,
256
+ relationships: incoming.relationships
257
+ ? {
258
+ ...(base.relationships ?? {}),
259
+ ...incoming.relationships
260
+ }
261
+ : base.relationships
262
+ }
263
+ : {
264
+ ...incoming,
265
+ version: incoming.version ?? '0.0.0'
266
+ }
267
+
268
+ validateDeploymentSpec(orgName, candidate)
269
+ validateRelationships(orgName, candidate)
270
+ }
271
+
272
+ /**
273
+ * Get a resource definition by ID
274
+ * Returns full definition (WorkflowDefinition or AgentDefinition)
275
+ * Check definition.config.type to determine if it's a workflow or agent
276
+ */
277
+ getResourceDefinition(organizationName: string, resourceId: string): WorkflowDefinition | AgentDefinition | null {
278
+ const orgResources = this.registry[organizationName]
279
+ if (!orgResources) return null
280
+
281
+ // Check workflows first
282
+ const workflow = orgResources.workflows?.find((w) => w.config.resourceId === resourceId)
283
+ if (workflow) return workflow
284
+
285
+ // Check agents
286
+ const agent = orgResources.agents?.find((a) => a.config.resourceId === resourceId)
287
+ if (agent) return agent
288
+
289
+ return null
290
+ }
291
+
292
+ /**
293
+ * List all resources for an organization
294
+ * Returns ResourceDefinition metadata (not full definitions)
295
+ *
296
+ * All resources are returned regardless of server environment.
297
+ * Pass an explicit `environment` filter to get only 'dev' or 'prod' resources.
298
+ */
299
+ listResourcesForOrganization(organizationName: string, environment?: ResourceStatus): ResourceList {
300
+ const orgResources = this.registry[organizationName]
301
+ if (!orgResources) {
302
+ return {
303
+ workflows: [],
304
+ agents: [],
305
+ total: 0,
306
+ organizationName,
307
+ environment
308
+ }
309
+ }
310
+
311
+ const resourcesById = new Map(Object.values(orgResources.organizationModel?.resources ?? {}).map((r) => [r.id, r]))
312
+ // Build a path-keyed map that includes nested systems (DFS via listAllSystems).
313
+ // resource.systemPath is the dot-separated system path used as the lookup key.
314
+ const omSystems = orgResources.organizationModel?.systems
315
+ const systemsByPath = new Map<string, SystemEntry>(
316
+ omSystems
317
+ ? listAllSystems({ systems: omSystems } as OrganizationModel).map(({ path, system }) => [path, system])
318
+ : []
319
+ )
320
+ const getGovernanceMetadata = (
321
+ resourceId: string,
322
+ descriptor: ResourceEntry | undefined
323
+ ): Pick<ResourceDefinition, 'systemPath' | 'system' | 'governanceStatus'> => {
324
+ const resource = descriptor ?? resourcesById.get(resourceId)
325
+ if (!resource) return {}
326
+
327
+ return {
328
+ systemPath: resource.systemPath,
329
+ system: summarizeSystem(systemsByPath.get(resource.systemPath)),
330
+ governanceStatus: resource.status
331
+ }
332
+ }
333
+
334
+ // Map workflows to ResourceDefinition metadata and filter by environment
335
+ const workflows: ResourceDefinition[] = (orgResources.workflows || [])
336
+ .map((def) => ({
337
+ resourceId: def.config.resourceId,
338
+ name: def.config.name,
339
+ description: def.config.description,
340
+ version: def.config.version,
341
+ type: def.config.type,
342
+ status: def.config.status,
343
+ links: def.config.links,
344
+ category: def.config.category,
345
+ origin: this.remoteResources.has(`${organizationName}/${def.config.resourceId}`)
346
+ ? ('remote' as const)
347
+ : ('local' as const),
348
+ ...getGovernanceMetadata(def.config.resourceId, def.config.resource)
349
+ }))
350
+ .filter((resource) => !environment || resource.status === environment)
351
+
352
+ // Map agents to ResourceDefinition metadata and filter by environment
353
+ const agents: ResourceDefinition[] = (orgResources.agents || [])
354
+ .map((def) => ({
355
+ resourceId: def.config.resourceId,
356
+ name: def.config.name,
357
+ description: def.config.description,
358
+ version: def.config.version,
359
+ type: def.config.type,
360
+ status: def.config.status,
361
+ links: def.config.links,
362
+ category: def.config.category,
363
+ sessionCapable: def.config.sessionCapable ?? false,
364
+ origin: this.remoteResources.has(`${organizationName}/${def.config.resourceId}`)
365
+ ? ('remote' as const)
366
+ : ('local' as const),
367
+ ...getGovernanceMetadata(def.config.resourceId, def.config.resource)
368
+ }))
369
+ .filter((resource) => !environment || resource.status === environment)
370
+
371
+ return {
372
+ workflows,
373
+ agents,
374
+ total: workflows.length + agents.length,
375
+ organizationName,
376
+ environment
377
+ }
378
+ }
379
+
380
+ /**
381
+ * List all resources from all organizations
382
+ * NOTE: For debugging only - returns raw registry data
383
+ */
384
+ listAllResources(): OrganizationRegistry {
385
+ return this.registry
386
+ }
387
+
388
+ // ============================================================================
389
+ // Runtime Organization Registration (External Deployments)
390
+ // ============================================================================
391
+
392
+ /**
393
+ * Register external resources at runtime
394
+ *
395
+ * Called during deploy pipeline when an external developer deploys their bundle.
396
+ * Merges the incoming stub definitions into the org's registry and stores
397
+ * per-resource remote config for worker thread execution branching.
398
+ *
399
+ * Static and remote resources coexist in the same org. If the org already
400
+ * has static resources, the incoming remote resources are merged alongside them.
401
+ * If redeploying (some resources already registered as remote for this org),
402
+ * the previous remote resources are unregistered first.
403
+ *
404
+ * @param orgName - Organization name (used as registry key)
405
+ * @param org - Stub resource definitions (workflows/agents with placeholder handlers)
406
+ * @param remote - Remote configuration (bundle path, deployment ID, env vars)
407
+ * @throws Error if incoming resourceId conflicts with a static resource
408
+ * @throws Error if incoming deployment contains duplicate resourceIds
409
+ */
410
+ registerOrganization(orgName: string, org: DeploymentSpec, remote: RemoteOrgConfig): void {
411
+ // Filter out archived resources before any processing
412
+ org = filterArchived(org)
413
+
414
+ // Collect all incoming resource IDs for conflict checking
415
+ const incomingWorkflowIds = (org.workflows ?? []).map((w) => w.config.resourceId)
416
+ const incomingAgentIds = (org.agents ?? []).map((a) => a.config.resourceId)
417
+ const incomingIds = [...incomingWorkflowIds, ...incomingAgentIds]
418
+
419
+ // Check for intra-deployment duplicates
420
+ const seen = new Set<string>()
421
+ for (const id of incomingIds) {
422
+ if (seen.has(id)) {
423
+ throw new Error(`Duplicate resource ID '${id}' in deployment. Each resource must have a unique ID.`)
424
+ }
425
+ seen.add(id)
426
+ }
427
+
428
+ // Check for reserved resource IDs (cannot be claimed by external deployments)
429
+ for (const id of incomingIds) {
430
+ if (isReservedResourceId(id)) {
431
+ throw new Error(
432
+ `Resource ID '${id}' is reserved for platform use. External deployments cannot use reserved resource IDs.`
433
+ )
434
+ }
435
+ }
436
+
437
+ // Check for conflicts against the static/surviving org state.
438
+ // On redeploy, current remote resources are excluded so a deployment can
439
+ // legitimately replace its own resource IDs without colliding with itself.
440
+ const validationBase = this.buildRegistrationBase(orgName)
441
+ if (validationBase) {
442
+ const staticWorkflowIds = new Set((validationBase.workflows ?? []).map((w) => w.config.resourceId))
443
+ const staticAgentIds = new Set((validationBase.agents ?? []).map((a) => a.config.resourceId))
444
+
445
+ for (const id of incomingIds) {
446
+ if (staticWorkflowIds.has(id) || staticAgentIds.has(id)) {
447
+ throw new Error(
448
+ `Resource '${id}' already exists in '${orgName}' as an internal resource. External deployments cannot override internal resources.`
449
+ )
450
+ }
451
+ }
452
+ }
453
+
454
+ // Validate the merged deployment shape before mutating the live registry.
455
+ // This keeps the current deployment intact if a redeploy introduces
456
+ // invalid relationships or other registry-level validation failures.
457
+ this.validateRegistrationCandidate(orgName, org)
458
+
459
+ // If redeploying (some resources already registered as remote for this org), clean up only
460
+ // after validation succeeds so a failed redeploy preserves the current remote resources.
461
+ if (this.isRemote(orgName)) {
462
+ this.unregisterOrganization(orgName)
463
+ }
464
+
465
+ // Merge incoming resources into the org (or create new org entry)
466
+ const existingOrg = this.registry[orgName]
467
+ if (existingOrg) {
468
+ existingOrg.workflows = [...(existingOrg.workflows ?? []), ...(org.workflows ?? [])]
469
+ existingOrg.agents = [...(existingOrg.agents ?? []), ...(org.agents ?? [])]
470
+ // Deployment-owned metadata: replace entirely (unregister cleared these)
471
+ existingOrg.triggers = org.triggers
472
+ existingOrg.integrations = org.integrations
473
+ existingOrg.humanCheckpoints = org.humanCheckpoints
474
+ existingOrg.externalResources = org.externalResources
475
+ if (org.relationships) {
476
+ existingOrg.relationships = {
477
+ ...(existingOrg.relationships ?? {}),
478
+ ...org.relationships
479
+ }
480
+ }
481
+ } else {
482
+ this.registry[orgName] = org
483
+ }
484
+
485
+ // Populate per-resource remote config entries
486
+ for (const id of incomingIds) {
487
+ this.remoteResources.set(`${orgName}/${id}`, remote)
488
+ }
489
+
490
+ // Rebuild serialized cache for the full merged org
491
+ this.serializedCache.set(orgName, serializeOrganization(this.registry[orgName]))
492
+ }
493
+
494
+ /**
495
+ * Patch serialized cache with pre-serialized schemas from an external manifest.
496
+ *
497
+ * External deployments use stub definitions with z.any() schemas (never called).
498
+ * The manifest carries the real schemas as pre-serialized JSON Schema from the worker.
499
+ * This method patches those into the serialized cache so describe/CLI display them.
500
+ *
501
+ * @param orgName - Organization name
502
+ * @param manifestSchemas - Map of resourceId -> { contract, steps } with JSON Schema
503
+ */
504
+ patchManifestSchemas(
505
+ orgName: string,
506
+ manifestSchemas: Array<{
507
+ resourceId: string
508
+ type: 'workflow' | 'agent'
509
+ contract?: { inputSchema?: object; outputSchema?: object }
510
+ steps?: Array<{ id: string; inputSchema?: object; outputSchema?: object }>
511
+ }>
512
+ ): void {
513
+ const cache = this.serializedCache.get(orgName)
514
+ if (!cache) return
515
+
516
+ for (const entry of manifestSchemas) {
517
+ if (entry.type === 'workflow') {
518
+ const def = cache.definitions.workflows.get(entry.resourceId)
519
+ if (!def) continue
520
+
521
+ // Patch contract schemas
522
+ if (entry.contract?.inputSchema) def.contract.inputSchema = entry.contract.inputSchema
523
+ if (entry.contract?.outputSchema) def.contract.outputSchema = entry.contract.outputSchema
524
+
525
+ // Patch step schemas
526
+ if (entry.steps && def.steps) {
527
+ for (const stepPatch of entry.steps) {
528
+ const step = def.steps.find((s) => s.id === stepPatch.id)
529
+ if (!step) continue
530
+ if (stepPatch.inputSchema) step.inputSchema = stepPatch.inputSchema
531
+ if (stepPatch.outputSchema) step.outputSchema = stepPatch.outputSchema
532
+ }
533
+ }
534
+ } else if (entry.type === 'agent') {
535
+ const def = cache.definitions.agents.get(entry.resourceId)
536
+ if (!def) continue
537
+ if (entry.contract?.inputSchema) def.contract.inputSchema = entry.contract.inputSchema
538
+ if (entry.contract?.outputSchema) def.contract.outputSchema = entry.contract.outputSchema
539
+ }
540
+ }
541
+ }
542
+
543
+ /**
544
+ * Register built-in platform resources (static, local execution)
545
+ *
546
+ * Unlike registerOrganization(), these resources:
547
+ * - Do NOT have remote config (execute in-process, not in worker threads)
548
+ * - Are NOT removed by unregisterOrganization() (persist across redeployments)
549
+ * - Use reserved resource IDs that external deployments cannot claim
550
+ *
551
+ * @param orgName - Organization name
552
+ * @param org - Resource definitions with real handlers (not stubs)
553
+ */
554
+ registerStaticResources(orgName: string, org: DeploymentSpec): void {
555
+ // Filter out archived resources before any processing
556
+ org = filterArchived(org)
557
+
558
+ const incomingWorkflowIds = (org.workflows ?? []).map((w) => w.config.resourceId)
559
+ const incomingAgentIds = (org.agents ?? []).map((a) => a.config.resourceId)
560
+ const incomingIds = [...incomingWorkflowIds, ...incomingAgentIds]
561
+
562
+ // Check for duplicates within incoming resources
563
+ const seen = new Set<string>()
564
+ for (const id of incomingIds) {
565
+ if (seen.has(id)) {
566
+ throw new Error(`Duplicate resource ID '${id}' in static resources.`)
567
+ }
568
+ seen.add(id)
569
+ }
570
+
571
+ // Check for conflicts with existing resources in this org
572
+ const existingOrg = this.registry[orgName]
573
+ if (existingOrg) {
574
+ const existingWorkflowIds = new Set((existingOrg.workflows ?? []).map((w) => w.config.resourceId))
575
+ const existingAgentIds = new Set((existingOrg.agents ?? []).map((a) => a.config.resourceId))
576
+
577
+ for (const id of incomingIds) {
578
+ if (existingWorkflowIds.has(id) || existingAgentIds.has(id)) {
579
+ throw new Error(`Static resource '${id}' conflicts with existing resource in '${orgName}'.`)
580
+ }
581
+ }
582
+ }
583
+
584
+ // Merge into registry (no remote config = local execution path)
585
+ if (existingOrg) {
586
+ existingOrg.workflows = [...(existingOrg.workflows ?? []), ...(org.workflows ?? [])]
587
+ existingOrg.agents = [...(existingOrg.agents ?? []), ...(org.agents ?? [])]
588
+ } else {
589
+ this.registry[orgName] = org
590
+ }
591
+
592
+ // Rebuild serialized cache
593
+ this.serializedCache.set(orgName, serializeOrganization(this.registry[orgName]))
594
+ }
595
+
596
+ /**
597
+ * Unregister runtime-registered resources for an organization
598
+ *
599
+ * Removes only resources that were registered at runtime (via registerOrganization).
600
+ * Static resources loaded at startup are preserved. If the org still has static
601
+ * resources after removal, the serialization cache is rebuilt. If no resources
602
+ * remain, the org is fully removed from the registry.
603
+ * No-op if the org has no remote resources.
604
+ *
605
+ * @param orgName - Organization name to unregister remote resources from
606
+ */
607
+ unregisterOrganization(orgName: string): void {
608
+ // Find all remote resource keys for this org
609
+ const prefix = `${orgName}/`
610
+ const remoteIds = new Set<string>()
611
+ for (const key of this.remoteResources.keys()) {
612
+ if (key.startsWith(prefix)) {
613
+ remoteIds.add(key.slice(prefix.length))
614
+ this.remoteResources.delete(key)
615
+ }
616
+ }
617
+
618
+ // No-op if org had no remote resources
619
+ if (remoteIds.size === 0) return
620
+
621
+ const orgResources = this.registry[orgName]
622
+ if (!orgResources) return
623
+
624
+ // Remove remote resources from the org's arrays
625
+ orgResources.workflows = (orgResources.workflows ?? []).filter((w) => !remoteIds.has(w.config.resourceId))
626
+ orgResources.agents = (orgResources.agents ?? []).filter((a) => !remoteIds.has(a.config.resourceId))
627
+
628
+ // Remove deployment-owned metadata (triggers, integrations, checkpoints, external resources)
629
+ // These are always fully owned by the deployment — replaced on each deploy
630
+ orgResources.triggers = undefined
631
+ orgResources.integrations = undefined
632
+ orgResources.humanCheckpoints = undefined
633
+ orgResources.externalResources = undefined
634
+
635
+ // Remove relationship entries for remote resource IDs
636
+ if (orgResources.relationships) {
637
+ for (const id of remoteIds) {
638
+ delete orgResources.relationships[id]
639
+ }
640
+ if (Object.keys(orgResources.relationships).length === 0) {
641
+ delete orgResources.relationships
642
+ }
643
+ }
644
+
645
+ // If the org still has static resources, rebuild cache; otherwise fully remove
646
+ // (triggers, integrations, checkpoints, externalResources were cleared above)
647
+ const remaining = (orgResources.workflows?.length ?? 0) + (orgResources.agents?.length ?? 0)
648
+
649
+ if (remaining > 0) {
650
+ this.serializedCache.set(orgName, serializeOrganization(orgResources))
651
+ } else {
652
+ delete this.registry[orgName]
653
+ this.serializedCache.delete(orgName)
654
+ }
655
+ }
656
+
657
+ /**
658
+ * Get remote configuration for a specific resource.
659
+ *
660
+ * Returns RemoteOrgConfig for externally-deployed resources, SystemConfig for
661
+ * first-class platform resources, or null for static in-process resources.
662
+ * Used by the execution coordinator to determine the execution path.
663
+ *
664
+ * @param orgName - Organization name
665
+ * @param resourceId - Resource ID
666
+ * @returns Remote or System config, or null
667
+ */
668
+ getRemoteConfig(orgName: string, resourceId: string): RemoteOrgConfig | SystemConfig | null {
669
+ const key = `${orgName}/${resourceId}`
670
+ return this.remoteResources.get(key) ?? this.systemConfigs.get(key) ?? null
671
+ }
672
+
673
+ /**
674
+ * Register a System config for a first-class platform resource.
675
+ *
676
+ * Called at startup alongside registerStaticResources() so that
677
+ * getRemoteConfig('system', resourceId) returns truthy and the execution
678
+ * coordinator routes the resource through the worker-thread path.
679
+ *
680
+ * @param orgName - Organization name (typically 'system')
681
+ * @param resourceId - Resource ID
682
+ * @param config - SystemConfig with kind:'static' and moduleId
683
+ */
684
+ registerSystemConfig(orgName: string, resourceId: string, config: SystemConfig): void {
685
+ this.systemConfigs.set(`${orgName}/${resourceId}`, config)
686
+ }
687
+
688
+ /**
689
+ * Check if an organization has any remote (externally deployed) resources
690
+ *
691
+ * @param orgName - Organization name
692
+ * @returns true if the org has at least one runtime-registered resource
693
+ */
694
+ isRemote(orgName: string): boolean {
695
+ const prefix = `${orgName}/`
696
+ for (const key of this.remoteResources.keys()) {
697
+ if (key.startsWith(prefix)) return true
698
+ }
699
+ return false
700
+ }
701
+
702
+ /**
703
+ * Get the remote config for any resource in an organization.
704
+ * Used when the specific resource ID is unknown (e.g., to clean up a
705
+ * temp file before unregistering an org -- all resources share one config).
706
+ *
707
+ * @param orgName - Organization name
708
+ * @returns Remote config or null if org has no remote resources
709
+ */
710
+ getAnyRemoteConfig(orgName: string): RemoteOrgConfig | null {
711
+ const prefix = `${orgName}/`
712
+ for (const [key, config] of this.remoteResources) {
713
+ if (key.startsWith(prefix)) return config
714
+ }
715
+ return null
716
+ }
717
+
718
+ /**
719
+ * Get statistics about remotely-deployed resources
720
+ * Used by the health endpoint for platform-wide deployment visibility.
721
+ */
722
+ getRemoteStats(): { activeOrgs: number; totalResources: number } {
723
+ const orgs = new Set<string>()
724
+ for (const key of this.remoteResources.keys()) {
725
+ const orgName = key.split('/')[0]
726
+ orgs.add(orgName)
727
+ }
728
+ return {
729
+ activeOrgs: orgs.size,
730
+ totalResources: this.remoteResources.size
731
+ }
732
+ }
733
+
734
+ // ============================================================================
735
+ // Resource Manifest Accessors
736
+ // ============================================================================
737
+
738
+ /**
739
+ * Get triggers for an organization
740
+ * @param organizationName - Organization name
741
+ * @returns Array of trigger definitions (empty if none defined)
742
+ */
743
+ getTriggers(organizationName: string): TriggerDefinition[] {
744
+ return this.registry[organizationName]?.triggers ?? []
745
+ }
746
+
747
+ /**
748
+ * Get integrations for an organization
749
+ * @param organizationName - Organization name
750
+ * @returns Array of integration definitions (empty if none defined)
751
+ */
752
+ getIntegrations(organizationName: string): IntegrationDefinition[] {
753
+ return this.registry[organizationName]?.integrations ?? []
754
+ }
755
+
756
+ /**
757
+ * Get resource relationships for an organization
758
+ * @param organizationName - Organization name
759
+ * @returns Resource relationships map (undefined if none defined)
760
+ */
761
+ getRelationships(organizationName: string): ResourceRelationships | undefined {
762
+ return this.registry[organizationName]?.relationships
763
+ }
764
+
765
+ /**
766
+ * Get a specific trigger by ID
767
+ * @param organizationName - Organization name
768
+ * @param triggerId - Trigger ID
769
+ * @returns Trigger definition or null if not found
770
+ */
771
+ getTrigger(organizationName: string, triggerId: string): TriggerDefinition | null {
772
+ const triggers = this.getTriggers(organizationName)
773
+ return triggers.find((t) => t.resourceId === triggerId) ?? null
774
+ }
775
+
776
+ /**
777
+ * Get a specific integration by ID
778
+ * @param organizationName - Organization name
779
+ * @param integrationId - Integration ID
780
+ * @returns Integration definition or null if not found
781
+ */
782
+ getIntegration(organizationName: string, integrationId: string): IntegrationDefinition | null {
783
+ const integrations = this.getIntegrations(organizationName)
784
+ return integrations.find((i) => i.resourceId === integrationId) ?? null
785
+ }
786
+
787
+ /**
788
+ * Get external resources for an organization
789
+ * @param organizationName - Organization name
790
+ * @returns Array of external resource definitions (empty if none defined)
791
+ */
792
+ getExternalResources(organizationName: string): ExternalResourceDefinition[] {
793
+ return this.registry[organizationName]?.externalResources ?? []
794
+ }
795
+
796
+ /**
797
+ * Get a specific external resource by ID
798
+ * @param organizationName - Organization name
799
+ * @param externalId - External resource ID
800
+ * @returns External resource definition or null if not found
801
+ */
802
+ getExternalResource(organizationName: string, externalId: string): ExternalResourceDefinition | null {
803
+ const externalResources = this.getExternalResources(organizationName)
804
+ return externalResources.find((e) => e.resourceId === externalId) ?? null
805
+ }
806
+
807
+ /**
808
+ * Get human checkpoints for an organization
809
+ * @param organizationName - Organization name
810
+ * @returns Array of human checkpoint definitions (empty if none defined)
811
+ */
812
+ getHumanCheckpoints(organizationName: string): HumanCheckpointDefinition[] {
813
+ return this.registry[organizationName]?.humanCheckpoints ?? []
814
+ }
815
+
816
+ /**
817
+ * Get a specific human checkpoint by ID
818
+ * @param organizationName - Organization name
819
+ * @param humanCheckpointId - Human checkpoint ID
820
+ * @returns Human checkpoint definition or null if not found
821
+ */
822
+ getHumanCheckpoint(organizationName: string, humanCheckpointId: string): HumanCheckpointDefinition | null {
823
+ const humanCheckpoints = this.getHumanCheckpoints(organizationName)
824
+ return humanCheckpoints.find((hc) => hc.resourceId === humanCheckpointId) ?? null
825
+ }
826
+
827
+ // ============================================================================
828
+ // Serialized Data Access (Pre-computed at Startup)
829
+ // ============================================================================
830
+
831
+ /**
832
+ * Get serialized resource definition (instant lookup)
833
+ * Use for API responses - returns pre-computed JSON-safe structure
834
+ *
835
+ * @param organizationName - Organization name
836
+ * @param resourceId - Resource ID
837
+ * @returns Serialized definition or null if not found
838
+ */
839
+ getSerializedDefinition(
840
+ organizationName: string,
841
+ resourceId: string
842
+ ): SerializedAgentDefinition | SerializedWorkflowDefinition | null {
843
+ const cache = this.serializedCache.get(organizationName)
844
+ if (!cache) return null
845
+
846
+ return cache.definitions.agents.get(resourceId) ?? cache.definitions.workflows.get(resourceId) ?? null
847
+ }
848
+
849
+ /**
850
+ * Get resource list for organization (instant lookup)
851
+ * Use for /resources endpoint - returns pre-computed ResourceDefinition array
852
+ *
853
+ * @param organizationName - Organization name
854
+ * @returns Resource list with workflows, agents, and total count
855
+ */
856
+ getResourceList(organizationName: string): {
857
+ workflows: ResourceDefinition[]
858
+ agents: ResourceDefinition[]
859
+ total: number
860
+ } {
861
+ const cache = this.serializedCache.get(organizationName)
862
+ if (!cache) {
863
+ return { workflows: [], agents: [], total: 0 }
864
+ }
865
+ return cache.resources
866
+ }
867
+
868
+ /**
869
+ * Get Command View data for organization (instant lookup)
870
+ * Use for /command-view endpoint - returns complete graph data
871
+ *
872
+ * @param organizationName - Organization name
873
+ * @returns Command View data with nodes and edges
874
+ */
875
+ getCommandViewData(organizationName: string): CommandViewData {
876
+ const cache = this.serializedCache.get(organizationName)
877
+ if (!cache) {
878
+ return {
879
+ workflows: [],
880
+ agents: [],
881
+ triggers: [],
882
+ integrations: [],
883
+ externalResources: [],
884
+ humanCheckpoints: [],
885
+ edges: []
886
+ }
887
+ }
888
+ return cache.commandView
889
+ }
890
+ }