Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions apps/sim/app/api/organizations/[id]/data-retention/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { getSession } from '@/lib/auth'
import { CLEANUP_CONFIG } from '@/lib/billing/cleanup-dispatcher'
import { isOrganizationOnEnterprisePlan } from '@/lib/billing/core/subscription'
import { isBillingEnabled } from '@/lib/core/config/env-flags'
import { isFeatureEnabled } from '@/lib/core/config/feature-flags'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'

const logger = createLogger('DataRetentionAPI')
Expand Down Expand Up @@ -76,6 +77,7 @@ export const GET = withRouteHandler(
}

const isEnterprise = !isBillingEnabled || (await isOrganizationOnEnterprisePlan(organizationId))
const piiRedactionEnabled = await isFeatureEnabled('pii-redaction')
const configured = normalizeConfigured(org.dataRetentionSettings)
const defaults = enterpriseDefaults()

Expand All @@ -86,6 +88,7 @@ export const GET = withRouteHandler(
defaults,
configured,
effective: isEnterprise ? configured : defaults,
piiRedactionEnabled,
},
})
}
Expand Down Expand Up @@ -154,6 +157,8 @@ export const PUT = withRouteHandler(
return NextResponse.json({ error: 'Organization not found' }, { status: 404 })
}

const piiRedactionEnabled = await isFeatureEnabled('pii-redaction')

