Skip to content

Commit 4717819

Browse files
committed
fix(logs): gate workflow input inherited from a prior execution
Greptile review: the boundary exemption assumed a non-nested trigger implies an inbound payload. A re-run with inputFromExecutionId breaks that. It copies the source run's workflowInput verbatim, and the destination presents whatever trigger type its caller asked for, so a value resolved inside a custom_block parent could arrive in a manual run and be exempted. The destination's own provenance cannot describe the source's secret resolution, so no matcher covers it. The read path could not tell an inherited input from a fresh one, so the re-run now records its source: LoggingSession.setInputSourceExecutionId stamps it into the existing trigger data channel, and the display projection withholds the exemption whenever it is present. Nested runs and inherited-input runs share one predicate - neither is a pre-resolution inbound payload. Adds setInputSourceExecutionId to the shared @sim/testing LoggingSession mock so the execute-route suites exercise the real call.
1 parent a4cd453 commit 4717819

5 files changed

Lines changed: 101 additions & 9 deletions

File tree

apps/sim/app/api/workflows/[id]/execute/route.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1174,6 +1174,15 @@ async function handleExecutePost(
11741174
loggingTriggerType,
11751175
requestId
11761176
)
1177+
/**
1178+
* Reusing a prior run's input copies that run's exposure with it, and this
1179+
* run's own provenance cannot describe a secret the source resolved. Record
1180+
* the source so the log display projection withholds the workflow-boundary
1181+
* exemption for this run.
1182+
*/
1183+
if (inputFromExecutionId) {
1184+
loggingSession.setInputSourceExecutionId(inputFromExecutionId)
1185+
}
11771186
if (copilotToolCallId) {
11781187
loggingSession.setTrustedExecutionCorrelation({
11791188
executionId,

apps/sim/lib/logs/execution/logging-session.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ export class LoggingSession {
201201
private postExecutionPromise: Promise<void> | null = null
202202
private resolvedSecretTraceRegistry?: ResolvedSecretTraceRegistry
203203
private traceLargeValueAccess: LargeValueStoreContext = {}
204+
private inputSourceExecutionId?: string
204205

205206
constructor(
206207
workflowId: string,
@@ -226,6 +227,20 @@ export class LoggingSession {
226227
this.resolvedSecretTraceRegistry = registry
227228
}
228229

230+
/**
231+
* Records that this run's input was copied from a prior execution
232+
* (`inputFromExecutionId`) rather than arriving with the trigger.
233+
*
234+
* The log display projection reads this to withhold the workflow-boundary
235+
* exemption. An inherited input carries the SOURCE run's exposure - a value
236+
* resolved inside a custom-block parent stays plaintext through the copy -
237+
* and this run's own provenance cannot describe that resolution, so it must
238+
* not be treated as a pre-resolution inbound payload.
239+
*/
240+
setInputSourceExecutionId(sourceExecutionId: string): void {
241+
this.inputSourceExecutionId = sourceExecutionId
242+
}
243+
229244
/** Adds server-validated lifecycle correlation without exposing it to executor metadata. */
230245
setTrustedExecutionCorrelation(
231246
correlation: NonNullable<NonNullable<ExecutionTrigger['data']>['correlation']>
@@ -626,9 +641,18 @@ export class LoggingSession {
626641
}
627642

628643
try {
629-
const effectiveTriggerData = this.trustedExecutionCorrelation
630-
? { ...triggerData, correlation: this.trustedExecutionCorrelation }
631-
: triggerData
644+
const derivedTriggerData = {
645+
...(this.trustedExecutionCorrelation
646+
? { correlation: this.trustedExecutionCorrelation }
647+
: {}),
648+
...(this.inputSourceExecutionId
649+
? { inputSourceExecutionId: this.inputSourceExecutionId }
650+
: {}),
651+
}
652+
const effectiveTriggerData =
653+
Object.keys(derivedTriggerData).length > 0 || triggerData
654+
? { ...triggerData, ...derivedTriggerData }
655+
: undefined
632656
this.trigger = createTriggerObject(this.triggerType, effectiveTriggerData)
633657
this.correlation = effectiveTriggerData?.correlation
634658
this.environment = createEnvironmentObject(

apps/sim/lib/logs/execution/trace-store.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,40 @@ describe('projectExecutionDataForDisplay', () => {
302302
}
303303
)
304304

305+
/**
306+
* A re-run presents whatever trigger type its caller asked for, so the value
307+
* copied out of a nested run would otherwise look inbound. The stamped source
308+
* id keeps it gated.
309+
*/
310+
it('gates workflowInput inherited from a prior execution regardless of trigger type', async () => {
311+
const displayData = await projectExecutionDataForDisplay(
312+
{
313+
trigger: {
314+
type: 'manual',
315+
source: 'manual',
316+
data: { inputSourceExecutionId: 'execution-source-1' },
317+
},
318+
workflowInput: { apiKey: 'INHERITED-FROM-CUSTOM-BLOCK-PARENT' },
319+
},
320+
CONTEXT
321+
)
322+
323+
expect(displayData).not.toHaveProperty('workflowInput')
324+
expect(JSON.stringify(displayData)).not.toContain('INHERITED-FROM-CUSTOM-BLOCK-PARENT')
325+
})
326+
327+
it('keeps the exemption for a manual run whose trigger data carries no input source', async () => {
328+
const displayData = await projectExecutionDataForDisplay(
329+
{
330+
trigger: { type: 'manual', source: 'manual', data: { correlation: { a: 1 } } },
331+
workflowInput: { question: 'typed by the user' },
332+
},
333+
CONTEXT
334+
)
335+
336+
expect(displayData.workflowInput).toEqual({ question: 'typed by the user' })
337+
})
338+
305339
it('still redacts and shows a nested execution input when provenance is complete', async () => {
306340
const displayData = await projectExecutionDataForDisplay(
307341
{

apps/sim/lib/logs/execution/trace-store.ts

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -246,13 +246,35 @@ const LOG_DISPLAY_BOUNDARY_SPAN_ID = 'secret-safe-log-display-boundary'
246246
*/
247247
const NESTED_EXECUTION_TRIGGER_TYPES = new Set(['workflow', 'custom_block'])
248248

249-
function isNestedExecution(executionData: Record<string, unknown>): boolean {
249+
function triggerRecord(
250+
executionData: Record<string, unknown>
251+
): Record<string, unknown> | undefined {
250252
const trigger = executionData.trigger
251-
if (!trigger || typeof trigger !== 'object' || Array.isArray(trigger)) return false
252-
const type = (trigger as Record<string, unknown>).type
253+
if (!trigger || typeof trigger !== 'object' || Array.isArray(trigger)) return undefined
254+
return trigger as Record<string, unknown>
255+
}
256+
257+
function isNestedExecution(executionData: Record<string, unknown>): boolean {
258+
const type = triggerRecord(executionData)?.type
253259
return typeof type === 'string' && NESTED_EXECUTION_TRIGGER_TYPES.has(type)
254260
}
255261

262+
/**
263+
* Whether this run's input was copied from a prior execution
264+
* (`inputFromExecutionId`), stamped by `LoggingSession.setInputSourceExecutionId`.
265+
*
266+
* A re-run presents whatever trigger type its caller asked for, so the nested
267+
* check alone cannot see that the value originated in a nested run. Without
268+
* this, a secret resolved inside a custom-block parent could be copied into a
269+
* `manual` run and exempted. The inherited value carries the SOURCE run's
270+
* exposure, which this run's provenance cannot describe, so it stays gated.
271+
*/
272+
function hasInheritedInput(executionData: Record<string, unknown>): boolean {
273+
const data = triggerRecord(executionData)?.data
274+
if (!data || typeof data !== 'object' || Array.isArray(data)) return false
275+
return typeof (data as Record<string, unknown>).inputSourceExecutionId === 'string'
276+
}
277+
256278
/**
257279
* Wraps display content in a span so it passes through the same secret
258280
* projection the executor's trace spans do.
@@ -319,10 +341,11 @@ export async function projectExecutionDataForDisplay(
319341
}
320342

321343
/**
322-
* A nested run's input came from its parent's resolved outputs, so it gets no
323-
* boundary exemption - every content key is gated for it.
344+
* A nested run's input came from its parent's resolved outputs, and a re-run's
345+
* came from another execution. Neither is a pre-resolution inbound payload, so
346+
* both forgo the boundary exemption - every content key is gated for them.
324347
*/
325-
const nested = isNestedExecution(executionData)
348+
const nested = isNestedExecution(executionData) || hasInheritedInput(executionData)
326349
const gatedKeys: readonly string[] = nested ? LOG_DISPLAY_CONTENT_KEYS : LOG_DISPLAY_GATED_KEYS
327350
const boundaryKeys: readonly string[] = nested ? [] : LOG_DISPLAY_BOUNDARY_KEYS
328351

packages/testing/src/mocks/logging-session.mock.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export const loggingSessionMockFns = {
2626
mockWaitForCompletion: vi.fn().mockResolvedValue(undefined),
2727
mockWaitForPostExecution: vi.fn().mockResolvedValue(undefined),
2828
mockSetTrustedExecutionCorrelation: vi.fn(),
29+
mockSetInputSourceExecutionId: vi.fn(),
2930
mockProjectBlockLogsForDisplay: vi.fn(async (logs: unknown) => logs),
3031
mockProjectDisplayContent: vi.fn(async (content: unknown) => content),
3132
mockProjectLiveDisplayText: vi.fn(async (_field: string, value: string) => ({ value })),
@@ -53,6 +54,7 @@ function buildLoggingSessionInstance() {
5354
waitForCompletion: loggingSessionMockFns.mockWaitForCompletion,
5455
waitForPostExecution: loggingSessionMockFns.mockWaitForPostExecution,
5556
setTrustedExecutionCorrelation: loggingSessionMockFns.mockSetTrustedExecutionCorrelation,
57+
setInputSourceExecutionId: loggingSessionMockFns.mockSetInputSourceExecutionId,
5658
projectBlockLogsForDisplay: loggingSessionMockFns.mockProjectBlockLogsForDisplay,
5759
projectDisplayContent: loggingSessionMockFns.mockProjectDisplayContent,
5860
projectLiveDisplayText: loggingSessionMockFns.mockProjectLiveDisplayText,

0 commit comments

Comments
 (0)