Skip to content

Commit a2805bb

Browse files
committed
fix
1 parent c182723 commit a2805bb

14 files changed

Lines changed: 522 additions & 120 deletions

apps/sim/executor/execution/block-executor.test.ts

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1064,16 +1064,119 @@ describe('BlockExecutor streaming pump', () => {
10641064
},
10651065
state
10661066
)
1067-
return { executor, block, state }
1067+
return { executor, block, state, resolver }
10681068
}
10691069

1070+
it('projects resolver-owned inputs for display without carrying them into output provenance', async () => {
1071+
const secret = 'x'
1072+
const handler: BlockHandler = {
1073+
canHandle: () => true,
1074+
execute: async (blockContext, _block, inputs) => {
1075+
expect(inputs.systemPrompt).toBe(secret)
1076+
const sourceRegistry = blockContext.resolvedSecretTraceRegistry
1077+
blockContext.resolvedSecretTraceRegistry = sourceRegistry?.forkForInputPaths([])
1078+
return { content: 'Box' }
1079+
},
1080+
}
1081+
const { executor, block, state } = createExecutor(handler)
1082+
block.config.params = { systemPrompt: '{{TOKEN}}' }
1083+
const ctx = createContext(state)
1084+
const registry = new ResolvedSecretTraceRegistry([
1085+
{ name: 'TOKEN', plaintext: secret, encryptedValue: 'encrypted-token' },
1086+
])
1087+
ctx.environmentVariables = { TOKEN: secret }
1088+
ctx.resolvedSecretTraceRegistry = registry
1089+
1090+
await executor.execute(ctx, createNode(block), block)
1091+
1092+
expect(ctx.blockLogs[0]).toMatchObject({
1093+
input: { systemPrompt: '{{TOKEN}}' },
1094+
output: { content: 'Box' },
1095+
})
1096+
expect(state.getBlockState(block.id)?.resolvedSecretTraceProvenance).toEqual({
1097+
version: 1,
1098+
complete: true,
1099+
entries: [],
1100+
})
1101+
expect(registry.getActiveMatches()).toEqual([])
1102+
})
1103+
1104+
it('keeps terminal error output provenance separate from low-entropy input provenance', async () => {
1105+
const secret = 'x'
1106+
const handler: BlockHandler = {
1107+
canHandle: () => true,
1108+
execute: async (blockContext, _block, inputs) => {
1109+
expect(inputs.systemPrompt).toBe(secret)
1110+
const sourceRegistry = blockContext.resolvedSecretTraceRegistry
1111+
blockContext.resolvedSecretTraceRegistry = sourceRegistry?.forkForInputPaths([])
1112+
throw new Error('Box')
1113+
},
1114+
}
1115+
const { executor, block, state } = createExecutor(handler)
1116+
block.config.params = { systemPrompt: '{{TOKEN}}' }
1117+
const ctx = createContext(state)
1118+
const registry = new ResolvedSecretTraceRegistry([
1119+
{ name: 'TOKEN', plaintext: secret, encryptedValue: 'encrypted-token' },
1120+
])
1121+
ctx.environmentVariables = { TOKEN: secret }
1122+
ctx.resolvedSecretTraceRegistry = registry
1123+
1124+
await expect(executor.execute(ctx, createNode(block), block)).rejects.toThrow('Agent: Box')
1125+
1126+
expect(ctx.blockLogs[0]).toMatchObject({
1127+
input: { systemPrompt: '{{TOKEN}}' },
1128+
output: { error: 'Box' },
1129+
})
1130+
expect(state.getBlockState(block.id)?.resolvedSecretTraceProvenance).toEqual({
1131+
version: 1,
1132+
complete: true,
1133+
entries: [],
1134+
})
1135+
expect(registry.getActiveMatches()).toEqual([])
1136+
})
1137+
1138+
it('suppresses an incomplete display input without failing block execution', async () => {
1139+
const handler: BlockHandler = {
1140+
canHandle: () => true,
1141+
execute: async (blockContext) => {
1142+
blockContext.resolvedSecretTraceRegistry =
1143+
blockContext.resolvedSecretTraceRegistry?.forkForInputPaths([])
1144+
return { content: 'done' }
1145+
},
1146+
}
1147+
const { executor, block, state, resolver } = createExecutor(handler)
1148+
const inputs = {
1149+
userPrompt: 'Use the configured tool.',
1150+
tools: [{ params: { apiKey: 'unknown-value' } }],
1151+
}
1152+
vi.spyOn(resolver, 'resolveInputs').mockImplementation(async (blockContext) => {
1153+
await blockContext.resolvedSecretTraceRegistry?.importProvenanceForValueAtInputPath(
1154+
{ version: 1 },
1155+
'unknown-value',
1156+
['tools', '0', 'params', 'apiKey'],
1157+
{ trusted: true }
1158+
)
1159+
return inputs
1160+
})
1161+
const ctx = createContext(state)
1162+
ctx.resolvedSecretTraceRegistry = new ResolvedSecretTraceRegistry()
1163+
1164+
await expect(executor.execute(ctx, createNode(block), block)).resolves.toEqual({
1165+
content: 'done',
1166+
})
1167+
1168+
expect(ctx.blockLogs[0]?.input).toEqual({})
1169+
expect(ctx.blockLogs[0]?.output).toEqual({ content: 'done' })
1170+
})
1171+
10701172
function createAgentEventsStreamingHandler(options: {
10711173
events: Array<Record<string, unknown>>
10721174
attachThinkingOnDrain?: string
10731175
failAfterText?: string
10741176
streamError?: Error
10751177
onFullContent?: (content: string) => void | Promise<void>
10761178
resolvedSecret?: { name: string; value: string }
1179+
separateResultRegistry?: boolean
10771180
}): BlockHandler {
10781181
return {
10791182
canHandle: () => true,
@@ -1084,6 +1187,12 @@ describe('BlockExecutor streaming pump', () => {
10841187
options.resolvedSecret.value
10851188
)
10861189
}
1190+
const diagnosticRegistry = options.separateResultRegistry
1191+
? blockContext.resolvedSecretTraceRegistry
1192+
: undefined
1193+
if (diagnosticRegistry) {
1194+
blockContext.resolvedSecretTraceRegistry = diagnosticRegistry.forkForInputPaths([])
1195+
}
10871196
const timeSegment: Record<string, unknown> = {
10881197
type: 'model',
10891198
name: 'claude-test',
@@ -1139,6 +1248,7 @@ describe('BlockExecutor streaming pump', () => {
11391248
},
11401249
},
11411250
onFullContent: options.onFullContent,
1251+
diagnosticResolvedSecretTraceRegistry: diagnosticRegistry,
11421252
}
11431253
},
11441254
}
@@ -1290,13 +1400,15 @@ describe('BlockExecutor streaming pump', () => {
12901400
failAfterText: 'partial',
12911401
streamError: rawError,
12921402
resolvedSecret: { name: 'API_KEY', value: secret },
1403+
separateResultRegistry: true,
12931404
})
12941405
const { executor, block, state } = createExecutor(handler)
12951406
const ctx = createContext(state)
12961407
ctx.resolvedSecretTraceRegistry = new ResolvedSecretTraceRegistry([
12971408
{ name: 'API_KEY', plaintext: secret, encryptedValue: 'encrypted-api-key' },
12981409
])
12991410
ctx.onStream = async (streamingExec) => {
1411+
expect(streamingExec).not.toHaveProperty('diagnosticResolvedSecretTraceRegistry')
13001412
const reader = streamingExec.stream.getReader()
13011413
try {
13021414
while (true) {

apps/sim/executor/execution/block-executor.ts

Lines changed: 55 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,10 @@ import {
5151
import { isJSONString } from '@/executor/utils/json'
5252
import { filterOutputForLog } from '@/executor/utils/output-filter'
5353
import { projectResolvedSecretDiagnosticError } from '@/executor/utils/resolved-secret-content-projection'
54-
import type { ResolvedSecretTraceProvenanceV1 } from '@/executor/utils/resolved-secret-trace-registry'
54+
import type {
55+
ResolvedSecretTraceProvenanceV1,
56+
ResolvedSecretTraceRegistry,
57+
} from '@/executor/utils/resolved-secret-trace-registry'
5558
import {
5659
buildBranchNodeId,
5760
buildOuterBranchScopedId,
@@ -104,6 +107,7 @@ export class BlockExecutor {
104107
const blockResolvedSecretTraceRegistry = parentResolvedSecretTraceRegistry?.forkForInputPaths(
105108
[]
106109
)
110+
const inputDisplayRegistry = blockResolvedSecretTraceRegistry
107111
const blockCtx = blockResolvedSecretTraceRegistry
108112
? { ...ctx, resolvedSecretTraceRegistry: blockResolvedSecretTraceRegistry }
109113
: ctx
@@ -195,7 +199,7 @@ export class BlockExecutor {
195199
}
196200

197201
if (blockLog) {
198-
blockLog.input = this.sanitizeInputsForLog(inputsForLog, block)
202+
blockLog.input = this.projectInputsForDisplay(inputsForLog, block, inputDisplayRegistry)
199203
}
200204
} catch (error) {
201205
cleanupSelfReference?.()
@@ -209,6 +213,7 @@ export class BlockExecutor {
209213
startTime,
210214
blockLog,
211215
inputsForLog,
216+
inputDisplayRegistry,
212217
isSentinel,
213218
'input_resolution'
214219
)
@@ -245,6 +250,8 @@ export class BlockExecutor {
245250
normalizeStringArray(blockCtx.selectedOutputs)
246251
)
247252
} catch (streamError) {
253+
blockCtx.resolvedSecretTraceRegistry =
254+
blockCtx.resolvedSecretTraceRegistry?.forkForPropagatedEntries()
248255
// Timeout / drain failures may still have projected answer text — keep it
249256
// for the failed block output so logs match what the client already saw.
250257
streamingPartialOutput = streamingExec.execution?.output
@@ -337,7 +344,7 @@ export class BlockExecutor {
337344
const displayOutput = filterOutputForLog(block.metadata?.id || '', normalizedOutput, {
338345
block,
339346
})
340-
const displayInput = this.sanitizeInputsForLog(inputsForLog, block)
347+
const displayInput = this.projectInputsForDisplay(inputsForLog, block, inputDisplayRegistry)
341348
blockLog.input = displayInput
342349
const displayProvenance = settledBlockRegistry?.exportCommittedProvenanceForValue({
343350
input: displayInput,
@@ -374,6 +381,7 @@ export class BlockExecutor {
374381
startTime,
375382
blockLog,
376383
inputsForLog,
384+
inputDisplayRegistry,
377385
isSentinel,
378386
'execution',
379387
streamingPartialOutput
@@ -459,6 +467,7 @@ export class BlockExecutor {
459467
startTime: number,
460468
blockLog: BlockLog | undefined,
461469
inputsForLog: Record<string, any>,
470+
inputDisplayRegistry: ResolvedSecretTraceRegistry | undefined,
462471
isSentinel: boolean,
463472
phase: 'input_resolution' | 'execution',
464473
streamingPartialOutput?: Record<string, any>
@@ -495,7 +504,7 @@ export class BlockExecutor {
495504
blockLog.durationMs = duration
496505
blockLog.success = true
497506
blockLog.error = undefined
498-
blockLog.input = this.sanitizeInputsForLog(input, block)
507+
blockLog.input = this.projectInputsForDisplay(input, block, inputDisplayRegistry)
499508
blockLog.output = filterOutputForLog(block.metadata?.id || '', softOutput, { block })
500509
}
501510

@@ -505,7 +514,7 @@ export class BlockExecutor {
505514
})
506515

507516
if (!isSentinel && blockLog) {
508-
const displayInput = this.sanitizeInputsForLog(input, block)
517+
const displayInput = this.projectInputsForDisplay(input, block, inputDisplayRegistry)
509518
const displayOutput = filterOutputForLog(block.metadata?.id || '', softOutput, { block })
510519
const displayProvenance =
511520
ctx.resolvedSecretTraceRegistry?.exportCommittedProvenanceForValue({
@@ -575,17 +584,25 @@ export class BlockExecutor {
575584
blockLog.durationMs = duration
576585
blockLog.success = false
577586
blockLog.error = errorMessage
578-
blockLog.input = this.sanitizeInputsForLog(input, block)
587+
blockLog.input = this.projectInputsForDisplay(input, block, inputDisplayRegistry)
579588
blockLog.output = filterOutputForLog(block.metadata?.id || '', errorOutput, { block })
580589

581590
if (ChildWorkflowError.isChildWorkflowError(error) && error.childTraceSpans.length > 0) {
582591
blockLog.childTraceSpans = error.childTraceSpans
583592
}
584593
}
585594

595+
const diagnosticRegistry = inputDisplayRegistry?.forkForToolCall()
596+
if (
597+
diagnosticRegistry &&
598+
ctx.resolvedSecretTraceRegistry &&
599+
ctx.resolvedSecretTraceRegistry !== inputDisplayRegistry
600+
) {
601+
diagnosticRegistry.mergeToolCallRegistry(ctx.resolvedSecretTraceRegistry)
602+
}
586603
const errorDiagnostic = projectResolvedSecretDiagnosticError(
587604
error,
588-
ctx.resolvedSecretTraceRegistry
605+
diagnosticRegistry ?? ctx.resolvedSecretTraceRegistry
589606
)
590607

591608
this.execLogger.error(
@@ -602,7 +619,7 @@ export class BlockExecutor {
602619
? error.childWorkflowInstanceId
603620
: undefined
604621
const displayOutput = filterOutputForLog(block.metadata?.id || '', errorOutput, { block })
605-
const displayInput = this.sanitizeInputsForLog(input, block)
622+
const displayInput = this.projectInputsForDisplay(input, block, inputDisplayRegistry)
606623
const displayProvenance = ctx.resolvedSecretTraceRegistry?.exportCommittedProvenanceForValue({
607624
input: displayInput,
608625
output: displayOutput,
@@ -729,6 +746,17 @@ export class BlockExecutor {
729746
return { result: output }
730747
}
731748

749+
/** Builds the log-facing input copy from resolver-recorded projections only. */
750+
private projectInputsForDisplay(
751+
inputs: Record<string, any>,
752+
block: SerializedBlock | undefined,
753+
registry: ResolvedSecretTraceRegistry | undefined
754+
): Record<string, any> {
755+
const projection = registry?.projectResolvedInputSelection(inputs)
756+
if (projection && !projection.complete) return {}
757+
return this.sanitizeInputsForLog(projection?.value ?? inputs, block)
758+
}
759+
732760
/**
733761
* Sanitizes inputs for log display.
734762
* - Filters out system fields (UI-only, readonly, internal flags)
@@ -974,6 +1002,16 @@ export class BlockExecutor {
9741002
const piiEnabled = Boolean(ctx.piiBlockOutputRedaction?.enabled)
9751003
// Live-forward only when a client stream exists and PII redaction is off.
9761004
const forwardToClient = Boolean(ctx.onStream) && !piiEnabled
1005+
const projectStreamDiagnosticError = (error: unknown): Record<string, unknown> => {
1006+
const sourceRegistry = streamingExec.diagnosticResolvedSecretTraceRegistry
1007+
const resultRegistry = ctx.resolvedSecretTraceRegistry
1008+
if (!sourceRegistry || sourceRegistry === resultRegistry) {
1009+
return projectResolvedSecretDiagnosticError(error, resultRegistry)
1010+
}
1011+
const diagnosticRegistry = sourceRegistry.forkForToolCall()
1012+
if (resultRegistry) diagnosticRegistry.mergeToolCallRegistry(resultRegistry)
1013+
return projectResolvedSecretDiagnosticError(error, diagnosticRegistry)
1014+
}
9771015

9781016
const responseFormat =
9791017
resolvedInputs?.responseFormat ??
@@ -993,6 +1031,10 @@ export class BlockExecutor {
9931031
let processedClientStream: ReadableStream<Uint8Array> | undefined
9941032

9951033
if (forwardToClient && ctx.onStream && pump.textStream) {
1034+
const {
1035+
diagnosticResolvedSecretTraceRegistry: _diagnosticRegistry,
1036+
...streamingExecutionForConsumer
1037+
} = streamingExec
9961038
processedClientStream = streamingResponseFormatProcessor.processStream(
9971039
pump.textStream,
9981040
blockId,
@@ -1005,7 +1047,7 @@ export class BlockExecutor {
10051047
// with `pump.run()`.
10061048
onStreamPromise = ctx
10071049
.onStream({
1008-
...streamingExec,
1050+
...streamingExecutionForConsumer,
10091051
stream: processedClientStream,
10101052
streamFormat: 'text',
10111053
subscribe: pump.subscribe,
@@ -1018,7 +1060,7 @@ export class BlockExecutor {
10181060
.catch(async (error) => {
10191061
this.execLogger.error('Error in onStream callback', {
10201062
blockId,
1021-
...projectResolvedSecretDiagnosticError(error, ctx.resolvedSecretTraceRegistry),
1063+
...projectStreamDiagnosticError(error),
10221064
})
10231065
await processedClientStream?.cancel().catch(() => {})
10241066
})
@@ -1030,7 +1072,7 @@ export class BlockExecutor {
10301072
} catch (error) {
10311073
this.execLogger.error('Error reading stream for block', {
10321074
blockId,
1033-
...projectResolvedSecretDiagnosticError(error, ctx.resolvedSecretTraceRegistry),
1075+
...projectStreamDiagnosticError(error),
10341076
})
10351077
if (onStreamPromise) {
10361078
await onStreamPromise.catch(() => {})
@@ -1121,7 +1163,7 @@ export class BlockExecutor {
11211163
} catch (error) {
11221164
this.execLogger.warn('Failed to parse streamed content for response format', {
11231165
blockId,
1124-
...projectResolvedSecretDiagnosticError(error, ctx.resolvedSecretTraceRegistry),
1166+
...projectStreamDiagnosticError(error),
11251167
})
11261168
}
11271169
}
@@ -1136,7 +1178,7 @@ export class BlockExecutor {
11361178
} catch (error) {
11371179
this.execLogger.error('onFullContent callback failed', {
11381180
blockId,
1139-
...projectResolvedSecretDiagnosticError(error, ctx.resolvedSecretTraceRegistry),
1181+
...projectStreamDiagnosticError(error),
11401182
})
11411183
}
11421184
}

0 commit comments

Comments
 (0)