mcp-rubber-duck 1.7.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +274 -2
  3. package/audit-ci.json +2 -1
  4. package/dist/config/config.d.ts +2 -0
  5. package/dist/config/config.d.ts.map +1 -1
  6. package/dist/config/config.js +144 -1
  7. package/dist/config/config.js.map +1 -1
  8. package/dist/config/types.d.ts +1084 -2
  9. package/dist/config/types.d.ts.map +1 -1
  10. package/dist/config/types.js +59 -0
  11. package/dist/config/types.js.map +1 -1
  12. package/dist/guardrails/context.d.ts +10 -0
  13. package/dist/guardrails/context.d.ts.map +1 -0
  14. package/dist/guardrails/context.js +35 -0
  15. package/dist/guardrails/context.js.map +1 -0
  16. package/dist/guardrails/errors.d.ts +26 -0
  17. package/dist/guardrails/errors.d.ts.map +1 -0
  18. package/dist/guardrails/errors.js +42 -0
  19. package/dist/guardrails/errors.js.map +1 -0
  20. package/dist/guardrails/index.d.ts +6 -0
  21. package/dist/guardrails/index.d.ts.map +1 -0
  22. package/dist/guardrails/index.js +11 -0
  23. package/dist/guardrails/index.js.map +1 -0
  24. package/dist/guardrails/plugins/base-plugin.d.ts +35 -0
  25. package/dist/guardrails/plugins/base-plugin.d.ts.map +1 -0
  26. package/dist/guardrails/plugins/base-plugin.js +70 -0
  27. package/dist/guardrails/plugins/base-plugin.js.map +1 -0
  28. package/dist/guardrails/plugins/index.d.ts +6 -0
  29. package/dist/guardrails/plugins/index.d.ts.map +1 -0
  30. package/dist/guardrails/plugins/index.js +6 -0
  31. package/dist/guardrails/plugins/index.js.map +1 -0
  32. package/dist/guardrails/plugins/pattern-blocker.d.ts +27 -0
  33. package/dist/guardrails/plugins/pattern-blocker.d.ts.map +1 -0
  34. package/dist/guardrails/plugins/pattern-blocker.js +140 -0
  35. package/dist/guardrails/plugins/pattern-blocker.js.map +1 -0
  36. package/dist/guardrails/plugins/pii-redactor/detectors.d.ts +40 -0
  37. package/dist/guardrails/plugins/pii-redactor/detectors.d.ts.map +1 -0
  38. package/dist/guardrails/plugins/pii-redactor/detectors.js +134 -0
  39. package/dist/guardrails/plugins/pii-redactor/detectors.js.map +1 -0
  40. package/dist/guardrails/plugins/pii-redactor/index.d.ts +28 -0
  41. package/dist/guardrails/plugins/pii-redactor/index.d.ts.map +1 -0
  42. package/dist/guardrails/plugins/pii-redactor/index.js +157 -0
  43. package/dist/guardrails/plugins/pii-redactor/index.js.map +1 -0
  44. package/dist/guardrails/plugins/pii-redactor/pseudonymizer.d.ts +33 -0
  45. package/dist/guardrails/plugins/pii-redactor/pseudonymizer.d.ts.map +1 -0
  46. package/dist/guardrails/plugins/pii-redactor/pseudonymizer.js +70 -0
  47. package/dist/guardrails/plugins/pii-redactor/pseudonymizer.js.map +1 -0
  48. package/dist/guardrails/plugins/rate-limiter.d.ts +28 -0
  49. package/dist/guardrails/plugins/rate-limiter.d.ts.map +1 -0
  50. package/dist/guardrails/plugins/rate-limiter.js +91 -0
  51. package/dist/guardrails/plugins/rate-limiter.js.map +1 -0
  52. package/dist/guardrails/plugins/token-limiter.d.ts +30 -0
  53. package/dist/guardrails/plugins/token-limiter.d.ts.map +1 -0
  54. package/dist/guardrails/plugins/token-limiter.js +98 -0
  55. package/dist/guardrails/plugins/token-limiter.js.map +1 -0
  56. package/dist/guardrails/service.d.ts +38 -0
  57. package/dist/guardrails/service.d.ts.map +1 -0
  58. package/dist/guardrails/service.js +183 -0
  59. package/dist/guardrails/service.js.map +1 -0
  60. package/dist/guardrails/types.d.ts +96 -0
  61. package/dist/guardrails/types.d.ts.map +1 -0
  62. package/dist/guardrails/types.js +2 -0
  63. package/dist/guardrails/types.js.map +1 -0
  64. package/dist/prompts/architecture.d.ts +6 -0
  65. package/dist/prompts/architecture.d.ts.map +1 -0
  66. package/dist/prompts/architecture.js +103 -0
  67. package/dist/prompts/architecture.js.map +1 -0
  68. package/dist/prompts/assumptions.d.ts +6 -0
  69. package/dist/prompts/assumptions.d.ts.map +1 -0
  70. package/dist/prompts/assumptions.js +72 -0
  71. package/dist/prompts/assumptions.js.map +1 -0
  72. package/dist/prompts/blindspots.d.ts +6 -0
  73. package/dist/prompts/blindspots.d.ts.map +1 -0
  74. package/dist/prompts/blindspots.js +71 -0
  75. package/dist/prompts/blindspots.js.map +1 -0
  76. package/dist/prompts/diverge-converge.d.ts +6 -0
  77. package/dist/prompts/diverge-converge.d.ts.map +1 -0
  78. package/dist/prompts/diverge-converge.js +85 -0
  79. package/dist/prompts/diverge-converge.js.map +1 -0
  80. package/dist/prompts/index.d.ts +22 -0
  81. package/dist/prompts/index.d.ts.map +1 -0
  82. package/dist/prompts/index.js +57 -0
  83. package/dist/prompts/index.js.map +1 -0
  84. package/dist/prompts/perspectives.d.ts +7 -0
  85. package/dist/prompts/perspectives.d.ts.map +1 -0
  86. package/dist/prompts/perspectives.js +65 -0
  87. package/dist/prompts/perspectives.js.map +1 -0
  88. package/dist/prompts/red-team.d.ts +6 -0
  89. package/dist/prompts/red-team.d.ts.map +1 -0
  90. package/dist/prompts/red-team.js +83 -0
  91. package/dist/prompts/red-team.js.map +1 -0
  92. package/dist/prompts/reframe.d.ts +6 -0
  93. package/dist/prompts/reframe.d.ts.map +1 -0
  94. package/dist/prompts/reframe.js +71 -0
  95. package/dist/prompts/reframe.js.map +1 -0
  96. package/dist/prompts/tradeoffs.d.ts +6 -0
  97. package/dist/prompts/tradeoffs.d.ts.map +1 -0
  98. package/dist/prompts/tradeoffs.js +87 -0
  99. package/dist/prompts/tradeoffs.js.map +1 -0
  100. package/dist/prompts/types.d.ts +14 -0
  101. package/dist/prompts/types.d.ts.map +1 -0
  102. package/dist/prompts/types.js +2 -0
  103. package/dist/prompts/types.js.map +1 -0
  104. package/dist/providers/duck-provider-enhanced.d.ts +2 -1
  105. package/dist/providers/duck-provider-enhanced.d.ts.map +1 -1
  106. package/dist/providers/duck-provider-enhanced.js +55 -6
  107. package/dist/providers/duck-provider-enhanced.js.map +1 -1
  108. package/dist/providers/enhanced-manager.d.ts +2 -1
  109. package/dist/providers/enhanced-manager.d.ts.map +1 -1
  110. package/dist/providers/enhanced-manager.js +3 -3
  111. package/dist/providers/enhanced-manager.js.map +1 -1
  112. package/dist/providers/manager.d.ts +3 -1
  113. package/dist/providers/manager.d.ts.map +1 -1
  114. package/dist/providers/manager.js +4 -2
  115. package/dist/providers/manager.js.map +1 -1
  116. package/dist/providers/provider.d.ts +3 -1
  117. package/dist/providers/provider.d.ts.map +1 -1
  118. package/dist/providers/provider.js +43 -3
  119. package/dist/providers/provider.js.map +1 -1
  120. package/dist/server.d.ts +1 -0
  121. package/dist/server.d.ts.map +1 -1
  122. package/dist/server.js +48 -7
  123. package/dist/server.js.map +1 -1
  124. package/dist/services/function-bridge.d.ts +3 -1
  125. package/dist/services/function-bridge.d.ts.map +1 -1
  126. package/dist/services/function-bridge.js +40 -1
  127. package/dist/services/function-bridge.js.map +1 -1
  128. package/package.json +1 -1
  129. package/src/config/config.ts +187 -1
  130. package/src/config/types.ts +73 -0
  131. package/src/guardrails/context.ts +37 -0
  132. package/src/guardrails/errors.ts +46 -0
  133. package/src/guardrails/index.ts +20 -0
  134. package/src/guardrails/plugins/base-plugin.ts +103 -0
  135. package/src/guardrails/plugins/index.ts +5 -0
  136. package/src/guardrails/plugins/pattern-blocker.ts +190 -0
  137. package/src/guardrails/plugins/pii-redactor/detectors.ts +200 -0
  138. package/src/guardrails/plugins/pii-redactor/index.ts +203 -0
  139. package/src/guardrails/plugins/pii-redactor/pseudonymizer.ts +91 -0
  140. package/src/guardrails/plugins/rate-limiter.ts +142 -0
  141. package/src/guardrails/plugins/token-limiter.ts +155 -0
  142. package/src/guardrails/service.ts +209 -0
  143. package/src/guardrails/types.ts +120 -0
  144. package/src/prompts/architecture.ts +111 -0
  145. package/src/prompts/assumptions.ts +80 -0
  146. package/src/prompts/blindspots.ts +79 -0
  147. package/src/prompts/diverge-converge.ts +92 -0
  148. package/src/prompts/index.ts +63 -0
  149. package/src/prompts/perspectives.ts +73 -0
  150. package/src/prompts/red-team.ts +91 -0
  151. package/src/prompts/reframe.ts +78 -0
  152. package/src/prompts/tradeoffs.ts +95 -0
  153. package/src/prompts/types.ts +14 -0
  154. package/src/providers/duck-provider-enhanced.ts +76 -7
  155. package/src/providers/enhanced-manager.ts +5 -3
  156. package/src/providers/manager.ts +6 -3
  157. package/src/providers/provider.ts +57 -6
  158. package/src/server.ts +55 -6
  159. package/src/services/function-bridge.ts +53 -2
  160. package/tests/guardrails/config.test.ts +267 -0
  161. package/tests/guardrails/errors.test.ts +109 -0
  162. package/tests/guardrails/plugins/pattern-blocker.test.ts +309 -0
  163. package/tests/guardrails/plugins/pii-redactor.test.ts +1004 -0
  164. package/tests/guardrails/plugins/rate-limiter.test.ts +310 -0
  165. package/tests/guardrails/plugins/token-limiter.test.ts +216 -0
  166. package/tests/guardrails/service.test.ts +911 -0
  167. package/tests/mcp-bridge.test.ts +248 -0
  168. package/tests/prompts.test.ts +314 -0
  169. package/tests/providers.test.ts +739 -0