const current = normalizeConfigured(currentOrg.dataRetentionSettings)
const merged: DataRetentionSettings = { ...current }
if (body.logRetentionHours !== undefined) {
Expand All @@ -166,6 +171,12 @@ export const PUT = withRouteHandler(
merged.taskCleanupHours = body.taskCleanupHours
}
if (body.piiRedaction !== undefined) {
if (!piiRedactionEnabled) {
return NextResponse.json(
{ error: 'PII redaction is not enabled for this organization' },
{ status: 403 }
)
}
merged.piiRedaction = body.piiRedaction
}

Expand Down Expand Up @@ -203,6 +214,7 @@ export const PUT = withRouteHandler(
defaults,
configured,
effective: configured,
piiRedactionEnabled,
},
})
}
Expand Down
148 changes: 75 additions & 73 deletions apps/sim/ee/data-retention/components/data-retention-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -492,89 +492,91 @@ export function DataRetentionSettings() {
</SettingRow>
</div>
</SettingsSection>
<SettingsSection label='PII Redaction'>
<div className='flex flex-col gap-6'>
<div className='flex flex-col gap-2'>
<div className='flex items-center justify-between gap-3'>
<span className='font-medium text-[var(--text-muted)] text-small'>
Default · all workspaces
</span>
{!defaultRule && (
<Chip leftIcon={Plus} onClick={openEditDefault}>
Add redaction
</Chip>
{data?.piiRedactionEnabled && (
<SettingsSection label='PII Redaction'>
<div className='flex flex-col gap-6'>
<div className='flex flex-col gap-2'>
<div className='flex items-center justify-between gap-3'>
<span className='font-medium text-[var(--text-muted)] text-small'>
Default · all workspaces
</span>
{!defaultRule && (
<Chip leftIcon={Plus} onClick={openEditDefault}>
Add redaction
</Chip>
)}
</div>
{defaultRule && (
<div className='flex items-center justify-between gap-3 rounded-lg border border-[var(--border-1)] px-3 py-2'>
<span className='truncate text-[var(--text-body)] text-small'>
{entitySummary(defaultRule.entityTypes)}
</span>
<div className='flex flex-shrink-0 items-center gap-2'>
<Chip onClick={openEditDefault}>Edit</Chip>
<Chip
onClick={() => removeRule(defaultRule.id)}
disabled={updateMutation.isPending}
>
Delete
</Chip>
</div>
</div>
)}
</div>
{defaultRule && (
<div className='flex items-center justify-between gap-3 rounded-lg border border-[var(--border-1)] px-3 py-2'>
<span className='truncate text-[var(--text-body)] text-small'>
{entitySummary(defaultRule.entityTypes)}
</span>
<div className='flex flex-shrink-0 items-center gap-2'>
<Chip onClick={openEditDefault}>Edit</Chip>
<div className='flex flex-col gap-2'>
<div className='flex items-center justify-between gap-3'>
<span className='font-medium text-[var(--text-muted)] text-small'>
Workspace overrides
</span>
<Chip
onClick={() => removeRule(defaultRule.id)}
disabled={updateMutation.isPending}
leftIcon={Plus}
onClick={openAddOverride}
disabled={freeWorkspaces.length === 0}
>
Delete
Add override
</Chip>
</div>
{overrideRules.length === 0 ? (
<p className='text-[var(--text-muted)] text-caption'>
No overrides — every workspace uses the default.
</p>
) : (
<div className='flex flex-col gap-2'>
{overrideRules.map((rule) => (
<div
key={rule.id}
className='flex items-center justify-between gap-3 rounded-lg border border-[var(--border-1)] px-3 py-2'
>
<div className='flex min-w-0 flex-col'>
<span className='truncate text-[var(--text-body)] text-small'>
{workspaceName(rule.workspaceId as string)}
</span>
<span className='truncate text-[var(--text-muted)] text-caption'>
{entitySummary(rule.entityTypes)}
</span>
</div>
<div className='flex flex-shrink-0 items-center gap-2'>
<Chip onClick={() => openEditOverride(rule)}>Edit</Chip>
<Chip
onClick={() => removeRule(rule.id)}
disabled={updateMutation.isPending}
>
Delete
</Chip>
</div>
</div>
))}
<span className='text-[var(--text-muted)] text-caption'>
Workspaces not listed use the default.
</span>
</div>
)}
</div>
)}
</div>
{defaultRule && (
<div className='flex flex-col gap-2'>
<div className='flex items-center justify-between gap-3'>
<span className='font-medium text-[var(--text-muted)] text-small'>
Workspace overrides
</span>
<Chip
leftIcon={Plus}
onClick={openAddOverride}
disabled={freeWorkspaces.length === 0}
>
Add override
</Chip>
</div>
{overrideRules.length === 0 ? (
<p className='text-[var(--text-muted)] text-caption'>
No overrides — every workspace uses the default.
</p>
) : (
<div className='flex flex-col gap-2'>
{overrideRules.map((rule) => (
<div
key={rule.id}
className='flex items-center justify-between gap-3 rounded-lg border border-[var(--border-1)] px-3 py-2'
>
<div className='flex min-w-0 flex-col'>
<span className='truncate text-[var(--text-body)] text-small'>
{workspaceName(rule.workspaceId as string)}
</span>
<span className='truncate text-[var(--text-muted)] text-caption'>
{entitySummary(rule.entityTypes)}
</span>
</div>
<div className='flex flex-shrink-0 items-center gap-2'>
<Chip onClick={() => openEditOverride(rule)}>Edit</Chip>
<Chip
onClick={() => removeRule(rule.id)}
disabled={updateMutation.isPending}
>
Delete
</Chip>
</div>
</div>
))}
<span className='text-[var(--text-muted)] text-caption'>
Workspaces not listed use the default.
</span>
</div>
)}
</div>
)}
</div>
</SettingsSection>
</SettingsSection>
)}
</div>
</div>
{modalDraft && (
Expand Down
1 change: 1 addition & 0 deletions apps/sim/lib/api/contracts/organization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ const organizationDataRetentionDataSchema = z.object({
defaults: organizationRetentionValuesSchema,
configured: organizationRetentionValuesSchema,
effective: organizationRetentionValuesSchema,
piiRedactionEnabled: z.boolean(),
})

export type OrganizationDataRetention = z.output<typeof organizationDataRetentionDataSchema>
Expand Down
1 change: 1 addition & 0 deletions apps/sim/lib/core/config/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export const env = createEnv({
FREE_API_DEPLOYMENT_GATE_ENABLED: z.boolean().optional(), // Block free-plan accounts from programmatic execution (API/MCP/A2A/generic webhooks/chat embeds). Requires BILLING_ENABLED. Off by default for dark rollout
TABLES_FRACTIONAL_ORDERING: z.boolean().optional(), // Order table rows by fractional order_key (O(1) insert/delete) instead of integer position
TABLE_SNAPSHOT_CACHE: z.boolean().optional(), // Mount tables into sandboxes by reference via a version-keyed CSV snapshot in object storage instead of draining the whole table into web-process heap
PII_REDACTION: z.boolean().optional(), // Redact PII from workflow logs via configurable Data Retention rules (Presidio at the logger persist choke point) and expose the Data Retention config UI

// Table feature limits (per plan). Apply when billing is disabled (free tier defaults) or for billed plans.
FREE_TABLES_LIMIT: z.number().optional(), // Max user tables per workspace on free tier (default: 5)
Expand Down
2 changes: 2 additions & 0 deletions apps/sim/lib/core/config/feature-flags.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ describe('getFeatureFlags', () => {
// All registered flags should be present, disabled (env vars unset in test env)
expect(flags['tables-fractional-ordering']).toEqual({ enabled: false })
expect(flags['mothership-beta']).toEqual({ enabled: false })
expect(flags['pii-redaction']).toEqual({ enabled: false })
expect(mockFetch).not.toHaveBeenCalled()
})

Expand All @@ -88,6 +89,7 @@ describe('getFeatureFlags', () => {
const flags = await getFeatureFlags()
expect(flags['tables-fractional-ordering']).toEqual({ enabled: false })
expect(flags['mothership-beta']).toEqual({ enabled: false })
expect(flags['pii-redaction']).toEqual({ enabled: false })
})

it('degrades gracefully on a malformed document', async () => {
Expand Down
8 changes: 8 additions & 0 deletions apps/sim/lib/core/config/feature-flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,14 @@ const FEATURE_FLAGS = {
'enabled:true for global rollout rather than per-user targeting.',
fallback: 'TABLE_SNAPSHOT_CACHE',
},
'pii-redaction': {
description:
'Redact PII from workflow logs via configurable Data Retention rules (Presidio at the ' +
'logger persist choke point) and expose the Data Retention config surfaces. Global on/off ' +
'only — evaluated without user/org context so the persist path and config routes always ' +
'agree.',
fallback: 'PII_REDACTION',
},
} satisfies Record<string, FeatureFlagDefinition>

/**
Expand Down
3 changes: 3 additions & 0 deletions apps/sim/lib/logs/execution/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
import { resolveEffectivePiiRedaction } from '@/lib/billing/retention'
import { checkAndBillOverageThreshold } from '@/lib/billing/threshold-billing'
import { isBillingEnabled } from '@/lib/core/config/env-flags'
import { isFeatureEnabled } from '@/lib/core/config/feature-flags'
import { redactApiKeys } from '@/lib/core/security/redaction'
import { filterForDisplay } from '@/lib/core/utils/display-filters'
import {
Expand Down Expand Up @@ -601,6 +602,8 @@ export class ExecutionLogger implements IExecutionLoggerService {
): Promise<RedactablePayload> {
if (!workspaceId) return payload

if (!(await isFeatureEnabled('pii-redaction'))) return payload

const [row] = await db
.select({ orgSettings: organization.dataRetentionSettings })
.from(workspace)
Expand Down
Loading