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.
- package/CHANGELOG.md +8 -0
- package/README.md +158 -1
- 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/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 +28 -6
- 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/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 +32 -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/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
|
-
|
|
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
|
|
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);
|
package/src/providers/manager.ts
CHANGED
|
@@ -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
|
|
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
|
|
90
|
-
|
|
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 {
|