@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
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "prisma-client-b708fc5a44cdec791f8903ca7af0faa8ee36ab36c4e12693b2e42c5a474e6685",
2
+ "name": "prisma-client-f1f592cca96cc07ff4c49ca6848788c0583fceb20133a8099c5c0d76247b55cc",
3
3
  "main": "index.js",
4
4
  "types": "index.d.ts",
5
5
  "browser": "default.js",
@@ -18,6 +18,7 @@ model Run {
18
18
  executionOptionsJson String? @map("execution_options_json")
19
19
  controlJson String? @map("control_json")
20
20
  workflowSnapshotJson String? @map("workflow_snapshot_json")
21
+ workflowSnapshotId String? @map("workflow_snapshot_id")
21
22
  policySnapshotJson String? @map("policy_snapshot_json")
22
23
  engineCountersJson String? @map("engine_counters_json")
23
24
  mutableStateJson String? @map("mutable_state_json")
@@ -33,10 +34,12 @@ model Run {
33
34
  slotProjection RunSlotProjection?
34
35
  testSuiteRun TestSuiteRun? @relation(fields: [testSuiteRunId], references: [id])
35
36
  testAssertions TestAssertion[]
37
+ workflowSnapshot WorkflowSnapshot? @relation(fields: [workflowSnapshotId], references: [id])
36
38
 
37
39
  @@index([workflowId, startedAt])
38
40
  @@index([workflowId, status, finishedAt])
39
41
  @@index([testSuiteRunId, testCaseIndex])
42
+ @@index([workflowSnapshotId])
40
43
  }
41
44
 
42
45
  model RunWorkItem {
@@ -107,6 +110,7 @@ model ExecutionInstance {
107
110
  iterationId String? @map("iteration_id")
108
111
  itemIndex Int? @map("item_index")
109
112
  parentInvocationId String? @map("parent_invocation_id")
113
+ childRunId String? @map("child_run_id")
110
114
 
111
115
  run Run @relation(fields: [runId], references: [runId], onDelete: Cascade)
112
116
 
@@ -259,6 +263,20 @@ model TelemetrySpan {
259
263
  @@index([retentionExpiresAt])
260
264
  }
261
265
 
266
+ model WorkflowSnapshot {
267
+ id String @id
268
+ workflowId String @map("workflow_id")
269
+ /// SHA-256 hex digest of snapshotJson — dedup key: same workflow content → one row.
270
+ snapshotHash String @map("snapshot_hash")
271
+ snapshotJson String @map("snapshot_json")
272
+ createdAt String @map("created_at")
273
+
274
+ runs Run[]
275
+
276
+ @@unique([workflowId, snapshotHash])
277
+ @@index([workflowId, snapshotHash])
278
+ }
279
+
262
280
  model TelemetryArtifact {
263
281
  artifactId String @id @map("artifact_id")
264
282
  traceId String @map("trace_id")
@@ -273,6 +291,7 @@ model TelemetryArtifact {
273
291
  previewJson String? @map("preview_json")
274
292
  payloadText String? @map("payload_text")
275
293
  payloadJson String? @map("payload_json")
294
+ payloadStorageKey String? @map("payload_storage_key")
276
295
  bytes Int?
277
296
  truncated Boolean?
278
297
  createdAt String @map("created_at")
@@ -469,3 +488,23 @@ model VerificationToken {
469
488
  @@unique([identifier, token])
470
489
  @@map("codemation_auth_verification_token")
471
490
  }
491
+
492
+ model WorkflowAuditLog {
493
+ id String @id @map("id")
494
+ occurredAt DateTime @map("occurred_at")
495
+ actorUserId String @map("actor_user_id")
496
+ actorSessionId String? @map("actor_session_id")
497
+ action String @map("action")
498
+ resourceType String @map("resource_type")
499
+ resourceId String @map("resource_id")
500
+ outcome String @map("outcome")
501
+ errorCode String? @map("error_code")
502
+ correlationId String? @map("correlation_id")
503
+ workflowId String @map("workflow_id")
504
+ runId String? @map("run_id")
505
+ nodeId String? @map("node_id")
506
+
507
+ @@index([actorUserId, occurredAt])
508
+ @@index([workflowId, occurredAt])
509
+ @@map("workflow_audit_log")
510
+ }
@@ -0,0 +1,18 @@
1
+ import { AppConfigLoader } from "../src/presentation/server/AppConfigLoader.ts";
2
+
3
+ const loader = new AppConfigLoader();
4
+ const result = await loader.load({
5
+ consumerRoot: "/home/cblokland/projects/made/codemation/apps/test-dev",
6
+ repoRoot: "/home/cblokland/projects/made/codemation",
7
+ env: process.env,
8
+ });
9
+ console.log("collections.length:", result.appConfig.collections.length);
10
+ console.log(
11
+ "collection names:",
12
+ result.appConfig.collections.map((c) => c.name),
13
+ );
14
+ const first = result.appConfig.collections[0];
15
+ console.log(
16
+ "first collection:",
17
+ first ? { name: first.name, fields: Object.keys(first.fields ?? {}), indexes: first.indexes } : "no collections",
18
+ );
@@ -1,4 +1,5 @@
1
1
  import { spawnSync } from "node:child_process";
2
+ import { execaSync } from "execa";
2
3
  import { existsSync, realpathSync } from "node:fs";
3
4
  import { createRequire } from "node:module";
4
5
  import path from "node:path";
@@ -15,7 +16,7 @@ class PrismaClientGenerator {
15
16
  this.reexecUnderSupportedNodeWhenNeeded();
16
17
  for (const provider of this.providers) {
17
18
  const result = spawnSync(process.execPath, [this.prismaCliEntrypoint, "generate"], {
18
- cwd: import.meta.dirname.replace(/\/scripts$/, ""),
19
+ cwd: path.dirname(import.meta.dirname),
19
20
  env: {
20
21
  ...process.env,
21
22
  CODEMATION_PRISMA_PROVIDER: provider,
@@ -41,7 +42,7 @@ class PrismaClientGenerator {
41
42
  );
42
43
  }
43
44
  const result = spawnSync(supportedNodeBinary, [new URL(import.meta.url).pathname], {
44
- cwd: import.meta.dirname.replace(/\/scripts$/, ""),
45
+ cwd: path.dirname(import.meta.dirname),
45
46
  env: {
46
47
  ...process.env,
47
48
  [this.reexecMarker]: "1",
@@ -80,16 +81,24 @@ class PrismaClientGenerator {
80
81
  if (npmExecPath) {
81
82
  return realpathSync(npmExecPath);
82
83
  }
83
- const result = spawnSync("bash", ["-lc", 'realpath "$(command -v pnpm)"'], {
84
- env: process.env,
85
- encoding: "utf8",
86
- shell: false,
87
- });
88
- if (result.status !== 0) {
89
- return undefined;
84
+ try {
85
+ // execa resolves bare command names against the OS-appropriate PATH (handles `.cmd` / `.exe`
86
+ // shims on Windows automatically), so this works on every platform we run dev on.
87
+ const result = execaSync("pnpm", ["root", "-g"], { reject: false });
88
+ if (result.exitCode === 0 && typeof result.stdout === "string" && result.stdout.trim().length > 0) {
89
+ // pnpm root -g prints "<pnpm-store>/global/5/node_modules"; the pnpm binary lives one level
90
+ // up under the install root. We only need *a* directory close to the pnpm binary so the
91
+ // sibling-`node` lookup below can probe candidate paths.
92
+ const globalRoot = result.stdout.trim();
93
+ const candidate = path.resolve(globalRoot, "..", "..", "pnpm");
94
+ if (existsSync(candidate)) {
95
+ return realpathSync(candidate);
96
+ }
97
+ }
98
+ } catch {
99
+ // fall through
90
100
  }
91
- const pnpmPath = result.stdout.trim();
92
- return pnpmPath.length > 0 ? pnpmPath : undefined;
101
+ return undefined;
93
102
  }
94
103
 
95
104
  static isSupportedNode(version) {
@@ -0,0 +1,96 @@
1
+ import type { Clock } from "@codemation/core";
2
+ import { inject, injectable } from "@codemation/core";
3
+ import { ApplicationTokens } from "../applicationTokens";
4
+ import type { Logger } from "./logging/Logger";
5
+ import type { AppConfig } from "../presentation/config/AppConfig";
6
+ import { ServerLoggerFactory } from "../infrastructure/logging/ServerLoggerFactory";
7
+ import {
8
+ PrismaDatabaseClientToken,
9
+ type PrismaDatabaseClient,
10
+ } from "../infrastructure/persistence/PrismaDatabaseClient";
11
+
12
+ /**
13
+ * Periodically deletes WorkflowAuditLog rows older than the configured retention period.
14
+ *
15
+ * Default retention: 90 days.
16
+ * Override: `CODEMATION_AUDIT_WORKFLOW_RETENTION_SECONDS` in env.
17
+ * Disable: `CODEMATION_AUDIT_PRUNE_ENABLED=false`.
18
+ */
19
+ @injectable()
20
+ export class WorkflowAuditLogPruneScheduler {
21
+ static readonly defaultIntervalMs = 60 * 60 * 1_000;
22
+ /** 90 days in seconds (default retention). */
23
+ static readonly defaultRetentionSeconds = 90 * 24 * 3600;
24
+
25
+ private timer: ReturnType<typeof setInterval> | undefined;
26
+ private readonly logger: Logger;
27
+
28
+ constructor(
29
+ @inject(ApplicationTokens.Clock) private readonly clock: Clock,
30
+ @inject(PrismaDatabaseClientToken) private readonly prisma: PrismaDatabaseClient,
31
+ @inject(ApplicationTokens.AppConfig) private readonly appConfig: AppConfig,
32
+ @inject(ServerLoggerFactory) loggerFactory: ServerLoggerFactory,
33
+ ) {
34
+ this.logger = loggerFactory.create("codemation.auditPrune");
35
+ }
36
+
37
+ start(): void {
38
+ if (this.appConfig.env.CODEMATION_AUDIT_PRUNE_ENABLED === "false") {
39
+ return;
40
+ }
41
+ if (this.timer) {
42
+ return;
43
+ }
44
+ const intervalMs = Number(
45
+ this.appConfig.env.CODEMATION_AUDIT_PRUNE_INTERVAL_MS ??
46
+ this.appConfig.env.CODEMATION_RUN_PRUNE_INTERVAL_MS ??
47
+ WorkflowAuditLogPruneScheduler.defaultIntervalMs,
48
+ );
49
+ void this.runScheduledTick();
50
+ this.timer = setInterval(() => {
51
+ void this.runScheduledTick();
52
+ }, intervalMs);
53
+ }
54
+
55
+ stop(): void {
56
+ if (this.timer) {
57
+ clearInterval(this.timer);
58
+ this.timer = undefined;
59
+ }
60
+ }
61
+
62
+ /** Exposed for tests; production path is the interval started by {@link start}. */
63
+ async runOnce(): Promise<void> {
64
+ const retentionSec = Number(
65
+ this.appConfig.env.CODEMATION_AUDIT_WORKFLOW_RETENTION_SECONDS ??
66
+ WorkflowAuditLogPruneScheduler.defaultRetentionSeconds,
67
+ );
68
+ const now = this.clock.now();
69
+ const cutoff = new Date(now.getTime() - retentionSec * 1000);
70
+ const limit = Number(this.appConfig.env.CODEMATION_TELEMETRY_PRUNE_LIMIT ?? 2_000);
71
+
72
+ // Two-step delete: find IDs first (respects limit), then delete by ID.
73
+ const rows = await this.prisma.workflowAuditLog.findMany({
74
+ where: { occurredAt: { lt: cutoff } },
75
+ select: { id: true },
76
+ take: limit,
77
+ });
78
+
79
+ if (rows.length === 0) {
80
+ return;
81
+ }
82
+
83
+ const ids = rows.map((r) => r.id);
84
+ await this.prisma.workflowAuditLog.deleteMany({ where: { id: { in: ids } } });
85
+
86
+ this.logger.info(`WorkflowAuditLog prune: deleted ${ids.length} row(s) older than ${cutoff.toISOString()}`);
87
+ }
88
+
89
+ private async runScheduledTick(): Promise<void> {
90
+ try {
91
+ await this.runOnce();
92
+ } catch (error) {
93
+ this.logger.warn(`WorkflowAuditLog prune tick failed: ${error instanceof Error ? error.message : String(error)}`);
94
+ }
95
+ }
96
+ }
@@ -2,4 +2,8 @@ export type AuthenticatedPrincipal = Readonly<{
2
2
  id: string;
3
3
  email: string | null;
4
4
  name: string | null;
5
+ /** Set to "managed-jwt" when the principal was verified from a CP-signed bearer token. */
6
+ source?: "managed-jwt";
7
+ /** The workspace ID from the JWT `aud` claim. Present when source === "managed-jwt". */
8
+ workspaceId?: string;
5
9
  }>;
@@ -21,6 +21,7 @@ import { CommandHandler } from "../bus/CommandHandler";
21
21
  import type { CreateRunRequest, RunCommandResult } from "../contracts/RunContracts";
22
22
  import { WorkflowDebuggerOverlayStateFactory } from "../workflows/WorkflowDebuggerOverlayStateFactory";
23
23
  import { StartWorkflowRunCommand } from "./StartWorkflowRunCommand";
24
+ import { CredentialBindingService } from "../../domain/credentials/CredentialBindingService";
24
25
 
25
26
  @HandlesCommand.forCommand(StartWorkflowRunCommand)
26
27
  export class StartWorkflowRunCommandHandler extends CommandHandler<StartWorkflowRunCommand, RunCommandResult> {
@@ -39,6 +40,8 @@ export class StartWorkflowRunCommandHandler extends CommandHandler<StartWorkflow
39
40
  private readonly workflowRunRepository: WorkflowRunRepository,
40
41
  @inject(ApplicationTokens.WorkflowDebuggerOverlayRepository)
41
42
  private readonly workflowDebuggerOverlayRepository: WorkflowDebuggerOverlayRepository,
43
+ @inject(CredentialBindingService)
44
+ private readonly credentialBindingService: CredentialBindingService,
42
45
  @inject(ApplicationTokens.LoggerFactory)
43
46
  loggerFactory: LoggerFactory,
44
47
  ) {
@@ -58,6 +61,7 @@ export class StartWorkflowRunCommandHandler extends CommandHandler<StartWorkflow
58
61
  if (!workflow) {
59
62
  throw new ApplicationRequestError(404, "Unknown workflowId");
60
63
  }
64
+ await this.credentialBindingService.assertRequiredCredentialsBound(body.workflowId);
61
65
  const executionOptions = body.mode
62
66
  ? {
63
67
  mode: body.mode,
@@ -23,6 +23,17 @@ export type WorkflowNodeDto = Readonly<{
23
23
  * mailbox / fixture file / data source the test cases originate from.
24
24
  */
25
25
  description?: string;
26
+ /**
27
+ * When present, the node is a SubWorkflow invocation targeting this workflow id. Surfaced in
28
+ * the properties panel as an "Open in editor" navigation link.
29
+ */
30
+ referencedWorkflowId?: string;
31
+ /**
32
+ * Static configuration summary for the inspector — short label/value pairs that describe
33
+ * what this node will do at a glance, before any run telemetry exists. Pulled from the
34
+ * node config's optional `inspectorSummary()` hook (`NodeConfigBase.inspectorSummary`).
35
+ */
36
+ inspectorSummary?: ReadonlyArray<Readonly<{ label: string; value: string }>>;
26
37
  }>;
27
38
 
28
39
  export type WorkflowEdgeDto = Readonly<{
@@ -1,8 +1,10 @@
1
1
  import type { RunEvent } from "@codemation/core";
2
+ import type { TelemetrySpanUpsert } from "../../domain/telemetry/TelemetryContracts";
2
3
 
3
4
  export type WorkflowWebsocketMessage =
4
5
  | Readonly<{ kind: "event"; event: RunEvent }>
5
6
  | Readonly<{ kind: "workflowChanged"; workflowId: string }>
6
7
  | Readonly<{ kind: "devBuildStarted"; workflowId: string; buildVersion?: string }>
7
8
  | Readonly<{ kind: "devBuildCompleted"; workflowId: string; buildVersion: string }>
8
- | Readonly<{ kind: "devBuildFailed"; workflowId: string; message: string }>;
9
+ | Readonly<{ kind: "devBuildFailed"; workflowId: string; message: string }>
10
+ | Readonly<{ kind: "telemetryEvent"; runId: string; span: TelemetrySpanUpsert }>;
@@ -13,6 +13,7 @@ import type {
13
13
  WorkflowNodeDto,
14
14
  WorkflowSummary,
15
15
  } from "../contracts/WorkflowViewContracts";
16
+ import { McpServerCatalog } from "../../mcp/McpServerCatalog";
16
17
  import type { DataMapper } from "./DataMapper";
17
18
  import { WorkflowPolicyUiPresentationFactory } from "./WorkflowPolicyUiPresentationFactory";
18
19
 
@@ -23,6 +24,8 @@ export class WorkflowDefinitionMapper implements DataMapper<WorkflowDefinition,
23
24
  private readonly policyUi: WorkflowPolicyUiPresentationFactory,
24
25
  @inject(CoreTokens.WorkflowActivationPolicy)
25
26
  private readonly workflowActivationPolicy: WorkflowActivationPolicy,
27
+ @inject(McpServerCatalog)
28
+ private readonly mcpCatalog: McpServerCatalog,
26
29
  ) {}
27
30
 
28
31
  async map(workflow: WorkflowDefinition): Promise<WorkflowDto> {
@@ -133,6 +136,8 @@ export class WorkflowDefinitionMapper implements DataMapper<WorkflowDefinition,
133
136
  ? ((node.config as { triggerKind?: "live" | "test" }).triggerKind ?? "live")
134
137
  : undefined;
135
138
  const description = (node.config as { description?: string }).description;
139
+ const referencedWorkflowId = (node.config as { workflowId?: string }).workflowId;
140
+ const inspectorSummary = this.readInspectorSummary(node.config);
136
141
  nodes.push({
137
142
  id: node.id,
138
143
  kind: node.kind,
@@ -145,6 +150,10 @@ export class WorkflowDefinitionMapper implements DataMapper<WorkflowDefinition,
145
150
  ...this.nodePortFieldsFromConfig(node.config),
146
151
  ...(triggerKind ? { triggerKind } : {}),
147
152
  ...(typeof description === "string" && description.trim().length > 0 ? { description } : {}),
153
+ ...(typeof referencedWorkflowId === "string" && referencedWorkflowId.trim().length > 0
154
+ ? { referencedWorkflowId }
155
+ : {}),
156
+ ...(inspectorSummary ? { inspectorSummary } : {}),
148
157
  });
149
158
  if (AgentConfigInspector.isAgentNodeConfig(node.config)) {
150
159
  this.appendVirtualConnectionNodes(
@@ -217,7 +226,9 @@ export class WorkflowDefinitionMapper implements DataMapper<WorkflowDefinition,
217
226
  if (!AgentConfigInspector.isAgentNodeConfig(node.config)) {
218
227
  continue;
219
228
  }
220
- const descriptors = AgentConnectionNodeCollector.collect(node.id, node.config);
229
+ const descriptors = AgentConnectionNodeCollector.collect(node.id, node.config, (id) =>
230
+ this.mcpCatalog.get(id),
231
+ );
221
232
  byAgentNodeId.set(node.id, descriptors);
222
233
  const byChildId = new Map<string, AgentConnectionNodeDescriptor>();
223
234
  for (const descriptor of descriptors) {
@@ -283,6 +294,38 @@ export class WorkflowDefinitionMapper implements DataMapper<WorkflowDefinition,
283
294
  * Omit optional port fields when undefined so persisted snapshot DTOs (which never serialize
284
295
  * undefined keys) stay aligned with live workflow mapping.
285
296
  */
297
+ private readInspectorSummary(
298
+ config: NodeDefinition["config"] | undefined,
299
+ ): ReadonlyArray<Readonly<{ label: string; value: string }>> | undefined {
300
+ if (!config || typeof config !== "object") {
301
+ return undefined;
302
+ }
303
+ const fn = (config as { inspectorSummary?: () => unknown }).inspectorSummary;
304
+ if (typeof fn !== "function") {
305
+ return undefined;
306
+ }
307
+ let raw: unknown;
308
+ try {
309
+ raw = fn.call(config);
310
+ } catch {
311
+ // A misbehaving inspectorSummary must not break workflow loading; skip silently.
312
+ return undefined;
313
+ }
314
+ if (!Array.isArray(raw)) {
315
+ return undefined;
316
+ }
317
+ const rows: Array<Readonly<{ label: string; value: string }>> = [];
318
+ for (const entry of raw) {
319
+ if (!entry || typeof entry !== "object") continue;
320
+ const { label, value } = entry as { label?: unknown; value?: unknown };
321
+ if (typeof label !== "string" || typeof value !== "string") continue;
322
+ const trimmedLabel = label.trim();
323
+ if (trimmedLabel.length === 0) continue;
324
+ rows.push({ label: trimmedLabel, value });
325
+ }
326
+ return rows.length > 0 ? rows : undefined;
327
+ }
328
+
286
329
  private nodePortFieldsFromConfig(
287
330
  config: NodeDefinition["config"] | undefined,
288
331
  ): Pick<WorkflowNodeDto, "continueWhenEmptyOutput" | "declaredOutputPorts" | "declaredInputPorts"> {
@@ -104,7 +104,13 @@ export class WorkflowRunRetentionPruneScheduler {
104
104
  if (this.appConfig.env.CODEMATION_TELEMETRY_PRUNE_ENABLED !== "false") {
105
105
  const telemetryLimit = Number(this.appConfig.env.CODEMATION_TELEMETRY_PRUNE_LIMIT ?? 2_000);
106
106
  prunedSpanCount = await this.telemetrySpanStore.pruneExpired({ nowIso, limit: telemetryLimit });
107
- prunedArtifactCount = await this.telemetryArtifactStore.pruneExpired({ nowIso, limit: telemetryLimit });
107
+ const { count: artifactCount, storageKeys: artifactStorageKeys } = await this.telemetryArtifactStore.pruneExpired(
108
+ { nowIso, limit: telemetryLimit },
109
+ );
110
+ for (const key of artifactStorageKeys) {
111
+ await this.binaryStorage.delete(key);
112
+ }
113
+ prunedArtifactCount = artifactCount;
108
114
  prunedMetricCount = await this.telemetryMetricPointStore.pruneExpired({ nowIso, limit: telemetryLimit });
109
115
  }
110
116
 
@@ -9,6 +9,7 @@ import { OtelIdentityFactory } from "./OtelIdentityFactory";
9
9
  import { TelemetryEnricherChain } from "./TelemetryEnricherChain";
10
10
  import { TelemetryPrivacyPolicy } from "./TelemetryPrivacyPolicy";
11
11
  import { TelemetryRetentionTimestampFactory } from "./TelemetryRetentionTimestampFactory";
12
+ import { NoOpTelemetrySpanPublisher, type TelemetrySpanPublisher } from "./TelemetrySpanPublisher";
12
13
 
13
14
  export type StoredExecutionTelemetryDeps = Readonly<{
14
15
  traceId: string;
@@ -24,8 +25,12 @@ export type StoredExecutionTelemetryDeps = Readonly<{
24
25
  telemetryPrivacyPolicy: TelemetryPrivacyPolicy;
25
26
  telemetryRetentionTimestampFactory: TelemetryRetentionTimestampFactory;
26
27
  otelIdentityFactory: OtelIdentityFactory;
28
+ /** Optional publisher for streaming span upserts over WebSocket. Defaults to no-op. */
29
+ telemetrySpanPublisher?: TelemetrySpanPublisher;
27
30
  }>;
28
31
 
32
+ export { NoOpTelemetrySpanPublisher };
33
+
29
34
  export type StoredSpanScopeArgs = StoredExecutionTelemetryDeps &
30
35
  Readonly<{
31
36
  spanId: string;
@@ -12,6 +12,7 @@ import { StoredExecutionTelemetry } from "./StoredExecutionTelemetry";
12
12
  import { TelemetryEnricherChain } from "./TelemetryEnricherChain";
13
13
  import { TelemetryPrivacyPolicy } from "./TelemetryPrivacyPolicy";
14
14
  import { TelemetryRetentionTimestampFactory } from "./TelemetryRetentionTimestampFactory";
15
+ import type { TelemetrySpanPublisher } from "./TelemetrySpanPublisher";
15
16
 
16
17
  @injectable()
17
18
  export class OtelExecutionTelemetryFactory implements ExecutionTelemetryFactory {
@@ -32,6 +33,8 @@ export class OtelExecutionTelemetryFactory implements ExecutionTelemetryFactory
32
33
  private readonly telemetryRetentionTimestampFactory: TelemetryRetentionTimestampFactory,
33
34
  @inject(OtelIdentityFactory)
34
35
  private readonly otelIdentityFactory: OtelIdentityFactory,
36
+ @inject(ApplicationTokens.TelemetrySpanPublisher)
37
+ private readonly telemetrySpanPublisher: TelemetrySpanPublisher,
35
38
  ) {}
36
39
 
37
40
  create(
@@ -51,6 +54,7 @@ export class OtelExecutionTelemetryFactory implements ExecutionTelemetryFactory
51
54
  telemetryPrivacyPolicy: this.telemetryPrivacyPolicy,
52
55
  telemetryRetentionTimestampFactory: this.telemetryRetentionTimestampFactory,
53
56
  otelIdentityFactory: this.otelIdentityFactory,
57
+ telemetrySpanPublisher: this.telemetrySpanPublisher,
54
58
  });
55
59
  }
56
60
  }
@@ -12,6 +12,7 @@ import type {
12
12
  } from "@codemation/core";
13
13
  import { NoOpTelemetryArtifactReference } from "@codemation/core";
14
14
  import type { TelemetrySpanUpsert } from "../../domain/telemetry/TelemetryContracts";
15
+ import { NoOpTelemetrySpanPublisher } from "./TelemetrySpanPublisher";
15
16
  import type { StoredSpanScopeArgs } from "./OtelExecutionTelemetry.types";
16
17
 
17
18
  export class StoredTelemetrySpanScope implements TelemetrySpanScope {
@@ -226,7 +227,7 @@ export class StoredTelemetrySpanScope implements TelemetrySpanScope {
226
227
  const retentionExpiresAt =
227
228
  update.retentionExpiresAt ??
228
229
  this.deps.telemetryRetentionTimestampFactory.createSpanExpiry(this.deps.policySnapshot, observedAt);
229
- await this.deps.telemetrySpanStore.upsert({
230
+ const upsertRecord: TelemetrySpanUpsert = {
230
231
  traceId: this.traceId,
231
232
  spanId: this.spanId,
232
233
  parentSpanId: this.parentSpanId,
@@ -241,7 +242,10 @@ export class StoredTelemetrySpanScope implements TelemetrySpanScope {
241
242
  nodeRole: enrichment.nodeRole,
242
243
  retentionExpiresAt,
243
244
  ...update,
244
- });
245
+ };
246
+ await this.deps.telemetrySpanStore.upsert(upsertRecord);
247
+ const publisher = this.deps.telemetrySpanPublisher ?? NoOpTelemetrySpanPublisher;
248
+ await publisher.publishSpan(upsertRecord);
245
249
  await this.touchTraceContextExpiry(
246
250
  this.deps.telemetryRetentionTimestampFactory.createTraceContextExpiry(this.deps.policySnapshot, observedAt),
247
251
  );
@@ -3,38 +3,48 @@ import { injectable } from "@codemation/core";
3
3
 
4
4
  @injectable()
5
5
  export class TelemetryRetentionTimestampFactory {
6
- createSpanExpiry(policySnapshot: PersistedRunPolicySnapshot | undefined, observedAt: Date): string | undefined {
7
- return this.createExpiry(policySnapshot?.telemetrySpanRetentionSeconds, observedAt);
6
+ /** Default span retention: 7 days (overridden by policySnapshot). */
7
+ static readonly defaultSpanRetentionSeconds = 7 * 24 * 3600;
8
+ /** Default artifact retention: 3 days (overridden by policySnapshot). */
9
+ static readonly defaultArtifactRetentionSeconds = 3 * 24 * 3600;
10
+ /** Default metric retention: 30 days (overridden by policySnapshot). */
11
+ static readonly defaultMetricRetentionSeconds = 30 * 24 * 3600;
12
+
13
+ createSpanExpiry(policySnapshot: PersistedRunPolicySnapshot | undefined, observedAt: Date): string {
14
+ return this.createExpiry(
15
+ policySnapshot?.telemetrySpanRetentionSeconds ?? TelemetryRetentionTimestampFactory.defaultSpanRetentionSeconds,
16
+ observedAt,
17
+ );
8
18
  }
9
19
 
10
- createArtifactExpiry(policySnapshot: PersistedRunPolicySnapshot | undefined, observedAt: Date): string | undefined {
11
- return this.createExpiry(policySnapshot?.telemetryArtifactRetentionSeconds, observedAt);
20
+ createArtifactExpiry(policySnapshot: PersistedRunPolicySnapshot | undefined, observedAt: Date): string {
21
+ return this.createExpiry(
22
+ policySnapshot?.telemetryArtifactRetentionSeconds ?? TelemetryRetentionTimestampFactory.defaultArtifactRetentionSeconds,
23
+ observedAt,
24
+ );
12
25
  }
13
26
 
14
- createMetricExpiry(policySnapshot: PersistedRunPolicySnapshot | undefined, observedAt: Date): string | undefined {
15
- return this.createExpiry(policySnapshot?.telemetryMetricRetentionSeconds, observedAt);
27
+ createMetricExpiry(policySnapshot: PersistedRunPolicySnapshot | undefined, observedAt: Date): string {
28
+ return this.createExpiry(
29
+ policySnapshot?.telemetryMetricRetentionSeconds ?? TelemetryRetentionTimestampFactory.defaultMetricRetentionSeconds,
30
+ observedAt,
31
+ );
16
32
  }
17
33
 
18
34
  createTraceContextExpiry(
19
35
  policySnapshot: PersistedRunPolicySnapshot | undefined,
20
36
  observedAt: Date,
21
- ): string | undefined {
37
+ ): string {
22
38
  const candidates = [
23
- policySnapshot?.telemetrySpanRetentionSeconds,
24
- policySnapshot?.telemetryArtifactRetentionSeconds,
25
- policySnapshot?.telemetryMetricRetentionSeconds,
39
+ policySnapshot?.telemetrySpanRetentionSeconds ?? TelemetryRetentionTimestampFactory.defaultSpanRetentionSeconds,
40
+ policySnapshot?.telemetryArtifactRetentionSeconds ?? TelemetryRetentionTimestampFactory.defaultArtifactRetentionSeconds,
41
+ policySnapshot?.telemetryMetricRetentionSeconds ?? TelemetryRetentionTimestampFactory.defaultMetricRetentionSeconds,
26
42
  ].filter((value): value is number => typeof value === "number" && value > 0);
27
- if (candidates.length === 0) {
28
- return undefined;
29
- }
30
43
  const maxSeconds = Math.max(...candidates);
31
44
  return this.createExpiry(maxSeconds, observedAt);
32
45
  }
33
46
 
34
- private createExpiry(retentionSeconds: number | undefined, observedAt: Date): string | undefined {
35
- if (!retentionSeconds || retentionSeconds <= 0) {
36
- return undefined;
37
- }
47
+ private createExpiry(retentionSeconds: number, observedAt: Date): string {
38
48
  return new Date(observedAt.getTime() + retentionSeconds * 1000).toISOString();
39
49
  }
40
50
  }
@@ -0,0 +1,11 @@
1
+ import type { TelemetrySpanUpsert } from "../../domain/telemetry/TelemetryContracts";
2
+
3
+ export interface TelemetrySpanPublisher {
4
+ publishSpan(span: TelemetrySpanUpsert): Promise<void>;
5
+ }
6
+
7
+ export const NoOpTelemetrySpanPublisher: TelemetrySpanPublisher = {
8
+ async publishSpan(_span: TelemetrySpanUpsert): Promise<void> {
9
+ // No-op: used in tests and when websocket relay is not wired in.
10
+ },
11
+ };
@@ -0,0 +1,31 @@
1
+ import { inject, injectable } from "@codemation/core";
2
+ import type { TelemetrySpanUpsert } from "../../domain/telemetry/TelemetryContracts";
3
+ import { ApplicationTokens } from "../../applicationTokens";
4
+ import type { TelemetrySpanPublisher } from "../telemetry/TelemetrySpanPublisher";
5
+ import type { WorkflowWebsocketPublisher } from "./WorkflowWebsocketPublisher";
6
+
7
+ /**
8
+ * Implements {@link TelemetrySpanPublisher} by forwarding each span upsert to a
9
+ * per-run WebSocket room (`run:<runId>`). Clients subscribe to this room when they
10
+ * open the inspector for a specific run.
11
+ *
12
+ * The relay fires *after* the span has been committed to persistent storage so that
13
+ * HTTP catch-up (on reconnect or initial mount) and WS pushes represent a consistent
14
+ * view of the data.
15
+ */
16
+ @injectable()
17
+ export class TelemetrySpanWebsocketRelay implements TelemetrySpanPublisher {
18
+ constructor(
19
+ @inject(ApplicationTokens.WorkflowWebsocketPublisher)
20
+ private readonly workflowWebsocketPublisher: WorkflowWebsocketPublisher,
21
+ ) {}
22
+
23
+ async publishSpan(span: TelemetrySpanUpsert): Promise<void> {
24
+ const roomId = `run:${span.runId}`;
25
+ await this.workflowWebsocketPublisher.publishToRoom(roomId, {
26
+ kind: "telemetryEvent",
27
+ runId: span.runId,
28
+ span,
29
+ });
30
+ }
31
+ }