mcp-rubber-duck 1.7.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +274 -2
  3. package/audit-ci.json +2 -1
  4. package/dist/config/config.d.ts +2 -0
  5. package/dist/config/config.d.ts.map +1 -1
  6. package/dist/config/config.js +144 -1
  7. package/dist/config/config.js.map +1 -1
  8. package/dist/config/types.d.ts +1084 -2
  9. package/dist/config/types.d.ts.map +1 -1
  10. package/dist/config/types.js +59 -0
  11. package/dist/config/types.js.map +1 -1
  12. package/dist/guardrails/context.d.ts +10 -0
  13. package/dist/guardrails/context.d.ts.map +1 -0
  14. package/dist/guardrails/context.js +35 -0
  15. package/dist/guardrails/context.js.map +1 -0
  16. package/dist/guardrails/errors.d.ts +26 -0
  17. package/dist/guardrails/errors.d.ts.map +1 -0
  18. package/dist/guardrails/errors.js +42 -0
  19. package/dist/guardrails/errors.js.map +1 -0
  20. package/dist/guardrails/index.d.ts +6 -0
  21. package/dist/guardrails/index.d.ts.map +1 -0
  22. package/dist/guardrails/index.js +11 -0
  23. package/dist/guardrails/index.js.map +1 -0
  24. package/dist/guardrails/plugins/base-plugin.d.ts +35 -0
  25. package/dist/guardrails/plugins/base-plugin.d.ts.map +1 -0
  26. package/dist/guardrails/plugins/base-plugin.js +70 -0
  27. package/dist/guardrails/plugins/base-plugin.js.map +1 -0
  28. package/dist/guardrails/plugins/index.d.ts +6 -0
  29. package/dist/guardrails/plugins/index.d.ts.map +1 -0
  30. package/dist/guardrails/plugins/index.js +6 -0
  31. package/dist/guardrails/plugins/index.js.map +1 -0
  32. package/dist/guardrails/plugins/pattern-blocker.d.ts +27 -0
  33. package/dist/guardrails/plugins/pattern-blocker.d.ts.map +1 -0
  34. package/dist/guardrails/plugins/pattern-blocker.js +140 -0
  35. package/dist/guardrails/plugins/pattern-blocker.js.map +1 -0
  36. package/dist/guardrails/plugins/pii-redactor/detectors.d.ts +40 -0
  37. package/dist/guardrails/plugins/pii-redactor/detectors.d.ts.map +1 -0
  38. package/dist/guardrails/plugins/pii-redactor/detectors.js +134 -0
  39. package/dist/guardrails/plugins/pii-redactor/detectors.js.map +1 -0
  40. package/dist/guardrails/plugins/pii-redactor/index.d.ts +28 -0
  41. package/dist/guardrails/plugins/pii-redactor/index.d.ts.map +1 -0
  42. package/dist/guardrails/plugins/pii-redactor/index.js +157 -0
  43. package/dist/guardrails/plugins/pii-redactor/index.js.map +1 -0
  44. package/dist/guardrails/plugins/pii-redactor/pseudonymizer.d.ts +33 -0
  45. package/dist/guardrails/plugins/pii-redactor/pseudonymizer.d.ts.map +1 -0
  46. package/dist/guardrails/plugins/pii-redactor/pseudonymizer.js +70 -0
  47. package/dist/guardrails/plugins/pii-redactor/pseudonymizer.js.map +1 -0
  48. package/dist/guardrails/plugins/rate-limiter.d.ts +28 -0
  49. package/dist/guardrails/plugins/rate-limiter.d.ts.map +1 -0
  50. package/dist/guardrails/plugins/rate-limiter.js +91 -0
  51. package/dist/guardrails/plugins/rate-limiter.js.map +1 -0
  52. package/dist/guardrails/plugins/token-limiter.d.ts +30 -0
  53. package/dist/guardrails/plugins/token-limiter.d.ts.map +1 -0
  54. package/dist/guardrails/plugins/token-limiter.js +98 -0
  55. package/dist/guardrails/plugins/token-limiter.js.map +1 -0
  56. package/dist/guardrails/service.d.ts +38 -0
  57. package/dist/guardrails/service.d.ts.map +1 -0
  58. package/dist/guardrails/service.js +183 -0
  59. package/dist/guardrails/service.js.map +1 -0
  60. package/dist/guardrails/types.d.ts +96 -0
  61. package/dist/guardrails/types.d.ts.map +1 -0
  62. package/dist/guardrails/types.js +2 -0
  63. package/dist/guardrails/types.js.map +1 -0
  64. package/dist/prompts/architecture.d.ts +6 -0
  65. package/dist/prompts/architecture.d.ts.map +1 -0
  66. package/dist/prompts/architecture.js +103 -0
  67. package/dist/prompts/architecture.js.map +1 -0
  68. package/dist/prompts/assumptions.d.ts +6 -0
  69. package/dist/prompts/assumptions.d.ts.map +1 -0
  70. package/dist/prompts/assumptions.js +72 -0
  71. package/dist/prompts/assumptions.js.map +1 -0
  72. package/dist/prompts/blindspots.d.ts +6 -0
  73. package/dist/prompts/blindspots.d.ts.map +1 -0
  74. package/dist/prompts/blindspots.js +71 -0
  75. package/dist/prompts/blindspots.js.map +1 -0
  76. package/dist/prompts/diverge-converge.d.ts +6 -0
  77. package/dist/prompts/diverge-converge.d.ts.map +1 -0
  78. package/dist/prompts/diverge-converge.js +85 -0
  79. package/dist/prompts/diverge-converge.js.map +1 -0
  80. package/dist/prompts/index.d.ts +22 -0
  81. package/dist/prompts/index.d.ts.map +1 -0
  82. package/dist/prompts/index.js +57 -0
  83. package/dist/prompts/index.js.map +1 -0
  84. package/dist/prompts/perspectives.d.ts +7 -0
  85. package/dist/prompts/perspectives.d.ts.map +1 -0
  86. package/dist/prompts/perspectives.js +65 -0
  87. package/dist/prompts/perspectives.js.map +1 -0
  88. package/dist/prompts/red-team.d.ts +6 -0
  89. package/dist/prompts/red-team.d.ts.map +1 -0
  90. package/dist/prompts/red-team.js +83 -0
  91. package/dist/prompts/red-team.js.map +1 -0
  92. package/dist/prompts/reframe.d.ts +6 -0
  93. package/dist/prompts/reframe.d.ts.map +1 -0
  94. package/dist/prompts/reframe.js +71 -0
  95. package/dist/prompts/reframe.js.map +1 -0
  96. package/dist/prompts/tradeoffs.d.ts +6 -0
  97. package/dist/prompts/tradeoffs.d.ts.map +1 -0
  98. package/dist/prompts/tradeoffs.js +87 -0
  99. package/dist/prompts/tradeoffs.js.map +1 -0
  100. package/dist/prompts/types.d.ts +14 -0
  101. package/dist/prompts/types.d.ts.map +1 -0
  102. package/dist/prompts/types.js +2 -0
  103. package/dist/prompts/types.js.map +1 -0
  104. package/dist/providers/duck-provider-enhanced.d.ts +2 -1
  105. package/dist/providers/duck-provider-enhanced.d.ts.map +1 -1
  106. package/dist/providers/duck-provider-enhanced.js +55 -6
  107. package/dist/providers/duck-provider-enhanced.js.map +1 -1
  108. package/dist/providers/enhanced-manager.d.ts +2 -1
  109. package/dist/providers/enhanced-manager.d.ts.map +1 -1
  110. package/dist/providers/enhanced-manager.js +3 -3
  111. package/dist/providers/enhanced-manager.js.map +1 -1
  112. package/dist/providers/manager.d.ts +3 -1
  113. package/dist/providers/manager.d.ts.map +1 -1
  114. package/dist/providers/manager.js +4 -2
  115. package/dist/providers/manager.js.map +1 -1
  116. package/dist/providers/provider.d.ts +3 -1
  117. package/dist/providers/provider.d.ts.map +1 -1
  118. package/dist/providers/provider.js +43 -3
  119. package/dist/providers/provider.js.map +1 -1
  120. package/dist/server.d.ts +1 -0
  121. package/dist/server.d.ts.map +1 -1
  122. package/dist/server.js +48 -7
  123. package/dist/server.js.map +1 -1
  124. package/dist/services/function-bridge.d.ts +3 -1
  125. package/dist/services/function-bridge.d.ts.map +1 -1
  126. package/dist/services/function-bridge.js +40 -1
  127. package/dist/services/function-bridge.js.map +1 -1
  128. package/package.json +1 -1
  129. package/src/config/config.ts +187 -1
  130. package/src/config/types.ts +73 -0
  131. package/src/guardrails/context.ts +37 -0
  132. package/src/guardrails/errors.ts +46 -0
  133. package/src/guardrails/index.ts +20 -0
  134. package/src/guardrails/plugins/base-plugin.ts +103 -0
  135. package/src/guardrails/plugins/index.ts +5 -0
  136. package/src/guardrails/plugins/pattern-blocker.ts +190 -0
  137. package/src/guardrails/plugins/pii-redactor/detectors.ts +200 -0
  138. package/src/guardrails/plugins/pii-redactor/index.ts +203 -0
  139. package/src/guardrails/plugins/pii-redactor/pseudonymizer.ts +91 -0
  140. package/src/guardrails/plugins/rate-limiter.ts +142 -0
  141. package/src/guardrails/plugins/token-limiter.ts +155 -0
  142. package/src/guardrails/service.ts +209 -0
  143. package/src/guardrails/types.ts +120 -0
  144. package/src/prompts/architecture.ts +111 -0
  145. package/src/prompts/assumptions.ts +80 -0
  146. package/src/prompts/blindspots.ts +79 -0
  147. package/src/prompts/diverge-converge.ts +92 -0
  148. package/src/prompts/index.ts +63 -0
  149. package/src/prompts/perspectives.ts +73 -0
  150. package/src/prompts/red-team.ts +91 -0
  151. package/src/prompts/reframe.ts +78 -0
  152. package/src/prompts/tradeoffs.ts +95 -0
  153. package/src/prompts/types.ts +14 -0
  154. package/src/providers/duck-provider-enhanced.ts +76 -7
  155. package/src/providers/enhanced-manager.ts +5 -3
  156. package/src/providers/manager.ts +6 -3
  157. package/src/providers/provider.ts +57 -6
  158. package/src/server.ts +55 -6
  159. package/src/services/function-bridge.ts +53 -2
  160. package/tests/guardrails/config.test.ts +267 -0
  161. package/tests/guardrails/errors.test.ts +109 -0
  162. package/tests/guardrails/plugins/pattern-blocker.test.ts +309 -0
  163. package/tests/guardrails/plugins/pii-redactor.test.ts +1004 -0
  164. package/tests/guardrails/plugins/rate-limiter.test.ts +310 -0
  165. package/tests/guardrails/plugins/token-limiter.test.ts +216 -0
  166. package/tests/guardrails/service.test.ts +911 -0
  167. package/tests/mcp-bridge.test.ts +248 -0
  168. package/tests/prompts.test.ts +314 -0
  169. package/tests/providers.test.ts +739 -0
