@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.
Files changed (243) hide show
  1. package/dist/index.d.ts +4343 -2690
  2. package/dist/index.js +1101 -156
  3. package/dist/knowledge/index.d.ts +574 -210
  4. package/dist/knowledge/index.js +104 -1
  5. package/dist/organization-model/index.d.ts +4343 -2690
  6. package/dist/organization-model/index.js +1101 -156
  7. package/dist/test-utils/index.d.ts +483 -109
  8. package/dist/test-utils/index.js +904 -144
  9. package/package.json +3 -3
  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 -12
  13. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +2137 -2093
  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 +1492 -1497
  37. package/src/business/acquisition/build-templates.test.ts +240 -240
  38. package/src/business/acquisition/build-templates.ts +98 -98
  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 -153
  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 -396
  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 +195 -197
  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 +32 -32
  123. package/src/knowledge/__tests__/queries.test.ts +626 -535
  124. package/src/knowledge/format.ts +99 -99
  125. package/src/knowledge/index.ts +5 -5
  126. package/src/knowledge/published.ts +5 -5
  127. package/src/knowledge/queries.ts +269 -218
  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 -149
  131. package/src/organization-model/__tests__/content-kinds-registry.test.ts +210 -210
  132. package/src/organization-model/__tests__/defaults.test.ts +168 -168
  133. package/src/organization-model/__tests__/domains/actions.test.ts +78 -56
  134. package/src/organization-model/__tests__/domains/customers.test.ts +299 -299
  135. package/src/organization-model/__tests__/domains/entities.test.ts +56 -56
  136. package/src/organization-model/__tests__/domains/goals.test.ts +493 -493
  137. package/src/organization-model/__tests__/domains/identity.test.ts +280 -280
  138. package/src/organization-model/__tests__/domains/navigation.test.ts +268 -268
  139. package/src/organization-model/__tests__/domains/offerings.test.ts +414 -414
  140. package/src/organization-model/__tests__/domains/policies.test.ts +323 -323
  141. package/src/organization-model/__tests__/domains/resource-mappings.test.ts +293 -293
  142. package/src/organization-model/__tests__/domains/resources.test.ts +387 -277
  143. package/src/organization-model/__tests__/domains/roles.test.ts +463 -463
  144. package/src/organization-model/__tests__/domains/statuses.test.ts +246 -246
  145. package/src/organization-model/__tests__/domains/systems.test.ts +209 -209
  146. package/src/organization-model/__tests__/domains/topology.test.ts +188 -0
  147. package/src/organization-model/__tests__/flatten-additive-merge.test.ts +362 -361
  148. package/src/organization-model/__tests__/foundation.test.ts +77 -77
  149. package/src/organization-model/__tests__/get-resources-for-system.test.ts +144 -144
  150. package/src/organization-model/__tests__/graph.test.ts +1312 -862
  151. package/src/organization-model/__tests__/icons.test.ts +10 -1
  152. package/src/organization-model/__tests__/knowledge.test.ts +251 -15
  153. package/src/organization-model/__tests__/lookup-helpers.test.ts +438 -438
  154. package/src/organization-model/__tests__/migration-helpers.test.ts +591 -591
  155. package/src/organization-model/__tests__/prospecting-ssot.test.ts +103 -103
  156. package/src/organization-model/__tests__/recursive-system-schema.test.ts +535 -506
  157. package/src/organization-model/__tests__/resolve.test.ts +274 -164
  158. package/src/organization-model/__tests__/schema.test.ts +844 -301
  159. package/src/organization-model/__tests__/surface-projection.test.ts +284 -284
  160. package/src/organization-model/catalogs/lead-gen.ts +144 -144
  161. package/src/organization-model/content-kinds/config.ts +36 -36
  162. package/src/organization-model/content-kinds/index.ts +76 -72
  163. package/src/organization-model/content-kinds/pipeline.ts +68 -68
  164. package/src/organization-model/content-kinds/registry.ts +44 -44
  165. package/src/organization-model/content-kinds/status.ts +71 -71
  166. package/src/organization-model/content-kinds/template.ts +83 -83
  167. package/src/organization-model/content-kinds/types.ts +117 -117
  168. package/src/organization-model/contracts.ts +27 -27
  169. package/src/organization-model/defaults.ts +42 -50
  170. package/src/organization-model/domains/actions.ts +333 -239
  171. package/src/organization-model/domains/customers.ts +78 -78
  172. package/src/organization-model/domains/entities.ts +144 -144
  173. package/src/organization-model/domains/goals.ts +83 -83
  174. package/src/organization-model/domains/knowledge.ts +117 -101
  175. package/src/organization-model/domains/navigation.ts +139 -139
  176. package/src/organization-model/domains/offerings.ts +71 -71
  177. package/src/organization-model/domains/policies.ts +102 -102
  178. package/src/organization-model/domains/projects.ts +14 -14
  179. package/src/organization-model/domains/prospecting.ts +395 -395
  180. package/src/organization-model/domains/resources.ts +202 -124
  181. package/src/organization-model/domains/roles.ts +96 -96
  182. package/src/organization-model/domains/sales.test.ts +218 -218
  183. package/src/organization-model/domains/sales.ts +380 -380
  184. package/src/organization-model/domains/shared.ts +63 -63
  185. package/src/organization-model/domains/statuses.ts +339 -339
  186. package/src/organization-model/domains/systems.ts +217 -172
  187. package/src/organization-model/domains/topology.ts +261 -0
  188. package/src/organization-model/foundation.ts +75 -75
  189. package/src/organization-model/graph/build.ts +1043 -867
  190. package/src/organization-model/graph/index.ts +4 -4
  191. package/src/organization-model/graph/link.ts +10 -10
  192. package/src/organization-model/graph/schema.ts +75 -68
  193. package/src/organization-model/graph/types.ts +71 -64
  194. package/src/organization-model/helpers.ts +289 -241
  195. package/src/organization-model/icons.ts +78 -66
  196. package/src/organization-model/index.ts +128 -125
  197. package/src/organization-model/migration-helpers.ts +247 -244
  198. package/src/organization-model/ontology.ts +658 -0
  199. package/src/organization-model/organization-graph.mdx +110 -90
  200. package/src/organization-model/organization-model.mdx +225 -213
  201. package/src/organization-model/published.ts +299 -222
  202. package/src/organization-model/resolve.ts +146 -91
  203. package/src/organization-model/schema.ts +818 -659
  204. package/src/organization-model/surface-projection.ts +212 -212
  205. package/src/organization-model/types.ts +179 -155
  206. package/src/platform/api/types.ts +38 -38
  207. package/src/platform/constants/versions.ts +3 -3
  208. package/src/platform/index.ts +23 -23
  209. package/src/platform/registry/__tests__/command-view.test.ts +10 -10
  210. package/src/platform/registry/__tests__/resource-link.test.ts +35 -35
  211. package/src/platform/registry/__tests__/resource-registry.integration.test.ts +20 -20
  212. package/src/platform/registry/__tests__/resource-registry.nested-systems.test.ts +245 -245
  213. package/src/platform/registry/__tests__/resource-registry.test.ts +2053 -2053
  214. package/src/platform/registry/__tests__/validation.test.ts +1444 -1259
  215. package/src/platform/registry/command-view.ts +10 -10
  216. package/src/platform/registry/index.ts +103 -103
  217. package/src/platform/registry/resource-link.ts +32 -32
  218. package/src/platform/registry/resource-registry.ts +886 -886
  219. package/src/platform/registry/serialization.ts +295 -295
  220. package/src/platform/registry/serialized-types.ts +166 -166
  221. package/src/platform/registry/stats-types.ts +68 -68
  222. package/src/platform/registry/types.ts +425 -425
  223. package/src/platform/registry/validation.ts +876 -684
  224. package/src/platform/utils/__tests__/validation.test.ts +1084 -1084
  225. package/src/platform/utils/validation.ts +425 -425
  226. package/src/projects/api-schemas.test.ts +39 -39
  227. package/src/projects/api-schemas.ts +291 -291
  228. package/src/reference/_generated/contracts.md +2136 -2093
  229. package/src/reference/glossary.md +76 -76
  230. package/src/scaffold-registry/__tests__/index.test.ts +206 -206
  231. package/src/scaffold-registry/__tests__/schema.test.ts +166 -166
  232. package/src/scaffold-registry/index.ts +392 -392
  233. package/src/scaffold-registry/schema.ts +243 -243
  234. package/src/server.ts +289 -289
  235. package/src/supabase/database.types.ts +3 -0
  236. package/src/test-utils/README.md +37 -37
  237. package/src/test-utils/entities.ts +108 -108
  238. package/src/test-utils/fixtures/memberships.ts +82 -82
  239. package/src/test-utils/index.ts +12 -12
  240. package/src/test-utils/organization-model.ts +65 -65
  241. package/src/test-utils/published.ts +6 -6
  242. package/src/test-utils/rls/RLSTestContext.ts +588 -588
  243. package/src/test-utils/test-utils.test.ts +44 -44
