@codemation/host 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. package/CHANGELOG.md +465 -0
  2. package/LICENSE +1 -37
  3. package/dist/{ApiPaths-CLTHphYZ.js → ApiPaths-Dv1dcHu_.js} +4 -4
  4. package/dist/ApiPaths-Dv1dcHu_.js.map +1 -0
  5. package/dist/{AppConfigFactory-CvpFScwB.js → AppConfigFactory-Cx4qQvRk.js} +114 -53
  6. package/dist/AppConfigFactory-Cx4qQvRk.js.map +1 -0
  7. package/dist/{AppConfigFactory-LK76niPc.d.ts → AppConfigFactory-DnLoQ9Li.d.ts} +8527 -5549
  8. package/dist/{AppContainerFactory-BlLrm6_h.js → AppContainerFactory-DqKYCRNP.js} +7656 -2090
  9. package/dist/AppContainerFactory-DqKYCRNP.js.map +1 -0
  10. package/dist/{CodemationAppContext-CvWi5gey.d.ts → CodemationAppContext-CKVv9W9q.d.ts} +8 -4
  11. package/dist/{CodemationAuthoring.types-BuKNTDC1.d.ts → CodemationAuthoring.types-DA3G3s6d.d.ts} +25 -5
  12. package/dist/{CodemationAuthoring.types-DZl-sJaM.js → CodemationAuthoring.types-NGkBcmmT.js} +18 -6
  13. package/dist/CodemationAuthoring.types-NGkBcmmT.js.map +1 -0
  14. package/dist/{CodemationConfigNormalizer-CYdR0PR5.d.ts → CodemationConfigNormalizer-BAKjetJ6.d.ts} +3 -3
  15. package/dist/{CodemationConsumerConfigLoader-BeAUS144.js → CodemationConsumerConfigLoader-GYpBBvqE.js} +79 -10
  16. package/dist/CodemationConsumerConfigLoader-GYpBBvqE.js.map +1 -0
  17. package/dist/{CodemationConsumerConfigLoader-C3nAj9Bj.d.ts → CodemationConsumerConfigLoader-nxOqvv46.d.ts} +17 -2
  18. package/dist/{CodemationPluginListMerger-B-W5Fa_X.js → CodemationPluginListMerger-D1B1IEbt.js} +1 -1
  19. package/dist/{CodemationPluginListMerger-B-W5Fa_X.js.map → CodemationPluginListMerger-D1B1IEbt.js.map} +1 -1
  20. package/dist/{CodemationPluginListMerger-D-gwVwtw.d.ts → CodemationPluginListMerger-DKLAHT2b.d.ts} +123 -16
  21. package/dist/CodemationTsyringeTypeInfoRegistrar-Bj6FJYFz.js +97 -0
  22. package/dist/CodemationTsyringeTypeInfoRegistrar-Bj6FJYFz.js.map +1 -0
  23. package/dist/{CodemationWhitelabelConfig-CWbcyQqn.d.ts → CodemationWhitelabelConfig-Ca2mCUeC.d.ts} +2 -2
  24. package/dist/{CollectionContracts.types-DdpHft0i.d.ts → CollectionContracts.types-DDyFYT_D.d.ts} +1 -1
  25. package/dist/{CredentialContractsRegistry-D7mcPed2.d.ts → CredentialContractsRegistry-Bq2bq28t.d.ts} +2 -2
  26. package/dist/{CredentialServices-DdCEP2xt.d.ts → CredentialServices-Be2I60Th.d.ts} +65 -20
  27. package/dist/{CredentialServices-CgxwguAv.js → CredentialServices-Dk8yypeL.js} +310 -51
  28. package/dist/CredentialServices-Dk8yypeL.js.map +1 -0
  29. package/dist/InternalHonoApiRouteRegistrar-Ce1yxpnO.d.ts +17 -0
  30. package/dist/InternalPingRegistrar-DY3kSfxP.js +221 -0
  31. package/dist/InternalPingRegistrar-DY3kSfxP.js.map +1 -0
  32. package/dist/{ItemsInputNormalizer-D1WppVMU.d.ts → ItemsInputNormalizer-_RwIfRIQ.d.ts} +108 -25
  33. package/dist/{LogLevelPolicyFactory-CampWO0l.d.ts → LogLevelPolicyFactory-ewCHLDLn.d.ts} +2 -2
  34. package/dist/{PublicFrontendBootstrap-DzBgwOnG.d.ts → PublicFrontendBootstrap-Cev3qK46.d.ts} +9 -2
  35. package/dist/PublicFrontendBootstrapFactory-CY2FS-5g.d.ts +82 -0
  36. package/dist/{PublicFrontendBootstrapJsonCodec-Cl_DLRh0.d.ts → PublicFrontendBootstrapJsonCodec-CXG9Dxft.d.ts} +3 -3
  37. package/dist/{PublicFrontendBootstrapJsonCodec-DzqvA0uo.js → PublicFrontendBootstrapJsonCodec-CegIF_ne.js} +7 -2
  38. package/dist/PublicFrontendBootstrapJsonCodec-CegIF_ne.js.map +1 -0
  39. package/dist/ServerLoggerFactory-Ckk52S3w.js +223 -0
  40. package/dist/ServerLoggerFactory-Ckk52S3w.js.map +1 -0
  41. package/dist/{TelemetryContracts-BsOD_Y17.d.ts → TelemetryContracts-BtDx84Cp.d.ts} +13 -4
  42. package/dist/{WorkflowPolicyUiPresentationFactory-DNE5oAI6.d.ts → WorkflowPolicyUiPresentationFactory-6MyjCvBO.d.ts} +2 -2
  43. package/dist/{WorkflowPolicyUiPresentationFactory-DhPqQ9aB.js → WorkflowPolicyUiPresentationFactory-Bb-ae_Zh.js} +1 -1
  44. package/dist/{WorkflowPolicyUiPresentationFactory-DhPqQ9aB.js.map → WorkflowPolicyUiPresentationFactory-Bb-ae_Zh.js.map} +1 -1
  45. package/dist/{WorkflowViewContracts-0ZgsHQdp.d.ts → WorkflowViewContracts-B7aFQcIw.d.ts} +15 -1
  46. package/dist/authoring.d.ts +5 -5
  47. package/dist/authoring.js +1 -1
  48. package/dist/client.d.ts +4 -4
  49. package/dist/client.js +2 -2
  50. package/dist/consumer.d.ts +6 -6
  51. package/dist/consumer.js +2 -2
  52. package/dist/credentials.d.ts +6 -6
  53. package/dist/credentials.js +1 -1
  54. package/dist/devServerSidecar.d.ts +2 -2
  55. package/dist/devServerSidecar.js +1 -94
  56. package/dist/devServerSidecar.js.map +1 -1
  57. package/dist/dto.d.ts +6 -6
  58. package/dist/{index-BlGs9e9Q.d.ts → index-DilAYwnH.d.ts} +49 -3
  59. package/dist/index.d.ts +110 -21
  60. package/dist/index.js +15 -13
  61. package/dist/mapping.d.ts +2 -2
  62. package/dist/mapping.js +1 -1
  63. package/dist/nextServer.d.ts +43 -88
  64. package/dist/nextServer.js +9 -7
  65. package/dist/pairing.d.ts +93 -0
  66. package/dist/pairing.js +5 -0
  67. package/dist/pairing.types-snfZ_OzB.d.ts +19 -0
  68. package/dist/{persistenceServer-CpNFYa_q.js → persistenceServer-C-hH4z6l.js} +2 -2
  69. package/dist/{persistenceServer-CpNFYa_q.js.map → persistenceServer-C-hH4z6l.js.map} +1 -1
  70. package/dist/persistenceServer-CeTHtC6E.d.ts +30 -0
  71. package/dist/persistenceServer.d.ts +8 -8
  72. package/dist/persistenceServer.js +3 -3
  73. package/dist/{server-CQWdkT7t.d.ts → server-C4bS62rg.d.ts} +21 -6
  74. package/dist/{server-BK43OKxW.js → server-Y7kxwtCK.js} +7 -6
  75. package/dist/{server-BK43OKxW.js.map → server-Y7kxwtCK.js.map} +1 -1
  76. package/dist/server.d.ts +14 -14
  77. package/dist/server.js +13 -11
  78. package/package.json +29 -42
  79. package/prisma/migrations/20260507120000_execution_instance_child_run_id/migration.sql +5 -0
  80. package/prisma/migrations/20260519000000_workflow_audit_log/migration.sql +23 -0
  81. package/prisma/migrations/20260519100000_storage_growth_fixes/migration.sql +61 -0
  82. package/prisma/migrations.sqlite/20260507120000_execution_instance_child_run_id/migration.sql +5 -0
  83. package/prisma/migrations.sqlite/20260519000000_workflow_audit_log/migration.sql +21 -0
  84. package/prisma/migrations.sqlite/20260519100000_storage_growth_fixes/migration.sql +29 -0
  85. package/prisma/schema.postgresql.prisma +56 -17
  86. package/prisma/schema.sqlite.prisma +56 -17
  87. package/prisma-generated/prisma-postgresql-client/edge.js +35 -6
  88. package/prisma-generated/prisma-postgresql-client/index-browser.js +31 -2
  89. package/prisma-generated/prisma-postgresql-client/index.d.ts +8971 -5718
  90. package/prisma-generated/prisma-postgresql-client/index.js +35 -6
  91. package/prisma-generated/prisma-postgresql-client/package.json +1 -1
  92. package/prisma-generated/prisma-postgresql-client/schema.prisma +39 -0
  93. package/prisma-generated/prisma-sqlite-client/edge.js +35 -6
  94. package/prisma-generated/prisma-sqlite-client/index-browser.js +31 -2
  95. package/prisma-generated/prisma-sqlite-client/index.d.ts +8963 -5715
  96. package/prisma-generated/prisma-sqlite-client/index.js +35 -6
  97. package/prisma-generated/prisma-sqlite-client/package.json +1 -1
  98. package/prisma-generated/prisma-sqlite-client/schema.prisma +39 -0
  99. package/scripts/check-collections.mjs +18 -0
  100. package/scripts/generate-prisma-clients.mjs +20 -11
  101. package/src/application/WorkflowAuditLogPruneScheduler.ts +96 -0
  102. package/src/application/auth/AuthenticatedPrincipal.ts +4 -0
  103. package/src/application/commands/StartWorkflowRunCommandHandler.ts +4 -0
  104. package/src/application/contracts/WorkflowViewContracts.ts +11 -0
  105. package/src/application/contracts/WorkflowWebsocketMessage.ts +3 -1
  106. package/src/application/mapping/WorkflowDefinitionMapper.ts +44 -1
  107. package/src/application/runs/WorkflowRunRetentionPruneScheduler.ts +7 -1
  108. package/src/application/telemetry/OtelExecutionTelemetry.types.ts +5 -0
  109. package/src/application/telemetry/OtelExecutionTelemetryFactory.ts +4 -0
  110. package/src/application/telemetry/StoredTelemetrySpanScope.ts +6 -2
  111. package/src/application/telemetry/TelemetryRetentionTimestampFactory.ts +27 -17
  112. package/src/application/telemetry/TelemetrySpanPublisher.ts +11 -0
  113. package/src/application/websocket/TelemetrySpanWebsocketRelay.ts +31 -0
  114. package/src/applicationTokens.ts +20 -1
  115. package/src/audit/IAuditEmitter.ts +32 -0
  116. package/src/audit/PrismaWorkflowAuditLogRepository.ts +34 -0
  117. package/src/audit/WorkflowAuditLogWriter.ts +125 -0
  118. package/src/auth/managed/ManagedAuthConfig.ts +29 -0
  119. package/src/auth/managed/ManagedAuthMiddleware.ts +52 -0
  120. package/src/auth/managed/ManagedCorsMiddleware.ts +43 -0
  121. package/src/auth/managed/ManagedModeBootGuard.ts +27 -0
  122. package/src/auth/managed/index.ts +5 -0
  123. package/src/bootstrap/AppContainerFactory.ts +277 -29
  124. package/src/bootstrap/AppContainerLifecycle.ts +31 -0
  125. package/src/bootstrap/perf/BootTimer.ts +168 -0
  126. package/src/bootstrap/runtime/AppConfigFactory.ts +21 -65
  127. package/src/bootstrap/runtime/FrontendRuntime.ts +4 -1
  128. package/src/bootstrap/runtime/WorkerRuntime.ts +2 -1
  129. package/src/credentials/BrokerClient.ts +49 -0
  130. package/src/credentials/BrokerRefreshError.ts +12 -0
  131. package/src/credentials/BrokerRefreshInvalidGrantError.ts +13 -0
  132. package/src/credentials/ControlPlaneCatalogFetcher.ts +261 -0
  133. package/src/credentials/CredentialOAuth2MaterialReader.ts +136 -0
  134. package/src/credentials/InternalCredentialsListRegistrar.ts +48 -0
  135. package/src/credentials/InternalCredentialsPushRegistrar.ts +125 -0
  136. package/src/credentials/LocalOAuthFlowExecutor.ts +316 -0
  137. package/src/credentials/ManagedOAuthFlowExecutor.ts +94 -0
  138. package/src/credentials/ManagedOAuthRefreshInvalidGrantError.ts +13 -0
  139. package/src/credentials/catalogTypes.ts +4 -0
  140. package/src/credentials/refresh/CredentialDisconnectedError.ts +11 -0
  141. package/src/domain/credentials/CredentialBindingService.ts +54 -2
  142. package/src/domain/credentials/CredentialKeyRotatedError.ts +22 -0
  143. package/src/domain/credentials/CredentialSecretCipher.ts +68 -6
  144. package/src/domain/credentials/CredentialTypeRegistryImpl.ts +117 -10
  145. package/src/domain/credentials/OAuth2RedirectUriResolver.ts +79 -0
  146. package/src/domain/credentials/WorkflowCredentialNodeResolver.ts +14 -5
  147. package/src/domain/telemetry/TelemetryContracts.ts +7 -1
  148. package/src/domain/workflows/WorkflowActivationPreflight.ts +24 -1
  149. package/src/domain/workflows/WorkflowActivationPreflightRules.ts +40 -1
  150. package/src/index.ts +6 -0
  151. package/src/infrastructure/binary/LocalFilesystemBinaryStorageRegistry.ts +29 -1
  152. package/src/infrastructure/binary/S3BinaryStorage.ts +169 -0
  153. package/src/infrastructure/binary/S3BinaryStorageConfig.ts +17 -0
  154. package/src/infrastructure/config/CodemationPluginRegistrar.ts +3 -1
  155. package/src/infrastructure/persistence/CodemationDatabaseUrlParser.ts +41 -0
  156. package/src/infrastructure/persistence/InMemoryTelemetryArtifactStore.ts +8 -3
  157. package/src/infrastructure/persistence/InMemoryWorkflowRunRepository.ts +1 -0
  158. package/src/infrastructure/persistence/PrismaMigrationDeployer.ts +21 -13
  159. package/src/infrastructure/persistence/PrismaTelemetryArtifactStore.ts +43 -8
  160. package/src/infrastructure/persistence/PrismaWorkflowRunRepository.ts +33 -3
  161. package/src/infrastructure/persistence/PrismaWorkflowSnapshotRepository.ts +48 -0
  162. package/src/mcp/AgentMcpIntegrationImpl.ts +344 -0
  163. package/src/mcp/McpClientFactory.ts +29 -0
  164. package/src/mcp/McpConnectionPool.ts +184 -0
  165. package/src/mcp/McpConnectionPool.types.ts +12 -0
  166. package/src/mcp/McpServerCatalog.ts +104 -0
  167. package/src/mcp/index.ts +5 -0
  168. package/src/pairing/HmacRequestSigner.ts +32 -0
  169. package/src/pairing/IncomingHmacVerifier.ts +82 -0
  170. package/src/pairing/InternalHmacAuthMiddleware.ts +33 -0
  171. package/src/pairing/InternalPingRegistrar.ts +25 -0
  172. package/src/pairing/PairedFetch.ts +33 -0
  173. package/src/pairing/PairingConfigFactory.ts +35 -0
  174. package/src/pairing/PairingConfigToken.ts +6 -0
  175. package/src/pairing/index.ts +14 -0
  176. package/src/pairing/pairing.types.ts +18 -0
  177. package/src/pairing.ts +17 -0
  178. package/src/persistenceServer.ts +1 -0
  179. package/src/presentation/config/AppConfig.ts +7 -1
  180. package/src/presentation/config/CodemationAuthConfig.ts +1 -1
  181. package/src/presentation/config/CodemationAuthoring.types.ts +54 -5
  182. package/src/presentation/config/CodemationConfig.ts +3 -0
  183. package/src/presentation/config/CodemationConfigNormalizer.ts +39 -1
  184. package/src/presentation/config/CodemationPlugin.ts +2 -1
  185. package/src/presentation/frontend/CodemationFrontendAuthSnapshot.ts +5 -0
  186. package/src/presentation/frontend/CodemationFrontendAuthSnapshotFactory.ts +7 -1
  187. package/src/presentation/frontend/PublicFrontendBootstrap.ts +2 -0
  188. package/src/presentation/frontend/PublicFrontendBootstrapFactory.ts +5 -1
  189. package/src/presentation/frontend/PublicFrontendBootstrapJsonCodec.ts +4 -1
  190. package/src/presentation/http/ApiPaths.ts +4 -4
  191. package/src/presentation/http/ServerHttpErrorResponseFactory.ts +39 -2
  192. package/src/presentation/http/hono/CodemationHonoApiAppFactory.ts +33 -8
  193. package/src/presentation/http/hono/InternalHonoApiRouteRegistrar.ts +12 -0
  194. package/src/presentation/http/hono/registrars/ManagedMeHonoApiRouteRegistrar.ts +35 -0
  195. package/src/presentation/http/hono/registrars/OAuth2HonoApiRouteRegistrar.ts +2 -2
  196. package/src/presentation/http/routeHandlers/CredentialHttpRouteHandler.ts +28 -0
  197. package/src/presentation/http/routeHandlers/OAuth2HttpRouteHandlerFactory.ts +98 -41
  198. package/src/presentation/server/CodemationConsumerConfigLoader.ts +54 -7
  199. package/src/presentation/server/CodemationPluginDiscovery.ts +5 -0
  200. package/src/presentation/server/WorkflowDefinitionExportsResolver.ts +18 -0
  201. package/src/presentation/server/WorkflowModulePathFinder.ts +12 -1
  202. package/src/presentation/websocket/ManagedWebsocketAuthenticator.ts +50 -0
  203. package/src/presentation/websocket/WebsocketAuthenticator.types.ts +12 -0
  204. package/src/presentation/websocket/WorkflowWebsocketServer.ts +24 -3
  205. package/src/process/ExecaProcessRunner.ts +41 -0
  206. package/src/process/ProcessRunner.types.ts +39 -0
  207. package/src/server.ts +2 -0
  208. package/src/workflows/InternalWorkflowActivationRegistrar.ts +42 -0
  209. package/src/workflows/InternalWorkflowDetailRegistrar.ts +33 -0
  210. package/src/workflows/InternalWorkflowTestRunRegistrar.ts +91 -0
  211. package/src/workflows/InternalWorkflowsListRegistrar.ts +28 -0
  212. package/src/workflows/discovery/WorkflowDirectoryDiscoverer.ts +79 -0
  213. package/tsconfig.json +2 -0
  214. package/vitest.shared.ts +5 -0
  215. package/dist/ApiPaths-CLTHphYZ.js.map +0 -1
  216. package/dist/AppConfigFactory-CvpFScwB.js.map +0 -1
  217. package/dist/AppContainerFactory-BlLrm6_h.js.map +0 -1
  218. package/dist/CodemationAuthoring.types-DZl-sJaM.js.map +0 -1
  219. package/dist/CodemationConsumerConfigLoader-BeAUS144.js.map +0 -1
  220. package/dist/CredentialServices-CgxwguAv.js.map +0 -1
  221. package/dist/PublicFrontendBootstrapFactory-BMWqNM9a.d.ts +0 -45
  222. package/dist/PublicFrontendBootstrapJsonCodec-DzqvA0uo.js.map +0 -1
  223. package/dist/ServerLoggerFactory-BKSIh9Xv.js +0 -98
  224. package/dist/ServerLoggerFactory-BKSIh9Xv.js.map +0 -1
  225. package/dist/persistenceServer-CIVt3UOX.d.ts +0 -9
  226. package/src/domain/credentials/OAuth2ConnectServiceFactory.ts +0 -411
