mcp-rubber-duck 1.8.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 (118) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +158 -1
  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/providers/duck-provider-enhanced.d.ts +2 -1
  65. package/dist/providers/duck-provider-enhanced.d.ts.map +1 -1
  66. package/dist/providers/duck-provider-enhanced.js +55 -6
  67. package/dist/providers/duck-provider-enhanced.js.map +1 -1
  68. package/dist/providers/enhanced-manager.d.ts +2 -1
  69. package/dist/providers/enhanced-manager.d.ts.map +1 -1
  70. package/dist/providers/enhanced-manager.js +3 -3
  71. package/dist/providers/enhanced-manager.js.map +1 -1
  72. package/dist/providers/manager.d.ts +3 -1
  73. package/dist/providers/manager.d.ts.map +1 -1
  74. package/dist/providers/manager.js +4 -2
  75. package/dist/providers/manager.js.map +1 -1
  76. package/dist/providers/provider.d.ts +3 -1
  77. package/dist/providers/provider.d.ts.map +1 -1
  78. package/dist/providers/provider.js +43 -3
  79. package/dist/providers/provider.js.map +1 -1
  80. package/dist/server.d.ts +1 -0
  81. package/dist/server.d.ts.map +1 -1
  82. package/dist/server.js +28 -6
  83. package/dist/server.js.map +1 -1
  84. package/dist/services/function-bridge.d.ts +3 -1
  85. package/dist/services/function-bridge.d.ts.map +1 -1
  86. package/dist/services/function-bridge.js +40 -1
  87. package/dist/services/function-bridge.js.map +1 -1
  88. package/package.json +1 -1
  89. package/src/config/config.ts +187 -1
  90. package/src/config/types.ts +73 -0
  91. package/src/guardrails/context.ts +37 -0
  92. package/src/guardrails/errors.ts +46 -0
  93. package/src/guardrails/index.ts +20 -0
  94. package/src/guardrails/plugins/base-plugin.ts +103 -0
  95. package/src/guardrails/plugins/index.ts +5 -0
  96. package/src/guardrails/plugins/pattern-blocker.ts +190 -0
  97. package/src/guardrails/plugins/pii-redactor/detectors.ts +200 -0
  98. package/src/guardrails/plugins/pii-redactor/index.ts +203 -0
  99. package/src/guardrails/plugins/pii-redactor/pseudonymizer.ts +91 -0
  100. package/src/guardrails/plugins/rate-limiter.ts +142 -0
  101. package/src/guardrails/plugins/token-limiter.ts +155 -0
  102. package/src/guardrails/service.ts +209 -0
  103. package/src/guardrails/types.ts +120 -0
  104. package/src/providers/duck-provider-enhanced.ts +76 -7
  105. package/src/providers/enhanced-manager.ts +5 -3
  106. package/src/providers/manager.ts +6 -3
  107. package/src/providers/provider.ts +57 -6
  108. package/src/server.ts +32 -6
  109. package/src/services/function-bridge.ts +53 -2
  110. package/tests/guardrails/config.test.ts +267 -0
  111. package/tests/guardrails/errors.test.ts +109 -0
  112. package/tests/guardrails/plugins/pattern-blocker.test.ts +309 -0
  113. package/tests/guardrails/plugins/pii-redactor.test.ts +1004 -0
  114. package/tests/guardrails/plugins/rate-limiter.test.ts +310 -0
  115. package/tests/guardrails/plugins/token-limiter.test.ts +216 -0
  116. package/tests/guardrails/service.test.ts +911 -0
  117. package/tests/mcp-bridge.test.ts +248 -0
  118. package/tests/providers.test.ts +739 -0
@@ -4,6 +4,8 @@ import { FunctionBridge } from '../services/function-bridge.js';
4
4
  import { ConversationMessage } from '../config/types.js';
5
5
  import { logger } from '../utils/logger.js';
6
6
  import { SafeLogger } from '../utils/safe-logger.js';
7
+ import { GuardrailsService } from '../guardrails/service.js';
8
+ import { GuardrailBlockError } from '../guardrails/errors.js';
7
9
 