@@ -1,890 +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 {
15
- OrganizationModel,
16
- OrganizationModelResources,
17
- OrganizationModelSystems
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
18
16
  } 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 {
17
+ import type { ResourceEntry } from '../../organization-model/domains/resources'
18
+ import type { SystemEntry } from '../../organization-model/domains/systems'
19
+ import { listAllSystems } from '../../organization-model/helpers'
20
+ import type {
21
+ ResourceStatus,
22
+ ResourceDefinition,
23
+ ResourceList,
24
+ TriggerDefinition,
25
+ IntegrationDefinition,
26
+ ResourceRelationships,
27
+ ExternalResourceDefinition,
28
+ HumanCheckpointDefinition
29
+ } from './types'
30
+ import type {
31
+ SerializedOrganizationData,
32
+ SerializedAgentDefinition,
33
+ SerializedWorkflowDefinition,
34
+ CommandViewData
35
+ } from './serialized-types'
36
+ import { validateDeploymentSpec, validateRelationships } from './validation'
37
+ import { serializeAllOrganizations, serializeOrganization } from './serialization'
38
+ import { isReservedResourceId } from './reserved'
39
+
40
+ /** Filter out archived resources from an organization's resource collection */
41
+ function filterArchived(org: DeploymentSpec): DeploymentSpec {
42
+ return {
43
+ ...org,
44
+ workflows: org.workflows?.filter((w) => !w.config.archived),
45
+ agents: org.agents?.filter((a) => !a.config.archived),
46
+ triggers: org.triggers?.filter((t) => !t.archived),
47
+ integrations: org.integrations?.filter((i) => !i.archived),
48
+ externalResources: org.externalResources?.filter((e) => !e.archived),
49
+ humanCheckpoints: org.humanCheckpoints?.filter((h) => !h.archived)
50
+ }
51
+ }
52
+
53
+ function summarizeSystem(system: SystemEntry | undefined): ResourceDefinition['system'] {
54
+ if (!system) return undefined
55
+
56
+ return {
57
+ id: system.id,
58
+ title: system.label ?? system.title,
59
+ description: system.description,
60
+ kind: system.kind,
61
+ lifecycle: system.lifecycle
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Configuration for a remotely-deployed organization
67
+ *
68
+ * Stored alongside runtime-registered organizations to support
69
+ * worker thread execution branching and credential management.
70
+ */
71
+ export interface RemoteOrgConfig {
72
+ /** Supabase Storage path: "{orgId}/{deploymentId}/bundle.js" */
73
+ storagePath: string
74
+ /** Deployment record ID */
75
+ deploymentId: string
76
+ /** OS temp path to bundle -- set after first download, used by worker threads */
77
+ cachedTempPath?: string
78
+ /** Platform tool name -> credential name mapping */
79
+ toolCredentials?: Record<string, string>
80
+ /** SDK version used to deploy this bundle */
81
+ sdkVersion?: string
82
+ /** Deployment version (semver) of the deployed bundle */
83
+ deploymentVersion?: string
84
+ }
85
+
86
+ /**
87
+ * Configuration for a first-class System resource.
88
+ *
89
+ * System resources are owned by the platform, registered under the 'system' org,
90
+ * and execute via the static-bundle loader mode in executeInWorker(). The moduleId
91
+ * maps to an entry in the API's STATIC_MODULE_MAP.
92
+ */
93
+ export interface SystemConfig {
94
+ kind: 'static'
95
+ moduleId: string
96
+ /** Always undefined for system resources; present for API compatibility with RemoteOrgConfig consumers */
97
+ sdkVersion?: never
98
+ }
99
+
100
+ /**
101
+ * Organization-specific resource collection
102
+ *
103
+ * Complete manifest of all automation resources for an organization.
104
+ * Used by ResourceRegistry for discovery and Command View for visualization.
105
+ */
106
+ export interface DeploymentSpec {
109
107
  /** Deployment version (semver) */
110
108
  version: string
111
109
  /** 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
- }
110
+ organizationModel?: Partial<
111
+ Pick<
112
+ OrganizationModel,
113
+ 'systems' | 'resources' | 'ontology' | 'topology' | 'roles' | 'policies' | 'entities' | 'actions'
114
+ >
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
+ }