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.
Files changed (120) hide show
  1. package/.github/workflows/semantic-release.yml +12 -1
  2. package/.releaserc.json +6 -1
  3. package/CHANGELOG.md +30 -0
  4. package/README.md +158 -1
  5. package/audit-ci.json +3 -1
  6. package/dist/config/config.d.ts +2 -0
  7. package/dist/config/config.d.ts.map +1 -1
  8. package/dist/config/config.js +144 -1
  9. package/dist/config/config.js.map +1 -1
  10. package/dist/config/types.d.ts +1084 -2
  11. package/dist/config/types.d.ts.map +1 -1
  12. package/dist/config/types.js +59 -0
  13. package/dist/config/types.js.map +1 -1
  14. package/dist/guardrails/context.d.ts +10 -0
  15. package/dist/guardrails/context.d.ts.map +1 -0
  16. package/dist/guardrails/context.js +35 -0
  17. package/dist/guardrails/context.js.map +1 -0
  18. package/dist/guardrails/errors.d.ts +26 -0
  19. package/dist/guardrails/errors.d.ts.map +1 -0
  20. package/dist/guardrails/errors.js +42 -0
  21. package/dist/guardrails/errors.js.map +1 -0
  22. package/dist/guardrails/index.d.ts +6 -0
  23. package/dist/guardrails/index.d.ts.map +1 -0
  24. package/dist/guardrails/index.js +11 -0
  25. package/dist/guardrails/index.js.map +1 -0
  26. package/dist/guardrails/plugins/base-plugin.d.ts +35 -0
  27. package/dist/guardrails/plugins/base-plugin.d.ts.map +1 -0
  28. package/dist/guardrails/plugins/base-plugin.js +70 -0
  29. package/dist/guardrails/plugins/base-plugin.js.map +1 -0
  30. package/dist/guardrails/plugins/index.d.ts +6 -0
  31. package/dist/guardrails/plugins/index.d.ts.map +1 -0
  32. package/dist/guardrails/plugins/index.js +6 -0
  33. package/dist/guardrails/plugins/index.js.map +1 -0
  34. package/dist/guardrails/plugins/pattern-blocker.d.ts +27 -0
  35. package/dist/guardrails/plugins/pattern-blocker.d.ts.map +1 -0
  36. package/dist/guardrails/plugins/pattern-blocker.js +140 -0
  37. package/dist/guardrails/plugins/pattern-blocker.js.map +1 -0
  38. package/dist/guardrails/plugins/pii-redactor/detectors.d.ts +40 -0
  39. package/dist/guardrails/plugins/pii-redactor/detectors.d.ts.map +1 -0
  40. package/dist/guardrails/plugins/pii-redactor/detectors.js +134 -0
  41. package/dist/guardrails/plugins/pii-redactor/detectors.js.map +1 -0
  42. package/dist/guardrails/plugins/pii-redactor/index.d.ts +28 -0
  43. package/dist/guardrails/plugins/pii-redactor/index.d.ts.map +1 -0
  44. package/dist/guardrails/plugins/pii-redactor/index.js +157 -0
  45. package/dist/guardrails/plugins/pii-redactor/index.js.map +1 -0
  46. package/dist/guardrails/plugins/pii-redactor/pseudonymizer.d.ts +33 -0
  47. package/dist/guardrails/plugins/pii-redactor/pseudonymizer.d.ts.map +1 -0
  48. package/dist/guardrails/plugins/pii-redactor/pseudonymizer.js +70 -0
  49. package/dist/guardrails/plugins/pii-redactor/pseudonymizer.js.map +1 -0
  50. package/dist/guardrails/plugins/rate-limiter.d.ts +28 -0
  51. package/dist/guardrails/plugins/rate-limiter.d.ts.map +1 -0
  52. package/dist/guardrails/plugins/rate-limiter.js +91 -0
  53. package/dist/guardrails/plugins/rate-limiter.js.map +1 -0
  54. package/dist/guardrails/plugins/token-limiter.d.ts +30 -0
  55. package/dist/guardrails/plugins/token-limiter.d.ts.map +1 -0
  56. package/dist/guardrails/plugins/token-limiter.js +98 -0
  57. package/dist/guardrails/plugins/token-limiter.js.map +1 -0
  58. package/dist/guardrails/service.d.ts +38 -0
  59. package/dist/guardrails/service.d.ts.map +1 -0
  60. package/dist/guardrails/service.js +183 -0
  61. package/dist/guardrails/service.js.map +1 -0
  62. package/dist/guardrails/types.d.ts +96 -0
  63. package/dist/guardrails/types.d.ts.map +1 -0
  64. package/dist/guardrails/types.js +2 -0
  65. package/dist/guardrails/types.js.map +1 -0
  66. package/dist/providers/duck-provider-enhanced.d.ts +2 -1
  67. package/dist/providers/duck-provider-enhanced.d.ts.map +1 -1
  68. package/dist/providers/duck-provider-enhanced.js +55 -6
  69. package/dist/providers/duck-provider-enhanced.js.map +1 -1
  70. package/dist/providers/enhanced-manager.d.ts +2 -1
  71. package/dist/providers/enhanced-manager.d.ts.map +1 -1
  72. package/dist/providers/enhanced-manager.js +3 -3
  73. package/dist/providers/enhanced-manager.js.map +1 -1
  74. package/dist/providers/manager.d.ts +3 -1
  75. package/dist/providers/manager.d.ts.map +1 -1
  76. package/dist/providers/manager.js +4 -2
  77. package/dist/providers/manager.js.map +1 -1
  78. package/dist/providers/provider.d.ts +3 -1
  79. package/dist/providers/provider.d.ts.map +1 -1
  80. package/dist/providers/provider.js +43 -3
  81. package/dist/providers/provider.js.map +1 -1
  82. package/dist/server.d.ts +1 -0
  83. package/dist/server.d.ts.map +1 -1
  84. package/dist/server.js +28 -6
  85. package/dist/server.js.map +1 -1
  86. package/dist/services/function-bridge.d.ts +3 -1
  87. package/dist/services/function-bridge.d.ts.map +1 -1
  88. package/dist/services/function-bridge.js +40 -1
  89. package/dist/services/function-bridge.js.map +1 -1
  90. package/package.json +5 -1
  91. package/src/config/config.ts +187 -1
  92. package/src/config/types.ts +73 -0
  93. package/src/guardrails/context.ts +37 -0
  94. package/src/guardrails/errors.ts +46 -0
  95. package/src/guardrails/index.ts +20 -0
  96. package/src/guardrails/plugins/base-plugin.ts +103 -0
  97. package/src/guardrails/plugins/index.ts +5 -0
  98. package/src/guardrails/plugins/pattern-blocker.ts +190 -0
  99. package/src/guardrails/plugins/pii-redactor/detectors.ts +200 -0
  100. package/src/guardrails/plugins/pii-redactor/index.ts +203 -0
  101. package/src/guardrails/plugins/pii-redactor/pseudonymizer.ts +91 -0
  102. package/src/guardrails/plugins/rate-limiter.ts +142 -0
  103. package/src/guardrails/plugins/token-limiter.ts +155 -0
  104. package/src/guardrails/service.ts +209 -0
  105. package/src/guardrails/types.ts +120 -0
  106. package/src/providers/duck-provider-enhanced.ts +76 -7
  107. package/src/providers/enhanced-manager.ts +5 -3
  108. package/src/providers/manager.ts +6 -3
  109. package/src/providers/provider.ts +57 -6
  110. package/src/server.ts +32 -6
  111. package/src/services/function-bridge.ts +53 -2
  112. package/tests/guardrails/config.test.ts +267 -0
  113. package/tests/guardrails/errors.test.ts +109 -0
  114. package/tests/guardrails/plugins/pattern-blocker.test.ts +309 -0
  115. package/tests/guardrails/plugins/pii-redactor.test.ts +1004 -0
  116. package/tests/guardrails/plugins/rate-limiter.test.ts +310 -0
  117. package/tests/guardrails/plugins/token-limiter.test.ts +216 -0
  118. package/tests/guardrails/service.test.ts +911 -0
  119. package/tests/mcp-bridge.test.ts +248 -0
  120. package/tests/providers.test.ts +739 -0
