mcp-rubber-duck 1.8.0 → 1.9.3
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.
- package/.github/workflows/semantic-release.yml +12 -1
- package/.releaserc.json +6 -1
- package/CHANGELOG.md +30 -0
- package/README.md +158 -1
- package/audit-ci.json +3 -1
- package/dist/config/config.d.ts +2 -0
- package/dist/config/config.d.ts.map +1 -1
- package/dist/config/config.js +144 -1
- package/dist/config/config.js.map +1 -1
- package/dist/config/types.d.ts +1084 -2
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +59 -0
- package/dist/config/types.js.map +1 -1
- package/dist/guardrails/context.d.ts +10 -0
- package/dist/guardrails/context.d.ts.map +1 -0
- package/dist/guardrails/context.js +35 -0
- package/dist/guardrails/context.js.map +1 -0
- package/dist/guardrails/errors.d.ts +26 -0
- package/dist/guardrails/errors.d.ts.map +1 -0
- package/dist/guardrails/errors.js +42 -0
- package/dist/guardrails/errors.js.map +1 -0
- package/dist/guardrails/index.d.ts +6 -0
- package/dist/guardrails/index.d.ts.map +1 -0
- package/dist/guardrails/index.js +11 -0
- package/dist/guardrails/index.js.map +1 -0
- package/dist/guardrails/plugins/base-plugin.d.ts +35 -0
- package/dist/guardrails/plugins/base-plugin.d.ts.map +1 -0
- package/dist/guardrails/plugins/base-plugin.js +70 -0
- package/dist/guardrails/plugins/base-plugin.js.map +1 -0
- package/dist/guardrails/plugins/index.d.ts +6 -0
- package/dist/guardrails/plugins/index.d.ts.map +1 -0
- package/dist/guardrails/plugins/index.js +6 -0
- package/dist/guardrails/plugins/index.js.map +1 -0
- package/dist/guardrails/plugins/pattern-blocker.d.ts +27 -0
- package/dist/guardrails/plugins/pattern-blocker.d.ts.map +1 -0
- package/dist/guardrails/plugins/pattern-blocker.js +140 -0
- package/dist/guardrails/plugins/pattern-blocker.js.map +1 -0
- package/dist/guardrails/plugins/pii-redactor/detectors.d.ts +40 -0
- package/dist/guardrails/plugins/pii-redactor/detectors.d.ts.map +1 -0
- package/dist/guardrails/plugins/pii-redactor/detectors.js +134 -0
- package/dist/guardrails/plugins/pii-redactor/detectors.js.map +1 -0
- package/dist/guardrails/plugins/pii-redactor/index.d.ts +28 -0
- package/dist/guardrails/plugins/pii-redactor/index.d.ts.map +1 -0
- package/dist/guardrails/plugins/pii-redactor/index.js +157 -0
- package/dist/guardrails/plugins/pii-redactor/index.js.map +1 -0
- package/dist/guardrails/plugins/pii-redactor/pseudonymizer.d.ts +33 -0
- package/dist/guardrails/plugins/pii-redactor/pseudonymizer.d.ts.map +1 -0
- package/dist/guardrails/plugins/pii-redactor/pseudonymizer.js +70 -0
- package/dist/guardrails/plugins/pii-redactor/pseudonymizer.js.map +1 -0
- package/dist/guardrails/plugins/rate-limiter.d.ts +28 -0
- package/dist/guardrails/plugins/rate-limiter.d.ts.map +1 -0
- package/dist/guardrails/plugins/rate-limiter.js +91 -0
- package/dist/guardrails/plugins/rate-limiter.js.map +1 -0
- package/dist/guardrails/plugins/token-limiter.d.ts +30 -0
- package/dist/guardrails/plugins/token-limiter.d.ts.map +1 -0
- package/dist/guardrails/plugins/token-limiter.js +98 -0
- package/dist/guardrails/plugins/token-limiter.js.map +1 -0
- package/dist/guardrails/service.d.ts +38 -0
- package/dist/guardrails/service.d.ts.map +1 -0
- package/dist/guardrails/service.js +183 -0
- package/dist/guardrails/service.js.map +1 -0
- package/dist/guardrails/types.d.ts +96 -0
- package/dist/guardrails/types.d.ts.map +1 -0
- package/dist/guardrails/types.js +2 -0
- package/dist/guardrails/types.js.map +1 -0
- package/dist/providers/duck-provider-enhanced.d.ts +2 -1
- package/dist/providers/duck-provider-enhanced.d.ts.map +1 -1
- package/dist/providers/duck-provider-enhanced.js +55 -6
- package/dist/providers/duck-provider-enhanced.js.map +1 -1
- package/dist/providers/enhanced-manager.d.ts +2 -1
- package/dist/providers/enhanced-manager.d.ts.map +1 -1
- package/dist/providers/enhanced-manager.js +3 -3
- package/dist/providers/enhanced-manager.js.map +1 -1
- package/dist/providers/manager.d.ts +3 -1
- package/dist/providers/manager.d.ts.map +1 -1
- package/dist/providers/manager.js +4 -2
- package/dist/providers/manager.js.map +1 -1
- package/dist/providers/provider.d.ts +3 -1
- package/dist/providers/provider.d.ts.map +1 -1
- package/dist/providers/provider.js +43 -3
- package/dist/providers/provider.js.map +1 -1
- package/dist/server.d.ts +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +28 -6
- package/dist/server.js.map +1 -1
- package/dist/services/function-bridge.d.ts +3 -1
- package/dist/services/function-bridge.d.ts.map +1 -1
- package/dist/services/function-bridge.js +40 -1
- package/dist/services/function-bridge.js.map +1 -1
- package/package.json +5 -1
- package/src/config/config.ts +187 -1
- package/src/config/types.ts +73 -0
- package/src/guardrails/context.ts +37 -0
- package/src/guardrails/errors.ts +46 -0
- package/src/guardrails/index.ts +20 -0
- package/src/guardrails/plugins/base-plugin.ts +103 -0
- package/src/guardrails/plugins/index.ts +5 -0
- package/src/guardrails/plugins/pattern-blocker.ts +190 -0
- package/src/guardrails/plugins/pii-redactor/detectors.ts +200 -0
- package/src/guardrails/plugins/pii-redactor/index.ts +203 -0
- package/src/guardrails/plugins/pii-redactor/pseudonymizer.ts +91 -0
- package/src/guardrails/plugins/rate-limiter.ts +142 -0
- package/src/guardrails/plugins/token-limiter.ts +155 -0
- package/src/guardrails/service.ts +209 -0
- package/src/guardrails/types.ts +120 -0
- package/src/providers/duck-provider-enhanced.ts +76 -7
- package/src/providers/enhanced-manager.ts +5 -3
- package/src/providers/manager.ts +6 -3
- package/src/providers/provider.ts +57 -6
- package/src/server.ts +32 -6
- package/src/services/function-bridge.ts +53 -2
- package/tests/guardrails/config.test.ts +267 -0
- package/tests/guardrails/errors.test.ts +109 -0
- package/tests/guardrails/plugins/pattern-blocker.test.ts +309 -0
- package/tests/guardrails/plugins/pii-redactor.test.ts +1004 -0
- package/tests/guardrails/plugins/rate-limiter.test.ts +310 -0
- package/tests/guardrails/plugins/token-limiter.test.ts +216 -0
- package/tests/guardrails/service.test.ts +911 -0
- package/tests/mcp-bridge.test.ts +248 -0
- package/tests/providers.test.ts +739 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals';
|
|
2
|
+
import { ConfigManager } from '../../src/config/config';
|
|
3
|
+
|
|
4
|
+
// Mock logger to avoid console noise during tests
|
|
5
|
+
jest.mock('../../src/utils/logger');
|
|
6
|
+
|
|
7
|
+
describe('ConfigManager - Guardrails Config', () => {
|
|
8
|
+
let originalEnv: NodeJS.ProcessEnv;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
originalEnv = { ...process.env };
|
|
12
|
+
// Clear guardrails env vars
|
|
13
|
+
Object.keys(process.env).forEach((key) => {
|
|
14
|
+
if (key.startsWith('GUARDRAILS_')) delete process.env[key];
|
|
15
|
+
});
|
|
16
|
+
// Ensure at least one provider exists
|
|
17
|
+
process.env.OPENAI_API_KEY = 'test-key';
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
process.env = originalEnv;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('basic guardrails config', () => {
|
|
25
|
+
it('should not have guardrails by default', () => {
|
|
26
|
+
const configManager = new ConfigManager();
|
|
27
|
+
expect(configManager.getConfig().guardrails).toBeUndefined();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should enable guardrails when GUARDRAILS_ENABLED=true', () => {
|
|
31
|
+
process.env.GUARDRAILS_ENABLED = 'true';
|
|
32
|
+
|
|
33
|
+
const configManager = new ConfigManager();
|
|
34
|
+
expect(configManager.getConfig().guardrails?.enabled).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should disable guardrails when GUARDRAILS_ENABLED=false', () => {
|
|
38
|
+
process.env.GUARDRAILS_ENABLED = 'false';
|
|
39
|
+
|
|
40
|
+
const configManager = new ConfigManager();
|
|
41
|
+
expect(configManager.getConfig().guardrails).toBeUndefined();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should set log_violations from environment', () => {
|
|
45
|
+
process.env.GUARDRAILS_ENABLED = 'true';
|
|
46
|
+
process.env.GUARDRAILS_LOG_VIOLATIONS = 'false';
|
|
47
|
+
|
|
48
|
+
const configManager = new ConfigManager();
|
|
49
|
+
expect(configManager.getConfig().guardrails?.log_violations).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should set log_modifications from environment', () => {
|
|
53
|
+
process.env.GUARDRAILS_ENABLED = 'true';
|
|
54
|
+
process.env.GUARDRAILS_LOG_MODIFICATIONS = 'true';
|
|
55
|
+
|
|
56
|
+
const configManager = new ConfigManager();
|
|
57
|
+
expect(configManager.getConfig().guardrails?.log_modifications).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should set fail_open from environment', () => {
|
|
61
|
+
process.env.GUARDRAILS_ENABLED = 'true';
|
|
62
|
+
process.env.GUARDRAILS_FAIL_OPEN = 'true';
|
|
63
|
+
|
|
64
|
+
const configManager = new ConfigManager();
|
|
65
|
+
expect(configManager.getConfig().guardrails?.fail_open).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('rate limiter config', () => {
|
|
70
|
+
it('should configure rate limiter from environment', () => {
|
|
71
|
+
process.env.GUARDRAILS_RATE_LIMITER_ENABLED = 'true';
|
|
72
|
+
process.env.GUARDRAILS_RATE_LIMITER_REQUESTS_PER_MINUTE = '30';
|
|
73
|
+
process.env.GUARDRAILS_RATE_LIMITER_REQUESTS_PER_HOUR = '500';
|
|
74
|
+
|
|
75
|
+
const configManager = new ConfigManager();
|
|
76
|
+
const config = configManager.getConfig();
|
|
77
|
+
|
|
78
|
+
expect(config.guardrails?.enabled).toBe(true);
|
|
79
|
+
expect(config.guardrails?.plugins?.rate_limiter?.enabled).toBe(true);
|
|
80
|
+
expect(config.guardrails?.plugins?.rate_limiter?.requests_per_minute).toBe(30);
|
|
81
|
+
expect(config.guardrails?.plugins?.rate_limiter?.requests_per_hour).toBe(500);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should configure per_provider setting', () => {
|
|
85
|
+
process.env.GUARDRAILS_RATE_LIMITER_ENABLED = 'true';
|
|
86
|
+
process.env.GUARDRAILS_RATE_LIMITER_PER_PROVIDER = 'true';
|
|
87
|
+
|
|
88
|
+
const configManager = new ConfigManager();
|
|
89
|
+
expect(configManager.getConfig().guardrails?.plugins?.rate_limiter?.per_provider).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should configure burst_allowance setting', () => {
|
|
93
|
+
process.env.GUARDRAILS_RATE_LIMITER_ENABLED = 'true';
|
|
94
|
+
process.env.GUARDRAILS_RATE_LIMITER_BURST_ALLOWANCE = '10';
|
|
95
|
+
|
|
96
|
+
const configManager = new ConfigManager();
|
|
97
|
+
expect(configManager.getConfig().guardrails?.plugins?.rate_limiter?.burst_allowance).toBe(10);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('token limiter config', () => {
|
|
102
|
+
it('should configure token limiter from environment', () => {
|
|
103
|
+
process.env.GUARDRAILS_TOKEN_LIMITER_ENABLED = 'true';
|
|
104
|
+
process.env.GUARDRAILS_TOKEN_LIMITER_MAX_INPUT_TOKENS = '4096';
|
|
105
|
+
|
|
106
|
+
const configManager = new ConfigManager();
|
|
107
|
+
const config = configManager.getConfig();
|
|
108
|
+
|
|
109
|
+
expect(config.guardrails?.enabled).toBe(true);
|
|
110
|
+
expect(config.guardrails?.plugins?.token_limiter?.enabled).toBe(true);
|
|
111
|
+
expect(config.guardrails?.plugins?.token_limiter?.max_input_tokens).toBe(4096);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should configure max_output_tokens setting', () => {
|
|
115
|
+
process.env.GUARDRAILS_TOKEN_LIMITER_ENABLED = 'true';
|
|
116
|
+
process.env.GUARDRAILS_TOKEN_LIMITER_MAX_OUTPUT_TOKENS = '2048';
|
|
117
|
+
|
|
118
|
+
const configManager = new ConfigManager();
|
|
119
|
+
expect(configManager.getConfig().guardrails?.plugins?.token_limiter?.max_output_tokens).toBe(2048);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should configure warn_at_percentage setting', () => {
|
|
123
|
+
process.env.GUARDRAILS_TOKEN_LIMITER_ENABLED = 'true';
|
|
124
|
+
process.env.GUARDRAILS_TOKEN_LIMITER_WARN_AT_PERCENTAGE = '90';
|
|
125
|
+
|
|
126
|
+
const configManager = new ConfigManager();
|
|
127
|
+
expect(configManager.getConfig().guardrails?.plugins?.token_limiter?.warn_at_percentage).toBe(90);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe('pattern blocker config', () => {
|
|
132
|
+
it('should configure pattern blocker from environment', () => {
|
|
133
|
+
process.env.GUARDRAILS_PATTERN_BLOCKER_ENABLED = 'true';
|
|
134
|
+
process.env.GUARDRAILS_PATTERN_BLOCKER_PATTERNS = 'password,secret,api_key';
|
|
135
|
+
|
|
136
|
+
const configManager = new ConfigManager();
|
|
137
|
+
const config = configManager.getConfig();
|
|
138
|
+
|
|
139
|
+
expect(config.guardrails?.enabled).toBe(true);
|
|
140
|
+
expect(config.guardrails?.plugins?.pattern_blocker?.enabled).toBe(true);
|
|
141
|
+
expect(config.guardrails?.plugins?.pattern_blocker?.blocked_patterns).toEqual([
|
|
142
|
+
'password',
|
|
143
|
+
'secret',
|
|
144
|
+
'api_key',
|
|
145
|
+
]);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should configure regex patterns', () => {
|
|
149
|
+
process.env.GUARDRAILS_PATTERN_BLOCKER_ENABLED = 'true';
|
|
150
|
+
process.env.GUARDRAILS_PATTERN_BLOCKER_PATTERNS_REGEX = 'pass.*word,secret\\d+';
|
|
151
|
+
|
|
152
|
+
const configManager = new ConfigManager();
|
|
153
|
+
expect(configManager.getConfig().guardrails?.plugins?.pattern_blocker?.blocked_patterns_regex).toEqual([
|
|
154
|
+
'pass.*word',
|
|
155
|
+
'secret\\d+',
|
|
156
|
+
]);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should configure case_sensitive setting', () => {
|
|
160
|
+
process.env.GUARDRAILS_PATTERN_BLOCKER_ENABLED = 'true';
|
|
161
|
+
process.env.GUARDRAILS_PATTERN_BLOCKER_CASE_SENSITIVE = 'true';
|
|
162
|
+
|
|
163
|
+
const configManager = new ConfigManager();
|
|
164
|
+
expect(configManager.getConfig().guardrails?.plugins?.pattern_blocker?.case_sensitive).toBe(true);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should configure action_on_match setting', () => {
|
|
168
|
+
process.env.GUARDRAILS_PATTERN_BLOCKER_ENABLED = 'true';
|
|
169
|
+
process.env.GUARDRAILS_PATTERN_BLOCKER_ACTION = 'redact';
|
|
170
|
+
|
|
171
|
+
const configManager = new ConfigManager();
|
|
172
|
+
expect(configManager.getConfig().guardrails?.plugins?.pattern_blocker?.action_on_match).toBe('redact');
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe('PII redactor config', () => {
|
|
177
|
+
it('should configure PII redactor from environment', () => {
|
|
178
|
+
process.env.GUARDRAILS_PII_REDACTOR_ENABLED = 'true';
|
|
179
|
+
|
|
180
|
+
const configManager = new ConfigManager();
|
|
181
|
+
const config = configManager.getConfig();
|
|
182
|
+
|
|
183
|
+
expect(config.guardrails?.enabled).toBe(true);
|
|
184
|
+
expect(config.guardrails?.plugins?.pii_redactor?.enabled).toBe(true);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should configure detection types', () => {
|
|
188
|
+
process.env.GUARDRAILS_PII_REDACTOR_ENABLED = 'true';
|
|
189
|
+
process.env.GUARDRAILS_PII_REDACTOR_DETECT_EMAILS = 'false';
|
|
190
|
+
process.env.GUARDRAILS_PII_REDACTOR_DETECT_PHONES = 'true';
|
|
191
|
+
process.env.GUARDRAILS_PII_REDACTOR_DETECT_SSN = 'false';
|
|
192
|
+
process.env.GUARDRAILS_PII_REDACTOR_DETECT_API_KEYS = 'true';
|
|
193
|
+
process.env.GUARDRAILS_PII_REDACTOR_DETECT_CREDIT_CARDS = 'false';
|
|
194
|
+
process.env.GUARDRAILS_PII_REDACTOR_DETECT_IP_ADDRESSES = 'true';
|
|
195
|
+
|
|
196
|
+
const configManager = new ConfigManager();
|
|
197
|
+
const piiConfig = configManager.getConfig().guardrails?.plugins?.pii_redactor;
|
|
198
|
+
|
|
199
|
+
expect(piiConfig?.detect_emails).toBe(false);
|
|
200
|
+
expect(piiConfig?.detect_phones).toBe(true);
|
|
201
|
+
expect(piiConfig?.detect_ssn).toBe(false);
|
|
202
|
+
expect(piiConfig?.detect_api_keys).toBe(true);
|
|
203
|
+
expect(piiConfig?.detect_credit_cards).toBe(false);
|
|
204
|
+
expect(piiConfig?.detect_ip_addresses).toBe(true);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('should configure allowlist', () => {
|
|
208
|
+
process.env.GUARDRAILS_PII_REDACTOR_ENABLED = 'true';
|
|
209
|
+
process.env.GUARDRAILS_PII_REDACTOR_ALLOWLIST = 'test@example.com,support@company.com';
|
|
210
|
+
|
|
211
|
+
const configManager = new ConfigManager();
|
|
212
|
+
expect(configManager.getConfig().guardrails?.plugins?.pii_redactor?.allowlist).toEqual([
|
|
213
|
+
'test@example.com',
|
|
214
|
+
'support@company.com',
|
|
215
|
+
]);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('should configure allowlist_domains', () => {
|
|
219
|
+
process.env.GUARDRAILS_PII_REDACTOR_ENABLED = 'true';
|
|
220
|
+
process.env.GUARDRAILS_PII_REDACTOR_ALLOWLIST_DOMAINS = 'company.com,internal.org';
|
|
221
|
+
|
|
222
|
+
const configManager = new ConfigManager();
|
|
223
|
+
expect(configManager.getConfig().guardrails?.plugins?.pii_redactor?.allowlist_domains).toEqual([
|
|
224
|
+
'company.com',
|
|
225
|
+
'internal.org',
|
|
226
|
+
]);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('should configure restore_on_response', () => {
|
|
230
|
+
process.env.GUARDRAILS_PII_REDACTOR_ENABLED = 'true';
|
|
231
|
+
process.env.GUARDRAILS_PII_REDACTOR_RESTORE_ON_RESPONSE = 'true';
|
|
232
|
+
|
|
233
|
+
const configManager = new ConfigManager();
|
|
234
|
+
expect(configManager.getConfig().guardrails?.plugins?.pii_redactor?.restore_on_response).toBe(true);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should configure log_detections', () => {
|
|
238
|
+
process.env.GUARDRAILS_PII_REDACTOR_ENABLED = 'true';
|
|
239
|
+
process.env.GUARDRAILS_PII_REDACTOR_LOG_DETECTIONS = 'false';
|
|
240
|
+
|
|
241
|
+
const configManager = new ConfigManager();
|
|
242
|
+
expect(configManager.getConfig().guardrails?.plugins?.pii_redactor?.log_detections).toBe(false);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe('auto-enable behavior', () => {
|
|
247
|
+
it('should auto-enable guardrails when a plugin is enabled', () => {
|
|
248
|
+
// Don't set GUARDRAILS_ENABLED, but enable a plugin
|
|
249
|
+
process.env.GUARDRAILS_RATE_LIMITER_ENABLED = 'true';
|
|
250
|
+
|
|
251
|
+
const configManager = new ConfigManager();
|
|
252
|
+
expect(configManager.getConfig().guardrails?.enabled).toBe(true);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('should auto-enable with multiple plugins', () => {
|
|
256
|
+
process.env.GUARDRAILS_RATE_LIMITER_ENABLED = 'true';
|
|
257
|
+
process.env.GUARDRAILS_PII_REDACTOR_ENABLED = 'true';
|
|
258
|
+
|
|
259
|
+
const configManager = new ConfigManager();
|
|
260
|
+
const config = configManager.getConfig();
|
|
261
|
+
|
|
262
|
+
expect(config.guardrails?.enabled).toBe(true);
|
|
263
|
+
expect(config.guardrails?.plugins?.rate_limiter?.enabled).toBe(true);
|
|
264
|
+
expect(config.guardrails?.plugins?.pii_redactor?.enabled).toBe(true);
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { describe, it, expect } from '@jest/globals';
|
|
2
|
+
import {
|
|
3
|
+
GuardrailBlockError,
|
|
4
|
+
GuardrailInitError,
|
|
5
|
+
GuardrailExecutionError,
|
|
6
|
+
} from '../../src/guardrails/errors.js';
|
|
7
|
+
|
|
8
|
+
describe('GuardrailBlockError', () => {
|
|
9
|
+
it('should create error with plugin name and reason', () => {
|
|
10
|
+
const error = new GuardrailBlockError('rate_limiter', 'Too many requests');
|
|
11
|
+
|
|
12
|
+
expect(error.pluginName).toBe('rate_limiter');
|
|
13
|
+
expect(error.reason).toBe('Too many requests');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should format message correctly', () => {
|
|
17
|
+
const error = new GuardrailBlockError('pii_redactor', 'Sensitive data detected');
|
|
18
|
+
|
|
19
|
+
expect(error.message).toBe(
|
|
20
|
+
"Request blocked by guardrail 'pii_redactor': Sensitive data detected"
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should have correct error name', () => {
|
|
25
|
+
const error = new GuardrailBlockError('test', 'reason');
|
|
26
|
+
|
|
27
|
+
expect(error.name).toBe('GuardrailBlockError');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should be instanceof Error', () => {
|
|
31
|
+
const error = new GuardrailBlockError('test', 'reason');
|
|
32
|
+
|
|
33
|
+
expect(error).toBeInstanceOf(Error);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('GuardrailInitError', () => {
|
|
38
|
+
it('should create error with plugin name and message', () => {
|
|
39
|
+
const error = new GuardrailInitError('token_limiter', 'Invalid configuration');
|
|
40
|
+
|
|
41
|
+
expect(error.pluginName).toBe('token_limiter');
|
|
42
|
+
expect(error.cause).toBeUndefined();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should format message correctly', () => {
|
|
46
|
+
const error = new GuardrailInitError('pattern_blocker', 'Missing required field');
|
|
47
|
+
|
|
48
|
+
expect(error.message).toBe(
|
|
49
|
+
"Failed to initialize guardrail plugin 'pattern_blocker': Missing required field"
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should store cause error when provided', () => {
|
|
54
|
+
const cause = new Error('Original error');
|
|
55
|
+
const error = new GuardrailInitError('test_plugin', 'Init failed', cause);
|
|
56
|
+
|
|
57
|
+
expect(error.cause).toBe(cause);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should have correct error name', () => {
|
|
61
|
+
const error = new GuardrailInitError('test', 'message');
|
|
62
|
+
|
|
63
|
+
expect(error.name).toBe('GuardrailInitError');
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('GuardrailExecutionError', () => {
|
|
68
|
+
it('should create error with plugin name, phase, and message', () => {
|
|
69
|
+
const error = new GuardrailExecutionError(
|
|
70
|
+
'rate_limiter',
|
|
71
|
+
'pre_request',
|
|
72
|
+
'Execution failed'
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
expect(error.pluginName).toBe('rate_limiter');
|
|
76
|
+
expect(error.phase).toBe('pre_request');
|
|
77
|
+
expect(error.cause).toBeUndefined();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should format message correctly', () => {
|
|
81
|
+
const error = new GuardrailExecutionError(
|
|
82
|
+
'pii_redactor',
|
|
83
|
+
'post_response',
|
|
84
|
+
'Pattern matching failed'
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
expect(error.message).toBe(
|
|
88
|
+
"Guardrail plugin 'pii_redactor' failed during 'post_response': Pattern matching failed"
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should store cause error when provided', () => {
|
|
93
|
+
const cause = new TypeError('Cannot read property');
|
|
94
|
+
const error = new GuardrailExecutionError(
|
|
95
|
+
'test_plugin',
|
|
96
|
+
'pre_tool_input',
|
|
97
|
+
'Unexpected error',
|
|
98
|
+
cause
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
expect(error.cause).toBe(cause);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should have correct error name', () => {
|
|
105
|
+
const error = new GuardrailExecutionError('test', 'phase', 'message');
|
|
106
|
+
|
|
107
|
+
expect(error.name).toBe('GuardrailExecutionError');
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
|
|
2
|
+
import { PatternBlockerPlugin } from '../../../src/guardrails/plugins/pattern-blocker';
|
|
3
|
+
import { createGuardrailContext } from '../../../src/guardrails/context';
|
|
4
|
+
|
|
5
|
+
// Mock logger to avoid console noise during tests
|
|
6
|
+
jest.mock('../../../src/utils/logger');
|
|
7
|
+
|
|
8
|
+
describe('PatternBlockerPlugin', () => {
|
|
9
|
+
let plugin: PatternBlockerPlugin;
|
|
10
|
+
|
|
11
|
+
beforeEach(async () => {
|
|
12
|
+
plugin = new PatternBlockerPlugin();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('initialization', () => {
|
|
16
|
+
it('should initialize with default values', async () => {
|
|
17
|
+
await plugin.initialize({ enabled: true });
|
|
18
|
+
|
|
19
|
+
expect(plugin.enabled).toBe(true);
|
|
20
|
+
expect(plugin.name).toBe('pattern_blocker');
|
|
21
|
+
expect(plugin.phases).toContain('pre_request');
|
|
22
|
+
expect(plugin.phases).toContain('pre_tool_input');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should initialize with custom patterns', async () => {
|
|
26
|
+
await plugin.initialize({
|
|
27
|
+
enabled: true,
|
|
28
|
+
blocked_patterns: ['password', 'secret'],
|
|
29
|
+
blocked_patterns_regex: ['api[_-]?key'],
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const patterns = plugin.getPatterns();
|
|
33
|
+
expect(patterns.simple).toEqual(['password', 'secret']);
|
|
34
|
+
expect(patterns.regex).toEqual(['api[_-]?key']);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('simple pattern matching', () => {
|
|
39
|
+
it('should block prompts containing blocked patterns', async () => {
|
|
40
|
+
await plugin.initialize({
|
|
41
|
+
enabled: true,
|
|
42
|
+
blocked_patterns: ['password', 'secret'],
|
|
43
|
+
action_on_match: 'block',
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const context = createGuardrailContext({
|
|
47
|
+
prompt: 'My password is hunter2',
|
|
48
|
+
});
|
|
49
|
+
const result = await plugin.execute('pre_request', context);
|
|
50
|
+
|
|
51
|
+
expect(result.action).toBe('block');
|
|
52
|
+
expect(result.blockedBy).toBe('pattern_blocker');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should allow prompts without blocked patterns', async () => {
|
|
56
|
+
await plugin.initialize({
|
|
57
|
+
enabled: true,
|
|
58
|
+
blocked_patterns: ['password', 'secret'],
|
|
59
|
+
action_on_match: 'block',
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const context = createGuardrailContext({
|
|
63
|
+
prompt: 'Hello world, how are you?',
|
|
64
|
+
});
|
|
65
|
+
const result = await plugin.execute('pre_request', context);
|
|
66
|
+
|
|
67
|
+
expect(result.action).toBe('allow');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should be case insensitive by default', async () => {
|
|
71
|
+
await plugin.initialize({
|
|
72
|
+
enabled: true,
|
|
73
|
+
blocked_patterns: ['password'],
|
|
74
|
+
case_sensitive: false,
|
|
75
|
+
action_on_match: 'block',
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const context = createGuardrailContext({
|
|
79
|
+
prompt: 'My PASSWORD is hunter2',
|
|
80
|
+
});
|
|
81
|
+
const result = await plugin.execute('pre_request', context);
|
|
82
|
+
|
|
83
|
+
expect(result.action).toBe('block');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should respect case sensitivity when configured', async () => {
|
|
87
|
+
await plugin.initialize({
|
|
88
|
+
enabled: true,
|
|
89
|
+
blocked_patterns: ['password'],
|
|
90
|
+
case_sensitive: true,
|
|
91
|
+
action_on_match: 'block',
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Lowercase should match
|
|
95
|
+
const context1 = createGuardrailContext({
|
|
96
|
+
prompt: 'My password is hunter2',
|
|
97
|
+
});
|
|
98
|
+
const result1 = await plugin.execute('pre_request', context1);
|
|
99
|
+
expect(result1.action).toBe('block');
|
|
100
|
+
|
|
101
|
+
// Uppercase should NOT match
|
|
102
|
+
const context2 = createGuardrailContext({
|
|
103
|
+
prompt: 'My PASSWORD is hunter2',
|
|
104
|
+
});
|
|
105
|
+
const result2 = await plugin.execute('pre_request', context2);
|
|
106
|
+
expect(result2.action).toBe('allow');
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('regex pattern matching', () => {
|
|
111
|
+
it('should match regex patterns', async () => {
|
|
112
|
+
await plugin.initialize({
|
|
113
|
+
enabled: true,
|
|
114
|
+
blocked_patterns_regex: ['api[_-]?key[_-]?[a-z0-9]{8,}'],
|
|
115
|
+
action_on_match: 'block',
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const context = createGuardrailContext({
|
|
119
|
+
prompt: 'My api_key_abc12345678 is sensitive',
|
|
120
|
+
});
|
|
121
|
+
const result = await plugin.execute('pre_request', context);
|
|
122
|
+
|
|
123
|
+
expect(result.action).toBe('block');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should handle invalid regex gracefully', async () => {
|
|
127
|
+
// Invalid regex should be skipped during initialization
|
|
128
|
+
await plugin.initialize({
|
|
129
|
+
enabled: true,
|
|
130
|
+
blocked_patterns_regex: ['[invalid(regex'],
|
|
131
|
+
action_on_match: 'block',
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const patterns = plugin.getPatterns();
|
|
135
|
+
expect(patterns.regex).toEqual([]); // Invalid regex should be skipped
|
|
136
|
+
|
|
137
|
+
const context = createGuardrailContext({
|
|
138
|
+
prompt: 'This should be allowed',
|
|
139
|
+
});
|
|
140
|
+
const result = await plugin.execute('pre_request', context);
|
|
141
|
+
expect(result.action).toBe('allow');
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe('action modes', () => {
|
|
146
|
+
it('should block when action is block', async () => {
|
|
147
|
+
await plugin.initialize({
|
|
148
|
+
enabled: true,
|
|
149
|
+
blocked_patterns: ['secret'],
|
|
150
|
+
action_on_match: 'block',
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const context = createGuardrailContext({
|
|
154
|
+
prompt: 'This is a secret message',
|
|
155
|
+
});
|
|
156
|
+
const result = await plugin.execute('pre_request', context);
|
|
157
|
+
|
|
158
|
+
expect(result.action).toBe('block');
|
|
159
|
+
expect(context.violations.some((v) => v.severity === 'error')).toBe(true);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should warn but allow when action is warn', async () => {
|
|
163
|
+
await plugin.initialize({
|
|
164
|
+
enabled: true,
|
|
165
|
+
blocked_patterns: ['secret'],
|
|
166
|
+
action_on_match: 'warn',
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const context = createGuardrailContext({
|
|
170
|
+
prompt: 'This is a secret message',
|
|
171
|
+
});
|
|
172
|
+
const result = await plugin.execute('pre_request', context);
|
|
173
|
+
|
|
174
|
+
expect(result.action).toBe('allow');
|
|
175
|
+
expect(context.violations.some((v) => v.severity === 'warning')).toBe(true);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('should redact when action is redact', async () => {
|
|
179
|
+
await plugin.initialize({
|
|
180
|
+
enabled: true,
|
|
181
|
+
blocked_patterns: ['secret'],
|
|
182
|
+
action_on_match: 'redact',
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const context = createGuardrailContext({
|
|
186
|
+
prompt: 'This is a secret message',
|
|
187
|
+
});
|
|
188
|
+
const result = await plugin.execute('pre_request', context);
|
|
189
|
+
|
|
190
|
+
expect(result.action).toBe('modify');
|
|
191
|
+
expect(context.prompt).toContain('[REDACTED]');
|
|
192
|
+
expect(context.prompt).not.toContain('secret');
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe('tool input checking', () => {
|
|
197
|
+
it('should check tool arguments', async () => {
|
|
198
|
+
await plugin.initialize({
|
|
199
|
+
enabled: true,
|
|
200
|
+
blocked_patterns: ['password'],
|
|
201
|
+
action_on_match: 'block',
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const context = createGuardrailContext({
|
|
205
|
+
toolName: 'some_tool',
|
|
206
|
+
toolArgs: { user: 'admin', password: 'hunter2' },
|
|
207
|
+
});
|
|
208
|
+
const result = await plugin.execute('pre_tool_input', context);
|
|
209
|
+
|
|
210
|
+
expect(result.action).toBe('block');
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe('phase handling', () => {
|
|
215
|
+
it('should skip phases not in its list (post_response)', async () => {
|
|
216
|
+
await plugin.initialize({
|
|
217
|
+
enabled: true,
|
|
218
|
+
blocked_patterns: ['secret'],
|
|
219
|
+
action_on_match: 'block',
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// post_response is not in pattern_blocker's phases
|
|
223
|
+
const context = createGuardrailContext({
|
|
224
|
+
response: 'This is a secret response',
|
|
225
|
+
});
|
|
226
|
+
const result = await plugin.execute('post_response', context);
|
|
227
|
+
|
|
228
|
+
// Should allow since phase is not handled
|
|
229
|
+
expect(result.action).toBe('allow');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should skip post_tool_output phase', async () => {
|
|
233
|
+
await plugin.initialize({
|
|
234
|
+
enabled: true,
|
|
235
|
+
blocked_patterns: ['secret'],
|
|
236
|
+
action_on_match: 'block',
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const context = createGuardrailContext({
|
|
240
|
+
toolResult: { data: 'secret data' },
|
|
241
|
+
});
|
|
242
|
+
const result = await plugin.execute('post_tool_output', context);
|
|
243
|
+
|
|
244
|
+
expect(result.action).toBe('allow');
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe('redact mode with messages', () => {
|
|
249
|
+
it('should update last message when redacting in pre_request phase', async () => {
|
|
250
|
+
await plugin.initialize({
|
|
251
|
+
enabled: true,
|
|
252
|
+
blocked_patterns: ['secret'],
|
|
253
|
+
action_on_match: 'redact',
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const context = createGuardrailContext({
|
|
257
|
+
prompt: 'Tell me the secret code',
|
|
258
|
+
messages: [
|
|
259
|
+
{ role: 'user', content: 'Tell me the secret code', timestamp: new Date() },
|
|
260
|
+
],
|
|
261
|
+
});
|
|
262
|
+
const result = await plugin.execute('pre_request', context);
|
|
263
|
+
|
|
264
|
+
expect(result.action).toBe('modify');
|
|
265
|
+
expect(context.prompt).toContain('[REDACTED]');
|
|
266
|
+
// Last message should also be updated
|
|
267
|
+
expect(context.messages[0].content).toContain('[REDACTED]');
|
|
268
|
+
expect(context.messages[0].content).not.toContain('secret');
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('should redact in tool args during pre_tool_input phase', async () => {
|
|
272
|
+
await plugin.initialize({
|
|
273
|
+
enabled: true,
|
|
274
|
+
blocked_patterns: ['password'],
|
|
275
|
+
action_on_match: 'redact',
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const context = createGuardrailContext({
|
|
279
|
+
toolName: 'login',
|
|
280
|
+
toolArgs: { user: 'admin', pass: 'password123' },
|
|
281
|
+
});
|
|
282
|
+
const result = await plugin.execute('pre_tool_input', context);
|
|
283
|
+
|
|
284
|
+
expect(result.action).toBe('modify');
|
|
285
|
+
// toolArgs should be updated (as string since JSON parse might fail)
|
|
286
|
+
const toolArgsStr = JSON.stringify(context.toolArgs);
|
|
287
|
+
expect(toolArgsStr).toContain('[REDACTED]');
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
describe('violations', () => {
|
|
292
|
+
it('should add violation with pattern details', async () => {
|
|
293
|
+
await plugin.initialize({
|
|
294
|
+
enabled: true,
|
|
295
|
+
blocked_patterns: ['password'],
|
|
296
|
+
action_on_match: 'block',
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
const context = createGuardrailContext({
|
|
300
|
+
prompt: 'My password is secret',
|
|
301
|
+
});
|
|
302
|
+
await plugin.execute('pre_request', context);
|
|
303
|
+
|
|
304
|
+
expect(context.violations.length).toBeGreaterThan(0);
|
|
305
|
+
expect(context.violations[0].rule).toBe('blocked_pattern');
|
|
306
|
+
expect(context.violations[0].details?.matches).toBeDefined();
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
});
|