mcp-rubber-duck 1.7.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +274 -2
  3. package/audit-ci.json +2 -1
  4. package/dist/config/config.d.ts +2 -0
  5. package/dist/config/config.d.ts.map +1 -1
  6. package/dist/config/config.js +144 -1
  7. package/dist/config/config.js.map +1 -1
  8. package/dist/config/types.d.ts +1084 -2
  9. package/dist/config/types.d.ts.map +1 -1
  10. package/dist/config/types.js +59 -0
  11. package/dist/config/types.js.map +1 -1
  12. package/dist/guardrails/context.d.ts +10 -0
  13. package/dist/guardrails/context.d.ts.map +1 -0
  14. package/dist/guardrails/context.js +35 -0
  15. package/dist/guardrails/context.js.map +1 -0
  16. package/dist/guardrails/errors.d.ts +26 -0
  17. package/dist/guardrails/errors.d.ts.map +1 -0
  18. package/dist/guardrails/errors.js +42 -0
  19. package/dist/guardrails/errors.js.map +1 -0
  20. package/dist/guardrails/index.d.ts +6 -0
  21. package/dist/guardrails/index.d.ts.map +1 -0
  22. package/dist/guardrails/index.js +11 -0
  23. package/dist/guardrails/index.js.map +1 -0
  24. package/dist/guardrails/plugins/base-plugin.d.ts +35 -0
  25. package/dist/guardrails/plugins/base-plugin.d.ts.map +1 -0
  26. package/dist/guardrails/plugins/base-plugin.js +70 -0
  27. package/dist/guardrails/plugins/base-plugin.js.map +1 -0
  28. package/dist/guardrails/plugins/index.d.ts +6 -0
  29. package/dist/guardrails/plugins/index.d.ts.map +1 -0
  30. package/dist/guardrails/plugins/index.js +6 -0
  31. package/dist/guardrails/plugins/index.js.map +1 -0
  32. package/dist/guardrails/plugins/pattern-blocker.d.ts +27 -0
  33. package/dist/guardrails/plugins/pattern-blocker.d.ts.map +1 -0
  34. package/dist/guardrails/plugins/pattern-blocker.js +140 -0
  35. package/dist/guardrails/plugins/pattern-blocker.js.map +1 -0
  36. package/dist/guardrails/plugins/pii-redactor/detectors.d.ts +40 -0
  37. package/dist/guardrails/plugins/pii-redactor/detectors.d.ts.map +1 -0
  38. package/dist/guardrails/plugins/pii-redactor/detectors.js +134 -0
  39. package/dist/guardrails/plugins/pii-redactor/detectors.js.map +1 -0
  40. package/dist/guardrails/plugins/pii-redactor/index.d.ts +28 -0
  41. package/dist/guardrails/plugins/pii-redactor/index.d.ts.map +1 -0
  42. package/dist/guardrails/plugins/pii-redactor/index.js +157 -0
  43. package/dist/guardrails/plugins/pii-redactor/index.js.map +1 -0
  44. package/dist/guardrails/plugins/pii-redactor/pseudonymizer.d.ts +33 -0
  45. package/dist/guardrails/plugins/pii-redactor/pseudonymizer.d.ts.map +1 -0
  46. package/dist/guardrails/plugins/pii-redactor/pseudonymizer.js +70 -0
  47. package/dist/guardrails/plugins/pii-redactor/pseudonymizer.js.map +1 -0
  48. package/dist/guardrails/plugins/rate-limiter.d.ts +28 -0
  49. package/dist/guardrails/plugins/rate-limiter.d.ts.map +1 -0
  50. package/dist/guardrails/plugins/rate-limiter.js +91 -0
  51. package/dist/guardrails/plugins/rate-limiter.js.map +1 -0
  52. package/dist/guardrails/plugins/token-limiter.d.ts +30 -0
  53. package/dist/guardrails/plugins/token-limiter.d.ts.map +1 -0
  54. package/dist/guardrails/plugins/token-limiter.js +98 -0
  55. package/dist/guardrails/plugins/token-limiter.js.map +1 -0
  56. package/dist/guardrails/service.d.ts +38 -0
  57. package/dist/guardrails/service.d.ts.map +1 -0
  58. package/dist/guardrails/service.js +183 -0
  59. package/dist/guardrails/service.js.map +1 -0
  60. package/dist/guardrails/types.d.ts +96 -0
  61. package/dist/guardrails/types.d.ts.map +1 -0
  62. package/dist/guardrails/types.js +2 -0
  63. package/dist/guardrails/types.js.map +1 -0
  64. package/dist/prompts/architecture.d.ts +6 -0
  65. package/dist/prompts/architecture.d.ts.map +1 -0
  66. package/dist/prompts/architecture.js +103 -0
  67. package/dist/prompts/architecture.js.map +1 -0
  68. package/dist/prompts/assumptions.d.ts +6 -0
  69. package/dist/prompts/assumptions.d.ts.map +1 -0
  70. package/dist/prompts/assumptions.js +72 -0
  71. package/dist/prompts/assumptions.js.map +1 -0
  72. package/dist/prompts/blindspots.d.ts +6 -0
  73. package/dist/prompts/blindspots.d.ts.map +1 -0
  74. package/dist/prompts/blindspots.js +71 -0
  75. package/dist/prompts/blindspots.js.map +1 -0
  76. package/dist/prompts/diverge-converge.d.ts +6 -0
  77. package/dist/prompts/diverge-converge.d.ts.map +1 -0
  78. package/dist/prompts/diverge-converge.js +85 -0
  79. package/dist/prompts/diverge-converge.js.map +1 -0
  80. package/dist/prompts/index.d.ts +22 -0
  81. package/dist/prompts/index.d.ts.map +1 -0
  82. package/dist/prompts/index.js +57 -0
  83. package/dist/prompts/index.js.map +1 -0
  84. package/dist/prompts/perspectives.d.ts +7 -0
  85. package/dist/prompts/perspectives.d.ts.map +1 -0
  86. package/dist/prompts/perspectives.js +65 -0
  87. package/dist/prompts/perspectives.js.map +1 -0
  88. package/dist/prompts/red-team.d.ts +6 -0
  89. package/dist/prompts/red-team.d.ts.map +1 -0
  90. package/dist/prompts/red-team.js +83 -0
  91. package/dist/prompts/red-team.js.map +1 -0
  92. package/dist/prompts/reframe.d.ts +6 -0
  93. package/dist/prompts/reframe.d.ts.map +1 -0
  94. package/dist/prompts/reframe.js +71 -0
  95. package/dist/prompts/reframe.js.map +1 -0
  96. package/dist/prompts/tradeoffs.d.ts +6 -0
  97. package/dist/prompts/tradeoffs.d.ts.map +1 -0
  98. package/dist/prompts/tradeoffs.js +87 -0
  99. package/dist/prompts/tradeoffs.js.map +1 -0
  100. package/dist/prompts/types.d.ts +14 -0
  101. package/dist/prompts/types.d.ts.map +1 -0
  102. package/dist/prompts/types.js +2 -0
  103. package/dist/prompts/types.js.map +1 -0
  104. package/dist/providers/duck-provider-enhanced.d.ts +2 -1
  105. package/dist/providers/duck-provider-enhanced.d.ts.map +1 -1
  106. package/dist/providers/duck-provider-enhanced.js +55 -6
  107. package/dist/providers/duck-provider-enhanced.js.map +1 -1
  108. package/dist/providers/enhanced-manager.d.ts +2 -1
  109. package/dist/providers/enhanced-manager.d.ts.map +1 -1
  110. package/dist/providers/enhanced-manager.js +3 -3
  111. package/dist/providers/enhanced-manager.js.map +1 -1
  112. package/dist/providers/manager.d.ts +3 -1
  113. package/dist/providers/manager.d.ts.map +1 -1
  114. package/dist/providers/manager.js +4 -2
  115. package/dist/providers/manager.js.map +1 -1
  116. package/dist/providers/provider.d.ts +3 -1
  117. package/dist/providers/provider.d.ts.map +1 -1
  118. package/dist/providers/provider.js +43 -3
  119. package/dist/providers/provider.js.map +1 -1
  120. package/dist/server.d.ts +1 -0
  121. package/dist/server.d.ts.map +1 -1
  122. package/dist/server.js +48 -7
  123. package/dist/server.js.map +1 -1
  124. package/dist/services/function-bridge.d.ts +3 -1
  125. package/dist/services/function-bridge.d.ts.map +1 -1
  126. package/dist/services/function-bridge.js +40 -1
  127. package/dist/services/function-bridge.js.map +1 -1
  128. package/package.json +1 -1
  129. package/src/config/config.ts +187 -1
  130. package/src/config/types.ts +73 -0
  131. package/src/guardrails/context.ts +37 -0
  132. package/src/guardrails/errors.ts +46 -0
  133. package/src/guardrails/index.ts +20 -0
  134. package/src/guardrails/plugins/base-plugin.ts +103 -0
  135. package/src/guardrails/plugins/index.ts +5 -0
  136. package/src/guardrails/plugins/pattern-blocker.ts +190 -0
  137. package/src/guardrails/plugins/pii-redactor/detectors.ts +200 -0
  138. package/src/guardrails/plugins/pii-redactor/index.ts +203 -0
  139. package/src/guardrails/plugins/pii-redactor/pseudonymizer.ts +91 -0
  140. package/src/guardrails/plugins/rate-limiter.ts +142 -0
  141. package/src/guardrails/plugins/token-limiter.ts +155 -0
  142. package/src/guardrails/service.ts +209 -0
  143. package/src/guardrails/types.ts +120 -0
  144. package/src/prompts/architecture.ts +111 -0
  145. package/src/prompts/assumptions.ts +80 -0
  146. package/src/prompts/blindspots.ts +79 -0
  147. package/src/prompts/diverge-converge.ts +92 -0
  148. package/src/prompts/index.ts +63 -0
  149. package/src/prompts/perspectives.ts +73 -0
  150. package/src/prompts/red-team.ts +91 -0
  151. package/src/prompts/reframe.ts +78 -0
  152. package/src/prompts/tradeoffs.ts +95 -0
  153. package/src/prompts/types.ts +14 -0
  154. package/src/providers/duck-provider-enhanced.ts +76 -7
  155. package/src/providers/enhanced-manager.ts +5 -3
  156. package/src/providers/manager.ts +6 -3
  157. package/src/providers/provider.ts +57 -6
  158. package/src/server.ts +55 -6
  159. package/src/services/function-bridge.ts +53 -2
  160. package/tests/guardrails/config.test.ts +267 -0
  161. package/tests/guardrails/errors.test.ts +109 -0
  162. package/tests/guardrails/plugins/pattern-blocker.test.ts +309 -0
  163. package/tests/guardrails/plugins/pii-redactor.test.ts +1004 -0
  164. package/tests/guardrails/plugins/rate-limiter.test.ts +310 -0
  165. package/tests/guardrails/plugins/token-limiter.test.ts +216 -0
  166. package/tests/guardrails/service.test.ts +911 -0
  167. package/tests/mcp-bridge.test.ts +248 -0
  168. package/tests/prompts.test.ts +314 -0
  169. package/tests/providers.test.ts +739 -0
