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.
- package/CHANGELOG.md +15 -0
- package/README.md +274 -2
- package/audit-ci.json +2 -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/prompts/architecture.d.ts +6 -0
- package/dist/prompts/architecture.d.ts.map +1 -0
- package/dist/prompts/architecture.js +103 -0
- package/dist/prompts/architecture.js.map +1 -0
- package/dist/prompts/assumptions.d.ts +6 -0
- package/dist/prompts/assumptions.d.ts.map +1 -0
- package/dist/prompts/assumptions.js +72 -0
- package/dist/prompts/assumptions.js.map +1 -0
- package/dist/prompts/blindspots.d.ts +6 -0
- package/dist/prompts/blindspots.d.ts.map +1 -0
- package/dist/prompts/blindspots.js +71 -0
- package/dist/prompts/blindspots.js.map +1 -0
- package/dist/prompts/diverge-converge.d.ts +6 -0
- package/dist/prompts/diverge-converge.d.ts.map +1 -0
- package/dist/prompts/diverge-converge.js +85 -0
- package/dist/prompts/diverge-converge.js.map +1 -0
- package/dist/prompts/index.d.ts +22 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +57 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/prompts/perspectives.d.ts +7 -0
- package/dist/prompts/perspectives.d.ts.map +1 -0
- package/dist/prompts/perspectives.js +65 -0
- package/dist/prompts/perspectives.js.map +1 -0
- package/dist/prompts/red-team.d.ts +6 -0
- package/dist/prompts/red-team.d.ts.map +1 -0
- package/dist/prompts/red-team.js +83 -0
- package/dist/prompts/red-team.js.map +1 -0
- package/dist/prompts/reframe.d.ts +6 -0
- package/dist/prompts/reframe.d.ts.map +1 -0
- package/dist/prompts/reframe.js +71 -0
- package/dist/prompts/reframe.js.map +1 -0
- package/dist/prompts/tradeoffs.d.ts +6 -0
- package/dist/prompts/tradeoffs.d.ts.map +1 -0
- package/dist/prompts/tradeoffs.js +87 -0
- package/dist/prompts/tradeoffs.js.map +1 -0
- package/dist/prompts/types.d.ts +14 -0
- package/dist/prompts/types.d.ts.map +1 -0
- package/dist/prompts/types.js +2 -0
- package/dist/prompts/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 +48 -7
- 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 +1 -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/prompts/architecture.ts +111 -0
- package/src/prompts/assumptions.ts +80 -0
- package/src/prompts/blindspots.ts +79 -0
- package/src/prompts/diverge-converge.ts +92 -0
- package/src/prompts/index.ts +63 -0
- package/src/prompts/perspectives.ts +73 -0
- package/src/prompts/red-team.ts +91 -0
- package/src/prompts/reframe.ts +78 -0
- package/src/prompts/tradeoffs.ts +95 -0
- package/src/prompts/types.ts +14 -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 +55 -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/prompts.test.ts +314 -0
- package/tests/providers.test.ts +739 -0
package/src/config/types.ts
CHANGED
|
@@ -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
|
+
}
|