77
88import { getBuiltinConditions } from "./conditions/builtins.js" ;
99import { getDefaultStage } from "./policy-stage-defaults.js" ;
10+ import { maskSensitiveData , maskPattern , maskBlocklistTerms } from "./mask.js" ;
1011
1112// ─── Types ──────────────────────────────────────────────────────
1213
@@ -20,7 +21,7 @@ export type PolicyAction =
2021 | "payment"
2122 | "custom" ;
2223
23- export type PolicyOutcome = "allow" | "block" | "warn" | "require_approval" ;
24+ export type PolicyOutcome = "allow" | "block" | "warn" | "require_approval" | "mask" ;
2425
2526export type PolicyStage = "preprocess" | "process" | "postprocess" ;
2627
@@ -80,6 +81,8 @@ export interface EnforcementDecision {
8081 outcome : PolicyOutcome ;
8182 evaluatedAt : string ;
8283 rulesEvaluated : number ;
84+ /** Redacted text when outcome is "mask" — the transformed version with sensitive data replaced */
85+ maskedText ?: string ;
8386}
8487
8588// ─── Condition Registry ─────────────────────────────────────────
@@ -179,19 +182,43 @@ export function createPolicyEngine(config: PolicyEngineConfig = {}): PolicyEngin
179182 const rules : PolicyRule [ ] = [ ...( config . rules ?? [ ] ) ] ;
180183 const defaultOutcome = config . defaultOutcome ?? "allow" ;
181184
185+ /** Compute masked text when outcome is "mask" based on the condition type and context. */
186+ function computeMaskedText ( rule : PolicyRule , ctx : EnforcementContext ) : string | undefined {
187+ const { type, params } = rule . condition ;
188+ const text = ctx . outputText ?? ( ctx . input ?. prompt as string | undefined ) ?? "" ;
189+ if ( ! text ) return undefined ;
190+
191+ if ( type === "sensitive_data_filter" ) {
192+ return maskSensitiveData ( text , params . patterns as string [ ] | undefined ) ;
193+ }
194+ if ( type === "output_pattern" || type === "input_pattern" ) {
195+ return maskPattern ( text , params . pattern as string , params . flags as string | undefined ) ;
196+ }
197+ if ( type === "blocklist" ) {
198+ return maskBlocklistTerms ( text , params . terms as string [ ] ) ;
199+ }
200+ // Fallback: return text unchanged (condition detected something but we don't know how to mask)
201+ return text ;
202+ }
203+
204+ function buildDecision ( rule : PolicyRule , ctx : EnforcementContext , rulesEvaluated : number ) : EnforcementDecision {
205+ return {
206+ blocked : rule . outcome === "block" || rule . outcome === "require_approval" ,
207+ reason : rule . reason ,
208+ ruleId : rule . id ,
209+ outcome : rule . outcome ,
210+ evaluatedAt : new Date ( ) . toISOString ( ) ,
211+ rulesEvaluated,
212+ ...( rule . outcome === "mask" ? { maskedText : computeMaskedText ( rule , ctx ) } : { } ) ,
213+ } ;
214+ }
215+
182216 function evaluate ( ctx : EnforcementContext ) : EnforcementDecision {
183217 const active = rules . filter ( ( r ) => r . enabled ) . sort ( ( a , b ) => b . priority - a . priority ) ;
184218
185219 for ( const rule of active ) {
186220 if ( evaluateCondition ( rule . condition , ctx ) ) {
187- return {
188- blocked : rule . outcome === "block" || rule . outcome === "require_approval" ,
189- reason : rule . reason ,
190- ruleId : rule . id ,
191- outcome : rule . outcome ,
192- evaluatedAt : new Date ( ) . toISOString ( ) ,
193- rulesEvaluated : active . length ,
194- } ;
221+ return buildDecision ( rule , ctx , active . length ) ;
195222 }
196223 }
197224
@@ -223,14 +250,7 @@ export function createPolicyEngine(config: PolicyEngineConfig = {}): PolicyEngin
223250
224251 for ( const rule of active ) {
225252 if ( evaluateCondition ( rule . condition , ctx ) ) {
226- return {
227- blocked : rule . outcome === "block" || rule . outcome === "require_approval" ,
228- reason : rule . reason ,
229- ruleId : rule . id ,
230- outcome : rule . outcome ,
231- evaluatedAt : new Date ( ) . toISOString ( ) ,
232- rulesEvaluated : active . length ,
233- } ;
253+ return buildDecision ( rule , ctx , active . length ) ;
234254 }
235255 }
236256
0 commit comments