Skip to content

Commit 1635ce1

Browse files
fix(logs): redact error/trigger/executionState; keep guardrails import lazy
- Extend PII redaction to span error/errorMessage/toolCalls and top-level error/completionFailure/trigger/executionState (Bugbot: PII in execution metadata). executionState is safe to redact — resume reads from the separate pausedExecutions table, not the log copy. - Lazy-import validate_pii in pii-redaction so the Python/child_process guardrails module stays out of the static middleware/RSC graph. - Type the org retention mutation to the contract body (optional, non-null).
1 parent 147b143 commit 1635ce1

5 files changed

Lines changed: 76 additions & 9 deletions

File tree

apps/sim/ee/data-retention/hooks/data-retention.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
getOrganizationDataRetentionContract,
77
type OrganizationDataRetention,
88
type OrganizationRetentionValues,
9+
type UpdateOrganizationDataRetentionBody,
910
updateOrganizationDataRetentionContract,
1011
} from '@/lib/api/contracts/organization'
1112
import {
@@ -48,7 +49,7 @@ export function useOrganizationRetention(orgId: string | undefined) {
4849

4950
interface UpdateRetentionVariables {
5051
orgId: string
51-
settings: Partial<RetentionValues>
52+
settings: UpdateOrganizationDataRetentionBody
5253
}
5354

5455
export function useUpdateOrganizationRetention() {

apps/sim/lib/api/contracts/organization.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,10 @@ export const updateOrganizationDataRetentionBodySchema = z.object({
111111
piiRedaction: piiRedactionSettingsSchema.optional(),
112112
})
113113

114+
export type UpdateOrganizationDataRetentionBody = z.input<
115+
typeof updateOrganizationDataRetentionBodySchema
116+
>
117+
114118
const organizationRetentionValuesSchema = z.object({
115119
logRetentionHours: z.number().int().nullable(),
116120
softDeleteRetentionHours: z.number().int().nullable(),

apps/sim/lib/logs/execution/logger.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -761,6 +761,14 @@ export class ExecutionLogger implements IExecutionLoggerService {
761761
traceSpans: redactedTraceSpans,
762762
finalOutput: redactedFinalOutput,
763763
...(redactedWorkflowInput !== undefined ? { workflowInput: redactedWorkflowInput } : {}),
764+
...(builtExecutionData.error !== undefined ? { error: builtExecutionData.error } : {}),
765+
...(builtExecutionData.completionFailure !== undefined
766+
? { completionFailure: builtExecutionData.completionFailure }
767+
: {}),
768+
...(builtExecutionData.trigger !== undefined ? { trigger: builtExecutionData.trigger } : {}),
769+
...(builtExecutionData.executionState !== undefined
770+
? { executionState: builtExecutionData.executionState }
771+
: {}),
764772
})
765773

766774
const rawDurationMs =
@@ -777,6 +785,14 @@ export class ExecutionLogger implements IExecutionLoggerService {
777785
traceSpans: pii.traceSpans as TraceSpan[],
778786
finalOutput: pii.finalOutput as BlockOutputData,
779787
...(pii.workflowInput !== undefined ? { workflowInput: pii.workflowInput } : {}),
788+
...(pii.error !== undefined ? { error: pii.error as string } : {}),
789+
...(pii.completionFailure !== undefined
790+
? { completionFailure: pii.completionFailure as string }
791+
: {}),
792+
...(pii.trigger !== undefined ? { trigger: pii.trigger as ExecutionTrigger } : {}),
793+
...(pii.executionState !== undefined
794+
? { executionState: pii.executionState as SerializableExecutionState }
795+
: {}),
780796
}
781797

782798
stripSpanCosts((cleanExecutionData as Record<string, unknown>).traceSpans)

apps/sim/lib/logs/execution/pii-redaction.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,28 @@ describe('redactPIIFromExecution', () => {
8585
expect(mockMaskPIIBatch.mock.calls[0][0]).toEqual(['pii'])
8686
})
8787

88+
it('masks span error/errorMessage and top-level error, trigger, executionState', async () => {
89+
const payload = {
90+
traceSpans: [{ blockId: 'b1', error: 'failed for bob@x.com', errorMessage: 'bad input z' }],
91+
error: 'run failed: a@b.com',
92+
completionFailure: 'cancelled by c@d.com',
93+
trigger: { type: 'webhook', data: { from: 'caller@x.com' } },
94+
executionState: { status: 'completed', note: 'state for e@f.com' },
95+
}
96+
97+
const result = await redactPIIFromExecution(payload, { entityTypes: ['EMAIL_ADDRESS'] })
98+
99+
const span = (result.traceSpans as any[])[0]
100+
expect(span.blockId).toBe('b1')
101+
expect(span.error).toBe('MASKED(failed for bob@x.com)')
102+
expect(span.errorMessage).toBe('MASKED(bad input z)')
103+
expect(result.error).toBe('MASKED(run failed: a@b.com)')
104+
expect(result.completionFailure).toBe('MASKED(cancelled by c@d.com)')
105+
expect((result.trigger as any).type).toBe('MASKED(webhook)')
106+
expect((result.trigger as any).data.from).toBe('MASKED(caller@x.com)')
107+
expect((result.executionState as any).note).toBe('MASKED(state for e@f.com)')
108+
})
109+
88110
it('returns payload unchanged when there is nothing to mask', async () => {
89111
const payload = { traceSpans: [{ blockId: 'b1', count: 5 }] }
90112
const result = await redactPIIFromExecution(payload, { entityTypes: [] })

apps/sim/lib/logs/execution/pii-redaction.ts

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { createLogger } from '@sim/logger'
22
import { getErrorMessage } from '@sim/utils/errors'
3-
import { maskPIIBatch } from '@/lib/guardrails/validate_pii'
43

54
const logger = createLogger('PiiRedaction')
65

@@ -29,10 +28,33 @@ export interface RedactablePayload {
2928
traceSpans?: unknown
3029
finalOutput?: unknown
3130
workflowInput?: unknown
31+
error?: unknown
32+
completionFailure?: unknown
33+
trigger?: unknown
34+
executionState?: unknown
3235
}
3336

37+
/** Keys of {@link RedactablePayload} processed by the redactor, in order. */
38+
const REDACTABLE_KEYS: (keyof RedactablePayload)[] = [
39+
'traceSpans',
40+
'finalOutput',
41+
'workflowInput',
42+
'error',
43+
'completionFailure',
44+
'trigger',
45+
'executionState',
46+
]
47+
3448
/** Trace-span fields that carry runtime content (and therefore possible PII). */
35-
const SPAN_CONTENT_FIELDS = ['input', 'output', 'thinking', 'modelToolCalls'] as const
49+
const SPAN_CONTENT_FIELDS = [
50+
'input',
51+
'output',
52+
'thinking',
53+
'modelToolCalls',
54+
'toolCalls',
55+
'error',
56+
'errorMessage',
57+
] as const
3658

3759
function isEligibleString(value: string): boolean {
3860
return value.length > 0 && Buffer.byteLength(value, 'utf8') <= PII_MAX_STRING_BYTES
@@ -110,12 +132,10 @@ export async function redactPIIFromExecution(
110132
const { entityTypes } = options
111133
const language = options.language ?? 'en'
112134

113-
const units: { key: keyof RedactablePayload; value: unknown }[] = []
114-
if (payload.traceSpans !== undefined) units.push({ key: 'traceSpans', value: payload.traceSpans })
115-
if (payload.finalOutput !== undefined)
116-
units.push({ key: 'finalOutput', value: payload.finalOutput })
117-
if (payload.workflowInput !== undefined)
118-
units.push({ key: 'workflowInput', value: payload.workflowInput })
135+
const units = REDACTABLE_KEYS.filter((key) => payload[key] !== undefined).map((key) => ({
136+
key,
137+
value: payload[key],
138+
}))
119139

120140
const collected: string[] = []
121141
let totalBytes = 0
@@ -138,6 +158,10 @@ export async function redactPIIFromExecution(
138158
masked = collected.map(() => REDACTION_FAILED_MARKER)
139159
} else {
140160
try {
161+
// Lazy import keeps the Python-spawning guardrails module (child_process +
162+
// a `lib/guardrails` dir reference) out of the static middleware/RSC graph;
163+
// it's only loaded at runtime on the Node log-persist path.
164+
const { maskPIIBatch } = await import('@/lib/guardrails/validate_pii')
141165
masked = await maskPIIBatch(collected, entityTypes, language)
142166
} catch (error) {
143167
logger.error('PII masking failed; scrubbing text to avoid leaking PII', {

0 commit comments

Comments
 (0)