@@ -30,6 +30,7 @@ import type {
3030 WorkflowState ,
3131} from '@/lib/logs/types'
3232import type { SerializableExecutionState } from '@/executor/execution/types'
33+ import type { EnvironmentSecretSanitizer } from '@/executor/utils/environment-secret-sanitizer'
3334
3435type TriggerData = Record < string , unknown > & {
3536 correlation ?: NonNullable < ExecutionTrigger [ 'data' ] > [ 'correlation' ]
@@ -86,6 +87,8 @@ const logger = createLogger('LoggingSession')
8687
8788type CompletionAttempt = 'complete' | 'error' | 'cancelled' | 'paused'
8889
90+ const identityEnvironmentSecretSanitizer : EnvironmentSecretSanitizer = < T > ( value : T ) : T => value
91+
8992export interface SessionStartParams {
9093 userId ?: string
9194 /** Explicit initiating actor for callers that do not populate `userId`. */
@@ -155,6 +158,8 @@ export class LoggingSession {
155158 private completionAttemptFailed = false
156159 private pendingProgressWrites = new Set < Promise < void > > ( )
157160 private postExecutionPromise : Promise < void > | null = null
161+ private environmentSecretSanitizer : EnvironmentSecretSanitizer =
162+ identityEnvironmentSecretSanitizer
158163
159164 constructor (
160165 workflowId : string ,
@@ -170,6 +175,19 @@ export class LoggingSession {
170175 this . requestId = requestId
171176 }
172177
178+ /**
179+ * Installs the workflow-scoped sanitizer used only for persisted and emitted
180+ * observability values. The closure is retained in memory for this session
181+ * and is never added to execution data.
182+ */
183+ setEnvironmentSecretSanitizer ( sanitizer : EnvironmentSecretSanitizer ) : void {
184+ this . environmentSecretSanitizer = sanitizer
185+ }
186+
187+ private sanitizeForLog < T > ( value : T ) : T {
188+ return this . environmentSecretSanitizer ( value )
189+ }
190+
173191 async onBlockStart (
174192 blockId : string ,
175193 blockName : string ,
@@ -287,17 +305,24 @@ export class LoggingSession {
287305 level ?: 'info' | 'error'
288306 status ?: 'completed' | 'failed' | 'cancelled' | 'pending'
289307 } ) : Promise < void > {
308+ const finalOutput = this . sanitizeForLog ( params . finalOutput )
309+ const traceSpans = this . sanitizeForLog ( params . traceSpans )
310+ const completionFailure =
311+ params . completionFailure === undefined
312+ ? undefined
313+ : this . sanitizeForLog ( params . completionFailure )
314+
290315 await executionLogger . completeWorkflowExecution ( {
291316 executionId : this . executionId ,
292317 endedAt : params . endedAt ,
293318 totalDurationMs : params . totalDurationMs ,
294319 costSummary : params . costSummary ,
295- finalOutput : params . finalOutput ,
296- traceSpans : params . traceSpans ,
320+ finalOutput,
321+ traceSpans,
297322 workflowInput : params . workflowInput ,
298323 executionState : params . executionState ,
299324 finalizationPath : params . finalizationPath ,
300- completionFailure : params . completionFailure ,
325+ completionFailure,
301326 isResume : this . isResume ,
302327 level : params . level ,
303328 status : params . status ,
@@ -403,34 +428,36 @@ export class LoggingSession {
403428 }
404429 this . completing = true
405430
406- const { endedAt, totalDurationMs, finalOutput, traceSpans, workflowInput, executionState } =
407- params
431+ const { endedAt, totalDurationMs, workflowInput, executionState } = params
432+ const finalOutput = this . sanitizeForLog ( params . finalOutput || { } )
433+ const rawTraceSpans = params . traceSpans || [ ]
434+ const traceSpans = this . sanitizeForLog ( rawTraceSpans )
408435
409436 try {
410- const costSummary = calculateCostSummary ( traceSpans || [ ] )
437+ const costSummary = calculateCostSummary ( rawTraceSpans )
411438 const endTime = endedAt || new Date ( ) . toISOString ( )
412439 const duration = totalDurationMs || 0
413440
414441 await this . completeExecutionWithFinalization ( {
415442 endedAt : endTime ,
416443 totalDurationMs : duration ,
417444 costSummary,
418- finalOutput : finalOutput || { } ,
419- traceSpans : traceSpans || [ ] ,
445+ finalOutput,
446+ traceSpans,
420447 workflowInput,
421448 executionState,
422449 finalizationPath : 'completed' ,
423450 } )
424451
425452 this . completed = true
426453
427- if ( traceSpans && traceSpans . length > 0 ) {
454+ if ( traceSpans . length > 0 ) {
428455 try {
429456 const { PlatformEvents, createOTelSpansForWorkflowExecution } = await import (
430457 '@/lib/core/telemetry'
431458 )
432459
433- const hasErrors = traceSpans . some ( ( span : any ) => {
460+ const hasErrors = rawTraceSpans . some ( ( span : any ) => {
434461 const checkForErrors = ( s : any ) : boolean => {
435462 if ( s . status === 'error' && ! s . errorHandled ) return true
436463 if ( s . children && Array . isArray ( s . children ) ) {
@@ -506,13 +533,15 @@ export class LoggingSession {
506533 return
507534 }
508535
509- const { endedAt, totalDurationMs, error, traceSpans, skipCost } = params
536+ const { endedAt, totalDurationMs, error, skipCost } = params
537+ const rawTraceSpans = params . traceSpans || [ ]
538+ const traceSpans = this . sanitizeForLog ( rawTraceSpans )
510539
511540 const endTime = endedAt ? new Date ( endedAt ) : new Date ( )
512541 const durationMs = typeof totalDurationMs === 'number' ? totalDurationMs : 0
513542 const startTime = new Date ( endTime . getTime ( ) - Math . max ( 1 , durationMs ) )
514543
515- const hasProvidedSpans = Array . isArray ( traceSpans ) && traceSpans . length > 0
544+ const hasProvidedSpans = traceSpans . length > 0
516545
517546 // calculateCostSummary([]) / (undefined) already returns the base-charge
518547 // summary, so the no-spans branch needs no separate literal.
@@ -528,9 +557,9 @@ export class LoggingSession {
528557 models : { } ,
529558 charges : { } ,
530559 }
531- : calculateCostSummary ( traceSpans )
560+ : calculateCostSummary ( rawTraceSpans )
532561
533- const message = error ?. message || 'Run failed before starting blocks'
562+ const message = this . sanitizeForLog ( error ?. message || 'Run failed before starting blocks' )
534563
535564 const errorSpan : TraceSpan = {
536565 id : 'workflow-error-root' ,
@@ -615,7 +644,9 @@ export class LoggingSession {
615644 this . completing = true
616645
617646 try {
618- const { endedAt, totalDurationMs, traceSpans } = params
647+ const { endedAt, totalDurationMs } = params
648+ const rawTraceSpans = params . traceSpans || [ ]
649+ const traceSpans = this . sanitizeForLog ( rawTraceSpans )
619650
620651 const endTime = endedAt ? new Date ( endedAt ) : new Date ( )
621652 const durationMs = typeof totalDurationMs === 'number' ? totalDurationMs : 0
@@ -639,14 +670,14 @@ export class LoggingSession {
639670
640671 // calculateCostSummary handles empty/undefined spans by returning the
641672 // base-charge summary, so no separate no-spans literal is needed.
642- const costSummary = calculateCostSummary ( traceSpans )
673+ const costSummary = calculateCostSummary ( rawTraceSpans )
643674
644675 await this . completeExecutionWithFinalization ( {
645676 endedAt : endTime . toISOString ( ) ,
646677 totalDurationMs : Math . max ( 1 , durationMs ) ,
647678 costSummary,
648679 finalOutput : { cancelled : true } ,
649- traceSpans : traceSpans || [ ] ,
680+ traceSpans,
650681 finalizationPath : 'cancelled' ,
651682 status : 'cancelled' ,
652683 } )
@@ -662,11 +693,11 @@ export class LoggingSession {
662693 durationMs : Math . max ( 1 , durationMs ) ,
663694 status : 'cancelled' ,
664695 trigger : this . triggerType ,
665- blocksExecuted : traceSpans ? .length || 0 ,
696+ blocksExecuted : traceSpans . length ,
666697 hasErrors : false ,
667698 } )
668699
669- if ( traceSpans && traceSpans . length > 0 ) {
700+ if ( traceSpans . length > 0 ) {
670701 const startTime = new Date ( endTime . getTime ( ) - Math . max ( 1 , durationMs ) )
671702 createOTelSpansForWorkflowExecution ( {
672703 workflowId : this . workflowId ,
@@ -709,7 +740,9 @@ export class LoggingSession {
709740 this . completing = true
710741
711742 try {
712- const { endedAt, totalDurationMs, traceSpans, workflowInput } = params
743+ const { endedAt, totalDurationMs, workflowInput } = params
744+ const rawTraceSpans = params . traceSpans || [ ]
745+ const traceSpans = this . sanitizeForLog ( rawTraceSpans )
713746
714747 const endTime = endedAt ? new Date ( endedAt ) : new Date ( )
715748 const durationMs = typeof totalDurationMs === 'number' ? totalDurationMs : 0
@@ -733,14 +766,14 @@ export class LoggingSession {
733766
734767 // calculateCostSummary handles empty/undefined spans by returning the
735768 // base-charge summary, so no separate no-spans literal is needed.
736- const costSummary = calculateCostSummary ( traceSpans )
769+ const costSummary = calculateCostSummary ( rawTraceSpans )
737770
738771 await this . completeExecutionWithFinalization ( {
739772 endedAt : endTime . toISOString ( ) ,
740773 totalDurationMs : Math . max ( 1 , durationMs ) ,
741774 costSummary,
742775 finalOutput : { paused : true } ,
743- traceSpans : traceSpans || [ ] ,
776+ traceSpans,
744777 workflowInput,
745778 finalizationPath : 'paused' ,
746779 status : 'pending' ,
@@ -757,12 +790,12 @@ export class LoggingSession {
757790 durationMs : Math . max ( 1 , durationMs ) ,
758791 status : 'paused' ,
759792 trigger : this . triggerType ,
760- blocksExecuted : traceSpans ? .length || 0 ,
793+ blocksExecuted : traceSpans . length ,
761794 hasErrors : false ,
762795 totalCost : costSummary . totalCost || 0 ,
763796 } )
764797
765- if ( traceSpans && traceSpans . length > 0 ) {
798+ if ( traceSpans . length > 0 ) {
766799 const startTime = new Date ( endTime . getTime ( ) - Math . max ( 1 , durationMs ) )
767800 createOTelSpansForWorkflowExecution ( {
768801 workflowId : this . workflowId ,
0 commit comments