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
20 changes: 18 additions & 2 deletions src/cli/primitives/PolicyPrimitive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,14 @@ import type { RemovalPreview, SchemaChange } from '../operations/remove/types';
import { runCliCommand, withCommandRunTelemetry } from '../telemetry/cli-command-run.js';
import { PolicyValidationMode, standardize } from '../telemetry/schemas/common-shapes.js';
import { requireTTY } from '../tui/guards/tty';
import { type PolicyEffect, authorizationPhaseForEffect, defaultDataPathForEffect } from '../tui/screens/policy/types';
import {
FILTERS_BY_CATEGORY,
type GuardrailCategoryType,
type PolicyEffect,
authorizationPhaseForEffect,
defaultDataPathForEffect,
invalidFiltersForCategory,
} from '../tui/screens/policy/types';
import { BasePrimitive } from './BasePrimitive';
import { SOURCE_CODE_NOTE } from './constants';
import type { AddResult, AddScreenComponent, RemovableResource } from './types';
Expand Down Expand Up @@ -408,6 +415,15 @@ export class PolicyPrimitive extends BasePrimitive<AddPolicyOptions, RemovablePo
const policyEffect = effect as PolicyEffect;
const filters = cliOptions.formFilters.split(',').map(s => s.trim());

const category = cliOptions.formCategory as GuardrailCategoryType;
const invalidFilters = invalidFiltersForCategory(category, filters);
if (invalidFilters.length > 0) {
throw new ValidationError(
`Invalid filter(s) for category '${category}': ${invalidFilters.join(', ')}. ` +
`Allowed: ${FILTERS_BY_CATEGORY[category].join(', ')}`
);
}

let resolvedGatewayArn: string | undefined;
let resolvedTargetName: string | undefined = cliOptions.target;
if (cliOptions.gateway) {
Expand Down Expand Up @@ -446,7 +462,7 @@ export class PolicyPrimitive extends BasePrimitive<AddPolicyOptions, RemovablePo

const statement = synthesizeCedar(
{
category: cliOptions.formCategory as 'contentFilter' | 'promptAttack' | 'sensitiveInformation',
category,
filters,
effect: policyEffect,
dataPath: cliOptions.formDataPath ?? defaultDataPathForEffect(policyEffect),
Expand Down
6 changes: 6 additions & 0 deletions src/cli/tui/screens/policy/__tests__/synthesize-cedar.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ describe('synthesizeCedar', () => {
expect(result).toContain('[context.input.prompt]');
});

it('generates a policy with the INSULTS content filter (canonical plural name)', () => {
const form: GuardrailFormConfig = { ...baseForm, filters: ['INSULTS'] };
const result = synthesizeCedar(form);
expect(result).toContain('["INSULTS"]');
});

it('generates permit policy with single filter using lessThanOrEqual', () => {
const form: GuardrailFormConfig = { ...baseForm, effect: 'permit' };
const result = synthesizeCedar(form);
Expand Down
19 changes: 19 additions & 0 deletions src/cli/tui/screens/policy/__tests__/types.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { invalidFiltersForCategory } from '../types.js';
import { describe, expect, it } from 'vitest';

describe('invalidFiltersForCategory', () => {
it('returns an empty array when every filter is valid for the category', () => {
expect(invalidFiltersForCategory('contentFilter', ['VIOLENCE', 'INSULTS'])).toEqual([]);
expect(invalidFiltersForCategory('promptAttack', ['JAILBREAK'])).toEqual([]);
expect(invalidFiltersForCategory('sensitiveInformation', ['EMAIL', 'PHONE'])).toEqual([]);
});

it('rejects an unknown filter value', () => {
expect(invalidFiltersForCategory('contentFilter', ['VIOLENCE', 'NOTAREAL'])).toEqual(['NOTAREAL']);
});

it('rejects the legacy singular INSULT (regression guard for #1571)', () => {
expect(invalidFiltersForCategory('contentFilter', ['INSULT'])).toEqual(['INSULT']);
expect(invalidFiltersForCategory('contentFilter', ['INSULTS'])).toEqual([]);
});
});
21 changes: 20 additions & 1 deletion src/cli/tui/screens/policy/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export type PolicySourceMethod = 'file' | 'inline' | 'generate' | 'form';

export type GuardrailCategoryType = 'contentFilter' | 'promptAttack' | 'sensitiveInformation';

export const CONTENT_FILTER_FILTERS = ['VIOLENCE', 'HATE', 'SEXUAL', 'MISCONDUCT', 'INSULT'] as const;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe as a future follow up, can we inherit these types from the sdk?

export const CONTENT_FILTER_FILTERS = ['VIOLENCE', 'HATE', 'SEXUAL', 'MISCONDUCT', 'INSULTS'] as const;
export type ContentFilterCategory = (typeof CONTENT_FILTER_FILTERS)[number];

export const PROMPT_ATTACK_FILTERS = ['JAILBREAK', 'PROMPT_INJECTION', 'PROMPT_LEAKAGE'] as const;
Expand Down Expand Up @@ -81,6 +81,25 @@ export const GUARDRAIL_CATEGORY_OPTIONS: {
},
];

/**
* Valid filters per guardrail category. Single source of truth for both the TUI options
* and client-side validation of the non-interactive `--form-filters` flag.
*/
export const FILTERS_BY_CATEGORY: Record<GuardrailCategoryType, readonly string[]> = {
contentFilter: CONTENT_FILTER_FILTERS,
promptAttack: PROMPT_ATTACK_FILTERS,
sensitiveInformation: SENSITIVE_INFO_FILTERS,
};

/**
* Returns the filter values that are not valid for the given category.
* An empty array means every supplied filter is valid.
*/
export function invalidFiltersForCategory(category: GuardrailCategoryType, filters: string[]): string[] {
const allowed = FILTERS_BY_CATEGORY[category];
return filters.filter(f => !allowed.includes(f));
}

export type PolicyEffect = 'permit' | 'forbid' | 'suppressOutput';

/**
Expand Down
Loading