@@ -3,6 +3,7 @@ import { toError } from '@sim/utils/errors'
33import { omit } from '@sim/utils/object'
44import { isLargeValueRef } from '@/lib/execution/payloads/large-value-ref'
55import { materializeLargeValueRef , storeLargeValue } from '@/lib/execution/payloads/store'
6+ import { FunctionalOutputsUnavailableError } from '@/lib/logs/execution/functional-outputs'
67import { projectTraceSpansForSecrets } from '@/lib/logs/execution/trace-secret-projection'
78import type { TraceSpan } from '@/lib/logs/types'
89import {
@@ -72,6 +73,11 @@ export interface TraceStoreReadContext {
7273 userId ?: string
7374}
7475
76+ export interface DisplayExecutionDataWithBlockOutputs {
77+ executionData : Record < string , unknown >
78+ blockOutputs : Map < string , unknown >
79+ }
80+
7581/**
7682 * Write-path context. Requires the execution owner's `userId`: the externalized
7783 * object is tracked in `workspace_files`, whose `user_id` column is NOT NULL
@@ -269,6 +275,100 @@ export async function materializeExecutionDataForDisplay(
269275 return projectExecutionDataForDisplay ( materialized , context )
270276}
271277
278+ /**
279+ * Materializes one trusted row into its display envelope plus secret-safe functional outputs.
280+ * Only requested execution-state outputs are projected and returned; trace spans remain display
281+ * data and the raw execution state never crosses the display boundary.
282+ */
283+ export async function materializeExecutionDataForDisplayWithBlockOutputs (
284+ executionData : Record < string , unknown > | null | undefined ,
285+ context : TraceStoreReadContext ,
286+ blockIds : readonly string [ ]
287+ ) : Promise < DisplayExecutionDataWithBlockOutputs > {
288+ const materialized = await materializeExecutionData ( executionData , context )
289+ const displayData = await projectExecutionDataForDisplay ( materialized , context )
290+ if ( blockIds . length === 0 ) {
291+ return { executionData : displayData , blockOutputs : new Map ( ) }
292+ }
293+
294+ const executionState = readRecord ( materialized . executionState )
295+ const blockStates = readRecord ( executionState ?. blockStates )
296+ if ( ! blockStates ) {
297+ if ( materialized . executionDataTruncated === true ) {
298+ throw new FunctionalOutputsUnavailableError ( )
299+ }
300+ return { executionData : displayData , blockOutputs : new Map ( ) }
301+ }
302+
303+ const runRegistry = await importResolvedSecretTraceRegistry (
304+ materialized [ RESOLVED_SECRET_PROVENANCE_KEY ] ??
305+ executionState ?. [ RESOLVED_SECRET_PROVENANCE_KEY ] ,
306+ 'traceStore.blockOutputRunProvenance'
307+ )
308+ const blockOutputs = new Map < string , unknown > ( )
309+ const projectionStore = createReadOnlyProjectionStore ( context )
310+
311+ for ( const blockId of new Set ( blockIds ) ) {
312+ const blockState = readRecord ( blockStates [ blockId ] )
313+ if ( ! blockState || blockState . output === undefined ) continue
314+
315+ const hasExactProvenance = Object . hasOwn ( blockState , RESOLVED_SECRET_PROVENANCE_KEY )
316+ const registry = hasExactProvenance
317+ ? await importResolvedSecretTraceRegistry (
318+ blockState [ RESOLVED_SECRET_PROVENANCE_KEY ] ,
319+ 'traceStore.blockOutputExactProvenance'
320+ )
321+ : runRegistry
322+ const now = new Date ( ) . toISOString ( )
323+ const [ projected ] = await projectTraceSpansForSecrets (
324+ [
325+ {
326+ id : `${ LOG_DISPLAY_PROJECTION_SPAN_ID } -block-output` ,
327+ name : 'Block Output Display Projection' ,
328+ type : 'display' ,
329+ duration : 0 ,
330+ startTime : now ,
331+ endTime : now ,
332+ output : { value : blockState . output } ,
333+ } ,
334+ ] ,
335+ { registry, allowLargeValueWrites : false , store : projectionStore }
336+ )
337+ if ( projected ?. output && Object . hasOwn ( projected . output , 'value' ) ) {
338+ blockOutputs . set ( blockId , projected . output . value )
339+ }
340+ }
341+
342+ return { executionData : displayData , blockOutputs }
343+ }
344+
345+ function readRecord ( value : unknown ) : Record < string , unknown > | undefined {
346+ return value && typeof value === 'object' && ! Array . isArray ( value )
347+ ? ( value as Record < string , unknown > )
348+ : undefined
349+ }
350+
351+ async function importResolvedSecretTraceRegistry (
352+ provenance : unknown ,
353+ origin : string
354+ ) : Promise < ResolvedSecretTraceRegistry | undefined > {
355+ if ( ! isResolvedSecretTraceProvenanceV1 ( provenance ) ) return undefined
356+
357+ const registry = new ResolvedSecretTraceRegistry ( [ ] , provenance . scope )
358+ await registry . importProvenance ( provenance , { trusted : true , origin } )
359+ return registry
360+ }
361+
362+ function createReadOnlyProjectionStore ( context : TraceStoreReadContext ) {
363+ return {
364+ workspaceId : context . workspaceId ?? undefined ,
365+ workflowId : context . workflowId ?? undefined ,
366+ executionId : context . executionId ,
367+ userId : context . userId ,
368+ trackReference : false ,
369+ }
370+ }
371+
272372/**
273373 * Projects execution-log content with the encrypted provenance saved by the
274374 * trusted executor. Current workflow input and final output values use their
@@ -284,12 +384,7 @@ export async function projectExecutionDataForDisplay(
284384 executionData : Record < string , unknown > ,
285385 context : TraceStoreReadContext
286386) : Promise < Record < string , unknown > > {
287- const executionState =
288- executionData . executionState &&
289- typeof executionData . executionState === 'object' &&
290- ! Array . isArray ( executionData . executionState )
291- ? ( executionData . executionState as Record < string , unknown > )
292- : undefined
387+ const executionState = readRecord ( executionData . executionState )
293388 const hasTopLevelProvenance = Object . hasOwn ( executionData , RESOLVED_SECRET_PROVENANCE_KEY )
294389 const stateProvenance = executionState ?. [ RESOLVED_SECRET_PROVENANCE_KEY ]
295390 const provenance = executionData [ RESOLVED_SECRET_PROVENANCE_KEY ] ?? stateProvenance
@@ -302,15 +397,7 @@ export async function projectExecutionDataForDisplay(
302397 return projectLegacyExecutionDataForDisplay ( executionData )
303398 }
304399
305- let registry : ResolvedSecretTraceRegistry | undefined
306-
307- if ( isResolvedSecretTraceProvenanceV1 ( provenance ) ) {
308- registry = new ResolvedSecretTraceRegistry ( [ ] , provenance . scope )
309- await registry . importProvenance ( provenance , {
310- trusted : true ,
311- origin : 'traceStore.spanProvenance' ,
312- } )
313- }
400+ const registry = await importResolvedSecretTraceRegistry ( provenance , 'traceStore.spanProvenance' )
314401
315402 /**
316403 * Compaction drops `executionState`, and with it the only copy of the
@@ -339,13 +426,7 @@ export async function projectExecutionDataForDisplay(
339426 } )
340427 }
341428
342- const projectionStore = {
343- workspaceId : context . workspaceId ?? undefined ,
344- workflowId : context . workflowId ?? undefined ,
345- executionId : context . executionId ,
346- userId : context . userId ,
347- trackReference : false ,
348- }
429+ const projectionStore = createReadOnlyProjectionStore ( context )
349430
350431 const exactValueProjections = new Map < string , unknown > ( )
351432 for ( const [ valueKey , provenanceKey ] of Object . entries ( EXACT_LOG_VALUE_PROVENANCE_KEYS ) ) {
0 commit comments