Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions plugins/f5-guardrails/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
{
"id": "f5-guardrails",
"description": "F5 Guardrails Plugin - Partner guardrail powered by F5 Guardrails",
"credentials": {
"type": "object",
"properties": {
"apiKey": {
"type": "string",
"label": "API Key",
"description": "Your F5 Guardrails API key for authentication",
"encrypted": true
},
"calypsoUrl": {
"type": "string",
"label": "F5 Guardrails URL",
"description": "The base URL for F5 Guardrails API. Defaults to https://us1.calypsoai.app",
"default": "https://us1.calypsoai.app"
}
},
"required": ["apiKey"]
},
"functions": [
{
"name": "F5 Guardrails",
"id": "scan",
"supportedHooks": ["beforeRequestHook", "afterRequestHook"],
"type": "guardrail",
"description": [
{
"type": "subHeading",
"text": "F5 Guardrails powered by F5 Guardrails provides advanced content moderation and PII detection capabilities for your LLM inputs and outputs."
}
],
"parameters": {
"type": "object",
"properties": {
"redact": {
"type": "boolean",
"label": "Redact",
"description": [
{
"type": "subHeading",
"text": "Whether to redact PII data detected by the F5 guardrail. When enabled, detected PII will be masked in the content."
}
],
"default": false
},
"projectId": {
"type": "string",
"label": "Project-Id",
"description": [
{
"type": "subHeading",
"text": "Your F5 Guardrails project identifier"
}
]
},
"timeout": {
"type": "number",
"label": "Timeout",
"description": [
{
"type": "subHeading",
"text": "The timeout in milliseconds for the F5 guardrail scan. Defaults to 5000."
}
],
"default": 5000
}
}
},
"required": ["projectId"]
}
]
}
71 changes: 71 additions & 0 deletions plugins/f5-guardrails/scan.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { handler } from './scan';
import testCreds from './.creds.json';
import { PluginContext } from '../types';

describe('f5GuardrailsScan', () => {
it('Should mask the NRIC if it is detected', async () => {
const context = {
request: {
text: 'My NRIC is S1234567A',
json: {
messages: [
{
role: 'user',
content: 'My NRIC is S1234567A',
},
],
},
},
requestType: 'chatComplete',
};
const result = await handler(
context as PluginContext,
{
credentials: {
apiKey: testCreds.apiKey,
},
projectId: testCreds.projectId,
redact: false,
},
'beforeRequestHook'
);
expect(result).toBeDefined();
expect(result.verdict).toBe(false);
expect(result.error).toBeNull();
expect(result.data).toBeDefined();
expect(result.data?.[0].redactedInput).toBe('My NRIC is *********');
});

it('Should return verdict true if redact is true', async () => {
const context = {
request: {
text: 'My NRIC is S1234567A',
json: {
messages: [
{
role: 'user',
content: 'My NRIC is S1234567A',
},
],
},
},
requestType: 'chatComplete',
};
const result = await handler(
context as PluginContext,
{
credentials: {
apiKey: testCreds.apiKey,
},
projectId: testCreds.projectId,
redact: true,
},
'beforeRequestHook'
);
expect(result).toBeDefined();
expect(result.verdict).toBe(true);
expect(result.error).toBeNull();
expect(result.data).toBeDefined();
expect(result.data?.[0].redactedInput).toBe('My NRIC is *********');
});
});
154 changes: 154 additions & 0 deletions plugins/f5-guardrails/scan.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import {
HookEventType,
PluginContext,
PluginHandler,
PluginParameters,
} from '../types';
import {
post,
getCurrentContentPart,
setCurrentContentPart,
HttpError,
} from '../utils';

interface F5GuardrailsCredentials {
projectId: string;
apiKey: string;
calypsoUrl?: string;
}

interface F5GuardrailsResponse {
id: string;
redactedInput: string;
result: {
scannerResults: Array<{
scannerId: string;
outcome: 'passed' | 'failed';
data: unknown;
}>;
outcome: 'cleared' | 'flagged' | 'redacted' | 'blocked';
};
}

export const handler: PluginHandler = async (
context: PluginContext,
parameters: PluginParameters,
eventType: HookEventType
) => {
let error = null;
let verdict = true;
let data = null;
const transformedData: Record<string, unknown> = {
request: {
json: null,
},
response: {
json: null,
},
};
let transformed = false;

const credentials = parameters.credentials as
| F5GuardrailsCredentials
| undefined;

if (!parameters?.projectId || !credentials?.apiKey) {
return {
error: new Error(`Missing required credentials`),
verdict: true,
data,
transformedData,
transformed,
};
}

const { content, textArray } = getCurrentContentPart(context, eventType);
if (!content) {
return {
error: { message: 'request or response json is empty' },
verdict: true,
data: null,
transformedData,
transformed,
};
}

const calypsoUrl = credentials?.calypsoUrl || 'https://us1.calypsoai.app';
const redact = parameters.redact as boolean | undefined;

const apiUrl = `${calypsoUrl}/backend/v1/scans`;

try {
// Process each text segment
const results = await Promise.all(
textArray.map(async (text) => {
if (!text) return null;

const requestBody = {
input: text,
project: parameters.projectId,
};

const requestOptions = {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${credentials.apiKey}`,
'User-Agent': 'portkey-ai-plugin/1.0.0',
},
};

const response = await post<F5GuardrailsResponse>(
apiUrl,
requestBody,
requestOptions,
parameters.timeout
);

return {
outcome: response.result.outcome,
redactedInput: response.redactedInput,
result: response.result,
};
})
);

let hasRedacted = false;
// Apply redaction only if the parameter is true
if (redact) {
const redactedTexts = results.map(
(result) => result?.redactedInput ?? null
);
hasRedacted = redactedTexts.some((text) => text !== null);
setCurrentContentPart(context, eventType, transformedData, redactedTexts);
transformed = true;
}
data = results;
const isRequestFlagged = !results.every(
(result) => result?.outcome === 'cleared'
);
if (isRequestFlagged && !hasRedacted) {
verdict = false;
}
} catch (e) {
if (e instanceof HttpError) {
error = {
message: e.response.body || e.message,
status: e.response.status,
};
} else {
error = e instanceof Error ? e.message : String(e);
}

// On error, default to allowing the request (fail open)
verdict = true;
data = null;
}

return {
error,
verdict,
data,
transformedData,
transformed,
};
};
5 changes: 4 additions & 1 deletion plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ import { handler as walledaiguardrails } from './walledai/walledprotect';
import { handler as defaultregexReplace } from './default/regexReplace';
import { handler as defaultallowedRequestTypes } from './default/allowedRequestTypes';
import { handler as javelinguardrails } from './javelin/guardrails';

import { handler as f5GuardrailsScan } from './f5-guardrails/scan';
export const plugins = {
default: {
regexMatch: defaultregexMatch,
Expand Down Expand Up @@ -174,4 +174,7 @@ export const plugins = {
javelin: {
guardrails: javelinguardrails,
},
'f5-guardrails': {
scan: f5GuardrailsScan,
},
};