@@ -22,6 +22,8 @@ import { inject, injectable } from "@codemation/core";
22
22
  import type { WorkflowRunRepository } from "../../domain/runs/WorkflowRunRepository";
23
23
  import type { Prisma } from "../../../prisma-generated/prisma-postgresql-client/client.js";
24
24
  import { PrismaDatabaseClientToken, type PrismaDatabaseClient } from "./PrismaDatabaseClient";
25
+ import type { WorkflowSnapshotRepository } from "./PrismaWorkflowSnapshotRepository";
26
+ import { PrismaWorkflowSnapshotRepository } from "./PrismaWorkflowSnapshotRepository";
25
27
 
26
28
  type ExecutionInstanceRow = {
27
29
  instanceId: string;
@@ -52,6 +54,7 @@ type ExecutionInstanceRow = {
52
54
  iterationId: string | null;
53
55
  itemIndex: number | null;
54
56
  parentInvocationId: string | null;
57
+ childRunId: string | null;
55
58
  };
56
59
 
57
60
  type RunWorkItemRecord = {
@@ -79,7 +82,10 @@ type RunSlotProjectionRow = {
79
82
 
80
83
  @injectable()
81
84
  export class PrismaWorkflowRunRepository implements WorkflowRunRepository, WorkflowExecutionRepository {
82
- constructor(@inject(PrismaDatabaseClientToken) private readonly prisma: PrismaDatabaseClient) {}
85
+ constructor(
86
+ @inject(PrismaDatabaseClientToken) private readonly prisma: PrismaDatabaseClient,
87
+ @inject(PrismaWorkflowSnapshotRepository) private readonly snapshotRepo: WorkflowSnapshotRepository,
88
+ ) {}
83
89
 
84
90
  async createRun(args: {
85
91
  runId: RunId;
@@ -95,6 +101,14 @@ export class PrismaWorkflowRunRepository implements WorkflowRunRepository, Workf
95
101
  }): Promise<void> {
96
102
  const now = new Date().toISOString();
97
103
  const testContext = args.executionOptions?.testContext;
104
+ const snapshotJson = args.workflowSnapshot ? JSON.stringify(args.workflowSnapshot) : null;
105
+ const workflowSnapshotId =
106
+ snapshotJson !== null
107
+ ? await this.snapshotRepo.findOrCreate({
108
+ workflowId: args.workflowId,
109
+ snapshotJson,
110
+ })
111
+ : null;
98
112
  await this.prisma.run.create({
99
113
  data: {
100
114
  runId: args.runId,
@@ -107,7 +121,8 @@ export class PrismaWorkflowRunRepository implements WorkflowRunRepository, Workf
107
121
  revision: 0,
108
122
  outputsByNodeJson: JSON.stringify({}),
109
123
  controlJson: args.control ? JSON.stringify(args.control) : null,
110
- workflowSnapshotJson: args.workflowSnapshot ? JSON.stringify(args.workflowSnapshot) : null,
124
+ workflowSnapshotJson: snapshotJson,
125
+ workflowSnapshotId,
111
126
  policySnapshotJson: args.policySnapshot ? JSON.stringify(args.policySnapshot) : null,
112
127
  engineCountersJson: args.engineCounters ? JSON.stringify(args.engineCounters) : null,
113
128
  mutableStateJson: args.mutableState ? JSON.stringify(args.mutableState) : null,
@@ -289,6 +304,14 @@ export class PrismaWorkflowRunRepository implements WorkflowRunRepository, Workf
289
304
  const workItems = this.buildWorkItems(state, now);
290
305
  const instances = this.buildExecutionInstances(state);
291
306
  const projectionJson = this.buildProjectionSlotStatesJson(state);
307
+ const snapshotJson = state.workflowSnapshot ? JSON.stringify(state.workflowSnapshot) : null;
308
+ const workflowSnapshotId =
309
+ snapshotJson !== null
310
+ ? await this.snapshotRepo.findOrCreate({
311
+ workflowId: state.workflowId,
312
+ snapshotJson,
313
+ })
314
+ : null;
292
315
 
293
316
  await this.prisma.$transaction(async (tx) => {
294
317
  await tx.runWorkItem.deleteMany({ where: { runId: state.runId } });
@@ -345,6 +368,7 @@ export class PrismaWorkflowRunRepository implements WorkflowRunRepository, Workf
345
368
  inputTruncated: instance.inputTruncated,
346
369
  outputTruncated: instance.outputTruncated,
347
370
  usedPinnedOutput: instance.usedPinnedOutput,
371
+ childRunId: instance.childRunId,
348
372
  },
349
373
  });
350
374
  continue;
@@ -387,7 +411,8 @@ export class PrismaWorkflowRunRepository implements WorkflowRunRepository, Workf
387
411
  parentJson: state.parent ? JSON.stringify(state.parent) : null,
388
412
  executionOptionsJson: state.executionOptions ? JSON.stringify(state.executionOptions) : null,
389
413
  controlJson: state.control ? JSON.stringify(state.control) : null,
390
- workflowSnapshotJson: state.workflowSnapshot ? JSON.stringify(state.workflowSnapshot) : null,
414
+ workflowSnapshotJson: snapshotJson,
415
+ workflowSnapshotId,
391
416
  policySnapshotJson: state.policySnapshot ? JSON.stringify(state.policySnapshot) : null,
392
417
  engineCountersJson: state.engineCounters ? JSON.stringify(state.engineCounters) : null,
393
418
  mutableStateJson: state.mutableState ? JSON.stringify(state.mutableState) : null,
@@ -510,6 +535,9 @@ export class PrismaWorkflowRunRepository implements WorkflowRunRepository, Workf
510
535
  inputsByPort,
511
536
  outputs,
512
537
  error,
538
+ ...(row.childRunId !== null && row.childRunId !== undefined
539
+ ? { childRunId: row.childRunId as NodeExecutionSnapshot["childRunId"] }
540
+ : {}),
513
541
  };
514
542
  }
515
543
 
@@ -564,6 +592,7 @@ export class PrismaWorkflowRunRepository implements WorkflowRunRepository, Workf
564
592
  iterationId: row.iterationId ?? undefined,
565
593
  itemIndex: row.itemIndex ?? undefined,
566
594
  parentInvocationId: row.parentInvocationId ?? undefined,
595
+ ...(row.childRunId !== null && row.childRunId !== undefined ? { childRunId: row.childRunId } : {}),
567
596
  };
568
597
  }
569
598
 
@@ -636,6 +665,7 @@ export class PrismaWorkflowRunRepository implements WorkflowRunRepository, Workf
636
665
  inputStorageKind: "inline",
637
666
  outputStorageKind: "inline",
638
667
  usedPinnedOutput: snap.usedPinnedOutput ?? null,
668
+ childRunId: snap.childRunId ?? null,
639
669
  });
640
670
  }
