Skip to content

Commit 5e86c7d

Browse files
fix(pii): coerce empty guardrails entity-type checkbox (null) so the contract accepts it
1 parent e23acaf commit 5e86c7d

2 files changed

Lines changed: 66 additions & 1 deletion

File tree

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { describe, expect, it } from 'vitest'
5+
import { guardrailsValidateContract } from '@/lib/api/contracts/hotspots'
6+
import { guardrailsValidateTool } from '@/tools/guardrails/validate'
7+
8+
// The block layer serializes an empty checkbox / table subBlock to `null`; the
9+
// tool's body builder must produce a shape the contract accepts (undefined, not null).
10+
const buildBody = (params: Record<string, unknown>) =>
11+
guardrailsValidateTool.request.body?.(params as never) as Record<string, unknown>
12+
13+
describe('guardrailsValidateTool.request.body', () => {
14+
it('coerces a null entity-type checkbox to omitted, and the contract accepts it', () => {
15+
const body = buildBody({ input: 'x', validationType: 'pii', piiEntityTypes: null })
16+
expect(body.piiEntityTypes).toBeUndefined()
17+
expect(guardrailsValidateContract.body.safeParse(body).success).toBe(true)
18+
})
19+
20+
it('passes a real entity-type array through unchanged', () => {
21+
const body = buildBody({
22+
input: 'x',
23+
validationType: 'pii',
24+
piiEntityTypes: ['EMAIL_ADDRESS'],
25+
})
26+
expect(body.piiEntityTypes).toEqual(['EMAIL_ADDRESS'])
27+
})
28+
29+
it('maps custom-pattern table rows to the wire shape and validates against the contract', () => {
30+
const body = buildBody({
31+
input: 'x',
32+
validationType: 'pii',
33+
piiEntityTypes: null,
34+
piiCustomPatterns: [
35+
{ cells: { Name: 'Emp', Pattern: 'EMP-\\d{6}', Replacement: 'EMPLOYEE_ID' } },
36+
],
37+
})
38+
expect(body.piiCustomPatterns).toEqual([
39+
{ name: 'Emp', regex: 'EMP-\\d{6}', replacement: 'EMPLOYEE_ID' },
40+
])
41+
expect(guardrailsValidateContract.body.safeParse(body).success).toBe(true)
42+
})
43+
44+
it('omits custom patterns when the table is empty/null', () => {
45+
const body = buildBody({
46+
input: 'x',
47+
validationType: 'pii',
48+
piiEntityTypes: null,
49+
piiCustomPatterns: null,
50+
})
51+
expect(body.piiCustomPatterns).toBeUndefined()
52+
})
53+
54+
it('regression guard: the contract rejects the raw null the block emits (why we coerce)', () => {
55+
const parsed = guardrailsValidateContract.body.safeParse({
56+
input: 'x',
57+
validationType: 'pii',
58+
piiEntityTypes: null,
59+
})
60+
expect(parsed.success).toBe(false)
61+
})
62+
})

apps/sim/tools/guardrails/validate.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,10 @@ export const guardrailsValidateTool: ToolConfig<GuardrailsValidateInput, Guardra
214214
bedrockAccessKeyId: params.bedrockAccessKeyId,
215215
bedrockSecretKey: params.bedrockSecretKey,
216216
bedrockRegion: params.bedrockRegion,
217-
piiEntityTypes: params.piiEntityTypes,
217+
// An empty entity-type checkbox serializes to null; the contract's array
218+
// field accepts undefined (omitted), not null — so coerce. This is the
219+
// common shape when only custom patterns are configured.
220+
piiEntityTypes: Array.isArray(params.piiEntityTypes) ? params.piiEntityTypes : undefined,
218221
piiMode: params.piiMode,
219222
piiLanguage: params.piiLanguage,
220223
piiCustomPatterns: toCustomPatterns(params.piiCustomPatterns),

0 commit comments

Comments
 (0)