Skip to content

Commit a64e50e

Browse files
authored
feat: add Superagent Guard and Redact plugin (#92)
* add superagent guardrails plugin * fix: restore step registry and integration types with superagent Restored the auto-generated registry files to include all plugins and added superagent guard/redact actions. * Fix superagent redact entities validation and increase icon size * fix: fail-safe validation for Superagent Guard API response Add validation to ensure Guard step fails instead of defaulting to 'allow' when API response lacks expected structure. This prevents security bypasses from unexpected API responses.
1 parent 6f83872 commit a64e50e

File tree

12 files changed

+500
-3
lines changed

12 files changed

+500
-3
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ Visit [http://localhost:3000](http://localhost:3000) to get started.
8484
- **Linear**: Create Ticket, Find Issues
8585
- **Resend**: Send Email
8686
- **Slack**: Send Slack Message
87+
- **Superagent**: Guard, Redact
8788
- **v0**: Create Chat, Send Message
8889
<!-- PLUGINS:END -->
8990

app/api/integrations/[integrationId]/test/route.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ export async function POST(
6868
integration.config.firecrawlApiKey
6969
);
7070
break;
71+
case "superagent":
72+
result = await testSuperagentConnection(
73+
integration.config.superagentApiKey
74+
);
75+
break;
7176
default:
7277
return NextResponse.json(
7378
{ error: "Invalid integration type" },
@@ -281,3 +286,45 @@ async function testFirecrawlConnection(
281286
};
282287
}
283288
}
289+
290+
async function testSuperagentConnection(
291+
apiKey?: string
292+
): Promise<TestConnectionResult> {
293+
try {
294+
if (!apiKey) {
295+
return {
296+
status: "error",
297+
message: "Superagent API Key is not configured",
298+
};
299+
}
300+
301+
const response = await fetch("https://app.superagent.sh/api/guard", {
302+
method: "POST",
303+
headers: {
304+
"Content-Type": "application/json",
305+
Authorization: `Bearer ${apiKey}`,
306+
},
307+
body: JSON.stringify({
308+
text: "Hello, this is a test message.",
309+
}),
310+
});
311+
312+
if (!response.ok) {
313+
const error = await response.text();
314+
return {
315+
status: "error",
316+
message: error || "Authentication failed",
317+
};
318+
}
319+
320+
return {
321+
status: "success",
322+
message: "Connected successfully",
323+
};
324+
} catch (error) {
325+
return {
326+
status: "error",
327+
message: error instanceof Error ? error.message : "Connection failed",
328+
};
329+
}
330+
}

lib/step-registry.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* This registry enables dynamic step imports that are statically analyzable
88
* by the bundler. Each action type maps to its step importer function.
99
*
10-
* Generated entries: 10
10+
* Generated entries: 12
1111
*/
1212

1313
import "server-only";
@@ -93,6 +93,14 @@ export const PLUGIN_STEP_IMPORTERS: Record<string, StepImporter> = {
9393
importer: () => import("@/plugins/slack/steps/send-slack-message"),
9494
stepFunction: "sendSlackMessageStep",
9595
},
96+
"superagent/guard": {
97+
importer: () => import("@/plugins/superagent/steps/guard"),
98+
stepFunction: "superagentGuardStep",
99+
},
100+
"superagent/redact": {
101+
importer: () => import("@/plugins/superagent/steps/redact"),
102+
stepFunction: "superagentRedactStep",
103+
},
96104
"v0/create-chat": {
97105
importer: () => import("@/plugins/v0/steps/create-chat"),
98106
stepFunction: "createChatStep",
@@ -124,6 +132,8 @@ export const ACTION_LABELS: Record<string, string> = {
124132
"linear/find-issues": "Find Issues",
125133
"resend/send-email": "Send Email",
126134
"slack/send-message": "Send Slack Message",
135+
"superagent/guard": "Guard",
136+
"superagent/redact": "Redact",
127137
"v0/create-chat": "Create Chat",
128138
"v0/send-message": "Send Message",
129139
Scrape: "Scrape URL",

lib/types/integration.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
* 2. Add a system integration to SYSTEM_INTEGRATION_TYPES in discover-plugins.ts
1010
* 3. Run: pnpm discover-plugins
1111
*
12-
* Generated types: ai-gateway, database, firecrawl, linear, resend, slack, v0
12+
* Generated types: ai-gateway, database, firecrawl, linear, resend, slack, superagent, v0
1313
*/
1414

1515
// Integration type union - plugins + system integrations
@@ -20,6 +20,7 @@ export type IntegrationType =
2020
| "linear"
2121
| "resend"
2222
| "slack"
23+
| "superagent"
2324
| "v0";
2425

2526
// Generic config type - plugins define their own keys via formFields[].configKey

plugins/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,15 @@
1313
* 1. Delete the plugin directory
1414
* 2. Run: pnpm discover-plugins (or it runs automatically on build)
1515
*
16-
* Discovered plugins: ai-gateway, firecrawl, linear, resend, slack, v0
16+
* Discovered plugins: ai-gateway, firecrawl, linear, resend, slack, superagent, v0
1717
*/
1818

1919
import "./ai-gateway";
2020
import "./firecrawl";
2121
import "./linear";
2222
import "./resend";
2323
import "./slack";
24+
import "./superagent";
2425
import "./v0";
2526

2627
export type {
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/**
2+
* Code generation template for Superagent Guard action
3+
* This template is used when exporting workflows to standalone Next.js projects
4+
* It uses environment variables instead of integrationId
5+
*/
6+
export const guardCodegenTemplate = `export async function superagentGuardStep(input: {
7+
text: string;
8+
}) {
9+
"use step";
10+
11+
const response = await fetch("https://app.superagent.sh/api/guard", {
12+
method: "POST",
13+
headers: {
14+
"Content-Type": "application/json",
15+
Authorization: \`Bearer \${process.env.SUPERAGENT_API_KEY!}\`,
16+
},
17+
body: JSON.stringify({
18+
text: input.text,
19+
}),
20+
});
21+
22+
if (!response.ok) {
23+
const error = await response.text();
24+
throw new Error(\`Guard API error: \${error}\`);
25+
}
26+
27+
const data = await response.json();
28+
const choice = data.choices?.[0];
29+
const content = choice?.message?.content;
30+
31+
// Validate response structure - fail safe instead of defaulting to "allow"
32+
if (!content || typeof content !== "object") {
33+
throw new Error(
34+
"Invalid Guard API response: missing or invalid content structure"
35+
);
36+
}
37+
38+
const classification = content.classification;
39+
if (!classification || (classification !== "allow" && classification !== "block")) {
40+
throw new Error(
41+
\`Invalid Guard API response: missing or invalid classification (received: \${JSON.stringify(classification)})\`
42+
);
43+
}
44+
45+
return {
46+
classification,
47+
violationTypes: content?.violation_types || [],
48+
cweCodes: content?.cwe_codes || [],
49+
reasoning: choice?.message?.reasoning,
50+
};
51+
}`;
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/**
2+
* Code generation template for Superagent Redact action
3+
* This template is used when exporting workflows to standalone Next.js projects
4+
* It uses environment variables instead of integrationId
5+
*/
6+
export const redactCodegenTemplate = `export async function superagentRedactStep(input: {
7+
text: string;
8+
entities?: string[] | string;
9+
}) {
10+
"use step";
11+
12+
const body: { text: string; entities?: string[] } = {
13+
text: input.text,
14+
};
15+
16+
// Parse entities from string or array, filter out empty strings
17+
if (input.entities) {
18+
let entitiesArray: string[];
19+
20+
if (typeof input.entities === "string") {
21+
// Parse comma-separated string
22+
entitiesArray = input.entities.split(",").map((e) => e.trim());
23+
} else if (Array.isArray(input.entities)) {
24+
entitiesArray = input.entities.map((e) => String(e).trim());
25+
} else {
26+
entitiesArray = [];
27+
}
28+
29+
// Filter out empty strings and ensure we have valid entities
30+
const validEntities = entitiesArray.filter((e) => e.length > 0);
31+
32+
if (validEntities.length > 0) {
33+
body.entities = validEntities;
34+
}
35+
}
36+
37+
const response = await fetch("https://app.superagent.sh/api/redact", {
38+
method: "POST",
39+
headers: {
40+
"Content-Type": "application/json",
41+
Authorization: \`Bearer \${process.env.SUPERAGENT_API_KEY!}\`,
42+
},
43+
body: JSON.stringify(body),
44+
});
45+
46+
if (!response.ok) {
47+
const error = await response.text();
48+
throw new Error(\`Redact API error: \${error}\`);
49+
}
50+
51+
const data = await response.json();
52+
const choice = data.choices?.[0];
53+
54+
return {
55+
redactedText: choice?.message?.content || input.text,
56+
reasoning: choice?.message?.reasoning,
57+
};
58+
}`;

plugins/superagent/icon.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
export function SuperagentIcon({ className }: { className?: string }) {
2+
return (
3+
<svg
4+
aria-label="Superagent"
5+
className={className}
6+
width="48"
7+
height="48"
8+
viewBox="0 0 690 690"
9+
fill="none"
10+
xmlns="http://www.w3.org/2000/svg"
11+
>
12+
<title>Superagent</title>
13+
<path
14+
d="M331.167 126.514L660.346 456.95L331.167 383.373L331.167 126.514Z"
15+
fill="#0671A2"
16+
/>
17+
<path
18+
d="M330.539 124L34.0258 456.949L330.539 380.307L330.539 124Z"
19+
fill="#FDCCD1"
20+
/>
21+
<path
22+
d="M333.68 565L29.0002 455.692L333.68 381.564L654.12 455.692L333.68 565Z"
23+
fill="#181818"
24+
/>
25+
</svg>
26+
);
27+
}

plugins/superagent/index.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import type { IntegrationPlugin } from "../registry";
2+
import { registerIntegration } from "../registry";
3+
import { guardCodegenTemplate } from "./codegen/guard";
4+
import { redactCodegenTemplate } from "./codegen/redact";
5+
import { SuperagentIcon } from "./icon";
6+
7+
const superagentPlugin: IntegrationPlugin = {
8+
type: "superagent",
9+
label: "Superagent",
10+
description: "AI guardrails for prompt injection detection and PII redaction",
11+
12+
icon: SuperagentIcon,
13+
14+
formFields: [
15+
{
16+
id: "superagentApiKey",
17+
label: "API Key",
18+
type: "password",
19+
placeholder: "sa-...",
20+
configKey: "superagentApiKey",
21+
envVar: "SUPERAGENT_API_KEY",
22+
helpText: "Get your API key from ",
23+
helpLink: {
24+
text: "superagent.sh",
25+
url: "https://app.superagent.sh",
26+
},
27+
},
28+
],
29+
30+
testConfig: {
31+
getTestFunction: async () => {
32+
const { testSuperagent } = await import("./test");
33+
return testSuperagent;
34+
},
35+
},
36+
37+
actions: [
38+
{
39+
slug: "guard",
40+
label: "Guard",
41+
description:
42+
"Detect prompt injection, system prompt extraction, or data exfiltration attempts",
43+
category: "Superagent",
44+
stepFunction: "superagentGuardStep",
45+
stepImportPath: "guard",
46+
configFields: [
47+
{
48+
key: "text",
49+
label: "Text",
50+
type: "template-textarea",
51+
placeholder: "Text to analyze or {{NodeName.text}}",
52+
example: "Analyze this user input for security threats",
53+
required: true,
54+
rows: 4,
55+
},
56+
],
57+
codegenTemplate: guardCodegenTemplate,
58+
},
59+
{
60+
slug: "redact",
61+
label: "Redact",
62+
description:
63+
"Remove sensitive information (PII/PHI) like SSNs, emails, and phone numbers from text",
64+
category: "Superagent",
65+
stepFunction: "superagentRedactStep",
66+
stepImportPath: "redact",
67+
configFields: [
68+
{
69+
key: "text",
70+
label: "Text",
71+
type: "template-textarea",
72+
placeholder: "Text to redact or {{NodeName.text}}",
73+
example: "My email is john@example.com and SSN is 123-45-6789",
74+
required: true,
75+
rows: 4,
76+
},
77+
{
78+
key: "entities",
79+
label: "Entity Types",
80+
type: "text",
81+
placeholder: "Optional: SSN, EMAIL, PHONE (comma-separated)",
82+
example: "",
83+
},
84+
],
85+
codegenTemplate: redactCodegenTemplate,
86+
},
87+
],
88+
};
89+
90+
// Auto-register on import
91+
registerIntegration(superagentPlugin);
92+
93+
export default superagentPlugin;

0 commit comments

Comments
 (0)