@@ -857,4 +857,743 @@ describe('DuckProvider Error Handling', () => {
857
857
  expect(callArgs.messages[0].role).toBe('system');
858
858
  expect(callArgs.messages[0].content).toBe('You are a helpful assistant');
859
859
  });
860
+ });
861
+
862
+ describe('DuckProvider with Guardrails', () => {
863
+ let mockGuardrailsService: {
864
+ isEnabled: jest.Mock;
865
+ createContext: jest.Mock;
866
+ execute: jest.Mock;
867
+ };
868
+
869
+ beforeEach(() => {
870
+ jest.clearAllMocks();
871
+
872
+ // Setup mock OpenAI response
873
+ mockCreate.mockResolvedValue({
874
+ choices: [{
875
+ message: { content: 'Mocked response' },
876
+ finish_reason: 'stop',
877
+ }],
878
+ usage: {
879
+ prompt_tokens: 10,
880
+ completion_tokens: 20,
881
+ total_tokens: 30,
882
+ },
883
+ model: 'test-model',
884
+ });
885
+
886
+ // Create mock guardrails service
887
+ mockGuardrailsService = {
888
+ isEnabled: jest.fn().mockReturnValue(true),
889
+ createContext: jest.fn().mockImplementation((params) => ({
890
+ requestId: 'test-request-id',
891
+ provider: params.provider,
892
+ model: params.model,
893
+ messages: params.messages || [],
894
+ prompt: params.prompt,
895
+ violations: [],
896
+ modifications: [],
897
+ metadata: new Map(),
898
+ })),
899
+ execute: jest.fn().mockResolvedValue({ action: 'allow', context: {} }),
900
+ };
901
+ });
902
+
903
+ it('should execute pre_request guardrails before chat', async () => {
904
+ const provider = new DuckProvider(
905
+ 'test',
906
+ 'Test Duck',
907
+ { apiKey: 'test-key', baseURL: 'https://api.test.com/v1', model: 'test-model' },
908
+ mockGuardrailsService as unknown as import('../src/guardrails/service').GuardrailsService
909
+ );
910
+ provider['client'].chat.completions.create = mockCreate;
911
+
912
+ await provider.chat({
913
+ messages: [{ role: 'user', content: 'Hello', timestamp: new Date() }],
914
+ });
915
+
916
+ expect(mockGuardrailsService.isEnabled).toHaveBeenCalled();
917
+ expect(mockGuardrailsService.createContext).toHaveBeenCalledWith(
918
+ expect.objectContaining({
919
+ provider: 'test',
920
+ model: 'test-model',
921
+ prompt: 'Hello',
922
+ })
923
+ );
924
+ expect(mockGuardrailsService.execute).toHaveBeenCalledWith('pre_request', expect.any(Object));
925
+ });
926
+
927
+ it('should execute post_response guardrails after chat', async () => {
928
+ const provider = new DuckProvider(
929
+ 'test',
930
+ 'Test Duck',
931
+ { apiKey: 'test-key', baseURL: 'https://api.test.com/v1', model: 'test-model' },
932
+ mockGuardrailsService as unknown as import('../src/guardrails/service').GuardrailsService
933
+ );
934
+ provider['client'].chat.completions.create = mockCreate;
935
+
936
+ await provider.chat({
937
+ messages: [{ role: 'user', content: 'Hello', timestamp: new Date() }],
938
+ });
939
+
940
+ // Should be called twice: pre_request and post_response
941
+ expect(mockGuardrailsService.execute).toHaveBeenCalledTimes(2);
942
+ expect(mockGuardrailsService.execute).toHaveBeenNthCalledWith(1, 'pre_request', expect.any(Object));
943
+ expect(mockGuardrailsService.execute).toHaveBeenNthCalledWith(2, 'post_response', expect.any(Object));
944
+ });
945
+
946
+ it('should block request when pre_request guardrails return block', async () => {
947
+ mockGuardrailsService.execute.mockResolvedValueOnce({
948
+ action: 'block',
949
+ blockedBy: 'rate_limiter',
950
+ blockReason: 'Too many requests',
951
+ context: {},
952
+ });
953
+
954
+ const provider = new DuckProvider(
955
+ 'test',
956
+ 'Test Duck',
957
+ { apiKey: 'test-key', baseURL: 'https://api.test.com/v1', model: 'test-model' },
958
+ mockGuardrailsService as unknown as import('../src/guardrails/service').GuardrailsService
959
+ );
960
+ provider['client'].chat.completions.create = mockCreate;
961
+
962
+ await expect(
963
+ provider.chat({
964
+ messages: [{ role: 'user', content: 'Hello', timestamp: new Date() }],
965
+ })
966
+ ).rejects.toThrow("Request blocked by guardrail 'rate_limiter': Too many requests");
967
+
968
+ // Should NOT call the LLM when blocked
969
+ expect(mockCreate).not.toHaveBeenCalled();
970
+ });
971
+
972
+ it('should block response when post_response guardrails return block', async () => {
973
+ // Pre-request allows, post-response blocks
974
+ mockGuardrailsService.execute
975
+ .mockResolvedValueOnce({ action: 'allow', context: {} })
976
+ .mockResolvedValueOnce({
977
+ action: 'block',
978
+ blockedBy: 'content_filter',
979
+ blockReason: 'Inappropriate content detected',
980
+ context: {},
981
+ });
982
+
983
+ const provider = new DuckProvider(
984
+ 'test',
985
+ 'Test Duck',
986
+ { apiKey: 'test-key', baseURL: 'https://api.test.com/v1', model: 'test-model' },
987
+ mockGuardrailsService as unknown as import('../src/guardrails/service').GuardrailsService
988
+ );
989
+ provider['client'].chat.completions.create = mockCreate;
990
+
991
+ await expect(
992
+ provider.chat({
993
+ messages: [{ role: 'user', content: 'Hello', timestamp: new Date() }],
994
+ })
995
+ ).rejects.toThrow("Request blocked by guardrail 'content_filter': Inappropriate content detected");
996
+
997
+ // LLM was called but response was blocked
998
+ expect(mockCreate).toHaveBeenCalled();
999
+ });
1000
+
1001
+ it('should modify messages when pre_request guardrails return modify', async () => {
1002
+ const modifiedMessages = [
1003
+ { role: 'user' as const, content: 'Hello [EMAIL_1]', timestamp: new Date() },
1004
+ ];
1005
+
1006
+ mockGuardrailsService.execute.mockImplementation((phase) => {
1007
+ if (phase === 'pre_request') {
1008
+ return Promise.resolve({
1009
+ action: 'modify',
1010
+ context: { messages: modifiedMessages },
1011
+ });
1012
+ }
1013
+ return Promise.resolve({ action: 'allow', context: {} });
1014
+ });
1015
+
1016
+ mockGuardrailsService.createContext.mockReturnValue({
1017
+ requestId: 'test-id',
1018
+ provider: 'test',
1019
+ model: 'test-model',
1020
+ messages: modifiedMessages,
1021
+ prompt: 'Hello [EMAIL_1]',
1022
+ violations: [],
1023
+ modifications: [],
1024
+ metadata: new Map(),
1025
+ });
1026
+
1027
+ const provider = new DuckProvider(
1028
+ 'test',
1029
+ 'Test Duck',
1030
+ { apiKey: 'test-key', baseURL: 'https://api.test.com/v1', model: 'test-model' },
1031
+ mockGuardrailsService as unknown as import('../src/guardrails/service').GuardrailsService
1032
+ );
1033
+ provider['client'].chat.completions.create = mockCreate;
1034
+
1035
+ await provider.chat({
1036
+ messages: [{ role: 'user', content: 'Hello test@example.com', timestamp: new Date() }],
1037
+ });
1038
+
1039
+ // The LLM should receive the modified (redacted) message
1040
+ const callArgs = mockCreate.mock.calls[0][0];
1041
+ expect(callArgs.messages[0].content).toBe('Hello [EMAIL_1]');
1042
+ });
1043
+
1044
+ it('should modify response when post_response guardrails return modify', async () => {
1045
+ // Create a shared context object that gets modified by execute
1046
+ const sharedContext = {
1047
+ requestId: 'test-id',
1048
+ provider: 'test',
1049
+ model: 'test-model',
1050
+ messages: [] as Array<{ role: string; content: string; timestamp: Date }>,
1051
+ prompt: 'Hello',
1052
+ response: '',
1053
+ violations: [],
1054
+ modifications: [],
1055
+ metadata: new Map(),
1056
+ };
1057
+
1058
+ mockGuardrailsService.createContext.mockReturnValue(sharedContext);
1059
+
1060
+ mockGuardrailsService.execute.mockImplementation((phase) => {
1061
+ if (phase === 'post_response') {
1062
+ // Modify the response in the shared context
1063
+ sharedContext.response = 'Modified response with restored PII';
1064
+ return Promise.resolve({
1065
+ action: 'modify',
1066
+ context: sharedContext,
1067
+ });
1068
+ }
1069
+ return Promise.resolve({ action: 'allow', context: sharedContext });
1070
+ });
1071
+
1072
+ const provider = new DuckProvider(
1073
+ 'test',
1074
+ 'Test Duck',
1075
+ { apiKey: 'test-key', baseURL: 'https://api.test.com/v1', model: 'test-model' },
1076
+ mockGuardrailsService as unknown as import('../src/guardrails/service').GuardrailsService
1077
+ );
1078
+ provider['client'].chat.completions.create = mockCreate;
1079
+
1080
+ const response = await provider.chat({
1081
+ messages: [{ role: 'user', content: 'Hello', timestamp: new Date() }],
1082
+ });
1083
+
1084
+ expect(response.content).toBe('Modified response with restored PII');
1085
+ });
1086
+
1087
+ it('should skip guardrails when service is disabled', async () => {
1088
+ mockGuardrailsService.isEnabled.mockReturnValue(false);
1089
+
1090
+ const provider = new DuckProvider(
1091
+ 'test',
1092
+ 'Test Duck',
1093
+ { apiKey: 'test-key', baseURL: 'https://api.test.com/v1', model: 'test-model' },
1094
+ mockGuardrailsService as unknown as import('../src/guardrails/service').GuardrailsService
1095
+ );
1096
+ provider['client'].chat.completions.create = mockCreate;
1097
+
1098
+ const response = await provider.chat({
1099
+ messages: [{ role: 'user', content: 'Hello', timestamp: new Date() }],
1100
+ });
1101
+
1102
+ expect(response.content).toBe('Mocked response');
1103
+ expect(mockGuardrailsService.createContext).not.toHaveBeenCalled();
1104
+ expect(mockGuardrailsService.execute).not.toHaveBeenCalled();
1105
+ });
1106
+
1107
+ it('should work without guardrails service (undefined)', async () => {
1108
+ const provider = new DuckProvider(
1109
+ 'test',
1110
+ 'Test Duck',
1111
+ { apiKey: 'test-key', baseURL: 'https://api.test.com/v1', model: 'test-model' }
1112
+ // No guardrails service passed
1113
+ );
1114
+ provider['client'].chat.completions.create = mockCreate;
1115
+
1116
+ const response = await provider.chat({
1117
+ messages: [{ role: 'user', content: 'Hello', timestamp: new Date() }],
1118
+ });
1119
+
1120
+ expect(response.content).toBe('Mocked response');
1121
+ });
1122
+
1123
+ it('should re-throw GuardrailBlockError without wrapping', async () => {
1124
+ mockGuardrailsService.execute.mockRejectedValueOnce(
1125
+ new (await import('../src/guardrails/errors')).GuardrailBlockError('test_plugin', 'Test block reason')
1126
+ );
1127
+
1128
+ const provider = new DuckProvider(
1129
+ 'test',
1130
+ 'Test Duck',
1131
+ { apiKey: 'test-key', baseURL: 'https://api.test.com/v1', model: 'test-model' },
1132
+ mockGuardrailsService as unknown as import('../src/guardrails/service').GuardrailsService
1133
+ );
1134
+ provider['client'].chat.completions.create = mockCreate;
1135
+
1136
+ await expect(
1137
+ provider.chat({
1138
+ messages: [{ role: 'user', content: 'Hello', timestamp: new Date() }],
1139
+ })
1140
+ ).rejects.toThrow("Request blocked by guardrail 'test_plugin': Test block reason");
1141
+ });
1142
+ });
1143
+
1144
+ describe('EnhancedDuckProvider with Guardrails', () => {
1145
+ let mockGuardrailsService: {
1146
+ isEnabled: jest.Mock;
1147
+ createContext: jest.Mock;
1148
+ execute: jest.Mock;
1149
+ };
1150
+
1151
+ let mockFunctionBridge: {
1152
+ getFunctionDefinitions: jest.Mock;
1153
+ handleFunctionCall: jest.Mock;
1154
+ getStats: jest.Mock;
1155
+ };
1156
+
1157
+ beforeEach(() => {
1158
+ jest.clearAllMocks();
1159
+
1160
+ mockGuardrailsService = {
1161
+ isEnabled: jest.fn().mockReturnValue(true),
1162
+ createContext: jest.fn().mockImplementation((params) => ({
1163
+ requestId: 'test-request-id',
1164
+ provider: params.provider,
1165
+ model: params.model,
1166
+ messages: params.messages || [],
1167
+ prompt: params.prompt,
1168
+ response: '',
1169
+ violations: [],
1170
+ modifications: [],
1171
+ metadata: new Map(),
1172
+ })),
1173
+ execute: jest.fn().mockResolvedValue({ action: 'allow', context: {} }),
1174
+ };
1175
+
1176
+ mockFunctionBridge = {
1177
+ getFunctionDefinitions: jest.fn().mockResolvedValue([]),
1178
+ handleFunctionCall: jest.fn(),
1179
+ getStats: jest.fn().mockReturnValue({ totalFunctions: 0, serverCount: 0, trustedToolCount: 0, connectedServers: [] }),
1180
+ };
1181
+
1182
+ // Setup mock response for regular chat
1183
+ mockCreate.mockResolvedValue({
1184
+ choices: [{
1185
+ message: { content: 'Mocked response' },
1186
+ finish_reason: 'stop',
1187
+ }],
1188
+ usage: {
1189
+ prompt_tokens: 10,
1190
+ completion_tokens: 20,
1191
+ total_tokens: 30,
1192
+ },
1193
+ model: 'test-model',
1194
+ });
1195
+ });
1196
+
1197
+ it('should execute pre_request guardrails before making request', async () => {
1198
+ const { EnhancedDuckProvider } = await import('../src/providers/duck-provider-enhanced');
1199
+
1200
+ const provider = new EnhancedDuckProvider(
1201
+ 'test',
1202
+ 'Test Duck',
1203
+ { apiKey: 'test-key', baseURL: 'https://api.test.com/v1', model: 'test-model' },
1204
+ mockFunctionBridge as unknown as import('../src/services/function-bridge').FunctionBridge,
1205
+ false, // mcpEnabled
1206
+ mockGuardrailsService as unknown as import('../src/guardrails/service').GuardrailsService
1207
+ );
1208
+ provider['client'].chat.completions.create = mockCreate;
1209
+
1210
+ await provider.chat({
1211
+ messages: [{ role: 'user', content: 'Hello', timestamp: new Date() }],
1212
+ });
1213
+
1214
+ expect(mockGuardrailsService.isEnabled).toHaveBeenCalled();
1215
+ expect(mockGuardrailsService.createContext).toHaveBeenCalledWith(
1216
+ expect.objectContaining({
1217
+ provider: 'test',
1218
+ model: 'test-model',
1219
+ })
1220
+ );
1221
+ expect(mockGuardrailsService.execute).toHaveBeenCalledWith('pre_request', expect.any(Object));
1222
+ });
1223
+
1224
+ it('should execute post_response guardrails after receiving response', async () => {
1225
+ const { EnhancedDuckProvider } = await import('../src/providers/duck-provider-enhanced');
1226
+
1227
+ const provider = new EnhancedDuckProvider(
1228
+ 'test',
1229
+ 'Test Duck',
1230
+ { apiKey: 'test-key', baseURL: 'https://api.test.com/v1', model: 'test-model' },
1231
+ mockFunctionBridge as unknown as import('../src/services/function-bridge').FunctionBridge,
1232
+ false,
1233
+ mockGuardrailsService as unknown as import('../src/guardrails/service').GuardrailsService
1234
+ );
1235
+ provider['client'].chat.completions.create = mockCreate;
1236
+
1237
+ await provider.chat({
1238
+ messages: [{ role: 'user', content: 'Hello', timestamp: new Date() }],
1239
+ });
1240
+
1241
+ // Should be called twice: pre_request and post_response
1242
+ expect(mockGuardrailsService.execute).toHaveBeenCalledTimes(2);
1243
+ expect(mockGuardrailsService.execute).toHaveBeenNthCalledWith(1, 'pre_request', expect.any(Object));
1244
+ expect(mockGuardrailsService.execute).toHaveBeenNthCalledWith(2, 'post_response', expect.any(Object));
1245
+ });
1246
+
1247
+ it('should block request when pre_request guardrails return block', async () => {
1248
+ const { EnhancedDuckProvider } = await import('../src/providers/duck-provider-enhanced');
1249
+
1250
+ mockGuardrailsService.execute.mockResolvedValueOnce({
1251
+ action: 'block',
1252
+ blockedBy: 'pattern_blocker',
1253
+ blockReason: 'Blocked pattern detected',
1254
+ context: {},
1255
+ });
1256
+
1257
+ const provider = new EnhancedDuckProvider(
1258
+ 'test',
1259
+ 'Test Duck',
1260
+ { apiKey: 'test-key', baseURL: 'https://api.test.com/v1', model: 'test-model' },
1261
+ mockFunctionBridge as unknown as import('../src/services/function-bridge').FunctionBridge,
1262
+ false,
1263
+ mockGuardrailsService as unknown as import('../src/guardrails/service').GuardrailsService
1264
+ );
1265
+ provider['client'].chat.completions.create = mockCreate;
1266
+
1267
+ await expect(
1268
+ provider.chat({
1269
+ messages: [{ role: 'user', content: 'Hello', timestamp: new Date() }],
1270
+ })
1271
+ ).rejects.toThrow("Request blocked by guardrail 'pattern_blocker': Blocked pattern detected");
1272
+
1273
+ // API should NOT have been called
1274
+ expect(mockCreate).not.toHaveBeenCalled();
1275
+ });
1276
+
1277
+ it('should block response when post_response guardrails return block', async () => {
1278
+ const { EnhancedDuckProvider } = await import('../src/providers/duck-provider-enhanced');
1279
+
1280
+ // Pre-request allows, post-response blocks
1281
+ mockGuardrailsService.execute
1282
+ .mockResolvedValueOnce({ action: 'allow', context: {} })
1283
+ .mockResolvedValueOnce({
1284
+ action: 'block',
1285
+ blockedBy: 'pii_redactor',
1286
+ blockReason: 'PII detected in response',
1287
+ context: {},
1288
+ });
1289
+
1290
+ const provider = new EnhancedDuckProvider(
1291
+ 'test',
1292
+ 'Test Duck',
1293
+ { apiKey: 'test-key', baseURL: 'https://api.test.com/v1', model: 'test-model' },
1294
+ mockFunctionBridge as unknown as import('../src/services/function-bridge').FunctionBridge,
1295
+ false,
1296
+ mockGuardrailsService as unknown as import('../src/guardrails/service').GuardrailsService
1297
+ );
1298
+ provider['client'].chat.completions.create = mockCreate;
1299
+
1300
+ await expect(
1301
+ provider.chat({
1302
+ messages: [{ role: 'user', content: 'Hello', timestamp: new Date() }],
1303
+ })
1304
+ ).rejects.toThrow("Request blocked by guardrail 'pii_redactor': PII detected in response");
1305
+ });
1306
+
1307
+ it('should modify messages when pre_request guardrails return modify', async () => {
1308
+ const { EnhancedDuckProvider } = await import('../src/providers/duck-provider-enhanced');
1309
+
1310
+ const modifiedMessages = [
1311
+ { role: 'user' as const, content: '[REDACTED]', timestamp: new Date() }
1312
+ ];
1313
+
1314
+ const sharedContext = {
1315
+ requestId: 'test-id',
1316
+ provider: 'test',
1317
+ model: 'test-model',
1318
+ messages: modifiedMessages,
1319
+ prompt: 'Hello',
1320
+ response: '',
1321
+ violations: [],
1322
+ modifications: [],
1323
+ metadata: new Map(),
1324
+ };
1325
+
1326
+ mockGuardrailsService.createContext.mockReturnValue(sharedContext);
1327
+ mockGuardrailsService.execute.mockImplementation((phase) => {
1328
+ if (phase === 'pre_request') {
1329
+ return Promise.resolve({ action: 'modify', context: sharedContext });
1330
+ }
1331
+ return Promise.resolve({ action: 'allow', context: sharedContext });
1332
+ });
1333
+
1334
+ const provider = new EnhancedDuckProvider(
1335
+ 'test',
1336
+ 'Test Duck',
1337
+ { apiKey: 'test-key', baseURL: 'https://api.test.com/v1', model: 'test-model' },
1338
+ mockFunctionBridge as unknown as import('../src/services/function-bridge').FunctionBridge,
1339
+ false,
1340
+ mockGuardrailsService as unknown as import('../src/guardrails/service').GuardrailsService
1341
+ );
1342
+ provider['client'].chat.completions.create = mockCreate;
1343
+
1344
+ await provider.chat({
1345
+ messages: [{ role: 'user', content: 'sensitive data', timestamp: new Date() }],
1346
+ });
1347
+
1348
+ // The API should receive the modified messages
1349
+ const callArgs = mockCreate.mock.calls[0][0] as { messages: Array<{ content: string }> };
1350
+ expect(callArgs.messages[0].content).toBe('[REDACTED]');
1351
+ });
1352
+
1353
+ it('should modify response when post_response guardrails return modify', async () => {
1354
+ const { EnhancedDuckProvider } = await import('../src/providers/duck-provider-enhanced');
1355
+
1356
+ const sharedContext = {
1357
+ requestId: 'test-id',
1358
+ provider: 'test',
1359
+ model: 'test-model',
1360
+ messages: [],
1361
+ prompt: 'Hello',
1362
+ response: '',
1363
+ violations: [],
1364
+ modifications: [],
1365
+ metadata: new Map(),
1366
+ };
1367
+
1368
+ mockGuardrailsService.createContext.mockReturnValue(sharedContext);
1369
+ mockGuardrailsService.execute.mockImplementation((phase) => {
1370
+ if (phase === 'post_response') {
1371
+ sharedContext.response = '[REDACTED RESPONSE]';
1372
+ return Promise.resolve({ action: 'modify', context: sharedContext });
1373
+ }
1374
+ return Promise.resolve({ action: 'allow', context: sharedContext });
1375
+ });
1376
+
1377
+ const provider = new EnhancedDuckProvider(
1378
+ 'test',
1379
+ 'Test Duck',
1380
+ { apiKey: 'test-key', baseURL: 'https://api.test.com/v1', model: 'test-model' },
1381
+ mockFunctionBridge as unknown as import('../src/services/function-bridge').FunctionBridge,
1382
+ false,
1383
+ mockGuardrailsService as unknown as import('../src/guardrails/service').GuardrailsService
1384
+ );
1385
+ provider['client'].chat.completions.create = mockCreate;
1386
+
1387
+ const result = await provider.chat({
1388
+ messages: [{ role: 'user', content: 'Hello', timestamp: new Date() }],
1389
+ });
1390
+
1391
+ expect(result.content).toBe('[REDACTED RESPONSE]');
1392
+ });
1393
+
1394
+ it('should skip guardrails when service is disabled', async () => {
1395
+ const { EnhancedDuckProvider } = await import('../src/providers/duck-provider-enhanced');
1396
+
1397
+ mockGuardrailsService.isEnabled.mockReturnValue(false);
1398
+
1399
+ const provider = new EnhancedDuckProvider(
1400
+ 'test',
1401
+ 'Test Duck',
1402
+ { apiKey: 'test-key', baseURL: 'https://api.test.com/v1', model: 'test-model' },
1403
+ mockFunctionBridge as unknown as import('../src/services/function-bridge').FunctionBridge,
1404
+ false,
1405
+ mockGuardrailsService as unknown as import('../src/guardrails/service').GuardrailsService
1406
+ );
1407
+ provider['client'].chat.completions.create = mockCreate;
1408
+
1409
+ await provider.chat({
1410
+ messages: [{ role: 'user', content: 'Hello', timestamp: new Date() }],
1411
+ });
1412
+
1413
+ expect(mockGuardrailsService.createContext).not.toHaveBeenCalled();
1414
+ expect(mockGuardrailsService.execute).not.toHaveBeenCalled();
1415
+ });
1416
+
1417
+ it('should work without guardrails service (undefined)', async () => {
1418
+ const { EnhancedDuckProvider } = await import('../src/providers/duck-provider-enhanced');
1419
+
1420
+ const provider = new EnhancedDuckProvider(
1421
+ 'test',
1422
+ 'Test Duck',
1423
+ { apiKey: 'test-key', baseURL: 'https://api.test.com/v1', model: 'test-model' },
1424
+ mockFunctionBridge as unknown as import('../src/services/function-bridge').FunctionBridge,
1425
+ false
1426
+ // No guardrails service
1427
+ );
1428
+ provider['client'].chat.completions.create = mockCreate;
1429
+
1430
+ const result = await provider.chat({
1431
+ messages: [{ role: 'user', content: 'Hello', timestamp: new Date() }],
1432
+ });
1433
+
1434
+ expect(result.content).toBe('Mocked response');
1435
+ });
1436
+
1437
+ it('should re-throw GuardrailBlockError without wrapping', async () => {
1438
+ const { EnhancedDuckProvider } = await import('../src/providers/duck-provider-enhanced');
1439
+ const { GuardrailBlockError } = await import('../src/guardrails/errors');
1440
+
1441
+ mockGuardrailsService.execute.mockRejectedValueOnce(
1442
+ new GuardrailBlockError('custom_plugin', 'Custom block reason')
1443
+ );
1444
+
1445
+ const provider = new EnhancedDuckProvider(
1446
+ 'test',
1447
+ 'Test Duck',
1448
+ { apiKey: 'test-key', baseURL: 'https://api.test.com/v1', model: 'test-model' },
1449
+ mockFunctionBridge as unknown as import('../src/services/function-bridge').FunctionBridge,
1450
+ false,
1451
+ mockGuardrailsService as unknown as import('../src/guardrails/service').GuardrailsService
1452
+ );
1453
+ provider['client'].chat.completions.create = mockCreate;
1454
+
1455
+ await expect(
1456
+ provider.chat({
1457
+ messages: [{ role: 'user', content: 'Hello', timestamp: new Date() }],
1458
+ })
1459
+ ).rejects.toThrow("Request blocked by guardrail 'custom_plugin': Custom block reason");
1460
+ });
1461
+
1462
+ it('should apply guardrails with tool calls - blocking post_response after tool execution', async () => {
1463
+ const { EnhancedDuckProvider } = await import('../src/providers/duck-provider-enhanced');
1464
+
1465
+ // First call returns tool_calls, second returns final response
1466
+ mockCreate
1467
+ .mockResolvedValueOnce({
1468
+ choices: [{
1469
+ message: {
1470
+ content: null,
1471
+ tool_calls: [{
1472
+ id: 'call_123',
1473
+ type: 'function',
1474
+ function: {
1475
+ name: 'mcp__test__tool',
1476
+ arguments: JSON.stringify({ arg: 'value' }),
1477
+ },
1478
+ }],
1479
+ },
1480
+ finish_reason: 'tool_calls',
1481
+ }],
1482
+ usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
1483
+ model: 'test-model',
1484
+ })
1485
+ .mockResolvedValueOnce({
1486
+ choices: [{
1487
+ message: { content: 'Final response with sensitive data' },
1488
+ finish_reason: 'stop',
1489
+ }],
1490
+ usage: { prompt_tokens: 20, completion_tokens: 30, total_tokens: 50 },
1491
+ model: 'test-model',
1492
+ });
1493
+
1494
+ mockFunctionBridge.handleFunctionCall.mockResolvedValue({
1495
+ success: true,
1496
+ data: { result: 'tool result' },
1497
+ });
1498
+
1499
+ // Pre-request allows, post-response blocks
1500
+ mockGuardrailsService.execute
1501
+ .mockResolvedValueOnce({ action: 'allow', context: {} }) // pre_request
1502
+ .mockResolvedValueOnce({
1503
+ action: 'block',
1504
+ blockedBy: 'pii_redactor',
1505
+ blockReason: 'Sensitive data in final response',
1506
+ context: {},
1507
+ }); // post_response
1508
+
1509
+ const provider = new EnhancedDuckProvider(
1510
+ 'test',
1511
+ 'Test Duck',
1512
+ { apiKey: 'test-key', baseURL: 'https://api.test.com/v1', model: 'test-model' },
1513
+ mockFunctionBridge as unknown as import('../src/services/function-bridge').FunctionBridge,
1514
+ true, // mcpEnabled
1515
+ mockGuardrailsService as unknown as import('../src/guardrails/service').GuardrailsService
1516
+ );
1517
+ provider['client'].chat.completions.create = mockCreate;
1518
+
1519
+ await expect(
1520
+ provider.chat({
1521
+ messages: [{ role: 'user', content: 'Run the tool', timestamp: new Date() }],
1522
+ })
1523
+ ).rejects.toThrow("Request blocked by guardrail 'pii_redactor': Sensitive data in final response");
1524
+ });
1525
+
1526
+ it('should modify tool result when post_response guardrails return modify after tool calls', async () => {
1527
+ const { EnhancedDuckProvider } = await import('../src/providers/duck-provider-enhanced');
1528
+
1529
+ mockCreate
1530
+ .mockResolvedValueOnce({
1531
+ choices: [{
1532
+ message: {
1533
+ content: null,
1534
+ tool_calls: [{
1535
+ id: 'call_123',
1536
+ type: 'function',
1537
+ function: {
1538
+ name: 'mcp__test__tool',
1539
+ arguments: JSON.stringify({ arg: 'value' }),
1540
+ },
1541
+ }],
1542
+ },
1543
+ finish_reason: 'tool_calls',
1544
+ }],
1545
+ usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
1546
+ model: 'test-model',
1547
+ })
1548
+ .mockResolvedValueOnce({
1549
+ choices: [{
1550
+ message: { content: 'Final response with SSN: 123-45-6789' },
1551
+ finish_reason: 'stop',
1552
+ }],
1553
+ usage: { prompt_tokens: 20, completion_tokens: 30, total_tokens: 50 },
1554
+ model: 'test-model',
1555
+ });
1556
+
1557
+ mockFunctionBridge.handleFunctionCall.mockResolvedValue({
1558
+ success: true,
1559
+ data: { result: 'tool result' },
1560
+ });
1561
+
1562
+ const sharedContext = {
1563
+ requestId: 'test-id',
1564
+ provider: 'test',
1565
+ model: 'test-model',
1566
+ messages: [],
1567
+ prompt: 'Run the tool',
1568
+ response: '',
1569
+ violations: [],
1570
+ modifications: [],
1571
+ metadata: new Map(),
1572
+ };
1573
+
1574
+ mockGuardrailsService.createContext.mockReturnValue(sharedContext);
1575
+ mockGuardrailsService.execute.mockImplementation((phase) => {
1576
+ if (phase === 'post_response') {
1577
+ sharedContext.response = 'Final response with SSN: [REDACTED]';
1578
+ return Promise.resolve({ action: 'modify', context: sharedContext });
1579
+ }
1580
+ return Promise.resolve({ action: 'allow', context: sharedContext });
1581
+ });
1582
+
1583
+ const provider = new EnhancedDuckProvider(
1584
+ 'test',
1585
+ 'Test Duck',
1586
+ { apiKey: 'test-key', baseURL: 'https://api.test.com/v1', model: 'test-model' },
1587
+ mockFunctionBridge as unknown as import('../src/services/function-bridge').FunctionBridge,
1588
+ true,
1589
+ mockGuardrailsService as unknown as import('../src/guardrails/service').GuardrailsService
1590
+ );
1591
+ provider['client'].chat.completions.create = mockCreate;
1592
+
1593
+ const result = await provider.chat({
1594
+ messages: [{ role: 'user', content: 'Run the tool', timestamp: new Date() }],
1595
+ });
1596
+
1597
+ expect(result.content).toBe('Final response with SSN: [REDACTED]');
1598
+ });
860
1599
  });