ecture Decisions
- In-Process Execution: Security checks run as code within your application runtime (Node.js, Next.js, Bun, Deno). This eliminates network latency and allows inspection of internal state.
- Tool Wrapper Pattern: Create a middleware layer that wraps every tool invocation. This wrapper performs validation and calls Arcjet rules before delegating to the actual tool implementation.
- Deterministic Validation: Do not rely on the LLM to self-police. Use code-based allowlists, path checks, and output scanning. System prompts are soft preferences; code is a hard boundary.
- Defense in Depth: In-process guards complement edge security. Keep your WAF for volumetric attacks and classic injection, but add in-process guards for agentic risks.
Implementation Strategy
The following TypeScript example demonstrates a ToolGuard class that integrates Arcjet to secure tool execution. This pattern wraps tool calls, validates arguments, and scans outputs.
import arcjet, { ShieldRule, SensitiveInfoRule, RateLimitRule } from "@arcjet/sdk";
import type { Arcjet, ArcjetDecision } from "@arcjet/sdk";
// Configuration for the agent tool guard
interface ToolGuardConfig {
arcjetClient: Arcjet;
allowedFilePatterns: RegExp[];
allowedFetchDomains: string[];
maxToolCallsPerWindow: number;
}
// Wrapper for tool execution with security checks
export class AgentToolGuard {
private config: ToolGuardConfig;
constructor(config: ToolGuardConfig) {
this.config = config;
}
// Generic execution wrapper
async execute<T>(
toolName: string,
args: Record<string, unknown>,
executor: () => Promise<T>
): Promise<T> {
// 1. Rate limiting per tool or session
const rateLimitDecision = await this.checkRateLimit(toolName);
if (rateLimitDecision.isDenied()) {
throw new Error(`Rate limit exceeded for tool: ${toolName}`);
}
// 2. Validate arguments based on tool type
await this.validateArguments(toolName, args);
// 3. Execute the tool
const result = await executor();
// 4. Scan output for sensitive data or injection payloads
await this.validateOutput(toolName, result);
return result;
}
private async checkRateLimit(toolName: string): Promise<ArcjetDecision> {
// Use Arcjet's in-process rate limiting
// In a real implementation, you'd pass a request context
return this.config.arcjetClient.protect(
{ ip: "127.0.0.1", headers: {} }, // Mock request for in-process context
{ rateLimit: { max: this.config.maxToolCallsPerWindow, window: "1m" } }
);
}
private async validateArguments(toolName: string, args: Record<string, unknown>): Promise<void> {
if (toolName === "readFile") {
const filePath = args.path as string;
if (!this.isPathAllowed(filePath)) {
throw new Error(`Access denied: Path not in allowlist: ${filePath}`);
}
}
if (toolName === "fetchUrl") {
const url = args.url as string;
if (!this.isDomainAllowed(url)) {
throw new Error(`Access denied: Domain not in allowlist: ${url}`);
}
}
// Run Arcjet shield rule on arguments to detect injection
const shieldDecision = await this.config.arcjetClient.protect(
{ ip: "127.0.0.1", headers: {}, json: args },
{ shield: { mode: "LIVE" } }
);
if (shieldDecision.isDenied()) {
throw new Error(`Shield blocked arguments for tool: ${toolName}`);
}
}
private isPathAllowed(filePath: string): boolean {
return this.config.allowedFilePatterns.some((pattern) => pattern.test(filePath));
}
private isDomainAllowed(url: string): boolean {
try {
const hostname = new URL(url).hostname;
return this.config.allowedFetchDomains.includes(hostname);
} catch {
return false;
}
}
private async validateOutput(toolName: string, result: unknown): Promise<void> {
if (typeof result === "string") {
// Scan output for sensitive information
const sensitiveInfoDecision = await this.config.arcjetClient.protect(
{ ip: "127.0.0.1", headers: {}, json: { output: result } },
{ sensitiveInfo: { mode: "LIVE" } }
);
if (sensitiveInfoDecision.isDenied()) {
console.warn(`Sensitive data detected in output of tool: ${toolName}`);
// Sanitize or block output based on policy
}
}
}
}
Usage Example
Integrate the guard into your agent's tool execution loop. This ensures every tool call passes through security checks.
import arcjet, { shield, sensitiveInfo, rateLimit } from "@arcjet/sdk";
import { AgentToolGuard } from "./tool-guard";
// Initialize Arcjet client
const aj = arcjet({
key: process.env.ARCJET_KEY!,
rules: [
shield({ mode: "LIVE" }),
sensitiveInfo({ mode: "LIVE" }),
rateLimit({ max: 10, window: "1m" }),
],
});
// Configure tool guard
const toolGuard = new AgentToolGuard({
arcjetClient: aj,
allowedFilePatterns: [/^\/safe\/dir\/.*\.txt$/],
allowedFetchDomains: ["api.example.com", "docs.internal.net"],
maxToolCallsPerWindow: 20,
});
// Example tool execution
async function runAgent() {
try {
const content = await toolGuard.execute(
"readFile",
{ path: "/safe/dir/data.txt" },
async () => {
// Actual file read logic
return "File content here";
}
);
console.log("Tool executed successfully:", content);
} catch (error) {
console.error("Tool execution blocked:", error.message);
}
}
Rationale:
- Wrapper Pattern: Centralizes security logic, making it easy to audit and update.
- Arcjet Integration: Leverages Arcjet's in-process capabilities for shield, sensitive info detection, and rate limiting without external dependencies.
- Allowlists: Enforces strict boundaries for file paths and domains, preventing SSRF and unauthorized file access.
- Output Scanning: Treats tool output as untrusted input, scanning for sensitive data before it returns to the LLM context.
Pitfall Guide
-
Relying on System Prompts for Security
- Explanation: Adding instructions like "never read sensitive files" to the system prompt is ineffective. LLMs can be tricked by adversarial inputs, and system prompts are soft preferences, not hard boundaries.
- Fix: Implement deterministic code checks for every tool action. Use allowlists and validation logic that does not depend on the model's judgment.
-
Blocklisting Instead of Allowlisting
- Explanation: Blocking known bad domains or paths is insufficient. Attackers can use variations, encoding, or internal IPs to bypass blocklists.
- Fix: Use allowlists for fetch destinations and file paths. Only permit access to resources the agent explicitly needs.
-
Ignoring Tool Output Injection
- Explanation: Tool outputs are often treated as trusted data. However, outputs can contain prompt injection payloads that influence subsequent agent behavior.
- Fix: Scan all tool outputs for injection patterns and sensitive information before returning them to the LLM context. Treat outputs as untrusted input.
-
Over-Provisioning Agent Credentials
- Explanation: Agents often run with broad permissions, increasing the blast radius of a successful attack.
- Fix: Scope agent credentials to the minimum required permissions. Use service accounts with restricted access and implement least-privilege principles.
-
Missing Tool Inventory
- Explanation: Failing to audit all available tools leads to unsecured capabilities. Agents may have access to dangerous tools that were overlooked during development.
- Fix: Maintain a comprehensive inventory of all tools, their arguments, and potential risks. Regularly review and update this inventory.
-
Assuming Edge WAFs Cover Agents
- Explanation: Edge WAFs only inspect network traffic. They cannot see in-memory tool executions or runtime decisions made by the agent.
- Fix: Deploy in-process guards alongside edge WAFs. Use edge WAFs for perimeter defense and in-process guards for agentic action security.
-
Neglecting Rate Limiting on Tools
- Explanation: Agents can be induced to make excessive tool calls, leading to resource exhaustion or abuse.
- Fix: Implement rate limiting on tool executions. Use Arcjet's rate limiting rules to control the frequency of tool calls per session or user.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Agent with File Access | In-Process Guard + Allowlist | Prevents unauthorized file reads and exfiltration. | Low (Code change) |
| Agent with HTTP Fetch | In-Process Guard + Domain Allowlist | Mitigates SSRF and internal network access. | Low (Code change) |
| High-Volume Agent | Edge WAF + In-Process Rate Limiting | Edge handles volumetric attacks; in-process limits tool abuse. | Medium (Infra + Code) |
| Sensitive Data Handling | In-Process Guard + Output Scanning | Detects and blocks sensitive data leakage. | Low (Code change) |
| Multi-Tenant Agent | Scoped Credentials + In-Process Validation | Ensures tenant isolation and prevents cross-tenant access. | Medium (Infra + Code) |
Configuration Template
Use this template to configure Arcjet for agentic security. Adjust rules based on your specific requirements.
import arcjet, { shield, sensitiveInfo, rateLimit } from "@arcjet/sdk";
export const aj = arcjet({
key: process.env.ARCJET_KEY!,
rules: [
shield({ mode: "LIVE" }),
sensitiveInfo({ mode: "LIVE" }),
rateLimit({
max: 10,
window: "1m",
// Customize rate limit key based on session or user
// key: (req) => req.headers.get("x-session-id"),
}),
],
});
Quick Start Guide
- Install Arcjet SDK: Run
npm install @arcjet/sdk in your project.
- Initialize Client: Create an Arcjet client with shield, sensitive info, and rate limit rules.
- Create Tool Guard: Implement a
ToolGuard class to wrap tool executions with security checks.
- Integrate with Agent: Replace direct tool calls with
toolGuard.execute() in your agent's logic.
- Test and Deploy: Validate security controls with adversarial tests and deploy to production.
By implementing in-process security guards, you can close the agentic action gap and protect your LLM applications from runtime exploitation. Arcjet provides the building blocks to enforce deterministic security policies at the tool execution boundary, ensuring that agent autonomy does not compromise application security.