641
671
  let cIdx = 0;
@@ -0,0 +1,48 @@
1
+ import { createHash } from "node:crypto";
2
+ import { inject, injectable } from "@codemation/core";
3
+ import { PrismaDatabaseClientToken, type PrismaDatabaseClient } from "./PrismaDatabaseClient";
4
+
5
+ export interface WorkflowSnapshotRepository {
6
+ /**
7
+ * Returns the id of an existing snapshot matching (workflowId, snapshotHash), or creates
8
+ * a new one from the provided snapshotJson. Deduplication is by content hash.
9
+ */
10
+ findOrCreate(args: Readonly<{ workflowId: string; snapshotJson: string }>): Promise<string>;
11
+ }
12
+
13
+ @injectable()
14
+ export class PrismaWorkflowSnapshotRepository implements WorkflowSnapshotRepository {
15
+ constructor(
16
+ @inject(PrismaDatabaseClientToken)
17
+ private readonly prisma: PrismaDatabaseClient,
18
+ ) {}
19
+
20
+ async findOrCreate(args: Readonly<{ workflowId: string; snapshotJson: string }>): Promise<string> {
21
+ const snapshotHash = createHash("sha256").update(args.snapshotJson, "utf8").digest("hex");
22
+ const existing = await this.prisma.workflowSnapshot.findUnique({
23
+ where: { workflowId_snapshotHash: { workflowId: args.workflowId, snapshotHash } },
24
+ select: { id: true },
25
+ });
26
+ if (existing) {
27
+ return existing.id;
28
+ }
29
+ const id = crypto.randomUUID();
30
+ await this.prisma.workflowSnapshot.upsert({
31
+ where: { workflowId_snapshotHash: { workflowId: args.workflowId, snapshotHash } },
32
+ create: {
33
+ id,
34
+ workflowId: args.workflowId,
35
+ snapshotHash,
36
+ snapshotJson: args.snapshotJson,
37
+ createdAt: new Date().toISOString(),
38
+ },
39
+ update: {},
40
+ });
41
+ // Re-fetch so the returned id is the winner under concurrent inserts
42
+ const row = await this.prisma.workflowSnapshot.findUniqueOrThrow({
43
+ where: { workflowId_snapshotHash: { workflowId: args.workflowId, snapshotHash } },
44
+ select: { id: true },
45
+ });
46
+ return row.id;
47
+ }
48
+ }
@@ -0,0 +1,344 @@
1
+ import type { ToolSet } from "ai";
2
+ import {
3
+ AgentBindError,
4
+ CodemationTelemetryAttributeNames,
5
+ ConnectionInvocationIdFactory,
6
+ ConnectionNodeIdFactory,
7
+ inject,
8
+ injectable,
9
+ type AgentMcpIntegration,
10
+ type AgentMcpToolMap,
11
+ type ConnectionInvocationAppendArgs,
12
+ type JsonValue,
13
+ type McpServerDeclaration,
14
+ type NeedsReconsentEvent,
15
+ type NodeActivationId,
16
+ type NodeIterationId,
17
+ type ConnectionInvocationId,
18
+ type TelemetrySpanEventRecord,
19
+ } from "@codemation/core";
20
+ import { ApplicationTokens } from "../applicationTokens";
21
+ import type { LoggerFactory } from "../application/logging/Logger";
22
+ import { McpServerCatalog } from "./McpServerCatalog";
23
+ import { McpConnectionPool } from "./McpConnectionPool";
24
+ import type { CredentialStore } from "../domain/credentials/CredentialServices";
25
+
26
+ /**
27
+ * Host-side implementation of AgentMcpIntegration.
28
+ *
29
+ * Resolves the credential binding for each declared MCP server via the standard
30
+ * credential-binding table — the binding lives on the MCP connection node itself
31
+ * (slot key `"credential"`), matching ChatModel/Tool connection nodes. Opens pool
32
+ * connections and returns a ToolSet map with execute callbacks wrapped for
33
+ * telemetry + 403 detection.
34
+ */
35
+ @injectable()
36
+ export class AgentMcpIntegrationImpl implements AgentMcpIntegration {
37
+ constructor(
38
+ @inject(McpServerCatalog) private readonly catalog: McpServerCatalog,
39
+ @inject(McpConnectionPool) private readonly pool: McpConnectionPool,
40
+ @inject(ApplicationTokens.CredentialStore) private readonly credentialStore: CredentialStore,
41
+ @inject(ApplicationTokens.LoggerFactory) private readonly loggers: LoggerFactory,
42
+ ) {}
43
+
44
+ async prepareMcpTools(args: Parameters<AgentMcpIntegration["prepareMcpTools"]>[0]): Promise<AgentMcpToolMap> {
45
+ const {
46
+ workflowId,
47
+ agentNodeId,
48
+ serverIds,
49
+ pinnedMcpTools: _pinnedMcpTools,
50
+ emitSpanEvent,
51
+ startChildSpan,
52
+ appendMcpInvocation,
53
+ parentAgentActivationId,
54
+ iterationId,
55
+ itemIndex,
56
+ parentInvocationId,
57
+ } = args;
58
+
59
+ const result = new Map<string, Readonly<Record<string, unknown>>>();
60
+ const logger = this.loggers.create("AgentMcpIntegrationImpl");
61
+
62
+ for (const serverId of serverIds) {
63
+ const decl = this.catalog.get(serverId);
64
+ if (!decl) {
65
+ throw new AgentBindError(`MCP server "${serverId}" not found in catalog`);
66
+ }
67
+
68
+ const credentialInstanceId = await this.resolveCredentialInstanceId(workflowId, agentNodeId, serverId);
69
+
70
+ // Validate scopes before opening the connection.
71
+ await this.validateScopes(decl, credentialInstanceId);
72
+
73
+ // Lazy-open via pool (single-flight, cached after first open).
74
+ await this.pool.getClient(credentialInstanceId, serverId);
75
+
76
+ // Fetch tool list from pool (cached after first fetch).
77
+ const rawTools = await this.pool.getTools(credentialInstanceId, serverId);
78
+
79
+ // Wrap each tool's execute for telemetry and 403 detection.
80
+ const wrappedTools = this.wrapToolExecutes({
81
+ tools: rawTools as ToolSet,
82
+ serverId,
83
+ credentialInstanceId,
84
+ agentNodeId,
85
+ emitSpanEvent,
86
+ startChildSpan,
87
+ logger,
88
+ appendMcpInvocation,
89
+ parentAgentActivationId,
90
+ iterationId,
91
+ itemIndex,
92
+ parentInvocationId,
93
+ });
94
+
95
+ result.set(serverId, wrappedTools as unknown as Readonly<Record<string, unknown>>);
96
+ }
97
+
98
+ return result;
99
+ }
100
+
101
+ /**
102
+ * Looks up the credential binding for the MCP connection node and verifies the
103
+ * referenced credential instance still exists.
104
+ */
105
+ private async resolveCredentialInstanceId(workflowId: string, agentNodeId: string, serverId: string): Promise<string> {
106
+ const mcpNodeId = ConnectionNodeIdFactory.mcpConnectionNodeId(agentNodeId, serverId);
107
+ const binding = await this.credentialStore.getBinding({ workflowId, nodeId: mcpNodeId, slotKey: "credential" });
108
+ if (!binding) {
109
+ throw new AgentBindError(
110
+ `MCP server "${serverId}" has no credential bound on connection node "${mcpNodeId}". ` +
111
+ `Bind a credential instance via the canvas credential dropdown before activation.`,
112
+ );
113
+ }
114
+ const instance = await this.credentialStore.getInstance(binding.instanceId);
115
+ if (!instance) {
116
+ throw new AgentBindError(
117
+ `Credential instance "${binding.instanceId}" not found for mcpServer "${serverId}" (connection node "${mcpNodeId}")`,
118
+ );
119
+ }
120
+ return instance.instanceId;
121
+ }
122
+
123
+ /**
124
+ * Validates that the credential instance's granted scopes cover the server's requiredScopes.
125
+ * Scopes are read from the OAuth2 material record (populated by the broker push endpoint).
126
+ */
127
+ private async validateScopes(decl: McpServerDeclaration, credentialInstanceId: string): Promise<void> {
128
+ if (!decl.requiredScopes?.length) {
129
+ return;
130
+ }
131
+
132
+ const material = await this.credentialStore.getOAuth2Material(credentialInstanceId);
133
+ const grantedScopes = new Set(material?.scopes ?? []);
134
+ const missing = decl.requiredScopes.filter((s) => !grantedScopes.has(s));
135
+
136
+ if (missing.length > 0) {
137
+ throw new AgentBindError(
138
+ `Credential instance "${credentialInstanceId}" lacks required scopes for server "${decl.id}": ${missing.join(", ")}. ` +
139
+ `Reconnect the credential to grant the missing scopes.`,
140
+ );
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Returns a new ToolSet where each tool's execute callback is replaced with a wrapped version
146
+ * that:
147
+ * - Opens a child telemetry span tagged with mcp.server_id and mcp.tool_name.
148
+ * - Calls the original tool's execute (from @ai-sdk/mcp), which internally calls the MCP server.
149
+ * - On 403 / permission errors: emits a NeedsReconsentEvent span event, closes the span with
150
+ * error status, and re-throws a descriptive error. The agent turn continues for other tools.
151
+ */
152
+ private wrapToolExecutes(args: {
153
+ tools: ToolSet;
154
+ serverId: string;
155
+ credentialInstanceId: string;
156
+ agentNodeId: string;
157
+ emitSpanEvent: (event: TelemetrySpanEventRecord) => void;
158
+ startChildSpan: (args: { name: string; attributes?: Record<string, string> }) => {
159
+ end: (args?: { status?: "ok" | "error"; statusMessage?: string }) => void;
160
+ };
161
+ logger: ReturnType<LoggerFactory["create"]>;
162
+ appendMcpInvocation?: (args: ConnectionInvocationAppendArgs) => Promise<void>;
163
+ parentAgentActivationId?: NodeActivationId;
164
+ iterationId?: NodeIterationId;
165
+ itemIndex?: number;
166
+ parentInvocationId?: ConnectionInvocationId;
167
+ }): ToolSet {
168
+ const {
169
+ tools,
170
+ serverId,
171
+ credentialInstanceId,
172
+ agentNodeId,
173
+ emitSpanEvent,
174
+ startChildSpan,
175
+ logger,
176
+ appendMcpInvocation,
177
+ parentAgentActivationId,
178
+ iterationId,
179
+ itemIndex,
180
+ parentInvocationId,
181
+ } = args;
182
+ const wrapped: Record<string, ToolSet[string]> = {};
183
+ const checkPermissionError = (err: unknown): boolean => this.isPermissionError(err);
184
+ const connectionNodeId = ConnectionNodeIdFactory.mcpConnectionNodeId(agentNodeId, serverId);
185
+
186
+ for (const [toolName, toolDef] of Object.entries(tools)) {
187
+ const originalExecute = (toolDef as { execute?: (input: unknown) => Promise<unknown> }).execute;
188
+ const wrappedDef = {
189
+ ...toolDef,
190
+ execute: async (input: unknown): Promise<unknown> => {
191
+ const span = startChildSpan({
192
+ name: "mcp.tool_call",
193
+ attributes: {
194
+ [CodemationTelemetryAttributeNames.mcpServerId]: serverId,
195
+ [CodemationTelemetryAttributeNames.mcpToolName]: toolName,
196
+ },
197
+ });
198
+ const invocationId = ConnectionInvocationIdFactory.create();
199
+ const startedAtIso = new Date().toISOString();
200
+ const baseRecord = {
201
+ invocationId,
202
+ connectionNodeId,
203
+ parentAgentNodeId: agentNodeId,
204
+ parentAgentActivationId: parentAgentActivationId ?? agentNodeId,
205
+ iterationId,
206
+ itemIndex,
207
+ parentInvocationId,
208
+ subjectName: toolName,
209
+ };
210
+ const summarizedInput = this.summarizeForInvocation(input);
211
+ if (appendMcpInvocation) {
212
+ await appendMcpInvocation({
213
+ ...baseRecord,
214
+ status: "running",
215
+ managedInput: summarizedInput,
216
+ queuedAt: startedAtIso,
217
+ startedAt: startedAtIso,
218
+ statusLabel: `calling ${toolName}`,
219
+ });
220
+ }
221
+ try {
222
+ if (!originalExecute) {
223
+ throw new Error(`MCP tool "${toolName}" on server "${serverId}" has no execute callback`);
224
+ }
225
+ const result = await originalExecute(input);
226
+ span.end({ status: "ok" });
227
+ if (appendMcpInvocation) {
228
+ const finishedAtIso = new Date().toISOString();
229
+ await appendMcpInvocation({
230
+ ...baseRecord,
231
+ status: "completed",
232
+ managedInput: summarizedInput,
233
+ managedOutput: this.summarizeForInvocation(result),
234
+ queuedAt: startedAtIso,
235
+ startedAt: startedAtIso,
236
+ finishedAt: finishedAtIso,
237
+ });
238
+ }
239
+ return result;
240
+ } catch (error) {
241
+ if (checkPermissionError(error)) {
242
+ const event: NeedsReconsentEvent = {
243
+ serverId,
244
+ credentialInstanceId,
245
+ };
246
+ const spanEvent: TelemetrySpanEventRecord = {
247
+ name: "mcp.needs_reconsent",
248
+ attributes: {
249
+ "mcp.server_id": serverId,
250
+ "mcp.credential_instance_id": credentialInstanceId,
251
+ },
252
+ };
253
+ emitSpanEvent(spanEvent);
254
+ span.end({ status: "error", statusMessage: "MCP tool permission error" });
255
+ logger.warn(
256
+ `AgentMcpIntegrationImpl: permission error from MCP tool "${toolName}" on server "${serverId}". ` +
257
+ `NeedsReconsentEvent emitted for credential instance "${credentialInstanceId}".`,
258
+ error instanceof Error ? error : undefined,
259
+ );
260
+ const wrapped = new Error(
261
+ `MCP tool "${toolName}" on server "${serverId}" returned a permission error. ` +
262
+ `Reconnect the credential "${credentialInstanceId}" via the Connect flow. ` +
263
+ `needsReconsent: ${JSON.stringify(event satisfies NeedsReconsentEvent)}`,
264
+ { cause: error },
265
+ );
266
+ if (appendMcpInvocation) {
267
+ await appendMcpInvocation({
268
+ ...baseRecord,
269
+ status: "failed",
270
+ managedInput: summarizedInput,
271
+ error: { message: wrapped.message, name: wrapped.name },
272
+ queuedAt: startedAtIso,
273
+ startedAt: startedAtIso,
274
+ finishedAt: new Date().toISOString(),
275
+ });
276
+ }
277
+ // The event carries the structured data; the agent turn continues for other tools.
278
+ throw wrapped;
279
+ }
280
+ const effectiveMessage = error instanceof Error ? error.message : String(error);
281
+ span.end({
282
+ status: "error",
283
+ statusMessage: effectiveMessage,
284
+ });
285
+ if (appendMcpInvocation) {
286
+ await appendMcpInvocation({
287
+ ...baseRecord,
288
+ status: "failed",
289
+ managedInput: summarizedInput,
290
+ error: { message: effectiveMessage, name: error instanceof Error ? error.name : undefined },
291
+ queuedAt: startedAtIso,
292
+ startedAt: startedAtIso,
293
+ finishedAt: new Date().toISOString(),
294
+ });
295
+ }
296
+ throw error;
297
+ }
298
+ },
299
+ };
300
+ wrapped[toolName] = wrappedDef as unknown as ToolSet[string];
301
+ }
302
+
303
+ return wrapped as ToolSet;
304
+ }
305
+
306
+ private summarizeForInvocation(value: unknown): JsonValue | undefined {
307
+ if (value === undefined) return undefined;
308
+ try {
309
+ const serialized = JSON.stringify(value);
310
+ if (serialized.length > 1024) {
311
+ return { truncated: true, preview: serialized.slice(0, 1024) };
312
+ }
313
+ return JSON.parse(serialized) as JsonValue;
314
+ } catch {
315
+ return undefined;
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Detects 403 / MCP-level permission / scope-insufficiency errors from callTool.
321
+ * The exact shape depends on how @ai-sdk/mcp surfaces them — we check for HTTP status
322
+ * 403, MCP error codes (insufficient_scope / UNAUTHORIZED), and common message patterns.
323
+ */
324
+ private isPermissionError(error: unknown): boolean {
325
+ if (!(error instanceof Error)) {
326
+ return false;
327
+ }
328
+ const msg = error.message.toLowerCase();
329
+ // HTTP 403 from the MCP transport
330
+ if (msg.includes("403") || msg.includes("forbidden")) {
331
+ return true;
332
+ }
333
+ // MCP-level error codes
334
+ if (msg.includes("insufficient_scope") || msg.includes("unauthorized") || msg.includes("unauthenticated")) {
335
+ return true;
336
+ }
337
+ // Check error name or code
338
+ const candidate = error as Error & { statusCode?: number; code?: string };
339
+ if (candidate.statusCode === 403 || candidate.code === "EUNAUTHORIZED") {
340
+ return true;
341
+ }
342
+ return false;
343
+ }
344
+ }
@@ -0,0 +1,29 @@
1
+ import { injectable } from "@codemation/core";
2
+ import { experimental_createMCPClient } from "@ai-sdk/mcp";
3
+ import type { MCPClient } from "@ai-sdk/mcp";
4
+
5
+ export type McpClientOpenArgs = Readonly<{
6
+ url: string;
7
+ headers: Record<string, string>;
8
+ }>;
9
+
10
+ export interface McpClientFactory {
11
+ open(args: McpClientOpenArgs): Promise<MCPClient>;
12
+ }
13
+
14
+ /**
15
+ * Default implementation — delegates to @ai-sdk/mcp's experimental_createMCPClient
16
+ * using the streamable HTTP transport.
17
+ */
18
+ @injectable()
19
+ export class DefaultMcpClientFactory implements McpClientFactory {
20
+ async open(args: McpClientOpenArgs): Promise<MCPClient> {
21
+ return experimental_createMCPClient({
22
+ transport: {
23
+ type: "http",
24
+ url: args.url,
25
+ headers: args.headers,
26
+ },
27
+ });
28
+ }
29
+ }