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
|
@@ -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
|
|
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
|
|
84
|
-
|
|
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
|
+
});
|