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