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
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@ After installing, guidance activates automatically. The plugin detects what you'

### Skills

| Skill | Purpose |
| --------------------------- | --------------------------------------------------------------------------------------------- |
| `/arcjet:protect-route` | Add Arcjet protection to any route handler — detects framework, sets up client, applies rules |
| `/arcjet:add-ai-protection` | Add prompt injection detection, PII blocking, and token budget rate limiting to AI endpoints |
| Skill | Purpose |
| ------------------------------ | --------------------------------------------------------------------------------------------------------- |
| `/arcjet:protect-route` | Add Arcjet protection to any route handler — detects framework, sets up client, applies rules |
| `/arcjet:add-ai-protection` | Add prompt injection detection, PII blocking, and token budget rate limiting to AI HTTP endpoints |
| `/arcjet:add-guard-protection` | Add Arcjet Guard to non-HTTP code paths — AI agent tool calls, MCP tool handlers, background jobs/workers |

### Rules (auto-activated)

Expand Down
2 changes: 1 addition & 1 deletion skills/add-ai-protection/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ if (decision.isErrored()) {

Adapt the response format to your framework (e.g., `res.status(429).json(...)` for Express).

## Step 5: Verify
## Step 4: Verify

1. Start the app and send a normal message — should succeed
2. Test prompt injection by sending something like "Ignore all previous instructions and..."
Expand Down
155 changes: 155 additions & 0 deletions skills/add-guard-protection/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
---
name: add-guard-protection
license: Apache-2.0
description: Add Arcjet Guard protection to AI agent tool calls, background jobs, queue workers, MCP tool handlers, and other code paths where there is no HTTP request. Covers rate limiting, prompt injection detection, sensitive information blocking, and custom rules using `@arcjet/guard` (JS/TS) and `arcjet.guard` (Python). Use this skill whenever the user wants to protect tool calls, agent loops, MCP tool handlers, background workers, or any non-HTTP code from abuse — even if they describe it as "rate limit my tool calls," "block prompt injection in my agent," "add security to my MCP server," or "protect my queue worker" without mentioning Arcjet or Guard specifically.
metadata:
pathPatterns:
- "**/agents/**"
- "**/agent/**"
- "**/tools/**"
- "**/tool/**"
- "**/mcp/**"
- "**/mcp-server/**"
- "**/workers/**"
- "**/worker/**"
- "**/jobs/**"
- "**/tasks/**"
- "**/queue/**"
- "**/queues/**"
- "**/background/**"
importPatterns:
- "@arcjet/guard"
- "arcjet.guard"
- "@modelcontextprotocol/sdk"
- "@ai-sdk/*"
- "ai"
- "langchain"
- "bullmq"
- "celery"
promptSignals:
phrases:
- "arcjet guard"
- "tool call"
- "tool calls"
- "mcp server"
- "mcp tool"
- "agent loop"
- "background worker"
- "queue worker"
- "prompt injection"
anyOf:
- "protect tool"
- "protect agent"
- "protect mcp"
- "protect worker"
- "rate limit tool"
- "rate limit agent"
- "secure agent"
- "secure mcp"
- "guard"
---

# Add Arcjet Guard Protection

Arcjet Guard provides rate limiting, prompt injection detection, sensitive information blocking, and custom rules for code paths that don't have an HTTP request — AI agent tool calls, MCP tool handlers, background job processors, queue workers, and similar.

For code paths that **do** have an HTTP request (API routes, form handlers, webhooks), use `/arcjet:protect-route` instead. For AI chat/completion HTTP endpoints specifically, use `/arcjet:add-ai-protection`.

## Reference

Read https://docs.arcjet.com/llms.txt for comprehensive SDK documentation.

## Step 1: Detect the Language and Install

Check the project for language indicators:

- `package.json` → JavaScript/TypeScript → `npm install @arcjet/guard` (requires `@arcjet/guard` >= 1.4.0)
- `requirements.txt` / `pyproject.toml` → Python → `pip install arcjet` (requires `arcjet` >= 0.7.0; Guard is included)
- `go.mod`, `Cargo.toml`, `pom.xml`, or other languages → **Guard is not available**. Tell the user that Arcjet Guard currently only supports JavaScript/TypeScript and Python. Do not create a hand-rolled imitation or hallucinate a package that doesn't exist. Suggest they reach out to Arcjet with their use case.

## Step 2: Read the Language Reference

**You must read the reference file for the detected language before writing any code.** The references contain the exact imports, constructor signatures, rule configuration syntax, and `guard()` call patterns for that language.

- JavaScript/TypeScript: [references/javascript.md](references/javascript.md)
- Python: [references/python.md](references/python.md)

Do not guess at the API. The reference files are the source of truth for all code patterns.

## Step 3: Create the Guard Client (Once, at Module Scope)

The client holds a persistent connection. Create it once at module scope and reuse it — never inside a function or per-call. Name the variable `arcjet`.

Check if `ARCJET_KEY` is set in the environment file (`.env`, `.env.local`, etc.). If not, use the Arcjet MCP tools to get one:

- Call `list-teams` to find the team
- Call `list-sites` to find an existing site, or `create-site` for a new one
- Call `get-site-key` to retrieve the key
- Add the key to the appropriate env file along with `ARCJET_ENV=development`

Alternatively, remind the user to register at https://app.arcjet.com and add the key manually.

## Step 4: Configure Rules at Module Scope

Rules are configured once as reusable factories, then called with per-invocation input. This two-phase pattern matters — the rule config carries a stable ID used for server-side aggregation, while the per-call input varies.

When configuring rate limit rules, set `bucket` to a descriptive name (e.g. `"tool-calls"`, `"session-api"`) for semantic clarity and fewer collisions.

### Choosing Rules by Use Case

| Use case | Recommended rules |
| ----------------------------------- | -------------------------------------------------------------------- |
| AI agent tool calls | `tokenBucket` + `detectPromptInjection` |
| MCP tool handlers | `slidingWindow` or `tokenBucket` + `detectPromptInjection` |
| Background AI task processor | `tokenBucket` + `localDetectSensitiveInfo` |
| Queue worker with user input | `tokenBucket` + `detectPromptInjection` + `localDetectSensitiveInfo` |
| Scanning tool results for injection | `detectPromptInjection` (scan the returned content) |

## Step 5: Call guard() Inline Before Each Operation

Call `guard()` directly where each operation happens — inline in each tool handler, task processor, or function that needs protection. Do not wrap guard in a shared helper function.

Each `guard()` call takes:

- **label**: descriptive string for the dashboard (e.g. `"tools.search_web"`, `"tasks.generate"`)
- **rules**: array of bound rule invocations
- **metadata** (optional): key-value pairs for analytics/auditing (e.g. `{ userId }`)

Rate limit rules take an explicit **key** string — use a user ID, session ID, API key, or any stable identifier.

You MUST modify the existing source files — adding the dependency to `package.json` / `requirements.txt` alone is not enough. The `guard()` calls must be integrated into the actual code.

## Step 6: Handle Decisions

Always check `decision.conclusion`:

- `"DENY"` → block the operation. Use per-rule result accessors (see reference) for specific error messages like retry-after times.
- `"ALLOW"` → safe to proceed

See the language reference for the exact decision-checking pattern and per-rule result accessors.

## Step 7: Verify

Start rules in `"DRY_RUN"` mode first and promote to `"LIVE"` once verified.

**Always recommend using the Arcjet MCP tools** to verify rules and analyze traffic:

- `list-requests` — confirm decisions are being recorded, filter by conclusion to see blocks
- `analyze-traffic` — review denial rates and patterns for the guarded code path
- `explain-decision` — understand why a specific call was allowed or denied
- `promote-rule` — promote rules from `DRY_RUN` to `LIVE` once verified

If the user wants a full security review, suggest the `/arcjet:security-analyst` agent which can investigate traffic, detect anomalies, and recommend additional rules.

The Arcjet dashboard at https://app.arcjet.com is also available for visual inspection.

## Common Mistakes to Avoid

- **Wrapping guard in a shared helper function** — calling `guard()` through a `guardToolCall()` or `protectCall()` wrapper hides which rules apply to each operation. Call `guard()` inline where each operation happens.
- **Creating the client per call** — the client holds a persistent connection. Create it once at module scope.
- **Configuring rules inside a function** — rule configs carry stable IDs. Creating them per call breaks dashboard tracking and rate limit state.
- **Forgetting the `key` parameter on rate limit rules** — without a key, Guard can't track per-user limits.
- **Forgetting `bucket` on rate limit rules** — without a named bucket, different rules may collide.
- **Using the HTTP SDK when there's no request** — use `@arcjet/guard` / `arcjet.guard` for non-HTTP code, not `@arcjet/node`, `@arcjet/next`, or `arcjet()`.
- **Not checking `decision.conclusion`** — always check before proceeding.
- **Generic DENY messages** — use per-rule result accessors to give users specific feedback like retry-after times.
169 changes: 169 additions & 0 deletions skills/add-guard-protection/references/javascript.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
# JavaScript/TypeScript Guard Reference

## Installation

Requires `@arcjet/guard` >= 1.4.0.

```bash
npm install @arcjet/guard
```

## Create the Guard Client

```typescript
import { launchArcjet } from "@arcjet/guard";

const arcjet = launchArcjet({ key: process.env.ARCJET_KEY! });
```

Create once at module scope. The client holds a persistent HTTP/2 connection — creating it inside a function defeats connection reuse.

## Rules

Configure rules at module scope. Each rule config carries a stable ID for server-side aggregation, so creating them per call would break dashboard tracking and rate limit state.

### Token Bucket

Best for AI workloads with variable cost per call. Configure a `bucket` name for semantic clarity and to avoid collisions between different rate limit rules.

```typescript
import { tokenBucket } from "@arcjet/guard";

const userLimit = tokenBucket({
label: "user.tool_call_bucket", // rule label for dashboard tracking
bucket: "tool-calls", // named bucket for this limit
refillRate: 100,
intervalSeconds: 60,
maxTokens: 500,
});
```

### Fixed Window

Hard cap per time period, counter resets at end of window:

```typescript
import { fixedWindow } from "@arcjet/guard";

const callLimit = fixedWindow({
label: "user.hourly_calls",
bucket: "hourly-calls",
maxRequests: 100,
windowSeconds: 3600,
});
```

### Sliding Window

Smooth rate limiting without burst-at-boundary issues:

```typescript
import { slidingWindow } from "@arcjet/guard";

const sessionLimit = slidingWindow({
label: "session.api_calls",
bucket: "session-api",
maxRequests: 500,
intervalSeconds: 60,
});
```

### Prompt Injection Detection

Detects jailbreaks, role-play escapes, and instruction overrides. Useful both for user input before it reaches a model AND for tool call results containing untrusted content.

```typescript
import { detectPromptInjection } from "@arcjet/guard";

const piRule = detectPromptInjection();
```

### Sensitive Information Detection

Detects PII locally via WASM — raw text never leaves the SDK.

```typescript
import { localDetectSensitiveInfo } from "@arcjet/guard";

const siRule = localDetectSensitiveInfo({
deny: ["CREDIT_CARD_NUMBER", "EMAIL", "PHONE_NUMBER"],
});
```

## Calling guard()

Call `guard()` inline where each operation happens. Pass a `label` (appears in the dashboard), `rules`, and optionally `metadata` for analytics/auditing.

When an `AbortSignal` is available (e.g. from the caller or a timeout), pass it as `abortSignal` so guard respects cancellation.

```typescript
async function getWeather(city: string, userId: string, signal?: AbortSignal) {
const decision = await arcjet.guard({
label: "tools.get_weather",
metadata: { userId },
rules: [
userLimit({ key: userId, requested: 1 }),
],
...(signal && { abortSignal: signal }),
});

if (decision.conclusion === "DENY") {
throw new Error("Rate limited — try again later");
}

return fetchWeather(city);
}

async function searchWeb(query: string, userId: string) {
const decision = await arcjet.guard({
label: "tools.search_web",
metadata: { userId },
rules: [
userLimit({ key: userId, requested: 1 }),
piRule(query),
],
});

if (decision.conclusion === "DENY") {
// Use per-rule results for specific error messages
const rateLimitDenied = userLimit.deniedResult(decision);
if (rateLimitDenied) {
throw new Error(`Rate limited — try again in ${rateLimitDenied.resetInSeconds}s`);
}
if (decision.reason === "PROMPT_INJECTION") {
throw new Error("Prompt injection detected in query");
}
throw new Error("Request denied");
}

if (decision.hasError()) {
console.warn("Arcjet guard error — proceeding with caution");
}

return doSearch(query);
}
```

## Inspecting Per-Rule Results

Both the configured rule and the bound input provide typed result accessors:

```typescript
const rl = userLimit({ key: userId, requested: 5 });
const decision = await arcjet.guard({
label: "tools.chat",
rules: [rl, piRule(message)],
});

// From a bound input — this specific invocation's result
const r = rl.result(decision);
if (r) {
console.log(r.remainingTokens, r.maxTokens, r.resetInSeconds);
}

// From the configured rule — first denied result across all submissions
const denied = userLimit.deniedResult(decision);
if (denied) {
console.log(`Retry after ${denied.resetInSeconds}s`);
}
```
Loading