@@ -0,0 +1,142 @@
1
+ import { BaseGuardrailPlugin } from './base-plugin.js';
2
+ import { GuardrailPhase, GuardrailContext, GuardrailResult } from '../types.js';
3
+ import { RateLimiterConfig } from '../../config/types.js';
4
+
5
+ interface RequestRecord {
6
+ timestamp: number;
7
+ }
8
+
9
+ /**
10
+ * Rate limiter plugin - limits requests per minute/hour
11
+ */
12
+ export class RateLimiterPlugin extends BaseGuardrailPlugin {
13
+ name = 'rate_limiter';
14
+ phases: GuardrailPhase[] = ['pre_request'];
15
+
16
+ private requestsPerMinute: number = 60;
17
+ private requestsPerHour: number = 1000;
18
+ private perProvider: boolean = false;
19
+ private burstAllowance: number = 5;
20
+
21
+ // Request history: key is provider (or 'global'), value is array of timestamps
22
+ private requestHistory: Map<string, RequestRecord[]> = new Map();
23
+
24
+ async initialize(config: Record<string, unknown>): Promise<void> {
25
+ await super.initialize(config);
26
+
27
+ const typedConfig = config as Partial<RateLimiterConfig>;
28
+ this.requestsPerMinute = typedConfig.requests_per_minute ?? 60;
29
+ this.requestsPerHour = typedConfig.requests_per_hour ?? 1000;
30
+ this.perProvider = typedConfig.per_provider ?? false;
31
+ this.burstAllowance = typedConfig.burst_allowance ?? 5;
32
+ this.priority = typedConfig.priority ?? 10;
33
+ }
34
+
35
+ execute(phase: GuardrailPhase, context: GuardrailContext): Promise<GuardrailResult> {
36
+ if (phase !== 'pre_request') {
37
+ return Promise.resolve(this.allow(context));
38
+ }
39
+
40
+ const key = this.perProvider ? context.provider : 'global';
41
+ const now = Date.now();
42
+
43
+ // Get or create request history for this key
44
+ let history = this.requestHistory.get(key);
45
+ if (!history) {
46
+ history = [];
47
+ this.requestHistory.set(key, history);
48
+ }
49
+
50
+ // Clean up old entries (older than 1 hour)
51
+ const oneHourAgo = now - 60 * 60 * 1000;
52
+ history = history.filter((r) => r.timestamp > oneHourAgo);
53
+
54
+ // Remove empty keys to prevent unbounded Map growth with perProvider mode
55
+ if (history.length === 0) {
56
+ this.requestHistory.delete(key);
57
+ history = [];
58
+ } else {
59
+ this.requestHistory.set(key, history);
60
+ }
61
+
62
+ // Count requests in last minute and last hour
63
+ const oneMinuteAgo = now - 60 * 1000;
64
+ const requestsLastMinute = history.filter((r) => r.timestamp > oneMinuteAgo).length;
65
+ const requestsLastHour = history.length;
66
+
67
+ // Check rate limits (with burst allowance)
68
+ const effectiveMinuteLimit = this.requestsPerMinute + this.burstAllowance;
69
+ const effectiveHourLimit = this.requestsPerHour + this.burstAllowance;
70
+
71
+ if (requestsLastMinute >= effectiveMinuteLimit) {
72
+ this.addViolation(
73
+ context,
74
+ phase,
75
+ 'requests_per_minute',
76
+ 'error',
77
+ `Rate limit exceeded: ${requestsLastMinute} requests in the last minute (limit: ${this.requestsPerMinute})`,
78
+ { requestsLastMinute, limit: this.requestsPerMinute }
79
+ );
80
+ return Promise.resolve(this.block(
81
+ context,
82
+ `Rate limit exceeded: ${requestsLastMinute}/${this.requestsPerMinute} requests per minute`
83
+ ));
84
+ }
85
+
86
+ if (requestsLastHour >= effectiveHourLimit) {
87
+ this.addViolation(
88
+ context,
89
+ phase,
90
+ 'requests_per_hour',
91
+ 'error',
92
+ `Rate limit exceeded: ${requestsLastHour} requests in the last hour (limit: ${this.requestsPerHour})`,
93
+ { requestsLastHour, limit: this.requestsPerHour }
94
+ );
95
+ return Promise.resolve(this.block(
96
+ context,
97
+ `Rate limit exceeded: ${requestsLastHour}/${this.requestsPerHour} requests per hour`
98
+ ));
99
+ }
100
+
101
+ // Log warning if approaching limit
102
+ if (requestsLastMinute >= this.requestsPerMinute * 0.8) {
103
+ this.addViolation(
104
+ context,
105
+ phase,
106
+ 'requests_per_minute_warning',
107
+ 'warning',
108
+ `Approaching rate limit: ${requestsLastMinute}/${this.requestsPerMinute} requests per minute`,
109
+ { requestsLastMinute, limit: this.requestsPerMinute }
110
+ );
111
+ }
112
+
113
+ // Record this request
114
+ history.push({ timestamp: now });
115
+ // Ensure history is stored in Map (needed after empty cleanup)
116
+ this.requestHistory.set(key, history);
117
+
118
+ return Promise.resolve(this.allow(context));
119
+ }
120
+
121
+ /**
122
+ * Get current request counts (for testing/monitoring)
123
+ */
124
+ getRequestCounts(key: string = 'global'): { lastMinute: number; lastHour: number } {
125
+ const now = Date.now();
126
+ const history = this.requestHistory.get(key) || [];
127
+ const oneMinuteAgo = now - 60 * 1000;
128
+ const oneHourAgo = now - 60 * 60 * 1000;
129
+
130
+ return {
131
+ lastMinute: history.filter((r) => r.timestamp > oneMinuteAgo).length,
132
+ lastHour: history.filter((r) => r.timestamp > oneHourAgo).length,
133
+ };
134
+ }
135
+
136
+ /**
137
+ * Reset request history (for testing)
138
+ */
139
+ reset(): void {
140
+ this.requestHistory.clear();
141
+ }
142
+ }
@@ -0,0 +1,155 @@
1
+ import { BaseGuardrailPlugin } from './base-plugin.js';
2
+ import { GuardrailPhase, GuardrailContext, GuardrailResult } from '../types.js';
3
+ import { TokenLimiterConfig } from '../../config/types.js';
4
+
5
+ /**
6
+ * Token limiter plugin - limits input/output token counts
7
+ */
8
+ export class TokenLimiterPlugin extends BaseGuardrailPlugin {
9
+ name = 'token_limiter';
10
+ phases: GuardrailPhase[] = ['pre_request', 'post_response'];
11
+
12
+ private maxInputTokens: number = 8192;
13
+ private maxOutputTokens: number | undefined;
14
+ private warnAtPercentage: number = 80;
15
+
16
+ async initialize(config: Record<string, unknown>): Promise<void> {
17
+ await super.initialize(config);
18
+
19
+ const typedConfig = config as Partial<TokenLimiterConfig>;
20
+ this.maxInputTokens = typedConfig.max_input_tokens ?? 8192;
21
+ this.maxOutputTokens = typedConfig.max_output_tokens;
22
+ this.warnAtPercentage = typedConfig.warn_at_percentage ?? 80;
23
+ this.priority = typedConfig.priority ?? 20;
24
+ }
25
+
26
+ execute(phase: GuardrailPhase, context: GuardrailContext): Promise<GuardrailResult> {
27
+ if (phase === 'pre_request') {
28
+ return this.checkInputTokens(context, phase);
29
+ } else if (phase === 'post_response') {
30
+ return this.checkOutputTokens(context, phase);
31
+ }
32
+ return Promise.resolve(this.allow(context));
33
+ }
34
+
35
+ private checkInputTokens(
36
+ context: GuardrailContext,
37
+ phase: GuardrailPhase
38
+ ): Promise<GuardrailResult> {
39
+ // Estimate token count from prompt
40
+ const prompt = context.prompt || '';
41
+ const estimatedTokens = this.estimateTokenCount(prompt);
42
+
43
+ // Also count messages if present
44
+ let totalTokens = estimatedTokens;
45
+ for (const msg of context.messages) {
46
+ totalTokens += this.estimateTokenCount(msg.content);
47
+ }
48
+
49
+ // Check if over limit
50
+ if (totalTokens > this.maxInputTokens) {
51
+ this.addViolation(
52
+ context,
53
+ phase,
54
+ 'max_input_tokens',
55
+ 'error',
56
+ `Token limit exceeded: estimated ${totalTokens} tokens (limit: ${this.maxInputTokens})`,
57
+ { estimatedTokens: totalTokens, limit: this.maxInputTokens }
58
+ );
59
+ return Promise.resolve(
60
+ this.block(context, `Token limit exceeded: ~${totalTokens}/${this.maxInputTokens} tokens`)
61
+ );
62
+ }
63
+
64
+ // Warn if approaching limit
65
+ const warnThreshold = this.maxInputTokens * (this.warnAtPercentage / 100);
66
+ if (totalTokens >= warnThreshold) {
67
+ this.addViolation(
68
+ context,
69
+ phase,
70
+ 'max_input_tokens_warning',
71
+ 'warning',
72
+ `Approaching token limit: estimated ${totalTokens}/${this.maxInputTokens} tokens (${Math.round((totalTokens / this.maxInputTokens) * 100)}%)`,
73
+ {
74
+ estimatedTokens: totalTokens,
75
+ limit: this.maxInputTokens,
76
+ percentage: Math.round((totalTokens / this.maxInputTokens) * 100),
77
+ }
78
+ );
79
+ }
80
+
81
+ return Promise.resolve(this.allow(context));
82
+ }
83
+
84
+ private checkOutputTokens(
85
+ context: GuardrailContext,
86
+ phase: GuardrailPhase
87
+ ): Promise<GuardrailResult> {
88
+ // Skip if no output limit configured
89
+ if (!this.maxOutputTokens) {
90
+ return Promise.resolve(this.allow(context));
91
+ }
92
+
93
+ const response = context.response || '';
94
+ const estimatedTokens = this.estimateTokenCount(response);
95
+
96
+ // Check if over limit
97
+ if (estimatedTokens > this.maxOutputTokens) {
98
+ this.addViolation(
99
+ context,
100
+ phase,
101
+ 'max_output_tokens',
102
+ 'error',
103
+ `Output token limit exceeded: estimated ${estimatedTokens} tokens (limit: ${this.maxOutputTokens})`,
104
+ { estimatedTokens, limit: this.maxOutputTokens }
105
+ );
106
+ return Promise.resolve(
107
+ this.block(
108
+ context,
109
+ `Output token limit exceeded: ~${estimatedTokens}/${this.maxOutputTokens} tokens`
110
+ )
111
+ );
112
+ }
113
+
114
+ // Warn if approaching limit
115
+ const warnThreshold = this.maxOutputTokens * (this.warnAtPercentage / 100);
116
+ if (estimatedTokens >= warnThreshold) {
117
+ this.addViolation(
118
+ context,
119
+ phase,
120
+ 'max_output_tokens_warning',
121
+ 'warning',
122
+ `Approaching output token limit: estimated ${estimatedTokens}/${this.maxOutputTokens} tokens (${Math.round((estimatedTokens / this.maxOutputTokens) * 100)}%)`,
123
+ {
124
+ estimatedTokens,
125
+ limit: this.maxOutputTokens,
126
+ percentage: Math.round((estimatedTokens / this.maxOutputTokens) * 100),
127
+ }
128
+ );
129
+ }
130
+
131
+ return Promise.resolve(this.allow(context));
132
+ }
133
+
134
+ /**
135
+ * Estimate token count from text
136
+ * Uses a simple heuristic: ~4 characters per token for English text
137
+ * This is a rough approximation - for accuracy, use tiktoken
138
+ */
139
+ estimateTokenCount(text: string): number {
140
+ if (!text) return 0;
141
+ // Rough approximation: 1 token ≈ 4 characters for English
142
+ // Add some overhead for special tokens
143
+ return Math.ceil(text.length / 4) + 4;
144
+ }
145
+
146
+ /**
147
+ * Get configured limits (for testing/monitoring)
148
+ */
149
+ getLimits(): { maxInputTokens: number; maxOutputTokens: number | undefined } {
150
+ return {
151
+ maxInputTokens: this.maxInputTokens,
152
+ maxOutputTokens: this.maxOutputTokens,
153
+ };
154
+ }
155
+ }
@@ -0,0 +1,209 @@
1
+ import { GuardrailPlugin, GuardrailPhase, GuardrailContext, GuardrailResult, CreateContextOptions } from './types.js';
2
+ import { createGuardrailContext } from './context.js';
3
+ import { GuardrailsConfig, GuardrailsPluginsConfig } from '../config/types.js';
4
+ import { logger } from '../utils/logger.js';
5
+
6
+ /**
7
+ * Main service that orchestrates guardrail plugins
8
+ */
9
+ export class GuardrailsService {
10
+ private plugins: GuardrailPlugin[] = [];
11
+ private config: GuardrailsConfig;
12
+ private enabled: boolean = false;
13
+
14
+ constructor(config?: Partial<GuardrailsConfig>) {
15
+ this.config = {
16
+ enabled: config?.enabled ?? false,
17
+ log_violations: config?.log_violations ?? true,
18
+ log_modifications: config?.log_modifications ?? false,
19
+ fail_open: config?.fail_open ?? false,
20
+ plugins: config?.plugins,
21
+ };
22
+ // Start disabled - will be enabled after successful initialization with plugins
23
+ this.enabled = false;
24
+ }
25
+
26
+ /**
27
+ * Initialize the service and all configured plugins
28
+ */
29
+ async initialize(): Promise<void> {
30
+ if (!this.config.enabled) {
31
+ logger.info('Guardrails disabled in configuration');
32
+ return;
33
+ }
34
+
35
+ const pluginConfigs = this.config.plugins || {};
36
+
37
+ // Load plugins in order
38
+ await this.loadPluginsFromConfig(pluginConfigs);
39
+
40
+ // Sort by priority (lower = runs first)
41
+ this.plugins.sort((a, b) => a.priority - b.priority);
42
+
43
+ this.enabled = this.plugins.length > 0;
44
+ logger.info(`Guardrails initialized with ${this.plugins.length} plugins`);
45
+ }
46
+
47
+ private async loadPluginsFromConfig(pluginConfigs: Partial<GuardrailsPluginsConfig>): Promise<void> {
48
+ const pluginOrder: Array<[string, unknown]> = [
49
+ ['rate_limiter', pluginConfigs.rate_limiter],
50
+ ['token_limiter', pluginConfigs.token_limiter],
51
+ ['pii_redactor', pluginConfigs.pii_redactor],
52
+ ['pattern_blocker', pluginConfigs.pattern_blocker],
53
+ ];
54
+
55
+ for (const [pluginName, pluginConfig] of pluginOrder) {
56
+ if (!pluginConfig || !(pluginConfig as { enabled?: boolean }).enabled) {
57
+ continue;
58
+ }
59
+
60
+ try {
61
+ const plugin = await this.loadPlugin(pluginName);
62
+ await plugin.initialize(pluginConfig as Record<string, unknown>);
63
+ if ((pluginConfig as { priority?: number }).priority !== undefined) {
64
+ plugin.priority = (pluginConfig as { priority: number }).priority;
65
+ }
66
+ this.plugins.push(plugin);
67
+ logger.info(`Guardrail plugin '${pluginName}' initialized`);
68
+ } catch (error) {
69
+ logger.error(`Failed to initialize guardrail plugin '${pluginName}':`, error);
70
+ }
71
+ }
72
+ }
73
+
74
+ private async loadPlugin(name: string): Promise<GuardrailPlugin> {
75
+ // Dynamic plugin loading
76
+ switch (name) {
77
+ case 'rate_limiter': {
78
+ const { RateLimiterPlugin } = await import('./plugins/rate-limiter.js');
79
+ return new RateLimiterPlugin();
80
+ }
81
+ case 'token_limiter': {
82
+ const { TokenLimiterPlugin } = await import('./plugins/token-limiter.js');
83
+ return new TokenLimiterPlugin();
84
+ }
85
+ case 'pattern_blocker': {
86
+ const { PatternBlockerPlugin } = await import('./plugins/pattern-blocker.js');
87
+ return new PatternBlockerPlugin();
88
+ }
89
+ case 'pii_redactor': {
90
+ const { PIIRedactorPlugin } = await import('./plugins/pii-redactor/index.js');
91
+ return new PIIRedactorPlugin();
92
+ }
93
+ default:
94
+ throw new Error(`Unknown guardrail plugin: ${name}`);
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Check if guardrails are enabled
100
+ */
101
+ isEnabled(): boolean {
102
+ return this.enabled;
103
+ }
104
+
105
+ /**
106
+ * Create a new context for guardrail execution
107
+ */
108
+ createContext(options: CreateContextOptions): GuardrailContext {
109
+ return createGuardrailContext(options);
110
+ }
111
+
112
+ /**
113
+ * Execute all relevant plugins for a given phase
114
+ */
115
+ async execute(phase: GuardrailPhase, context: GuardrailContext): Promise<GuardrailResult> {
116
+ if (!this.enabled) {
117
+ return { action: 'allow', context };
118
+ }
119
+
120
+ const relevantPlugins = this.plugins.filter(
121
+ (p) => p.enabled && p.phases.includes(phase)
122
+ );
123
+
124
+ // Track logged items to avoid duplicates
125
+ let lastViolationCount = 0;
126
+ let lastModificationCount = 0;
127
+ let wasModified = false;
128
+
129
+ for (const plugin of relevantPlugins) {
130
+ try {
131
+ const result = await plugin.execute(phase, context);
132
+
133
+ // Log only NEW violations if configured
134
+ if (this.config.log_violations && context.violations.length > lastViolationCount) {
135
+ for (let i = lastViolationCount; i < context.violations.length; i++) {
136
+ const violation = context.violations[i];
137
+ logger.warn(`Guardrail violation: ${violation.pluginName} - ${violation.message}`, {
138
+ rule: violation.rule,
139
+ severity: violation.severity,
140
+ details: violation.details,
141
+ });
142
+ }
143
+ lastViolationCount = context.violations.length;
144
+ }
145
+
146
+ // Log only NEW modifications if configured
147
+ if (this.config.log_modifications && context.modifications.length > lastModificationCount) {
148
+ for (let i = lastModificationCount; i < context.modifications.length; i++) {
149
+ const mod = context.modifications[i];
150
+ logger.info(`Guardrail modification: ${mod.pluginName} - ${mod.reason}`, {
151
+ field: mod.field,
152
+ });
153
+ }
154
+ lastModificationCount = context.modifications.length;
155
+ }
156
+
157
+ if (result.action === 'block') {
158
+ logger.warn(`Request blocked by guardrail '${plugin.name}': ${result.blockReason}`);
159
+ return result;
160
+ }
161
+
162
+ // Track if any plugin modified the context
163
+ if (result.action === 'modify') {
164
+ wasModified = true;
165
+ }
166
+
167
+ // Update context for next plugin
168
+ context = result.context;
169
+ } catch (error) {
170
+ const errorMessage = error instanceof Error ? error.message : String(error);
171
+ logger.error(`Guardrail plugin '${plugin.name}' error:`, error);
172
+
173
+ if (!this.config.fail_open) {
174
+ return {
175
+ action: 'block',
176
+ context,
177
+ blockedBy: plugin.name,
178
+ blockReason: `Plugin error: ${errorMessage}`,
179
+ };
180
+ }
181
+ // fail_open: continue to next plugin
182
+ }
183
+ }
184
+
185
+ return { action: wasModified ? 'modify' : 'allow', context };
186
+ }
187
+
188
+ /**
189
+ * Shutdown the service and all plugins
190
+ */
191
+ async shutdown(): Promise<void> {
192
+ for (const plugin of this.plugins) {
193
+ try {
194
+ await plugin.shutdown();
195
+ } catch (error) {
196
+ logger.error(`Error shutting down plugin '${plugin.name}':`, error);
197
+ }
198
+ }
199
+ this.plugins = [];
200
+ this.enabled = false;
201
+ }
202
+
203
+ /**
204
+ * Get list of loaded plugins
205
+ */
206
+ getPlugins(): GuardrailPlugin[] {
207
+ return [...this.plugins];
208
+ }
209
+ }
@@ -0,0 +1,120 @@
1
+ import { ConversationMessage } from '../config/types.js';
2
+
3
+ /**
4
+ * Phases in the guardrail pipeline where plugins can intercept
5
+ */
6
+ export type GuardrailPhase =
7
+ | 'pre_request' // Before LLM API call
8
+ | 'post_response' // After LLM response, before tool handling
9
+ | 'pre_tool_input' // Before MCP tool execution
10
+ | 'post_tool_output' // After MCP tool returns
11
+ | 'pre_cache'; // Before caching response
12
+
13
+ /**
14
+ * Action to take after guardrail evaluation
15
+ */
16
+ export type GuardrailAction = 'allow' | 'block' | 'modify';
17
+
18
+ /**
19
+ * Severity levels for violations
20
+ */
21
+ export type ViolationSeverity = 'info' | 'warning' | 'error' | 'critical';
22
+
23
+ /**
24
+ * A violation detected by a guardrail plugin
25
+ */
26
+ export interface GuardrailViolation {
27
+ pluginName: string;
28
+ phase: GuardrailPhase;
29
+ rule: string;
30
+ severity: ViolationSeverity;
31
+ message: string;
32
+ details?: Record<string, unknown>;
33
+ }
34
+
35
+ /**
36
+ * A modification made by a guardrail plugin
37
+ */
38
+ export interface GuardrailModification {
39
+ pluginName: string;
40
+ phase: GuardrailPhase;
41
+ field: string;
42
+ originalValue?: unknown;
43
+ newValue?: unknown;
44
+ reason: string;
45
+ }
46
+
47
+ /**
48
+ * Context passed through the guardrail pipeline
49
+ */
50
+ export interface GuardrailContext {
51
+ // Request metadata
52
+ requestId: string;
53
+ provider: string;
54
+ model: string;
55
+ timestamp: Date;
56
+
57
+ // Phase-specific data (mutable by plugins)
58
+ messages: ConversationMessage[];
59
+ prompt?: string;
60
+ response?: string;
61
+ toolName?: string;
62
+ toolArgs?: Record<string, unknown>;
63
+ toolResult?: unknown;
64
+
65
+ // Tracking data (persisted across phases)
66
+ metadata: Map<string, unknown>; // For plugins to store state
67
+ violations: GuardrailViolation[]; // Accumulated violations
68
+ modifications: GuardrailModification[]; // Tracking changes made
69
+ }
70
+
71
+ /**
72
+ * Result from a guardrail plugin execution
73
+ */
74
+ export interface GuardrailResult {
75
+ action: GuardrailAction;
76
+ context: GuardrailContext;
77
+ blockedBy?: string; // Plugin name that blocked
78
+ blockReason?: string;
79
+ }
80
+
81
+ /**
82
+ * Base interface for guardrail plugins
83
+ */
84
+ export interface GuardrailPlugin {
85
+ /** Unique plugin name */
86
+ name: string;
87
+
88
+ /** Whether the plugin is currently enabled */
89
+ enabled: boolean;
90
+
91
+ /** Execution priority (lower = runs first) */
92
+ priority: number;
93
+
94
+ /** Which phases this plugin handles */
95
+ phases: GuardrailPhase[];
96
+
97
+ /** Initialize the plugin with its configuration */
98
+ initialize(config: Record<string, unknown>): Promise<void>;
99
+
100
+ /** Execute the plugin for a specific phase */
101
+ execute(phase: GuardrailPhase, context: GuardrailContext): Promise<GuardrailResult>;
102
+
103
+ /** Cleanup plugin resources */
104
+ shutdown(): Promise<void>;
105
+ }
106
+
107
+ /**
108
+ * Options for creating a guardrail context
109
+ */
110
+ export interface CreateContextOptions {
111
+ requestId?: string;
112
+ provider?: string;
113
+ model?: string;
114
+ messages?: ConversationMessage[];
115
+ prompt?: string;
116
+ response?: string;
117
+ toolName?: string;
118
+ toolArgs?: Record<string, unknown>;
119
+ toolResult?: unknown;
120
+ }