8
10
  export interface EnhancedChatResponse extends ChatResponse {
9
11
  pendingApprovals?: {
@@ -22,15 +24,43 @@ export class EnhancedDuckProvider extends DuckProvider {
22
24
  nickname: string,
23
25
  options: ProviderOptions,
24
26
  functionBridge: FunctionBridge,
25
- mcpEnabled: boolean = true
27
+ mcpEnabled: boolean = true,
28
+ guardrailsService?: GuardrailsService
26
29
  ) {
27
- super(name, nickname, options);
30
+ super(name, nickname, options, guardrailsService);
28
31
  this.functionBridge = functionBridge;
29
32
  this.mcpEnabled = mcpEnabled;
30
33
  }
31
34
 
32
35
  async chat(options: ChatOptions): Promise<EnhancedChatResponse> {
33
36
  try {
37
+ const modelToUse = options.model || this.options.model;
38
+
39
+ // Create guardrail context if service is enabled
40
+ const guardrailContext = this.guardrailsService?.isEnabled()
41
+ ? this.guardrailsService.createContext({
42
+ provider: this.name,
43
+ model: modelToUse,
44
+ messages: options.messages,
45
+ prompt: options.messages[options.messages.length - 1]?.content,
46
+ })
47
+ : undefined;
48
+
49
+ // Execute pre_request guardrails
50
+ if (guardrailContext && this.guardrailsService?.isEnabled()) {
51
+ const preResult = await this.guardrailsService.execute('pre_request', guardrailContext);
52
+ if (preResult.action === 'block') {
53
+ throw new GuardrailBlockError(
54
+ preResult.blockedBy || 'unknown',
55
+ preResult.blockReason || 'Request blocked by guardrails'
56
+ );
57
+ }
58
+ // Update messages if modified by guardrails (e.g., PII redaction)
59
+ if (preResult.action === 'modify' && guardrailContext.messages.length > 0) {
60
+ options = { ...options, messages: guardrailContext.messages };
61
+ }
62
+ }
63
+
34
64
  // If MCP is enabled, add function definitions
35
65
  if (this.mcpEnabled) {
36
66
  const functions = await this.functionBridge.getFunctionDefinitions();
@@ -43,7 +73,6 @@ export class EnhancedDuckProvider extends DuckProvider {
43
73
 
44
74
  // Prepare messages for function calling
45
75
  const messages = this.prepareMessages(options.messages, options.systemPrompt);
46
- const modelToUse = options.model || this.options.model;
47
76
 
48
77
  const baseParams: Partial<OpenAIChatParams> = {
49
78
  model: modelToUse,
@@ -75,17 +104,52 @@ export class EnhancedDuckProvider extends DuckProvider {
75
104
 
76
105
  // Check if the model wants to call functions
77
106
  if (choice.message?.tool_calls && choice.message.tool_calls.length > 0) {
78
- return await this.handleToolCalls(
107
+ const toolResult = await this.handleToolCalls(
79
108
  choice.message.tool_calls,
80
109
  messages as OpenAIMessage[],
81
110
  baseParams,
82
- modelToUse
111
+ modelToUse,
112
+ guardrailContext
83
113
  );
114
+
115
+ // Execute post_response guardrails on final result
116
+ if (guardrailContext && this.guardrailsService?.isEnabled()) {
117
+ guardrailContext.response = toolResult.content;
118
+ const postResult = await this.guardrailsService.execute('post_response', guardrailContext);
119
+ if (postResult.action === 'block') {
120
+ throw new GuardrailBlockError(
121
+ postResult.blockedBy || 'unknown',
122
+ postResult.blockReason || 'Response blocked by guardrails'
123
+ );
124
+ }
125
+ if (postResult.action === 'modify' && guardrailContext.response) {
126
+ toolResult.content = guardrailContext.response;
127
+ }
128
+ }
129
+
130
+ return toolResult;
131
+ }
132
+
133
+ let content = choice.message?.content || '';
134
+
135
+ // Execute post_response guardrails
136
+ if (guardrailContext && this.guardrailsService?.isEnabled()) {
137
+ guardrailContext.response = content;
138
+ const postResult = await this.guardrailsService.execute('post_response', guardrailContext);
139
+ if (postResult.action === 'block') {
140
+ throw new GuardrailBlockError(
141
+ postResult.blockedBy || 'unknown',
142
+ postResult.blockReason || 'Response blocked by guardrails'
143
+ );
144
+ }
145
+ if (postResult.action === 'modify' && guardrailContext.response) {
146
+ content = guardrailContext.response;
147
+ }
84
148
  }
85
149
 
86
150
  // No tool calls, return regular response
87
151
  return {
88
- content: choice.message?.content || '',
152
+ content,
89
153
  usage: response.usage ? {
90
154
  promptTokens: response.usage.prompt_tokens,
91
155
  completionTokens: response.usage.completion_tokens,
@@ -96,6 +160,10 @@ export class EnhancedDuckProvider extends DuckProvider {
96
160
  };
97
161
 
98
162
  } catch (error: unknown) {
163
+ // Re-throw GuardrailBlockError as-is
164
+ if (error instanceof GuardrailBlockError) {
165
+ throw error;
166
+ }
99
167
  logger.error(`Enhanced provider ${this.name} chat error:`, error);
100
168
  const errorMessage = error instanceof Error ? error.message : String(error);
101
169
  throw new Error(`Duck ${this.nickname} couldn't respond: ${errorMessage}`);
@@ -106,7 +174,8 @@ export class EnhancedDuckProvider extends DuckProvider {
106
174
  toolCalls: OpenAIToolCall[],
107
175
  messages: OpenAIMessage[],
108
176
  baseParams: Partial<OpenAIChatParams>,
109
- modelToUse: string
177
+ modelToUse: string,
178
+ _guardrailContext?: import('../guardrails/types.js').GuardrailContext
110
179
  ): Promise<EnhancedChatResponse> {
111
180
  const pendingApprovals: { id: string; message: string }[] = [];
112
181
  const toolMessages: OpenAIMessage[] = [];
@@ -3,6 +3,7 @@ import { ProviderManager } from './manager.js';
3
3
  import { ConfigManager } from '../config/config.js';
4
4
  import { FunctionBridge } from '../services/function-bridge.js';
5
5
  import { UsageService } from '../services/usage.js';
6
+ import { GuardrailsService } from '../guardrails/service.js';
6
7
  import { DuckResponse } from '../config/types.js';
7
8
  import { ChatOptions, MCPResult } from './types.js';
8
9
  import { logger } from '../utils/logger.js';
@@ -12,8 +13,8 @@ export class EnhancedProviderManager extends ProviderManager {
12
13
  private functionBridge?: FunctionBridge;
13
14
  private mcpEnabled: boolean = false;
14
15
 
15
- constructor(configManager: ConfigManager, functionBridge?: FunctionBridge, usageService?: UsageService) {
16
- super(configManager, usageService);
16
+ constructor(configManager: ConfigManager, functionBridge?: FunctionBridge, usageService?: UsageService, guardrailsService?: GuardrailsService) {
17
+ super(configManager, usageService, guardrailsService);
17
18
  this.functionBridge = functionBridge;
18
19
  this.mcpEnabled = !!functionBridge &&
19
20
  (configManager.getConfig().mcp_bridge?.enabled || false);
@@ -49,7 +50,8 @@ export class EnhancedProviderManager extends ProviderManager {
49
50
  systemPrompt: providerConfig.system_prompt,
50
51
  },
51
52
  this.functionBridge,
52
- this.mcpEnabled
53
+ this.mcpEnabled,
54
+ this.guardrailsService
53
55
  );
54
56
 
55
57
  this.enhancedProviders.set(name, enhancedProvider);
@@ -3,6 +3,7 @@ import { ConfigManager } from '../config/config.js';
3
3
  import { ProviderHealth, DuckResponse } from '../config/types.js';
4
4
  import { ChatOptions, ModelInfo } from './types.js';
5
5
  import { UsageService } from '../services/usage.js';
6
+ import { GuardrailsService } from '../guardrails/service.js';
6
7
  import { logger } from '../utils/logger.js';
7
8
  import { getRandomDuckMessage } from '../utils/ascii-art.js';
8
9
 
@@ -11,11 +12,13 @@ export class ProviderManager {
11
12
  private healthStatus: Map<string, ProviderHealth> = new Map();
12
13
  protected configManager: ConfigManager;
13
14
  protected usageService?: UsageService;
15
+ protected guardrailsService?: GuardrailsService;
14
16
  private defaultProvider?: string;
15
17
 
16
- constructor(configManager: ConfigManager, usageService?: UsageService) {
18
+ constructor(configManager: ConfigManager, usageService?: UsageService, guardrailsService?: GuardrailsService) {
17
19
  this.configManager = configManager;
18
20
  this.usageService = usageService;
21
+ this.guardrailsService = guardrailsService;
19
22
  this.initializeProviders();
20
23
  }
21
24
 
@@ -34,7 +37,7 @@ export class ProviderManager {
34
37
  timeout: providerConfig.timeout,
35
38
  maxRetries: providerConfig.max_retries,
36
39
  systemPrompt: providerConfig.system_prompt,
37
- });
40
+ }, this.guardrailsService);
38
41
 
39
42
  this.providers.set(name, provider);
40
43
  logger.info(`Initialized provider: ${name} (${providerConfig.nickname})`);
@@ -44,7 +47,7 @@ export class ProviderManager {
44
47
  }
45
48
 
46
49
  this.defaultProvider = config.default_provider;
47
-
50
+
48
51
  if (this.providers.size === 0) {
49
52
  throw new Error('No providers could be initialized');
50
53
  }
@@ -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
@@ -20,6 +20,7 @@ import { UsageService } from './services/usage.js';
20
20
  import { DuckResponse } from './config/types.js';
21
21
  import { ApprovalService } from './services/approval.js';
22
22
  import { FunctionBridge } from './services/function-bridge.js';
23
+ import { GuardrailsService } from './guardrails/service.js';
23
24
  import { logger } from './utils/logger.js';
24
25
  import { duckArt, getRandomDuckMessage } from './utils/ascii-art.js';
25
26
 
@@ -52,6 +53,7 @@ export class RubberDuckServer {
52
53
  private configManager: ConfigManager;
53
54
  private pricingService: PricingService;
54
55
  private usageService: UsageService;
56
+ private guardrailsService?: GuardrailsService;
55
57
  private providerManager: ProviderManager;
56
58
  private enhancedProviderManager?: EnhancedProviderManager;
57
59
  private conversationManager: ConversationManager;
@@ -86,8 +88,13 @@ export class RubberDuckServer {
86
88
  this.pricingService = new PricingService(config.pricing);
87
89
  this.usageService = new UsageService(this.pricingService);
88
90
 
89
- // Initialize provider manager with usage tracking
90
- 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);
91
98
  this.conversationManager = new ConversationManager();
92
99
  this.cache = new ResponseCache(config.cache_ttl);
93
100
  this.healthMonitor = new HealthMonitor(this.providerManager);
@@ -116,20 +123,22 @@ export class RubberDuckServer {
116
123
  // Initialize approval service
117
124
  this.approvalService = new ApprovalService(mcpConfig.approval_timeout);
118
125
 
119
- // Initialize function bridge
126
+ // Initialize function bridge with guardrails
120
127
  this.functionBridge = new FunctionBridge(
121
128
  this.mcpClientManager,
122
129
  this.approvalService,
123
130
  mcpConfig.trusted_tools,
124
131
  mcpConfig.approval_mode,
125
- mcpConfig.trusted_tools_by_server || {}
132
+ mcpConfig.trusted_tools_by_server || {},
133
+ this.guardrailsService
126
134
  );
127
135
 
128
- // Initialize enhanced provider manager with usage tracking
136
+ // Initialize enhanced provider manager with usage tracking and guardrails
129
137
  this.enhancedProviderManager = new EnhancedProviderManager(
130
138
  this.configManager,
131
139
  this.functionBridge,
132
- this.usageService
140
+ this.usageService,
141
+ this.guardrailsService
133
142
  );
134
143
 
135
144
  this.mcpEnabled = true;
@@ -824,6 +833,18 @@ export class RubberDuckServer {
824
833
  console.log('\n' + getRandomDuckMessage('startup'));
825
834
  }
826
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
+
827
848
  // Initialize MCP connections if enabled
828
849
  if (this.mcpEnabled && this.mcpClientManager) {
829
850
  try {
@@ -853,6 +874,11 @@ export class RubberDuckServer {
853
874
  // Cleanup usage service (flush pending writes)
854
875
  this.usageService.shutdown();
855
876
 
877
+ // Cleanup guardrails service
878
+ if (this.guardrailsService) {
879
+ await this.guardrailsService.shutdown();
880
+ }
881
+
856
882
  // Cleanup MCP resources
857
883
  if (this.approvalService) {
858
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 {