Skip to content

Commit 8266e4d

Browse files
authored
Merge pull request #1440 from b4s36t4/feat/f5-guardrails
feat: add f5-guardrail plugin
2 parents b6eedd3 + 9263085 commit 8266e4d

4 files changed

Lines changed: 303 additions & 1 deletion

File tree

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
{
2+
"id": "f5-guardrails",
3+
"description": "F5 Guardrails Plugin - Partner guardrail powered by F5 Guardrails",
4+
"credentials": {
5+
"type": "object",
6+
"properties": {
7+
"apiKey": {
8+
"type": "string",
9+
"label": "API Key",
10+
"description": "Your F5 Guardrails API key for authentication",
11+
"encrypted": true
12+
},
13+
"calypsoUrl": {
14+
"type": "string",
15+
"label": "F5 Guardrails URL",
16+
"description": "The base URL for F5 Guardrails API. Defaults to https://us1.calypsoai.app",
17+
"default": "https://us1.calypsoai.app"
18+
}
19+
},
20+
"required": ["apiKey"]
21+
},
22+
"functions": [
23+
{
24+
"name": "F5 Guardrails",
25+
"id": "scan",
26+
"supportedHooks": ["beforeRequestHook", "afterRequestHook"],
27+
"type": "guardrail",
28+
"description": [
29+
{
30+
"type": "subHeading",
31+
"text": "F5 Guardrails powered by F5 Guardrails provides advanced content moderation and PII detection capabilities for your LLM inputs and outputs."
32+
}
33+
],
34+
"parameters": {
35+
"type": "object",
36+
"properties": {
37+
"redact": {
38+
"type": "boolean",
39+
"label": "Redact",
40+
"description": [
41+
{
42+
"type": "subHeading",
43+
"text": "Whether to redact PII data detected by the F5 guardrail. When enabled, detected PII will be masked in the content."
44+
}
45+
],
46+
"default": false
47+
},
48+
"projectId": {
49+
"type": "string",
50+
"label": "Project-Id",
51+
"description": [
52+
{
53+
"type": "subHeading",
54+
"text": "Your F5 Guardrails project identifier"
55+
}
56+
]
57+
},
58+
"timeout": {
59+
"type": "number",
60+
"label": "Timeout",
61+
"description": [
62+
{
63+
"type": "subHeading",
64+
"text": "The timeout in milliseconds for the F5 guardrail scan. Defaults to 5000."
65+
}
66+
],
67+
"default": 5000
68+
}
69+
}
70+
},
71+
"required": ["projectId"]
72+
}
73+
]
74+
}

plugins/f5-guardrails/scan.test.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { handler } from './scan';
2+
import testCreds from './.creds.json';
3+
import { PluginContext } from '../types';
4+
5+
describe('f5GuardrailsScan', () => {
6+
it('Should mask the NRIC if it is detected', async () => {
7+
const context = {
8+
request: {
9+
text: 'My NRIC is S1234567A',
10+
json: {
11+
messages: [
12+
{
13+
role: 'user',
14+
content: 'My NRIC is S1234567A',
15+
},
16+
],
17+
},
18+
},
19+
requestType: 'chatComplete',
20+
};
21+
const result = await handler(
22+
context as PluginContext,
23+
{
24+
credentials: {
25+
apiKey: testCreds.apiKey,
26+
},
27+
projectId: testCreds.projectId,
28+
redact: false,
29+
},
30+
'beforeRequestHook'
31+
);
32+
expect(result).toBeDefined();
33+
expect(result.verdict).toBe(false);
34+
expect(result.error).toBeNull();
35+
expect(result.data).toBeDefined();
36+
expect(result.data?.[0].redactedInput).toBe('My NRIC is *********');
37+
});
38+
39+
it('Should return verdict true if redact is true', async () => {
40+
const context = {
41+
request: {
42+
text: 'My NRIC is S1234567A',
43+
json: {
44+
messages: [
45+
{
46+
role: 'user',
47+
content: 'My NRIC is S1234567A',
48+
},
49+
],
50+
},
51+
},
52+
requestType: 'chatComplete',
53+
};
54+
const result = await handler(
55+
context as PluginContext,
56+
{
57+
credentials: {
58+
apiKey: testCreds.apiKey,
59+
},
60+
projectId: testCreds.projectId,
61+
redact: true,
62+
},
63+
'beforeRequestHook'
64+
);
65+
expect(result).toBeDefined();
66+
expect(result.verdict).toBe(true);
67+
expect(result.error).toBeNull();
68+
expect(result.data).toBeDefined();
69+
expect(result.data?.[0].redactedInput).toBe('My NRIC is *********');
70+
});
71+
});