@@ -2,18 +2,22 @@ import OpenAI from 'openai';
2
2
  import { ChatOptions, ChatResponse, ProviderOptions, ModelInfo, OpenAIChatParams, OpenAIChatResponse, OpenAIMessage } from './types.js';
3
3
  import { ConversationMessage } from '../config/types.js';
4
4
  import { logger } from '../utils/logger.js';
5
+ import { GuardrailsService } from '../guardrails/service.js';
6
+ import { GuardrailBlockError } from '../guardrails/errors.js';
5
7
 
6
8
  export class DuckProvider {
7
9
  protected client: OpenAI;
8
10
  protected options: ProviderOptions;
11
+ protected guardrailsService?: GuardrailsService;
9
12
  public name: string;
10
13
  public nickname: string;
11
14
 
12
- constructor(name: string, nickname: string, options: ProviderOptions) {
15
+ constructor(name: string, nickname: string, options: ProviderOptions, guardrailsService?: GuardrailsService) {
13
16
  this.name = name;
14
17
  this.nickname = nickname;
15
18
  this.options = options;
16
-
19
+ this.guardrailsService = guardrailsService;
20
+
17
21
  this.client = new OpenAI({
18
22
  apiKey: options.apiKey || 'not-needed',
19
23
  baseURL: options.baseURL,
@@ -34,9 +38,35 @@ export class DuckProvider {
34
38
 
35
39
  async chat(options: ChatOptions): Promise<ChatResponse> {
36
40
  try {
37
- const messages = this.prepareMessages(options.messages, options.systemPrompt);
38
41
  const modelToUse = options.model || this.options.model;
39
-
42
+
43
+ // Create guardrail context if service is enabled
44
+ const guardrailContext = this.guardrailsService?.isEnabled()
45
+ ? this.guardrailsService.createContext({
46
+ provider: this.name,
47
+ model: modelToUse,
48
+ messages: options.messages,
49
+ prompt: options.messages[options.messages.length - 1]?.content,
50
+ })
51
+ : undefined;
52
+
53
+ // Execute pre_request guardrails
54
+ if (guardrailContext && this.guardrailsService?.isEnabled()) {
55
+ const preResult = await this.guardrailsService.execute('pre_request', guardrailContext);
56
+ if (preResult.action === 'block') {
57
+ throw new GuardrailBlockError(
58
+ preResult.blockedBy || 'unknown',
59
+ preResult.blockReason || 'Request blocked by guardrails'
60
+ );
61
+ }
62
+ // Update messages if modified by guardrails (e.g., PII redaction)
63
+ if (preResult.action === 'modify' && guardrailContext.messages.length > 0) {
64
+ options = { ...options, messages: guardrailContext.messages };
65
+ }
66
+ }
67
+
68
+ const messages = this.prepareMessages(options.messages, options.systemPrompt);
69
+
40
70
  const baseParams: Partial<OpenAIChatParams> = {
41
71
  model: modelToUse,
42
72
  messages: messages as OpenAIMessage[],
@@ -50,9 +80,26 @@ export class DuckProvider {
50
80
 
51
81
  const response = await this.createChatCompletion(baseParams);
52
82
  const choice = response.choices[0];
53
-
83
+ let content = choice.message?.content || '';
84
+
85
+ // Execute post_response guardrails
86
+ if (guardrailContext && this.guardrailsService?.isEnabled()) {
87
+ guardrailContext.response = content;
88
+ const postResult = await this.guardrailsService.execute('post_response', guardrailContext);
89
+ if (postResult.action === 'block') {
90
+ throw new GuardrailBlockError(
91
+ postResult.blockedBy || 'unknown',
92
+ postResult.blockReason || 'Response blocked by guardrails'
93
+ );
94
+ }
95
+ // Use potentially modified response (e.g., PII restoration)
96
+ if (postResult.action === 'modify' && guardrailContext.response) {
97
+ content = guardrailContext.response;
98
+ }
99
+ }
100
+
54
101
  return {
55
- content: choice.message?.content || '',
102
+ content,
56
103
  usage: response.usage ? {
57
104
  promptTokens: response.usage.prompt_tokens,
58
105
  completionTokens: response.usage.completion_tokens,
@@ -62,6 +109,10 @@ export class DuckProvider {
62
109
  finishReason: choice.finish_reason || undefined,
63
110
  };
64
111
  } catch (error: unknown) {
112
+ // Re-throw GuardrailBlockError as-is
113
+ if (error instanceof GuardrailBlockError) {
114
+ throw error;
115
+ }
65
116
  logger.error(`Provider ${this.name} chat error:`, error);
66
117
  const errorMessage = error instanceof Error ? error.message : String(error);
67
118
  throw new Error(`Duck ${this.nickname} couldn't respond: ${errorMessage}`);
package/src/server.ts CHANGED
@@ -3,6 +3,8 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
3
3
  import {
4
4
  CallToolRequestSchema,
5
5
  ListToolsRequestSchema,
6
+ ListPromptsRequestSchema,
7
+ GetPromptRequestSchema,
6
8
  Tool,
7
9
  } from '@modelcontextprotocol/sdk/types.js';
8
10
 
@@ -18,6 +20,7 @@ import { UsageService } from './services/usage.js';
18
20
  import { DuckResponse } from './config/types.js';
19
21
  import { ApprovalService } from './services/approval.js';
20
22
  import { FunctionBridge } from './services/function-bridge.js';
23
+ import { GuardrailsService } from './guardrails/service.js';
21
24
  import { logger } from './utils/logger.js';
22
25
  import { duckArt, getRandomDuckMessage } from './utils/ascii-art.js';
23
26
 
@@ -42,11 +45,15 @@ import { mcpStatusTool } from './tools/mcp-status.js';
42
45
  // Import usage stats tool
43
46
  import { getUsageStatsTool } from './tools/get-usage-stats.js';
44
47
 
48
+ // Import prompts
49
+ import { getPrompts, getPrompt } from './prompts/index.js';
50
+
45
51
  export class RubberDuckServer {
46
52
  private server: Server;
47
53
  private configManager: ConfigManager;
48
54
  private pricingService: PricingService;
49
55
  private usageService: UsageService;
56
+ private guardrailsService?: GuardrailsService;
50
57
  private providerManager: ProviderManager;
51
58
  private enhancedProviderManager?: EnhancedProviderManager;
52
59
  private conversationManager: ConversationManager;
@@ -68,6 +75,7 @@ export class RubberDuckServer {
68
75
  {
69
76
  capabilities: {
70
77
  tools: {},
78
+ prompts: {},
71
79
  },
72
80
  }
73
81
  );
@@ -80,8 +88,13 @@ export class RubberDuckServer {
80
88
  this.pricingService = new PricingService(config.pricing);
81
89
  this.usageService = new UsageService(this.pricingService);
82
90
 
83
- // Initialize provider manager with usage tracking
84
- this.providerManager = new ProviderManager(this.configManager, this.usageService);
91
+ // Initialize guardrails service if configured
92
+ if (config.guardrails?.enabled) {
93
+ this.guardrailsService = new GuardrailsService(config.guardrails);
94
+ }
95
+
96
+ // Initialize provider manager with usage tracking and guardrails
97
+ this.providerManager = new ProviderManager(this.configManager, this.usageService, this.guardrailsService);
85
98
  this.conversationManager = new ConversationManager();
86
99
  this.cache = new ResponseCache(config.cache_ttl);
87
100
  this.healthMonitor = new HealthMonitor(this.providerManager);
@@ -110,20 +123,22 @@ export class RubberDuckServer {
110
123
  // Initialize approval service
111
124
  this.approvalService = new ApprovalService(mcpConfig.approval_timeout);
112
125
 
113
- // Initialize function bridge
126
+ // Initialize function bridge with guardrails
114
127
  this.functionBridge = new FunctionBridge(
115
128
  this.mcpClientManager,
116
129
  this.approvalService,
117
130
  mcpConfig.trusted_tools,
118
131
  mcpConfig.approval_mode,
119
- mcpConfig.trusted_tools_by_server || {}
132
+ mcpConfig.trusted_tools_by_server || {},
133
+ this.guardrailsService
120
134
  );
121
135
 
122
- // Initialize enhanced provider manager with usage tracking
136
+ // Initialize enhanced provider manager with usage tracking and guardrails
123
137
  this.enhancedProviderManager = new EnhancedProviderManager(
124
138
  this.configManager,
125
139
  this.functionBridge,
126
- this.usageService
140
+ this.usageService,
141
+ this.guardrailsService
127
142
  );
128
143
 
129
144
  this.mcpEnabled = true;
@@ -142,6 +157,23 @@ export class RubberDuckServer {
142
157
  return { tools: this.getTools() };
143
158
  });
144
159
 
160
+ // List available prompts
161
+ this.server.setRequestHandler(ListPromptsRequestSchema, () => {
162
+ return { prompts: getPrompts() };
163
+ });
164
+
165
+ // Get specific prompt
166
+ this.server.setRequestHandler(GetPromptRequestSchema, (request) => {
167
+ const { name, arguments: args } = request.params;
168
+ try {
169
+ return getPrompt(name, args || {});
170
+ } catch (error: unknown) {
171
+ const errorMessage = error instanceof Error ? error.message : String(error);
172
+ logger.error(`Prompt error for ${name}:`, errorMessage);
173
+ throw error;
174
+ }
175
+ });
176
+
145
177
  // Handle tool calls
146
178
  this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
147
179
  const { name, arguments: args } = request.params;
@@ -801,6 +833,18 @@ export class RubberDuckServer {
801
833
  console.log('\n' + getRandomDuckMessage('startup'));
802
834
  }
803
835
 
836
+ // Initialize guardrails service if configured
837
+ if (this.guardrailsService) {
838
+ try {
839
+ await this.guardrailsService.initialize();
840
+ logger.info('Guardrails service initialized successfully');
841
+ } catch (error: unknown) {
842
+ const errorMessage = error instanceof Error ? error.message : String(error);
843
+ logger.error('Failed to initialize guardrails:', errorMessage);
844
+ logger.warn('Guardrails functionality may not be available');
845
+ }
846
+ }
847
+
804
848
  // Initialize MCP connections if enabled
805
849
  if (this.mcpEnabled && this.mcpClientManager) {
806
850
  try {
@@ -830,6 +874,11 @@ export class RubberDuckServer {
830
874
  // Cleanup usage service (flush pending writes)
831
875
  this.usageService.shutdown();
832
876
 
877
+ // Cleanup guardrails service
878
+ if (this.guardrailsService) {
879
+ await this.guardrailsService.shutdown();
880
+ }
881
+
833
882
  // Cleanup MCP resources
834
883
  if (this.approvalService) {
835
884
  this.approvalService.shutdown();
@@ -2,6 +2,9 @@ import { MCPClientManager, MCPTool } from './mcp-client-manager.js';
2
2
  import { ApprovalService } from './approval.js';
3
3
  import { logger } from '../utils/logger.js';
4
4
  import Ajv, { ValidateFunction } from 'ajv';
5
+ import { GuardrailsService } from '../guardrails/service.js';
6
+ import { GuardrailContext } from '../guardrails/types.js';
7
+ import { GuardrailBlockError } from '../guardrails/errors.js';
5
8
 
6
9
  export interface FunctionDefinition {
7
10
  name: string;
@@ -26,13 +29,15 @@ export class FunctionBridge {
26
29
  private ajv: unknown;
27
30
  private toolSchemas: Map<string, Record<string, unknown>> = new Map();
28
31
  private approvalMode: 'always' | 'trusted' | 'never';
32
+ private guardrailsService?: GuardrailsService;
29
33
 
30
34
  constructor(
31
35
  mcpManager: MCPClientManager,
32
36
  approvalService: ApprovalService,
33
37
  trustedTools: string[] = [],
34
38
  approvalMode: 'always' | 'trusted' | 'never' = 'always',
35
- trustedToolsByServer: Record<string, string[]> = {}
39
+ trustedToolsByServer: Record<string, string[]> = {},
40
+ guardrailsService?: GuardrailsService
36
41
  ) {
37
42
  this.mcpManager = mcpManager;
38
43
  this.approvalService = approvalService;
@@ -40,7 +45,8 @@ export class FunctionBridge {
40
45
  // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
41
46
  this.ajv = new (Ajv as unknown as new (options: unknown) => unknown)({ allErrors: true, removeAdditional: 'all' });
42
47
  this.approvalMode = approvalMode;
43
-
48
+ this.guardrailsService = guardrailsService;
49
+
44
50
  // Initialize per-server trusted tools
45
51
  Object.entries(trustedToolsByServer).forEach(([serverName, tools]) => {
46
52
  this.trustedToolsByServer.set(serverName, new Set(tools));
@@ -236,16 +242,61 @@ export class FunctionBridge {
236
242
  }
237
243
  }
238
244
 
245
+ // Create guardrail context if service is enabled
246
+ let guardrailContext: GuardrailContext | undefined;
247
+ if (this.guardrailsService?.isEnabled()) {
248
+ guardrailContext = this.guardrailsService.createContext({
249
+ toolName: `${mcpServer}:${mcpTool}`,
250
+ toolArgs: cleanArgs,
251
+ });
252
+
253
+ // Execute pre_tool_input guardrails
254
+ const preResult = await this.guardrailsService.execute('pre_tool_input', guardrailContext);
255
+ if (preResult.action === 'block') {
256
+ throw new GuardrailBlockError(
257
+ preResult.blockedBy || 'unknown',
258
+ preResult.blockReason || 'Tool input blocked by guardrails'
259
+ );
260
+ }
261
+ // Use potentially modified args (e.g., PII redaction)
262
+ if (preResult.action === 'modify' && guardrailContext.toolArgs) {
263
+ Object.assign(cleanArgs, guardrailContext.toolArgs);
264
+ }
265
+ }
266
+
239
267
  // Execute the MCP tool
240
268
  logger.info(`Executing MCP tool ${mcpServer}:${mcpTool} for ${duckName}`);
241
269
  const result = await this.mcpManager.callTool(mcpServer, mcpTool, cleanArgs);
242
270
 
271
+ // Execute post_tool_output guardrails
272
+ if (guardrailContext && this.guardrailsService?.isEnabled()) {
273
+ guardrailContext.toolResult = result;
274
+ const postResult = await this.guardrailsService.execute('post_tool_output', guardrailContext);
275
+ if (postResult.action === 'block') {
276
+ throw new GuardrailBlockError(
277
+ postResult.blockedBy || 'unknown',
278
+ postResult.blockReason || 'Tool output blocked by guardrails'
279
+ );
280
+ }
281
+ // Return potentially modified result
282
+ if (postResult.action === 'modify') {
283
+ return {
284
+ success: true,
285
+ data: guardrailContext.toolResult,
286
+ };
287
+ }
288
+ }
289
+
243
290
  return {
244
291
  success: true,
245
292
  data: result,
246
293
  };
247
294
 
248
295
  } catch (error: unknown) {
296
+ // Re-throw GuardrailBlockError as-is
297
+ if (error instanceof GuardrailBlockError) {
298
+ throw error;
299
+ }
249
300
  const errorMessage = error instanceof Error ? error.message : String(error);
250
301
  logger.error(`Function call failed for ${functionName}:`, errorMessage);
251
302
  return {
@@ -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
+ });