diff --git a/apps/sim/app/api/organizations/[id]/data-retention/route.ts b/apps/sim/app/api/organizations/[id]/data-retention/route.ts index e67d229df1..37fbbaabb9 100644 --- a/apps/sim/app/api/organizations/[id]/data-retention/route.ts +++ b/apps/sim/app/api/organizations/[id]/data-retention/route.ts @@ -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') @@ -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() @@ -86,6 +88,7 @@ export const GET = withRouteHandler( defaults, configured, effective: isEnterprise ? configured : defaults, + piiRedactionEnabled, }, }) } @@ -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) { @@ -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 } @@ -203,6 +214,7 @@ export const PUT = withRouteHandler( defaults, configured, effective: configured, + piiRedactionEnabled, }, }) } diff --git a/apps/sim/ee/data-retention/components/data-retention-settings.tsx b/apps/sim/ee/data-retention/components/data-retention-settings.tsx index d1ef5aa1ad..bca54112d5 100644 --- a/apps/sim/ee/data-retention/components/data-retention-settings.tsx +++ b/apps/sim/ee/data-retention/components/data-retention-settings.tsx @@ -492,89 +492,91 @@ export function DataRetentionSettings() { - -
-
-
- - Default · all workspaces - - {!defaultRule && ( - - Add redaction - + {data?.piiRedactionEnabled && ( + +
+
+
+ + Default · all workspaces + + {!defaultRule && ( + + Add redaction + + )} +
+ {defaultRule && ( +
+ + {entitySummary(defaultRule.entityTypes)} + +
+ Edit + removeRule(defaultRule.id)} + disabled={updateMutation.isPending} + > + Delete + +
+
)}
{defaultRule && ( -
- - {entitySummary(defaultRule.entityTypes)} - -
- Edit +
+
+ + Workspace overrides + removeRule(defaultRule.id)} - disabled={updateMutation.isPending} + leftIcon={Plus} + onClick={openAddOverride} + disabled={freeWorkspaces.length === 0} > - Delete + Add override
+ {overrideRules.length === 0 ? ( +

+ No overrides — every workspace uses the default. +

+ ) : ( +
+ {overrideRules.map((rule) => ( +
+
+ + {workspaceName(rule.workspaceId as string)} + + + {entitySummary(rule.entityTypes)} + +
+
+ openEditOverride(rule)}>Edit + removeRule(rule.id)} + disabled={updateMutation.isPending} + > + Delete + +
+
+ ))} + + Workspaces not listed use the default. + +
+ )}
)}
- {defaultRule && ( -
-
- - Workspace overrides - - - Add override - -
- {overrideRules.length === 0 ? ( -

- No overrides — every workspace uses the default. -

- ) : ( -
- {overrideRules.map((rule) => ( -
-
- - {workspaceName(rule.workspaceId as string)} - - - {entitySummary(rule.entityTypes)} - -
-
- openEditOverride(rule)}>Edit - removeRule(rule.id)} - disabled={updateMutation.isPending} - > - Delete - -
-
- ))} - - Workspaces not listed use the default. - -
- )} -
- )} -
- + + )}
{modalDraft && ( diff --git a/apps/sim/lib/api/contracts/organization.ts b/apps/sim/lib/api/contracts/organization.ts index 8ba84cfc4e..4993e95901 100644 --- a/apps/sim/lib/api/contracts/organization.ts +++ b/apps/sim/lib/api/contracts/organization.ts @@ -129,6 +129,7 @@ const organizationDataRetentionDataSchema = z.object({ defaults: organizationRetentionValuesSchema, configured: organizationRetentionValuesSchema, effective: organizationRetentionValuesSchema, + piiRedactionEnabled: z.boolean(), }) export type OrganizationDataRetention = z.output diff --git a/apps/sim/lib/core/config/env.ts b/apps/sim/lib/core/config/env.ts index 27d810ed73..09c2e4fe51 100644 --- a/apps/sim/lib/core/config/env.ts +++ b/apps/sim/lib/core/config/env.ts @@ -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) diff --git a/apps/sim/lib/core/config/feature-flags.test.ts b/apps/sim/lib/core/config/feature-flags.test.ts index ac5fa766a8..a38f434063 100644 --- a/apps/sim/lib/core/config/feature-flags.test.ts +++ b/apps/sim/lib/core/config/feature-flags.test.ts @@ -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() }) @@ -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 () => { diff --git a/apps/sim/lib/core/config/feature-flags.ts b/apps/sim/lib/core/config/feature-flags.ts index 85107d7455..6fa8ec0ebe 100644 --- a/apps/sim/lib/core/config/feature-flags.ts +++ b/apps/sim/lib/core/config/feature-flags.ts @@ -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 /** diff --git a/apps/sim/lib/logs/execution/logger.ts b/apps/sim/lib/logs/execution/logger.ts index 9a77a0ec42..9a531b7293 100644 --- a/apps/sim/lib/logs/execution/logger.ts +++ b/apps/sim/lib/logs/execution/logger.ts @@ -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 { @@ -601,6 +602,8 @@ export class ExecutionLogger implements IExecutionLoggerService { ): Promise { if (!workspaceId) return payload + if (!(await isFeatureEnabled('pii-redaction'))) return payload + const [row] = await db .select({ orgSettings: organization.dataRetentionSettings }) .from(workspace)