Skip to content

Commit 971c72a

Browse files
authored
Merge pull request #1332 from abhijitjavelin/main
feat: add javelin guardrails
2 parents c1c7e7d + f0575f7 commit 971c72a

File tree

4 files changed

+825
-0
lines changed

4 files changed

+825
-0
lines changed

plugins/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import { handler as defaultrequiredMetadataKeys } from './default/requiredMetada
5353
import { handler as walledaiguardrails } from './walledai/guardrails';
5454
import { handler as defaultregexReplace } from './default/regexReplace';
5555
import { handler as defaultallowedRequestTypes } from './default/allowedRequestTypes';
56+
import { handler as javelinguardrails } from './javelin/guardrails';
5657

5758
export const plugins = {
5859
default: {
@@ -144,4 +145,7 @@ export const plugins = {
144145
walledai: {
145146
guardrails: walledaiguardrails,
146147
},
148+
javelin: {
149+
guardrails: javelinguardrails,
150+
},
147151
};

plugins/javelin/guardrails.ts

Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
import {
2+
HookEventType,
3+
PluginContext,
4+
PluginHandler,
5+
PluginParameters,
6+
} from '../types';
7+
import { getCurrentContentPart } from '../utils';
8+
9+
interface JavelinCredentials {
10+
apiKey: string;
11+
domain?: string;
12+
application?: string;
13+
}
14+
15+
interface GuardrailAssessment {
16+
[key: string]: {
17+
categories?: Record<string, boolean>;
18+
category_scores?: Record<string, number>;
19+
results?: {
20+
categories?: Record<string, boolean>;
21+
category_scores?: Record<string, number>;
22+
lang?: string;
23+
prob?: number;
24+
reject_prompt?: string;
25+
};
26+
config?: {
27+
threshold_used?: number;
28+
};
29+
request_reject?: boolean;
30+
};
31+
}
32+
33+
interface GuardrailsResponse {
34+
assessments: Array<GuardrailAssessment>;
35+
}
36+
37+
async function callJavelinGuardrails(
38+
text: string,
39+
credentials: JavelinCredentials
40+
): Promise<GuardrailsResponse> {
41+
// Strip https:// or http:// from domain if present
42+
let domain = credentials.domain || 'api-dev.javelin.live';
43+
domain = domain.replace(/^https?:\/\//, '');
44+
45+
const apiUrl = `https://${domain}/v1/guardrails/apply`;
46+
47+
console.log('[Javelin] Calling API:', apiUrl);
48+
console.log('[Javelin] Application:', credentials.application);
49+
50+
const headers: Record<string, string> = {
51+
'Content-Type': 'application/json',
52+
'x-javelin-apikey': credentials.apiKey,
53+
};
54+
55+
if (credentials.application) {
56+
headers['x-javelin-application'] = credentials.application;
57+
}
58+
59+
const requestBody = {
60+
input: { text },
61+
config: {},
62+
metadata: {},
63+
};
64+
65+
console.log('[Javelin] Request body:', JSON.stringify(requestBody));
66+
67+
const response = await fetch(apiUrl, {
68+
method: 'POST',
69+
headers,
70+
body: JSON.stringify(requestBody),
71+
});
72+
73+
console.log('[Javelin] Response status:', response.status);
74+
75+
if (!response.ok) {
76+
const errorText = await response.text();
77+
console.error('[Javelin] API error:', errorText);
78+
throw new Error(
79+
`Javelin Guardrails API error: ${response.status} ${response.statusText} - ${errorText}`
80+
);
81+
}
82+
83+
const responseData = await response.json();
84+
85+
return responseData as GuardrailsResponse;
86+
}
87+
88+
export const handler: PluginHandler = async (
89+
context: PluginContext,
90+
parameters: PluginParameters,
91+
eventType: HookEventType
92+
) => {
93+
console.log('[Javelin] Handler called with eventType:', eventType);
94+
console.log(
95+
'[Javelin] Full parameters object:',
96+
JSON.stringify(parameters, null, 2)
97+
);
98+
console.log('[Javelin] Parameters keys:', Object.keys(parameters));
99+
100+
let error = null;
101+
let verdict = true;
102+
let data = null;
103+
104+
// Try multiple ways to get credentials
105+
let credentials = parameters.credentials as unknown as JavelinCredentials;
106+
107+
// If credentials not at root, check if they're nested or direct properties
108+
if (!credentials || !credentials.apiKey) {
109+
console.log('[Javelin] Credentials not found at parameters.credentials');
110+
console.log('[Javelin] Trying direct properties...');
111+
112+
// Check if credentials are passed as direct properties
113+
if (parameters.apiKey) {
114+
console.log('[Javelin] Found credentials as direct properties');
115+
credentials = {
116+
apiKey: parameters.apiKey as string,
117+
domain: parameters.domain as string | undefined,
118+
application: parameters.application as string | undefined,
119+
};
120+
}
121+
}
122+
123+
console.log('[Javelin] Final credentials check:', {
124+
hasApiKey: !!credentials?.apiKey,
125+
hasDomain: !!credentials?.domain,
126+
hasApplication: !!credentials?.application,
127+
apiKeyLength: credentials?.apiKey?.length || 0,
128+
domain: credentials?.domain || 'none',
129+
application: credentials?.application || 'none',
130+
});
131+
132+
if (!credentials?.apiKey) {
133+
console.error('[Javelin] Missing API key after all checks');
134+
return {
135+
error: `'parameters.credentials.apiKey' must be set. Received parameters keys: ${Object.keys(parameters).join(', ')}`,
136+
verdict: true,
137+
data,
138+
};
139+
}
140+
141+
if (!credentials?.application) {
142+
console.error('[Javelin] Missing application name');
143+
return {
144+
error: `'parameters.credentials.application' must be set. Received: ${JSON.stringify(credentials)}`,
145+
verdict: true,
146+
data,
147+
};
148+
}
149+
150+
const { content, textArray } = getCurrentContentPart(context, eventType);
151+
if (!content) {
152+
console.error('[Javelin] No content to check');
153+
return {
154+
error: { message: 'request or response json is empty' },
155+
verdict: true,
156+
data: null,
157+
};
158+
}
159+
160+
const text = textArray.filter((text) => text).join('\n');
161+
console.log('[Javelin] Text to check (length):', text.length);
162+
163+
try {
164+
const response = await callJavelinGuardrails(text, credentials);
165+
const assessments = response.assessments || [];
166+
167+
console.log('[Javelin] Received', assessments.length, 'assessments');
168+
169+
if (assessments.length === 0) {
170+
console.warn('[Javelin] No assessments in response');
171+
return {
172+
error: { message: 'No assessments in Javelin response' },
173+
verdict: true,
174+
data: null,
175+
};
176+
}
177+
178+
let shouldReject = false;
179+
let rejectPrompt = '';
180+
const flaggedAssessments: Array<{
181+
type: string;
182+
request_reject: boolean;
183+
categories?: Record<string, boolean>;
184+
category_scores?: Record<string, number>;
185+
threshold_used?: number;
186+
}> = [];
187+
188+
// Check all assessments for violations
189+
for (const assessment of assessments) {
190+
for (const [assessmentType, assessmentData] of Object.entries(
191+
assessment
192+
)) {
193+
console.log(
194+
'[Javelin] Assessment:',
195+
assessmentType,
196+
'request_reject:',
197+
assessmentData.request_reject
198+
);
199+
200+
if (assessmentData.request_reject === true) {
201+
shouldReject = true;
202+
203+
// Extract reject prompt from results
204+
const results = assessmentData.results || {};
205+
if (results.reject_prompt && !rejectPrompt) {
206+
rejectPrompt = results.reject_prompt;
207+
}
208+
209+
// Collect flagged assessment details
210+
flaggedAssessments.push({
211+
type: assessmentType,
212+
request_reject: true,
213+
categories: assessmentData.categories || results.categories,
214+
category_scores:
215+
assessmentData.category_scores || results.category_scores,
216+
threshold_used: assessmentData.config?.threshold_used,
217+
});
218+
}
219+
}
220+
}
221+
222+
if (shouldReject) {
223+
// Use a default message if no reject_prompt was found
224+
if (!rejectPrompt) {
225+
rejectPrompt =
226+
'Request blocked by Javelin guardrails due to policy violation';
227+
}
228+
229+
console.log('[Javelin] Request REJECTED:', rejectPrompt);
230+
231+
// Return with verdict false and NO error field for policy violations
232+
// Portkey will handle the deny logic based on guardrail actions
233+
verdict = false;
234+
error = null;
235+
data = {
236+
flagged_assessments: flaggedAssessments,
237+
reject_prompt: rejectPrompt,
238+
javelin_response: response,
239+
};
240+
} else {
241+
console.log('[Javelin] Request PASSED all guardrails');
242+
243+
// All guardrails passed
244+
verdict = true;
245+
error = null;
246+
data = {
247+
assessments: assessments,
248+
all_passed: true,
249+
};
250+
}
251+
} catch (e: any) {
252+
// Handle API errors - still return verdict true so Portkey doesn't block
253+
console.error('[Javelin] Error calling API:', e.message);
254+
console.error('[Javelin] Error details:', e);
255+
256+
// Create a serializable error object
257+
error = {
258+
message: e.message || 'Unknown error calling Javelin API',
259+
name: e.name,
260+
...(e.cause && { cause: e.cause }),
261+
};
262+
verdict = true; // Don't block on API errors
263+
data = {
264+
error_occurred: true,
265+
error_message: e.message,
266+
};
267+
}
268+
269+
console.log('[Javelin] Returning:', {
270+
verdict,
271+
hasError: !!error,
272+
hasData: !!data,
273+
});
274+
275+
return { error, verdict, data };
276+
};

0 commit comments

Comments
 (0)