plugins/f5-guardrails/scan.ts

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import {
2+
HookEventType,
3+
PluginContext,
4+
PluginHandler,
5+
PluginParameters,
6+
} from '../types';
7+
import {
8+
post,
9+
getCurrentContentPart,
10+
setCurrentContentPart,
11+
HttpError,
12+
} from '../utils';
13+
14+
interface F5GuardrailsCredentials {
15+
projectId: string;
16+
apiKey: string;
17+
calypsoUrl?: string;
18+
}
19+
20+
interface F5GuardrailsResponse {
21+
id: string;
22+
redactedInput: string;
23+
result: {
24+
scannerResults: Array<{
25+
scannerId: string;
26+
outcome: 'passed' | 'failed';
27+
data: unknown;
28+
}>;
29+
outcome: 'cleared' | 'flagged' | 'redacted' | 'blocked';
30+
};
31+
}
32+
33+
export const handler: PluginHandler = async (
34+
context: PluginContext,
35+
parameters: PluginParameters,
36+
eventType: HookEventType
37+
) => {
38+
let error = null;
39+
let verdict = true;
40+
let data = null;
41+
const transformedData: Record<string, unknown> = {
42+
request: {
43+
json: null,
44+
},
45+
response: {
46+
json: null,
47+
},
48+
};
49+
let transformed = false;
50+
51+
const credentials = parameters.credentials as
52+
| F5GuardrailsCredentials
53+
| undefined;
54+
55+
if (!parameters?.projectId || !credentials?.apiKey) {
56+
return {
57+
error: new Error(`Missing required credentials`),
58+
verdict: true,
59+
data,
60+
transformedData,
61+
transformed,
62+
};
63+
}
64+
65+
const { content, textArray } = getCurrentContentPart(context, eventType);
66+
if (!content) {
67+
return {
68+
error: { message: 'request or response json is empty' },
69+
verdict: true,
70+
data: null,
71+
transformedData,
72+
transformed,
73+
};
74+
}
75+
76+
const calypsoUrl = credentials?.calypsoUrl || 'https://us1.calypsoai.app';
77+
const redact = parameters.redact as boolean | undefined;
78+
79+
const apiUrl = `${calypsoUrl}/backend/v1/scans`;
80+
81+
try {
82+
// Process each text segment
83+
const results = await Promise.all(
84+
textArray.map(async (text) => {
85+
if (!text) return null;
86+
87+
const requestBody = {
88+
input: text,
89+
project: parameters.projectId,
90+
};
91+
92+
const requestOptions = {
93+
headers: {
94+
'Content-Type': 'application/json',
95+
Authorization: `Bearer ${credentials.apiKey}`,
96+
'User-Agent': 'portkey-ai-plugin/1.0.0',
97+
},
98+
};
99+
100+
const response = await post<F5GuardrailsResponse>(
101+
apiUrl,
102+
requestBody,
103+
requestOptions,
104+
parameters.timeout
105+
);
106+
107+
return {
108+
outcome: response.result.outcome,
109+
redactedInput: response.redactedInput,
110+
result: response.result,
111+
};
112+
})
113+
);
114+
115+
let hasRedacted = false;
116+
// Apply redaction only if the parameter is true
117+
if (redact) {
118+
const redactedTexts = results.map(
119+
(result) => result?.redactedInput ?? null
120+
);
121+
hasRedacted = redactedTexts.some((text) => text !== null);
122+
setCurrentContentPart(context, eventType, transformedData, redactedTexts);
123+
transformed = true;
124+
}
125+
data = results;
126+
const isRequestFlagged = !results.every(
127+
(result) => result?.outcome === 'cleared'
128+
);
129+
if (isRequestFlagged && !hasRedacted) {
130+
verdict = false;
131+
}
132+
} catch (e) {
133+
if (e instanceof HttpError) {
134+
error = {
135+
message: e.response.body || e.message,
136+
status: e.response.status,
137+
};
138+
} else {
139+
error = e instanceof Error ? e.message : String(e);
140+
}
141+
142+
// On error, default to allowing the request (fail open)
143+
verdict = true;
144+
data = null;
145+
}
146+
147+
return {
148+
error,
149+
verdict,
150+
data,
151+
transformedData,
152+
transformed,
153+
};
154+
};

plugins/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ import { handler as walledaiguardrails } from './walledai/walledprotect';
6666
import { handler as defaultregexReplace } from './default/regexReplace';
6767
import { handler as defaultallowedRequestTypes } from './default/allowedRequestTypes';
6868
import { handler as javelinguardrails } from './javelin/guardrails';
69-
69+
import { handler as f5GuardrailsScan } from './f5-guardrails/scan';
7070
export const plugins = {
7171
default: {
7272
regexMatch: defaultregexMatch,
@@ -174,4 +174,7 @@ export const plugins = {
174174
javelin: {
175175
guardrails: javelinguardrails,
176176
},
177+
'f5-guardrails': {
178+
scan: f5GuardrailsScan,
179+
},
177180
};

0 commit comments

Comments
 (0)