@@ -43,6 +43,72 @@ export const PricingConfigSchema = z.record(
43
43
  z.record(z.string(), ModelPricingSchema) // model name -> pricing
44
44
  );
45
45
 
46
+ // Guardrails Plugin Configs
47
+ export const RateLimiterConfigSchema = z.object({
48
+ enabled: z.boolean().default(false),
49
+ priority: z.number().min(0).max(1000).default(10),
50
+ requests_per_minute: z.number().min(1).default(60),
51
+ requests_per_hour: z.number().min(1).default(1000),
52
+ per_provider: z.boolean().default(false),
53
+ burst_allowance: z.number().min(0).default(5),
54
+ });
55
+
56
+ export const TokenLimiterConfigSchema = z.object({
57
+ enabled: z.boolean().default(false),
58
+ priority: z.number().min(0).max(1000).default(20),
59
+ max_input_tokens: z.number().min(1).default(8192),
60
+ max_output_tokens: z.number().min(1).optional(),
61
+ warn_at_percentage: z.number().min(0).max(100).default(80),
62
+ });
63
+
64
+ export const PatternBlockerConfigSchema = z.object({
65
+ enabled: z.boolean().default(false),
66
+ priority: z.number().min(0).max(1000).default(30),
67
+ blocked_patterns: z.array(z.string()).default([]),
68
+ blocked_patterns_regex: z.array(z.string()).default([]),
69
+ case_sensitive: z.boolean().default(false),
70
+ action_on_match: z.enum(['block', 'warn', 'redact']).default('block'),
71
+ });
72
+
73
+ export const PIIRedactorConfigSchema = z.object({
74
+ enabled: z.boolean().default(false),
75
+ priority: z.number().min(0).max(1000).default(25),
76
+ detect_emails: z.boolean().default(true),
77
+ detect_phones: z.boolean().default(true),
78
+ detect_ssn: z.boolean().default(true),
79
+ detect_api_keys: z.boolean().default(true),
80
+ detect_credit_cards: z.boolean().default(true),
81
+ detect_ip_addresses: z.boolean().default(false),
82
+ custom_patterns: z
83
+ .array(
84
+ z.object({
85
+ name: z.string(),
86
+ pattern: z.string(),
87
+ placeholder: z.string(),
88
+ })
89
+ )
90
+ .default([]),
91
+ allowlist: z.array(z.string()).default([]),
92
+ allowlist_domains: z.array(z.string()).default([]),
93
+ restore_on_response: z.boolean().default(false),
94
+ log_detections: z.boolean().default(true),
95
+ });
96
+
97
+ export const GuardrailsPluginsConfigSchema = z.object({
98
+ rate_limiter: RateLimiterConfigSchema.optional(),
99
+ token_limiter: TokenLimiterConfigSchema.optional(),
100
+ pattern_blocker: PatternBlockerConfigSchema.optional(),
101
+ pii_redactor: PIIRedactorConfigSchema.optional(),
102
+ });
103
+
104
+ export const GuardrailsConfigSchema = z.object({
105
+ enabled: z.boolean().default(false),
106
+ log_violations: z.boolean().default(true),
107
+ log_modifications: z.boolean().default(false),
108
+ fail_open: z.boolean().default(false), // If true, allow on plugin errors
109
+ plugins: GuardrailsPluginsConfigSchema.optional(),
110
+ });
111
+
46
112
  export const ConfigSchema = z.object({
47
113
  providers: z.record(z.string(), ProviderConfigSchema),
48
114
  default_provider: z.string().optional(),
@@ -52,6 +118,7 @@ export const ConfigSchema = z.object({
52
118
  log_level: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
53
119
  mcp_bridge: MCPBridgeConfigSchema.optional(),
54
120
  pricing: PricingConfigSchema.optional(),
121
+ guardrails: GuardrailsConfigSchema.optional(),
55
122
  });
56
123
 
57
124
  export type ProviderConfig = z.infer<typeof ProviderConfigSchema>;
@@ -59,6 +126,12 @@ export type MCPServerConfig = z.infer<typeof MCPServerConfigSchema>;
59
126
  export type MCPBridgeConfig = z.infer<typeof MCPBridgeConfigSchema>;
60
127
  export type ModelPricing = z.infer<typeof ModelPricingSchema>;
61
128
  export type PricingConfig = z.infer<typeof PricingConfigSchema>;
129
+ export type RateLimiterConfig = z.infer<typeof RateLimiterConfigSchema>;
130
+ export type TokenLimiterConfig = z.infer<typeof TokenLimiterConfigSchema>;
131
+ export type PatternBlockerConfig = z.infer<typeof PatternBlockerConfigSchema>;
132
+ export type PIIRedactorConfig = z.infer<typeof PIIRedactorConfigSchema>;
133
+ export type GuardrailsPluginsConfig = z.infer<typeof GuardrailsPluginsConfigSchema>;
134
+ export type GuardrailsConfig = z.infer<typeof GuardrailsConfigSchema>;
62
135
  export type Config = z.infer<typeof ConfigSchema>;
63
136
 
64
137
  export interface ConversationMessage {
@@ -0,0 +1,37 @@
1
+ import { randomUUID } from 'crypto';
2
+ import { GuardrailContext, CreateContextOptions } from './types.js';
3
+
4
+ /**
5
+ * Create a new guardrail context with defaults
6
+ */
7
+ export function createGuardrailContext(options: CreateContextOptions = {}): GuardrailContext {
8
+ return {
9
+ requestId: options.requestId || randomUUID(),
10
+ provider: options.provider || 'unknown',
11
+ model: options.model || 'unknown',
12
+ timestamp: new Date(),
13
+ messages: options.messages || [],
14
+ prompt: options.prompt,
15
+ response: options.response,
16
+ toolName: options.toolName,
17
+ toolArgs: options.toolArgs,
18
+ toolResult: options.toolResult,
19
+ metadata: new Map(),
20
+ violations: [],
21
+ modifications: [],
22
+ };
23
+ }
24
+
25
+ /**
26
+ * Clone a guardrail context (deep copy metadata but shallow copy violations/modifications)
27
+ */
28
+ export function cloneContext(context: GuardrailContext): GuardrailContext {
29
+ return {
30
+ ...context,
31
+ messages: [...context.messages],
32
+ toolArgs: context.toolArgs ? { ...context.toolArgs } : undefined,
33
+ metadata: new Map(context.metadata),
34
+ violations: [...context.violations],
35
+ modifications: [...context.modifications],
36
+ };
37
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Error thrown when a guardrail blocks a request
3
+ */
4
+ export class GuardrailBlockError extends Error {
5
+ public readonly pluginName: string;
6
+ public readonly reason: string;
7
+
8
+ constructor(pluginName: string, reason: string) {
9
+ super(`Request blocked by guardrail '${pluginName}': ${reason}`);
10
+ this.name = 'GuardrailBlockError';
11
+ this.pluginName = pluginName;
12
+ this.reason = reason;
13
+ }
14
+ }
15
+
16
+ /**
17
+ * Error thrown when a guardrail plugin fails to initialize
18
+ */
19
+ export class GuardrailInitError extends Error {
20
+ public readonly pluginName: string;
21
+ public readonly cause: Error | undefined;
22
+
23
+ constructor(pluginName: string, message: string, cause?: Error) {
24
+ super(`Failed to initialize guardrail plugin '${pluginName}': ${message}`);
25
+ this.name = 'GuardrailInitError';
26
+ this.pluginName = pluginName;
27
+ this.cause = cause;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Error thrown when a guardrail plugin execution fails
33
+ */
34
+ export class GuardrailExecutionError extends Error {
35
+ public readonly pluginName: string;
36
+ public readonly phase: string;
37
+ public readonly cause: Error | undefined;
38
+
39
+ constructor(pluginName: string, phase: string, message: string, cause?: Error) {
40
+ super(`Guardrail plugin '${pluginName}' failed during '${phase}': ${message}`);
41
+ this.name = 'GuardrailExecutionError';
42
+ this.pluginName = pluginName;
43
+ this.phase = phase;
44
+ this.cause = cause;
45
+ }
46
+ }
@@ -0,0 +1,20 @@
1
+ // Core types
2
+ export * from './types.js';
3
+
4
+ // Errors
5
+ export * from './errors.js';
6
+
7
+ // Context
8
+ export { createGuardrailContext, cloneContext } from './context.js';
9
+
10
+ // Service
11
+ export { GuardrailsService } from './service.js';
12
+
13
+ // Plugins
14
+ export {
15
+ BaseGuardrailPlugin,
16
+ RateLimiterPlugin,
17
+ TokenLimiterPlugin,
18
+ PatternBlockerPlugin,
19
+ PIIRedactorPlugin,
20
+ } from './plugins/index.js';
@@ -0,0 +1,103 @@
1
+ import {
2
+ GuardrailPlugin,
3
+ GuardrailPhase,
4
+ GuardrailContext,
5
+ GuardrailResult,
6
+ } from '../types.js';
7
+
8
+ /**
9
+ * Abstract base class for guardrail plugins
10
+ */
11
+ export abstract class BaseGuardrailPlugin implements GuardrailPlugin {
12
+ abstract name: string;
13
+ abstract phases: GuardrailPhase[];
14
+
15
+ enabled: boolean = false;
16
+ priority: number = 100;
17
+
18
+ protected config: Record<string, unknown> = {};
19
+
20
+ initialize(config: Record<string, unknown>): Promise<void> {
21
+ this.config = config;
22
+ this.enabled = true;
23
+ if (typeof config.priority === 'number') {
24
+ this.priority = config.priority;
25
+ }
26
+ return Promise.resolve();
27
+ }
28
+
29
+ abstract execute(phase: GuardrailPhase, context: GuardrailContext): Promise<GuardrailResult>;
30
+
31
+ shutdown(): Promise<void> {
32
+ this.enabled = false;
33
+ return Promise.resolve();
34
+ }
35
+
36
+ /**
37
+ * Helper to create an 'allow' result
38
+ */
39
+ protected allow(context: GuardrailContext): GuardrailResult {
40
+ return { action: 'allow', context };
41
+ }
42
+
43
+ /**
44
+ * Helper to create a 'block' result
45
+ */
46
+ protected block(context: GuardrailContext, reason: string): GuardrailResult {
47
+ return {
48
+ action: 'block',
49
+ context,
50
+ blockedBy: this.name,
51
+ blockReason: reason,
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Helper to create a 'modify' result
57
+ */
58
+ protected modify(context: GuardrailContext): GuardrailResult {
59
+ return { action: 'modify', context };
60
+ }
61
+
62
+ /**
63
+ * Helper to add a violation to context
64
+ */
65
+ protected addViolation(
66
+ context: GuardrailContext,
67
+ phase: GuardrailPhase,
68
+ rule: string,
69
+ severity: 'info' | 'warning' | 'error' | 'critical',
70
+ message: string,
71
+ details?: Record<string, unknown>
72
+ ): void {
73
+ context.violations.push({
74
+ pluginName: this.name,
75
+ phase,
76
+ rule,
77
+ severity,
78
+ message,
79
+ details,
80
+ });
81
+ }
82
+
83
+ /**
84
+ * Helper to add a modification to context
85
+ */
86
+ protected addModification(
87
+ context: GuardrailContext,
88
+ phase: GuardrailPhase,
89
+ field: string,
90
+ reason: string,
91
+ originalValue?: unknown,
92
+ newValue?: unknown
93
+ ): void {
94
+ context.modifications.push({
95
+ pluginName: this.name,
96
+ phase,
97
+ field,
98
+ originalValue,
99
+ newValue,
100
+ reason,
101
+ });
102
+ }
103
+ }
@@ -0,0 +1,5 @@
1
+ export { BaseGuardrailPlugin } from './base-plugin.js';
2
+ export { RateLimiterPlugin } from './rate-limiter.js';
3
+ export { TokenLimiterPlugin } from './token-limiter.js';
4
+ export { PatternBlockerPlugin } from './pattern-blocker.js';
5
+ export { PIIRedactorPlugin } from './pii-redactor/index.js';
@@ -0,0 +1,190 @@
1
+ import { BaseGuardrailPlugin } from './base-plugin.js';
2
+ import { GuardrailPhase, GuardrailContext, GuardrailResult } from '../types.js';
3
+ import { PatternBlockerConfig } from '../../config/types.js';
4
+
5
+ interface PatternMatch {
6
+ pattern: string;
7
+ isRegex: boolean;
8
+ matchedText: string;
9
+ position: number;
10
+ }
11
+
12
+ /**
13
+ * Pattern blocker plugin - blocks or warns on specific patterns
14
+ */
15
+ export class PatternBlockerPlugin extends BaseGuardrailPlugin {
16
+ name = 'pattern_blocker';
17
+ phases: GuardrailPhase[] = ['pre_request', 'pre_tool_input'];
18
+
19
+ private blockedPatterns: string[] = [];
20
+ private blockedPatternsRegex: RegExp[] = [];
21
+ private caseSensitive: boolean = false;
22
+ private actionOnMatch: 'block' | 'warn' | 'redact' = 'block';
23
+
24
+ async initialize(config: Record<string, unknown>): Promise<void> {
25
+ await super.initialize(config);
26
+
27
+ const typedConfig = config as Partial<PatternBlockerConfig>;
28
+ this.blockedPatterns = typedConfig.blocked_patterns ?? [];
29
+ this.caseSensitive = typedConfig.case_sensitive ?? false;
30
+ this.actionOnMatch = typedConfig.action_on_match ?? 'block';
31
+ this.priority = typedConfig.priority ?? 30;
32
+
33
+ // Compile regex patterns
34
+ this.blockedPatternsRegex = [];
35
+ for (const pattern of typedConfig.blocked_patterns_regex ?? []) {
36
+ try {
37
+ const flags = this.caseSensitive ? 'g' : 'gi';
38
+ this.blockedPatternsRegex.push(new RegExp(pattern, flags));
39
+ } catch (error) {
40
+ // Invalid regex, skip it
41
+ }
42
+ }
43
+ }
44
+
45
+ execute(phase: GuardrailPhase, context: GuardrailContext): Promise<GuardrailResult> {
46
+ if (!this.phases.includes(phase)) {
47
+ return Promise.resolve(this.allow(context));
48
+ }
49
+
50
+ // Get text to check based on phase
51
+ let textToCheck: string;
52
+ let fieldName: string;
53
+
54
+ if (phase === 'pre_request') {
55
+ textToCheck = context.prompt || '';
56
+ fieldName = 'prompt';
57
+ } else if (phase === 'pre_tool_input') {
58
+ textToCheck = JSON.stringify(context.toolArgs || {});
59
+ fieldName = 'toolArgs';
60
+ } else {
61
+ return Promise.resolve(this.allow(context));
62
+ }
63
+
64
+ // Find matches
65
+ const matches = this.findMatches(textToCheck);
66
+
67
+ if (matches.length === 0) {
68
+ return Promise.resolve(this.allow(context));
69
+ }
70
+
71
+ // Handle matches based on action
72
+ const matchSummary = matches.map((m) => m.pattern).join(', ');
73
+
74
+ if (this.actionOnMatch === 'block') {
75
+ this.addViolation(
76
+ context,
77
+ phase,
78
+ 'blocked_pattern',
79
+ 'error',
80
+ `Blocked patterns found: ${matchSummary}`,
81
+ { matches: matches.map((m) => ({ pattern: m.pattern, position: m.position })) }
82
+ );
83
+ return Promise.resolve(this.block(context, `Blocked pattern detected: ${matchSummary}`));
84
+ }
85
+
86
+ if (this.actionOnMatch === 'warn') {
87
+ this.addViolation(
88
+ context,
89
+ phase,
90
+ 'blocked_pattern_warning',
91
+ 'warning',
92
+ `Suspicious patterns found: ${matchSummary}`,
93
+ { matches: matches.map((m) => ({ pattern: m.pattern, position: m.position })) }
94
+ );
95
+ return Promise.resolve(this.allow(context));
96
+ }
97
+
98
+ if (this.actionOnMatch === 'redact') {
99
+ // Redact matches from text
100
+ let redactedText = textToCheck;
101
+ for (const match of matches) {
102
+ redactedText = redactedText.replace(
103
+ match.matchedText,
104
+ '[REDACTED]'
105
+ );
106
+ }
107
+
108
+ this.addModification(
109
+ context,
110
+ phase,
111
+ fieldName,
112
+ `Redacted ${matches.length} blocked patterns`,
113
+ textToCheck,
114
+ redactedText
115
+ );
116
+
117
+ // Update context
118
+ if (phase === 'pre_request') {
119
+ context.prompt = redactedText;
120
+ // Also update last message if present (create new object to avoid mutating original)
121
+ if (context.messages.length > 0) {
122
+ const lastIndex = context.messages.length - 1;
123
+ context.messages[lastIndex] = {
124
+ ...context.messages[lastIndex],
125
+ content: redactedText,
126
+ };
127
+ }
128
+ } else if (phase === 'pre_tool_input') {
129
+ try {
130
+ context.toolArgs = JSON.parse(redactedText) as Record<string, unknown>;
131
+ } catch {
132
+ // If parse fails, leave as is
133
+ }
134
+ }
135
+
136
+ return Promise.resolve(this.modify(context));
137
+ }
138
+
139
+ return Promise.resolve(this.allow(context));
140
+ }
141
+
142
+ /**
143
+ * Find all pattern matches in text
144
+ */
145
+ private findMatches(text: string): PatternMatch[] {
146
+ const matches: PatternMatch[] = [];
147
+ const searchText = this.caseSensitive ? text : text.toLowerCase();
148
+
149
+ // Check simple string patterns
150
+ for (const pattern of this.blockedPatterns) {
151
+ const searchPattern = this.caseSensitive ? pattern : pattern.toLowerCase();
152
+ let position = searchText.indexOf(searchPattern);
153
+ while (position !== -1) {
154
+ matches.push({
155
+ pattern,
156
+ isRegex: false,
157
+ matchedText: text.substring(position, position + pattern.length),
158
+ position,
159
+ });
160
+ position = searchText.indexOf(searchPattern, position + 1);
161
+ }
162
+ }
163
+
164
+ // Check regex patterns
165
+ for (const regex of this.blockedPatternsRegex) {
166
+ regex.lastIndex = 0; // Reset regex state
167
+ let match;
168
+ while ((match = regex.exec(text)) !== null) {
169
+ matches.push({
170
+ pattern: regex.source,
171
+ isRegex: true,
172
+ matchedText: match[0],
173
+ position: match.index,
174
+ });
175
+ }
176
+ }
177
+
178
+ return matches;
179
+ }
180
+
181
+ /**
182
+ * Get configured patterns (for testing)
183
+ */
184
+ getPatterns(): { simple: string[]; regex: string[] } {
185
+ return {
186
+ simple: [...this.blockedPatterns],
187
+ regex: this.blockedPatternsRegex.map((r) => r.source),
188
+ };
189
+ }
190
+ }
@@ -0,0 +1,200 @@
1
+ export type PIIType =
2
+ | 'email'
3
+ | 'phone'
4
+ | 'ssn'
5
+ | 'api_key'
6
+ | 'credit_card'
7
+ | 'ip_address'
8
+ | 'custom';
9
+
10
+ export interface PIIDetection {
11
+ type: PIIType;
12
+ value: string;
13
+ startIndex: number;
14
+ endIndex: number;
15
+ confidence: number;
16
+ }
17
+
18
+ export interface PIIDetectorConfig {
19
+ detectEmails: boolean;
20
+ detectPhones: boolean;
21
+ detectSSN: boolean;
22
+ detectAPIKeys: boolean;
23
+ detectCreditCards: boolean;
24
+ detectIPAddresses: boolean;
25
+ customPatterns: Array<{ name: string; pattern: string; placeholder: string }>;
26
+ allowlist: string[];
27
+ allowlistDomains: string[];
28
+ }
29
+
30
+ /**
31
+ * PII detector using regex patterns
32
+ */
33
+ export class PIIDetector {
34
+ private patterns: Map<PIIType, RegExp> = new Map();
35
+ private allowlist: Set<string>;
36
+ private allowlistDomains: Set<string>;
37
+ private customPatterns: Array<{ name: string; regex: RegExp; placeholder: string }> = [];
38
+
39
+ constructor(config: PIIDetectorConfig) {
40
+ this.allowlist = new Set(config.allowlist.map((a) => a.toLowerCase()));
41
+ this.allowlistDomains = new Set(config.allowlistDomains.map((d) => d.toLowerCase()));
42
+
43
+ // Initialize built-in patterns
44
+ if (config.detectEmails) {
45
+ this.patterns.set(
46
+ 'email',
47
+ /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g
48
+ );
49
+ }
50
+
51
+ if (config.detectPhones) {
52
+ // Handles multiple phone formats: US, international, with/without country code
53
+ this.patterns.set(
54
+ 'phone',
55
+ /\b(?:\+?1[-.\s]?)?\(?[0-9]{3}\)?[-.\s]?[0-9]{3}[-.\s]?[0-9]{4}\b/g
56
+ );
57
+ }
58
+
59
+ if (config.detectSSN) {
60
+ // US SSN format: XXX-XX-XXXX or XXXXXXXXX
61
+ this.patterns.set(
62
+ 'ssn',
63
+ /\b[0-9]{3}[-\s]?[0-9]{2}[-\s]?[0-9]{4}\b/g
64
+ );
65
+ }
66
+
67
+ if (config.detectAPIKeys) {
68
+ // Common API key patterns
69
+ this.patterns.set(
70
+ 'api_key',
71
+ /\b(sk-[a-zA-Z0-9]{20,}|gsk_[a-zA-Z0-9]{20,}|api[_-]?key[_-]?[a-zA-Z0-9]{16,})\b/gi
72
+ );
73
+ }
74
+
75
+ if (config.detectCreditCards) {
76
+ // Credit card patterns (Visa, Mastercard, Amex, Discover)
77
+ // Simplified - doesn't validate Luhn, just matches format
78
+ this.patterns.set(
79
+ 'credit_card',
80
+ /\b(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|3[47][0-9]{13}|6(?:011|5[0-9]{2})[0-9]{12})\b/g
81
+ );
82
+ }
83
+
84
+ if (config.detectIPAddresses) {
85
+ // IPv4 addresses
86
+ this.patterns.set(
87
+ 'ip_address',
88
+ /\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b/g
89
+ );
90
+ }
91
+
92
+ // Custom patterns
93
+ for (const custom of config.customPatterns) {
94
+ try {
95
+ this.customPatterns.push({
96
+ name: custom.name,
97
+ regex: new RegExp(custom.pattern, 'g'),
98
+ placeholder: custom.placeholder,
99
+ });
100
+ } catch {
101
+ // Invalid regex, skip it
102
+ }
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Detect PII in text
108
+ */
109
+ detect(text: string): PIIDetection[] {
110
+ const detections: PIIDetection[] = [];
111
+
112
+ // Check built-in patterns
113
+ for (const [type, pattern] of this.patterns) {
114
+ pattern.lastIndex = 0; // Reset regex state
115
+ let match;
116
+ while ((match = pattern.exec(text)) !== null) {
117
+ const value = match[0];
118
+
119
+ // Check allowlist
120
+ if (this.isAllowlisted(value, type)) {
121
+ continue;
122
+ }
123
+
124
+ detections.push({
125
+ type,
126
+ value,
127
+ startIndex: match.index,
128
+ endIndex: match.index + value.length,
129
+ confidence: this.calculateConfidence(type, value),
130
+ });
131
+ }
132
+ }
133
+
134
+ // Check custom patterns
135
+ for (const custom of this.customPatterns) {
136
+ custom.regex.lastIndex = 0;
137
+ let match;
138
+ while ((match = custom.regex.exec(text)) !== null) {
139
+ const value = match[0];
140
+
141
+ if (this.allowlist.has(value.toLowerCase())) {
142
+ continue;
143
+ }
144
+
145
+ detections.push({
146
+ type: 'custom',
147
+ value,
148
+ startIndex: match.index,
149
+ endIndex: match.index + value.length,
150
+ confidence: 0.9,
151
+ });
152
+ }
153
+ }
154
+
155
+ // Sort by position (for consistent pseudonymization)
156
+ return detections.sort((a, b) => a.startIndex - b.startIndex);
157
+ }
158
+
159
+ private isAllowlisted(value: string, type: PIIType): boolean {
160
+ const lowerValue = value.toLowerCase();
161
+
162
+ if (this.allowlist.has(lowerValue)) {
163
+ return true;
164
+ }
165
+
166
+ // For emails, check domain allowlist
167
+ if (type === 'email') {
168
+ const domain = lowerValue.split('@')[1];
169
+ if (domain && this.allowlistDomains.has(domain)) {
170
+ return true;
171
+ }
172
+ }
173
+
174
+ return false;
175
+ }
176
+
177
+ private calculateConfidence(type: PIIType, value: string): number {
178
+ // Simple confidence scoring based on format strictness
179
+ switch (type) {
180
+ case 'ssn':
181
+ return 0.95; // High confidence for strict format
182
+ case 'credit_card':
183
+ return 0.95; // High confidence for strict format
184
+ case 'email':
185
+ return 0.9;
186
+ case 'phone':
187
+ return 0.85;
188
+ case 'api_key':
189
+ // Higher confidence for longer keys or known prefixes
190
+ if (value.startsWith('sk-') || value.startsWith('gsk_')) {
191
+ return 0.95;
192
+ }
193
+ return 0.7; // Lower confidence due to possible false positives
194
+ case 'ip_address':
195
+ return 0.8;
196
+ default:
197
+ return 0.8;
198
+ }
199
+ }
200
+ }