diff --git a/.devcontainer/devcontainer-lock.json b/.devcontainer/devcontainer-lock.json new file mode 100644 index 0000000..6703402 --- /dev/null +++ b/.devcontainer/devcontainer-lock.json @@ -0,0 +1,14 @@ +{ + "features": { + "ghcr.io/devcontainers/features/common-utils:2.5.7": { + "version": "2.5.7", + "resolved": "ghcr.io/devcontainers/features/common-utils@sha256:dbf431d6b42d55cde50fa1df75c7f7c3999a90cde6d73f7a7071174b3c3d0cc4", + "integrity": "sha256:dbf431d6b42d55cde50fa1df75c7f7c3999a90cde6d73f7a7071174b3c3d0cc4" + }, + "ghcr.io/devcontainers/features/github-cli:1.1.0": { + "version": "1.1.0", + "resolved": "ghcr.io/devcontainers/features/github-cli@sha256:d22f50b70ed75339b4eed1ba9ecde3a1791f90e88d37936517e3bace0bbad671", + "integrity": "sha256:d22f50b70ed75339b4eed1ba9ecde3a1791f90e88d37936517e3bace0bbad671" + } + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md index f213661..fcb0098 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,20 @@ this project adheres to [Semantic Versioning](https://semver.org/). ## [Unreleased] +### Changed + +- Replaced separate `add-request-protection` and `add-guard-protection` skills + with the unified `arcjet` skill from + [arcjet/skills](https://github.com/arcjet/skills). The unified skill covers + both HTTP route protection and non-HTTP code paths (Guard) in a single + workflow with shared references. +- `skills/add-request-protection/`, `skills/add-guard-protection/`, + `skills/protect-route/`, and `skills/add-ai-protection/` are now + deprecation stubs pointing to the unified `arcjet` skill. The alias + directories are preserved so saved transcripts and existing workflows + continue to resolve. +- README updated to reflect the unified skill structure. + ### Added - Arcjet CLI integration. The plugin now invokes the CLI for capabilities diff --git a/README.md b/README.md index 28be574..525f351 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ The [Arcjet plugin](https://github.com/arcjet/arcjet-plugin) turns any supported - **MCP integration** — connects to the [Arcjet MCP Server](https://docs.arcjet.com/mcp-server) for traffic analysis, request inspection, IP investigation, and remote rule management - **CLI integration** — invokes the [Arcjet CLI](https://docs.arcjet.com/cli) for authentication, site/key setup, live request streaming, and remote rule management - **Security-aware coding rules** — framework-specific guidance activates automatically when you work in route handlers, API endpoints, and AI/LLM code -- **Skills** — task-oriented workflows sourced from [arcjet/skills](https://github.com/arcjet/skills) for adding protection to HTTP routes and non-HTTP code paths +- **Skills** — task-oriented workflow sourced from [arcjet/skills](https://github.com/arcjet/skills) for adding protection to any code path (HTTP routes and non-HTTP code) - **Security analyst agent** — investigates threats, analyzes traffic, and manages rules via MCP ## Installation @@ -37,19 +37,20 @@ After installing, guidance activates automatically. The plugin detects what you' The plugin's skills are sourced from [arcjet/skills](https://github.com/arcjet/skills), the canonical agent skills surface for Arcjet. -| Skill | Purpose | -| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `/arcjet:add-request-protection` | Add Arcjet protection to any HTTP route or endpoint — detects framework, sets up client, applies rules. Includes AI/LLM endpoint guidance (chat, completion). | -| `/arcjet:add-guard-protection` | Add Arcjet Guard to non-HTTP code paths — AI agent tool calls, MCP tool handlers, background jobs/workers | +| Skill | Purpose | +| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `/arcjet` | Add Arcjet security protection to any code path — HTTP route handlers, API endpoints, AI agent tool calls, MCP servers, background jobs, and queue workers. The unified Arcjet skill. | #### Deprecated aliases The previous skill names are kept as deprecation aliases. Invoking them tells the user the new name and then proceeds with the canonical workflow — existing prompts, prompts in saved transcripts, and project-local references continue to work. -| Deprecated alias | Replacement | -| --------------------------- | --------------------------------------------------------------------------------------------------- | -| `/arcjet:protect-route` | `/arcjet:add-request-protection` | -| `/arcjet:add-ai-protection` | `/arcjet:add-request-protection` (HTTP endpoints) or `/arcjet:add-guard-protection` (non-HTTP code) | +| Deprecated alias | Replacement | +| -------------------------------- | ----------- | +| `/arcjet:add-request-protection` | `/arcjet` | +| `/arcjet:add-guard-protection` | `/arcjet` | +| `/arcjet:protect-route` | `/arcjet` | +| `/arcjet:add-ai-protection` | `/arcjet` | ### Rules (auto-activated) diff --git a/agents/security-analyst.md b/agents/security-analyst.md index 5a8f502..c160f16 100644 --- a/agents/security-analyst.md +++ b/agents/security-analyst.md @@ -92,7 +92,7 @@ Understand the boundary: - **Remote rules** (managed via MCP, immediate effect, no deploy): `rate_limit`, `bot`, `shield`, `filter` - **SDK rules** (require code changes and deployment): `prompt_injection`, `sensitive_info`, `email`, `signup` -When recommending rules that need request body analysis, explain that these must be added via the SDK and provide guidance on which skill to use (`/arcjet:protect-route` or `/arcjet:add-ai-protection`). +When recommending rules that need request body analysis, explain that these must be added via the SDK and provide guidance on using the `/arcjet` skill. ## Tone diff --git a/skills/add-ai-protection/SKILL.md b/skills/add-ai-protection/SKILL.md index c9d9022..96d26ac 100644 --- a/skills/add-ai-protection/SKILL.md +++ b/skills/add-ai-protection/SKILL.md @@ -1,24 +1,14 @@ --- name: add-ai-protection license: Apache-2.0 -description: "Deprecated alias. Use /arcjet:add-request-protection for HTTP AI/LLM endpoints (chat, completion routes) or /arcjet:add-guard-protection for non-HTTP code (agent tool calls, MCP handlers, background workers). Covers prompt injection detection, PII blocking, and token budget rate limiting." +description: "Deprecated: use the `arcjet` skill instead. Adds security protection to AI/LLM endpoints and non-HTTP code paths — prompt injection detection, PII blocking, and token budget rate limiting." metadata: author: arcjet internal: true --- -# Deprecated — Use `/arcjet:add-request-protection` or `/arcjet:add-guard-protection` +# Deprecated — Use `arcjet` -`/arcjet:add-ai-protection` has been split into two canonical skills: +This skill has been replaced by the unified `arcjet` skill, which covers HTTP route protection (including AI/LLM endpoints) plus non-HTTP code paths (tool calls, MCP handlers, queue workers) in a single skill. -- **`/arcjet:add-request-protection`** — for HTTP routes serving AI/LLM endpoints (chat, completion, generation). Covers prompt injection detection, PII blocking, token budget rate limiting, and bot/shield protection at the HTTP layer. -- **`/arcjet:add-guard-protection`** — for non-HTTP code (AI agent tool calls, MCP tool handlers, background jobs, queue workers). Same protections via `@arcjet/guard` / `arcjet.guard`. - -## Instructions for the agent - -1. **Tell the user:** "`/arcjet:add-ai-protection` is deprecated. Use `/arcjet:add-request-protection` for HTTP AI endpoints, or `/arcjet:add-guard-protection` for non-HTTP code (agent tool calls, MCP handlers, background workers)." -2. **Pick the right replacement based on context:** - - If the file under consideration is an HTTP route handler (e.g. `app/api/chat/route.ts`, `pages/api/completion.ts`, FastAPI/Flask endpoint) → follow `/arcjet:add-request-protection` (`skills/add-request-protection/SKILL.md`) and use its "AI / LLM Endpoints" section. - - If the file is a tool handler, MCP server handler, agent loop, queue worker, or other non-HTTP code path → follow `/arcjet:add-guard-protection` (`skills/add-guard-protection/SKILL.md`). - - If unclear, ask the user which context applies before proceeding. -3. Do not duplicate the canonical skill content here — read and follow the chosen skill directly. +Please use the `arcjet` skill instead. diff --git a/skills/add-guard-protection/SKILL.md b/skills/add-guard-protection/SKILL.md index 0e1bbc6..c0b3a61 100644 --- a/skills/add-guard-protection/SKILL.md +++ b/skills/add-guard-protection/SKILL.md @@ -1,258 +1,14 @@ --- 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. Uses the Arcjet CLI (`npx @arcjet/cli` or `brew install arcjet`) for authentication and site/key setup. +description: "Deprecated: use the `arcjet` skill instead. Adds Arcjet Guard protection to AI agent tool calls, background jobs, queue workers, and other non-HTTP code paths." metadata: author: arcjet - 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" + internal: true --- -# Add Arcjet Guard Protection +# Deprecated — Use `arcjet` -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. +This skill has been replaced by the unified `arcjet` skill, which covers Guard protection (this skill's scope) plus HTTP route protection in a single skill. -For code paths that **do** have an HTTP request (API routes, form handlers, webhooks, AI chat/completion endpoints), use `/arcjet:add-request-protection` instead. - -## Step 0: Set Up the Arcjet CLI - -The Arcjet CLI is the primary tool for authenticating, managing sites, configuring remote rules, and monitoring traffic. Install it if not already available: - -```bash -# Via npx (no install required) -npx @arcjet/cli --help - -# Or install globally via npm -npm install -g @arcjet/cli - -# Or via Homebrew -brew install arcjet -``` - -### Authenticate - -```bash -arcjet auth login -``` - -Opens the browser for authentication. Check status with `arcjet auth status`. - -### Site & Key Setup - -```bash -# List your teams -arcjet teams list - -# List sites for a team -arcjet sites list --team-id - -# Create a new site -arcjet sites create --team-id --name "My Guard App" --confirm - -# Get the SDK key for a site -arcjet sites get-key --site-id -``` - -Add the key to your environment file (`.env`, `.env.local`, etc.) as `ARCJET_KEY`. - -## 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, obtain the key in this priority order: - -1. **CLI (preferred):** Run `arcjet sites get-key --site-id ` (requires `arcjet auth login` first — see Step 0) -2. **MCP:** If the Arcjet MCP server is connected, use it to list sites and retrieve the key -3. **Manual (last resort):** Add a placeholder and tell the user to get a key from https://app.arcjet.com - -## 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 Guard Decisions with the CLI (Coming Soon) - -> **Note:** The `arcjet guards` CLI subcommand is not yet released. Once available, use this feedback loop to verify guard decisions are firing correctly. - -After adding guard code, use the CLI to verify decisions are firing correctly. This creates a feedback loop: run the app, trigger a guard, inspect the decision, adjust if needed. - -### 1. Start Watching - -In a separate terminal, start streaming guard decisions: - -```bash -arcjet guards watch --site-id -``` - -This polls for new guard decisions and prints them as they arrive. Use `--conclusion DENY` to filter to denials only, or `--interval 2` for faster polling. - -### 2. Trigger the Guard - -Run the application and exercise the code paths that call `guard()`. Each call should produce a decision visible in the watch output. - -### 3. Inspect Decisions - -If a decision doesn't match expectations, inspect it: - -```bash -# List recent guard decisions -arcjet guards list --site-id - -# Get per-rule breakdown for a specific decision -arcjet guards details --site-id --decision-id -``` - -The details view shows each rule execution, its mode (live/dry-run), conclusion, reason, and whether it was skipped — use this to diagnose why a guard allowed or denied unexpectedly. - -### 4. Adjust and Repeat - -If rules aren't firing as expected: - -- Check the `label` matches what appears in the decision -- Verify the `key` is correct for rate limit rules (wrong key = wrong bucket) -- Confirm the `bucket` name is unique per rule -- Check rule ordering — rules execute in array order and a DENY from an earlier rule short-circuits later ones - -Then re-run and watch again until decisions match expectations. - -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. - -## CLI Quick Reference - -| Task | Command | -| ---------------------- | ----------------------------------------------------------------------- | -| Install/run CLI | `npx @arcjet/cli` or `brew install arcjet` | -| Authenticate | `arcjet auth login` | -| Check auth status | `arcjet auth status` | -| List teams | `arcjet teams list` | -| List sites | `arcjet sites list --team-id ` | -| Create site | `arcjet sites create --team-id --name "Name" --confirm` | -| Get SDK key | `arcjet sites get-key --site-id ` | -| Watch guard decisions | `arcjet guards watch --site-id ` (coming soon) | -| List guard decisions | `arcjet guards list --site-id ` (coming soon) | -| Guard decision details | `arcjet guards details --site-id --decision-id ` (coming soon) | - -### Global Flags - -All commands support: - -- `--output text|json` — output format (default: text on TTY, json otherwise) -- `--fields ` — comma-separated fields to include in JSON output -- `--no-color` — disable ANSI colors (also honors `NO_COLOR` env var) -- `--timeout ` — max execution time (e.g. `30s`, `5m`; 0 disables) - -### Exit Codes - -| Code | Meaning | -| ---- | ----------------------------------------------------------- | -| 0 | Success | -| 1 | General error (unknown command, API failure, network error) | -| 2 | Authentication error (not logged in, token expired) | -| 3 | Input validation error (invalid ID, value out of range) | -| 4 | Confirmation required (mutation needs `--confirm`) | +Please use the `arcjet` skill instead. diff --git a/skills/add-guard-protection/references/javascript.md b/skills/add-guard-protection/references/javascript.md deleted file mode 100644 index 74e90ab..0000000 --- a/skills/add-guard-protection/references/javascript.md +++ /dev/null @@ -1,171 +0,0 @@ -# JavaScript/TypeScript Guard Reference - -## Installation - -Requires `@arcjet/guard` >= 1.4.0. - -```bash -npm install @arcjet/guard -``` - -Requires `@arcjet/guard` >= 1.4.0. - -## 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`); -} -``` diff --git a/skills/add-guard-protection/references/python.md b/skills/add-guard-protection/references/python.md deleted file mode 100644 index a2f8277..0000000 --- a/skills/add-guard-protection/references/python.md +++ /dev/null @@ -1,169 +0,0 @@ -# Python Guard Reference - -## Installation - -Requires `arcjet` >= 0.7.0. Guard is included in the `arcjet` package. - -```bash -pip install arcjet -``` - -## Create the Guard Client - -### Async - -```python -import os -from arcjet.guard import launch_arcjet - -arcjet = launch_arcjet(key=os.environ["ARCJET_KEY"]) -``` - -### Sync (for non-async code) - -```python -from arcjet.guard import launch_arcjet_sync - -arcjet = launch_arcjet_sync(key=os.environ["ARCJET_KEY"]) -``` - -Create once at module scope. The client holds a persistent 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. - -```python -from arcjet.guard import TokenBucket - -user_limit = TokenBucket( - label="user.task_bucket", - bucket="task-calls", - refill_rate=100, - interval_seconds=60, - max_tokens=500, -) -``` - -### Fixed Window - -Hard cap per time period: - -```python -from arcjet.guard import FixedWindow - -call_limit = FixedWindow( - label="user.hourly_calls", - bucket="hourly-calls", - max_requests=100, - window_seconds=3600, -) -``` - -### Sliding Window - -Smooth rate limiting: - -```python -from arcjet.guard import SlidingWindow - -api_limit = SlidingWindow( - label="session.api_calls", - bucket="session-api", - max_requests=500, - interval_seconds=60, -) -``` - -### Prompt Injection Detection - -Detects jailbreaks, role-play escapes, and instruction overrides. - -```python -from arcjet.guard import DetectPromptInjection - -pi_rule = DetectPromptInjection() -``` - -### Sensitive Information Detection - -Detects PII locally — raw text never leaves the SDK. - -```python -from arcjet.guard import LocalDetectSensitiveInfo - -si_rule = LocalDetectSensitiveInfo( - deny=["CREDIT_CARD_NUMBER", "EMAIL", "PHONE_NUMBER"], -) -``` - -## Calling guard() - -Call `guard()` inline where each operation happens. Pass a `label`, `rules`, and optionally `metadata` for analytics/auditing. - -### Async - -```python -async def process_task(user_id: str, message: str): - decision = await arcjet.guard( - label="tasks.generate", - metadata={"user_id": user_id}, - rules=[ - user_limit(key=user_id, requested=1), - pi_rule(message), - ], - ) - - if decision.conclusion == "DENY": - # Use per-rule results for specific error messages - rate_denied = user_limit.denied_result(decision) - if rate_denied: - raise RuntimeError(f"Rate limited — try again in {rate_denied.reset_in_seconds}s") - raise RuntimeError(f"Blocked: {decision.reason}") - - if decision.has_error(): - print("Arcjet guard error — proceeding with caution") - - # Safe to proceed... -``` - -### Sync - -```python -def process_task(user_id: str, message: str): - decision = arcjet.guard( - label="tasks.generate", - metadata={"user_id": user_id}, - rules=[ - user_limit(key=user_id, requested=1), - pi_rule(message), - ], - ) - - if decision.conclusion == "DENY": - raise RuntimeError(f"Blocked: {decision.reason}") - - # Safe to proceed... -``` - -## Inspecting Per-Rule Results - -```python -rl = user_limit(key=user_id, requested=5) -decision = await arcjet.guard( - label="tools.chat", - rules=[rl, pi_rule(message)], -) - -r = rl.result(decision) -if r: - print(r.remaining_tokens, r.max_tokens, r.reset_in_seconds) - -denied = user_limit.denied_result(decision) -if denied: - print(f"Retry after {denied.reset_in_seconds}s") -``` diff --git a/skills/add-request-protection/SKILL.md b/skills/add-request-protection/SKILL.md index 888c3c6..3e98ba0 100644 --- a/skills/add-request-protection/SKILL.md +++ b/skills/add-request-protection/SKILL.md @@ -1,380 +1,14 @@ --- name: add-request-protection license: Apache-2.0 -description: Add security protection to a server-side route or endpoint — rate limiting, bot detection, email validation, and abuse prevention. Works across frameworks including Next.js, Express, Fastify, SvelteKit, Remix, Bun, Deno, NestJS, and Python (Django/Flask). Use this skill when the user wants to protect an API route, form handler, auth endpoint, or webhook from abuse, even if they describe it as "add rate limiting," "block bots," "prevent brute force," or "secure my endpoint" without mentioning Arcjet specifically. Uses the Arcjet CLI (`npx @arcjet/cli` or `brew install arcjet`) for authentication, site/key setup, remote rule management, and traffic verification. +description: "Deprecated: use the `arcjet` skill instead. Adds security protection to a server-side route or endpoint — rate limiting, bot detection, email validation, and abuse prevention." metadata: author: arcjet - pathPatterns: - - "app/**/route.ts" - - "app/**/route.js" - - "app/**/page.{ts,tsx}" - - "pages/api/**" - - "src/pages/api/**" - - "src/app/**/route.*" - - "**/server.{ts,js}" - - "**/app.{ts,js}" - - "**/routes/**" - - "**/*.py" - - "app/api/chat/**" - - "app/api/completion/**" - - "src/app/api/chat/**" - - "src/app/api/completion/**" - - "**/chat/**" - - "**/ai/**" - - "**/llm/**" - - "**/api/generate*" - - "**/api/chat*" - - "**/api/completion*" - importPatterns: - - "@arcjet/*" - - "arcjet" - - "ai" - - "@ai-sdk/*" - - "openai" - - "@anthropic-ai/sdk" - - "langchain" - promptSignals: - phrases: - - "arcjet" - - "rate limit" - - "bot protection" - - "bot detection" - - "waf" - - "shield" - - "prompt injection" - - "pii" - - "sensitive info" - - "ai security" - - "llm security" - anyOf: - - "protect route" - - "protect endpoint" - - "add security" - - "block bots" - - "rate limiting" - - "protect ai" - - "block pii" - - "detect injection" - - "token budget" + internal: true --- -# Add Arcjet Protection to a Route +# Deprecated — Use `arcjet` -Add runtime security to a route handler using Arcjet. This skill guides you through setting up the CLI, detecting the framework, configuring rules, and verifying protection. +This skill has been replaced by the unified `arcjet` skill, which covers HTTP route protection (this skill's scope) plus non-HTTP code paths (tool calls, MCP handlers, queue workers) in a single skill. -For non-HTTP code paths (AI agent tool calls, MCP tool handlers, background jobs, queue workers), use `/arcjet:add-guard-protection` instead. - -## Reference - -Read https://docs.arcjet.com/llms.txt for comprehensive SDK documentation covering all frameworks, rule types, and configuration options. - -## Step 0: Set Up the Arcjet CLI - -The Arcjet CLI is the primary tool for authenticating, managing sites, configuring remote rules, and verifying protection. Install it if not already available: - -```bash -# Via npx (no install required) -npx @arcjet/cli --help - -# Or install globally via npm -npm install -g @arcjet/cli - -# Or via Homebrew -brew install arcjet -``` - -### Authenticate - -```bash -arcjet auth login -``` - -Opens the browser for authentication. Check status with `arcjet auth status`. - -### Site & Key Setup - -```bash -# List your teams -arcjet teams list - -# List sites for a team -arcjet sites list --team-id - -# Create a new site -arcjet sites create --team-id --name "My App" --confirm - -# Get the SDK key for a site -arcjet sites get-key --site-id -``` - -Add the key to your environment file (`.env.local` for Next.js/Astro, `.env` for others) as `ARCJET_KEY`. - -## Step 1: Detect the Framework - -Check the project for framework indicators: - -- `package.json` dependencies: `next`, `express`, `fastify`, `@nestjs/core`, `@sveltejs/kit`, `hono`, `@remix-run/node`, `react-router`, `astro`, `nuxt` -- `bun.lockb` or `bun.lock` → Bun runtime -- `deno.json` → Deno runtime -- `pyproject.toml` or `requirements.txt` with `fastapi` or `flask` → Python - -Select the correct Arcjet adapter package: - -| Framework | Package | -| ------------------------ | ---------------------- | -| Next.js | `@arcjet/next` | -| Express / Node.js / Hono | `@arcjet/node` | -| Fastify | `@arcjet/fastify` | -| NestJS | `@arcjet/nest` | -| SvelteKit | `@arcjet/sveltekit` | -| Remix | `@arcjet/remix` | -| React Router | `@arcjet/react-router` | -| Astro | `@arcjet/astro` | -| Bun | `@arcjet/bun` | -| Deno | `npm:@arcjet/deno` | -| Python (FastAPI/Flask) | `arcjet` (pip) | - -## Step 2: Check for Existing Arcjet Setup - -Search the project for an existing shared Arcjet client file (commonly `lib/arcjet.ts`, `src/lib/arcjet.ts`, `lib/arcjet.py`, or similar). - -**If no client exists:** - -1. Install the correct adapter package. -2. Check if `ARCJET_KEY` is set in the environment file (`.env.local` for Next.js/Astro, `.env` for others). If not, obtain the key in this priority order: - 1. **CLI (preferred):** Run `arcjet sites get-key --site-id ` (requires `arcjet auth login` first — see Step 0) - 2. **MCP:** If the Arcjet MCP server is connected, use `list-teams` → `list-sites` → `get-site-key` - 3. **Manual (last resort):** Add a placeholder and tell the user to get a key from https://app.arcjet.com - - Also add `ARCJET_ENV=development` to the env file -3. Create a shared client file with `shield()` as the base rule. This file should export the Arcjet instance for reuse across routes with `withRule()`. - -**If a client already exists:** Import it. Do not create a new instance. - -## Step 3: Choose Protection Rules - -Select rules based on the route's purpose. If the user specified what they want, use that. Otherwise, infer from context: - -| Route type | Recommended rules | -| ----------------------- | ------------------------------------------------------------------------------------------------------------ | -| Public API endpoint | `shield()` + `detectBot()` + `slidingWindow()` (use `fixedWindow()` only if hard per-window caps are needed) | -| Form handler / signup | `shield()` + `validateEmail()` + `slidingWindow()` | -| Authentication endpoint | `shield()` + `slidingWindow()` (strict, low limits) | -| AI / LLM endpoint | `shield()` + `detectBot()` + `tokenBucket()` + content filtering (see AI Endpoints section below) | -| Webhook receiver | `shield()` + filter rules for allowed IPs | -| General server route | `shield()` + `detectBot()` | - -For routes that need to detect sophisticated bots (headless browsers, advanced scrapers) — especially form submissions, login/signup pages, and other abuse-prone endpoints — recommend adding Arcjet advanced signals. This is a browser-based detection system using client-side telemetry that complements server-side `detectBot()` rules. See https://docs.arcjet.com/bot-protection/advanced-signals for setup instructions. - -Apply route-specific rules using `withRule()` on the shared instance — do not modify the shared instance directly. - -## Step 4: Add Protection to the Handler - -Call `protect()` **inside** the route handler (not in middleware), only **once** per request, passing the framework's request object directly. For Next.js pages/server components: use `import { request } from "@arcjet/next"` then `const req = await request()`. - -Use this pattern: - -```typescript -const decision = await aj.protect(req); - -if (decision.isDenied()) { - if (decision.reason.isRateLimit()) { - return Response.json( - { error: "Too many requests" }, - { status: 429 }, - ); - } - if (decision.reason.isBot() || decision.reason.isShield() || decision.reason.isFilterRule()) { - return Response.json( - { error: "Forbidden" }, - { status: 403 }, - ); - } - if (decision.reason.isSensitiveInfo()) { - return Response.json( - { error: "Bad request" }, - { status: 400 }, - ); - } -} - -// Arcjet fails open — log errors but allow the request -if (decision.isErrored()) { - console.warn("Arcjet error:", decision.reason.message); -} - -// Proceed with route handler logic... -``` - -Adapt the response format to your framework (e.g., `res.status(429).json(...)` for Express, `JsonResponse` for Django). - -## AI / LLM Endpoints - -For HTTP routes serving AI chat or completion endpoints, layer these rules on top of the base protection: - -- **Prompt injection detection** — `detectPromptInjection()` (JS) / `detect_prompt_injection()` (Python). Pass the user message via `detectPromptInjectionMessage` / `detect_prompt_injection_message` at `protect()` time. -- **Sensitive info / PII blocking** — `sensitiveInfo({ deny: ["EMAIL", "CREDIT_CARD_NUMBER", "PHONE_NUMBER", "IP_ADDRESS"] })` (JS) / `detect_sensitive_info(deny=[...])` (Python). Pass the user message via `sensitiveInfoValue` / `sensitive_info_value` at `protect()` time. Sensitive info detection runs **locally in WASM** — no user data leaves the process. Only available in route handlers, not in Next.js pages or server actions. -- **Token budget rate limiting** — use `tokenBucket()` / `token_bucket()` instead of `slidingWindow()` for AI endpoints. The `requested` parameter can be set proportional to actual model token usage, directly linking rate limiting to cost. Recommended starting config: `capacity: 10`, `refillRate: 5`, `interval: "10s"`. Set `characteristics: ["userId"]` for authenticated tracking. - -Compose all parameters in a single `protect()` call: - -```typescript -const userMessage = req.body.message; - -const decision = await aj.protect(req, { - requested: 1, - sensitiveInfoValue: userMessage, - detectPromptInjectionMessage: userMessage, -}); - -if (decision.isDenied()) { - if (decision.reason.isPromptInjection()) { - return Response.json( - { error: "Your message was flagged as potentially harmful." }, - { status: 400 }, - ); - } - if (decision.reason.isSensitiveInfo()) { - return Response.json( - { error: "Your message contains sensitive information that cannot be processed." }, - { status: 400 }, - ); - } - // ... fall through to base rate-limit/bot/shield handling above -} -``` - -**Streaming responses:** Call `protect()` **before** opening the stream. If denied, return a plain error response — don't start streaming and then abort. - -**Vercel AI SDK / multi-provider:** Arcjet operates at the HTTP layer, independent of the model provider. Call `protect()` before `streamText()` / `generateText()`. - -For non-HTTP AI code paths (agent tool calls, MCP tool handlers, background workers), use `/arcjet:add-guard-protection` instead. - -## Step 5: Verify with the CLI - -After adding protection, use the CLI to verify decisions are firing correctly. This creates a feedback loop: start the app, hit the route, inspect decisions, adjust if needed. - -### 1. Start Watching - -In a separate terminal, stream live request decisions: - -```bash -arcjet watch --site-id -``` - -This polls for new decisions and prints them as they arrive. Use `--conclusion DENY` to filter to denials only, or `--interval 2` for faster polling. - -### 2. Hit the Protected Route - -Start the app and send requests to the protected route. Each request should produce a decision visible in the watch output. - -### 3. Inspect Decisions - -If a decision doesn't match expectations: - -```bash -# List recent requests (filter to denials) -arcjet requests list --site-id --conclusion DENY --limit 10 - -# Get full details for a specific request -arcjet requests details --site-id --request-id - -# Plain-English explanation of why a request was allowed/denied -arcjet requests explain --site-id --request-id -``` - -### 4. Adjust and Repeat - -If rules aren't firing as expected, adjust the code and re-test. Use `arcjet watch` to confirm each change produces the expected decisions. - -The Arcjet dashboard at https://app.arcjet.com is also available for visual inspection. - -If the user wants a full security review, suggest the `/arcjet:security-analyst` agent which can investigate traffic, detect anomalies, and recommend additional rules. - -## Step 6: Manage Remote Rules via CLI (Optional) - -Remote rules apply globally to all requests for a site and can be managed without code changes or redeployment. Supported types: `rate_limit`, `bot`, `shield`, `filter`. - -```bash -# Create a rule (always starts in DRY_RUN) -arcjet rules create --site-id --type rate_limit --max 100 --window 60 --confirm -arcjet rules create --site-id --type shield --confirm -arcjet rules create --site-id --type bot --deny CATEGORY:SEARCH_ENGINE --confirm - -# Check what a dry-run rule would block -arcjet analyze dry-run-impact --site-id - -# Promote to LIVE once verified -arcjet rules promote --site-id --rule-id --confirm - -# List / update / delete rules -arcjet rules list --site-id -arcjet rules update --site-id --rule-id --max 200 --confirm -arcjet rules delete --site-id --rule-id --confirm -``` - -## Step 7: Traffic Analysis - -Use the CLI to monitor traffic patterns and investigate issues: - -```bash -# Full security briefing (traffic, denials, quota, active rules) -arcjet briefing --site-id - -# Traffic analysis over 14 days -arcjet analyze traffic --site-id --days 14 - -# Detect anomalies (spikes, geographic shifts, new threats) -arcjet analyze anomalies --site-id - -# Investigate a specific IP -arcjet analyze ip --site-id --ip 1.2.3.4 -``` - -## Common Mistakes to Avoid - -- Creating a new Arcjet instance per request (causes connection overhead) -- Using Arcjet in Next.js middleware (fires on every request, no route context) -- Calling `protect()` multiple times in one request (double-counts rate limits) -- Hardcoding `ARCJET_KEY` instead of using environment variables -- Using `app.use()` as Express middleware instead of per-route protection -- For AI endpoints: starting a stream before calling `protect()` — if the request is denied mid-stream, the client gets a broken response -- For AI endpoints: forgetting `sensitiveInfoValue` / `detectPromptInjectionMessage` at `protect()` time silently skips those checks - -## CLI Quick Reference - -| Task | Command | -| --------------------- | -------------------------------------------------------------- | -| Install/run CLI | `npx @arcjet/cli` or `brew install arcjet` | -| Authenticate | `arcjet auth login` | -| Check auth status | `arcjet auth status` | -| List teams | `arcjet teams list` | -| List sites | `arcjet sites list --team-id ` | -| Create site | `arcjet sites create --team-id --name "Name" --confirm` | -| Get SDK key | `arcjet sites get-key --site-id ` | -| Watch live requests | `arcjet watch --site-id ` | -| List requests | `arcjet requests list --site-id ` | -| Explain a decision | `arcjet requests explain --site-id --request-id ` | -| Create rule (DRY_RUN) | `arcjet rules create --site-id --type ...` | -| List rules | `arcjet rules list --site-id ` | -| Promote to LIVE | `arcjet rules promote --site-id --rule-id --confirm` | -| Security briefing | `arcjet briefing --site-id ` | -| Analyze traffic | `arcjet analyze traffic --site-id ` | - -### Global Flags - -All commands support: - -- `--output text|json` — output format (default: text on TTY, json otherwise) -- `--fields ` — comma-separated fields to include in JSON output -- `--no-color` — disable ANSI colors (also honors `NO_COLOR` env var) -- `--timeout ` — max execution time (e.g. `30s`, `5m`; 0 disables) - -### Exit Codes - -| Code | Meaning | -| ---- | ----------------------------------------------------------- | -| 0 | Success | -| 1 | General error (unknown command, API failure, network error) | -| 2 | Authentication error (not logged in, token expired) | -| 3 | Input validation error (invalid ID, value out of range) | -| 4 | Confirmation required (mutation needs `--confirm`) | +Please use the `arcjet` skill instead. diff --git a/skills/arcjet/SKILL.md b/skills/arcjet/SKILL.md new file mode 100644 index 0000000..108ea01 --- /dev/null +++ b/skills/arcjet/SKILL.md @@ -0,0 +1,163 @@ +--- +name: arcjet +license: Apache-2.0 +description: Add Arcjet security protection to any code path — HTTP route handlers, API endpoints, AI agent tool calls, MCP servers, background jobs, and queue workers. Covers rate limiting, bot detection, email validation, prompt injection detection, sensitive information blocking, and abuse prevention. Works across Next.js, Express, Fastify, SvelteKit, Remix, Bun, Deno, NestJS, FastAPI, Flask, and non-HTTP contexts. Use this skill when the user wants to add security, rate limiting, bot protection, or abuse prevention to any part of their application — whether they say "protect my API," "rate limit tool calls," "block bots," "secure my endpoint," "add security to my MCP server," or "prevent abuse" without mentioning Arcjet specifically. +metadata: + author: arcjet +--- + +# Arcjet + +## Contents + +- [Add Arcjet Protection to Your App](#add-arcjet-protection-to-your-app) +- [Choosing Protections](#choosing-protections) +- [Resources](#resources) + +## Add Arcjet Protection to Your App + +### Checklist + +- [ ] **Step 1:** Verify language support (JS/TS or Python only — stop if unsupported) +- [ ] **Step 2:** Connect to Arcjet platform (CLI → MCP → manual dashboard) +- [ ] **Step 3:** Detect protection type and read the appropriate reference file +- [ ] **Step 4:** Implement protection (separate client file, correct SDK, correct patterns) +- [ ] **Step 5:** Verify decisions are firing correctly (trigger a real call, then check CLI / MCP / dashboard) + +### Step 1: Check Language Support + +If the project's server-side code is not JavaScript, TypeScript, or Python → tell the user in chat that Arcjet doesn't support their language yet. Don't modify the project, don't write a `NOTES.md`, don't invent a package. Just say it and stop. + +### Step 2: Get an ARCJET_KEY into the project's env file + +Before writing any code, the project needs a real `ARCJET_KEY` in its env file. Don't write Arcjet code first and "leave the key as a TODO" — that just produces dead code. Get the key first, then wire it up. + +**In order of preference:** + +1. **Arcjet CLI** (preferred). Check whether you're already signed in, then retrieve a key. +2. **Arcjet MCP server** (endpoint: `https://api.arcjet.com/mcp`) — for clients with built-in MCP. See [references/mcp.md](references/mcp.md). +3. **Manual** (last resort): tell the user to grab a key from https://app.arcjet.com. + +#### CLI bootstrap (the normal path) + +```bash +npx -y @arcjet/cli@latest auth status # is the user already signed in? +# if not signed in: +npx -y @arcjet/cli@latest auth login # browser device flow, see references/cli.md + +npx -y @arcjet/cli@latest teams list --output json --fields id,name +npx -y @arcjet/cli@latest sites list --team-id --output json --fields id,name +# if no suitable site exists: +npx -y @arcjet/cli@latest sites create --team-id --name "" + +npx -y @arcjet/cli@latest sites get-key --site-id --output json --fields key +``` + +Write the `key` value to the project's env file as `ARCJET_KEY=ajkey_...`. Match whatever the project already does — filename, `.env.example` companion, `.gitignore` entry. If the project doesn't have a convention yet, default to whatever the framework expects and add the env file to `.gitignore`. Never hardcode the key in source. + +See [references/cli.md](references/cli.md) for install options beyond `npx`, agent-mode flags, and the full command reference. + +#### Install the SDK with the project's package manager + +Once you know which SDK you need (Step 3 below), install it via the package manager the project already uses — `npm install`, `pnpm add`, `yarn add`, `bun add`, `pip install`, `uv add`, `poetry add`, etc. Don't hand-edit `package.json` / `requirements.txt` and guess a version: typed versions tend to be wrong (`arcjet>=1.0.0` doesn't exist for the Python SDK; `^1.0.0` is stale for `@arcjet/next`), and the lockfile won't get updated. Let the package manager pick the real version and pin it. + +### Step 3: Detect Protection Type and Read Reference + +Determine which protection type applies: + +| | **Request-based** | **Guard** | +| --------------- | ------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | +| **When to use** | Code has an HTTP request object (Express `req`, Next.js `Request`, FastAPI `Request`, etc.) | No HTTP request (tool calls, MCP handlers, queue workers, background jobs, agent loops) | +| **JS/TS SDK** | `@arcjet/next`, `@arcjet/node`, `@arcjet/fastify`, etc. | `@arcjet/guard` (>= 1.4.0) | +| **Python SDK** | `arcjet` (with `arcjet()` / `arcjet_sync()`) | `arcjet` (with `launch_arcjet()` / `launch_arcjet_sync()`) | +| **Entry point** | `protect(request)` | `guard(label, rules)` | + +A single project can use both — e.g. request-based on API routes and guard on agent tool calls. + +**Common misclassifications to watch for:** + +- **MCP servers**: the word "server" is misleading. MCP tools don't receive HTTP requests — they're invoked by an MCP client over stdio or SSE. Use **Guard**, not request-based. +- **Background jobs / queue consumers**: no HTTP request at the protection site. Use **Guard**. +- **Server actions / RPC over HTTP** (Next.js server actions, tRPC, etc.): there _is_ an HTTP request underneath. Use **request-based**. +- **Agent tool calls inside a request handler**: if you want to limit per-user-per-route, request-based is fine. If you want per-tool budgets independent of any HTTP boundary, use Guard at the tool call site. + +Read the appropriate reference: + +- **Request-based JS/TS**: [references/requests_javascript.md](references/requests_javascript.md) +- **Request-based Python**: [references/requests_python.md](references/requests_python.md) +- **Guard JS/TS**: [references/guards_javascript.md](references/guards_javascript.md) +- **Guard Python**: [references/guards_python.md](references/guards_python.md) + +These references explain architectural decisions and patterns that can't be inferred from the source code alone. For exact API signatures, read the installed package's types and doc comments. + +### Step 4: Implement Protection + +Follow the patterns in the reference file from Step 3. Key principles: + +#### Request-based (HTTP routes): + +- Shared Arcjet client in its own file with `shield()` as a base rule. +- `withRule()` to layer route-specific rules. +- Call `protect()` inside each route handler (not in app-level middleware), once per request. +- Map `decision.isDenied()` reasons to HTTP responses. Only branch on reasons that produce a _different_ response — there's no point in an `else if (reason.isShield())` arm that returns the same status as the default 403. +- Put `characteristics: ["userId"]` (or similar) on the specific rule that needs it, not on the global client. + +#### Guard (non-HTTP code): + +- Client at module scope with `launchArcjet()` (JS) or `launch_arcjet()` / `launch_arcjet_sync()` (Python — pick async vs sync to match the function you're protecting). +- Rules declared at module scope. Give each rule a meaningful `label` so they show up usefully in the dashboard. +- **One `guard()` call per specific operation, with a hardcoded `label`** like `"tools.get-weather"` or `"queue.summarize"`. Put it wherever you already know exactly what's happening — that can be inside the tool/task function itself, or right before calling it from a dispatch arm. Both work; pick whichever makes error propagation cleaner. What to avoid is the generic-dispatcher pattern (`handleToolCall(name, args)` calling `guard(label=f"tools.{name}")`) — interpolated labels break grep and produce messy dashboard groupings. +- **Label naming rules**: labels are validated server-side as slugs — **lowercase letters, digits, dash (`-`), and dot (`.`) only**, must start and end with a letter or digit, max 256 bytes. Underscores, uppercase, and slashes are rejected even though some SDK TSDoc comments claim otherwise. Use `tools.get-weather`, not `tools.get_weather` or `Tools.GetWeather`. +- **Pass `metadata` on the `guard()` call** when you have useful auditing context (`metadata={"user_id": user_id, "request_id": ...}`). It appears in the dashboard alongside the decision. +- **Branch on which rule denied**, not just on `DENY`. Use the per-rule accessors (e.g. `userLimit.deniedResult(decision)` for retry-after info) or the flat reason string (`decision.reason === "PROMPT_INJECTION"` in JS, `decision.reason == "PROMPT_INJECTION"` in Python) so the error you surface to the caller tells them _why_ — "rate limited, retry in 12s" vs "input flagged as prompt injection" — instead of a generic "blocked." Note: guard's `decision.reason` is a flat string literal, unlike the request-based SDK's tagged-helper API. +- Every rate-limit rule needs a `key` and a `bucket`: + - **Per-user context** (agent tool calls inside a logged-in session, queue jobs with a `user_id`): use the user/session id as the key. + - **No user context** (stdio MCP server, single-tenant worker): use a stable identifier you control — instance id, deployment name, or a literal like `"default"`. Just be explicit. +- Check `decision.conclusion === "DENY"` (JS) or `decision.conclusion == "DENY"` (Python) before proceeding. + +#### Conventions outside the Arcjet flow + +For everything that _isn't_ an Arcjet-specific decision — dev scripts, file/module layout, named-vs-default exports, comment style, env-file naming, type hints, error class patterns — match the project's existing conventions. If the project has no convention yet, default to modern best practice for the language. This skill is opinionated about _where Arcjet goes_ and _how its API is used_; it shouldn't reach further than that. + +### Step 5: Verify Decisions + +After wiring up protection, confirm it's actually firing. Three steps: + +**1. Type-check / build first.** Run `tsc`, `next build`, `python -m py_compile`, or whatever check command the project uses. Catches wrong imports, wrong rule names, and stale type signatures before the user does. + +**2. Trigger a real call so a decision exists to check.** Without one, the dashboard and CLI are empty and you can't tell whether protection is actually wired up. + +- **Request-based**: start the dev server (`npm run dev`, `uvicorn main:app --reload`, etc.) and `curl` the protected route. To trip a rate limit, loop the call: `for i in {1..50}; do curl -s -o /dev/null -w "%{http_code}\n" http://localhost:3000/api/your-route; done` — you should see a mix of 200s and 429s once the limit is hit. +- **Guard**: invoke the protected function directly. A tiny script that imports the tool/task function and calls it twice (once to allow, once to exceed the limit) is usually the fastest path — e.g. `node -e "import('./src/tools.js').then(m => m.getWeather('SF', 'user_123'))"` or `python -c "from worker import process_job; process_job({'user_id': 'user_123'})"`. For MCP servers, send a tool call via the MCP client / inspector. For queue workers, enqueue a real job. Don't try to test guard by `curl`ing anything — there's no HTTP surface. + +**3. Confirm the decision in the Arcjet platform.** + +- **CLI**: `npx -y @arcjet/cli@latest requests list --site-id ` (request-based) or `... guards list --site-id ` (Guard) +- **MCP**: `list-requests` / `list-guards` +- **Dashboard**: https://app.arcjet.com + +For deeper investigation: `arcjet requests explain --site-id --request-id ` or `arcjet guards explain --site-id --guard-id `. + +If you can't run the app in the current environment, tell the user exactly what to do (which command to run, what to look for in the output) instead of silently skipping verification. + +### Gotchas + +- **Wrong SDK**: `@arcjet/guard` is for non-HTTP code. `@arcjet/node` / `@arcjet/next` / etc. are for HTTP routes. Using the wrong one is the most common mistake. +- **Wrong placement**: `protect()` must not be called in Express middleware or Next.js middleware. Call it inside each route handler. +- **Wrong layer for `guard()`**: don't put `guard()` in a `handleToolCall(name, args)` dispatcher — put it inside each specific tool / task function so the `label` and metadata can be hardcoded. +- **Hand-edited dependency manifests**: don't append `"arcjet": "^1.0.0"` to `package.json` or `arcjet>=1.0.0` to `requirements.txt`. Run the project's package manager so the version is real and the lockfile updates. +- **Double-counting**: Calling `protect()` or `guard()` multiple times for the same operation counts against rate limits multiple times. +- **Never hardcode `ARCJET_KEY`** — always use environment variables. + +## Choosing Protections + +When you need to pick which rules address the user's concern — bot abuse, rate limits, prompt injection, signup spam, PII, IP filtering, etc. — load [references/choosing_protections.md](references/choosing_protections.md). It maps common problems to Arcjet rules and explains the tradeoffs between strategies (e.g. token bucket vs sliding window). The mapping doesn't need to be in your context for the rest of the workflow. + +## Resources + +For exact API signatures, parameter names, and the full set of rules and helpers, read the installed SDK's source — types and docstrings are the source of truth: + +- **Python SDK**: https://github.com/arcjet/arcjet-py — `arcjet` package (request protection) and `arcjet.guard` subpackage (non-HTTP guard). +- **JavaScript / TypeScript SDK**: https://github.com/arcjet/arcjet-js — monorepo with framework-specific packages (`@arcjet/next`, `@arcjet/node`, `@arcjet/fastify`, `@arcjet/sveltekit`, `@arcjet/guard`, etc.). +- **Docs**: https://docs.arcjet.com — narrative guides, blueprints, and product reference. +- **Dashboard**: https://app.arcjet.com — sites, keys, and decision history. diff --git a/skills/arcjet/references/choosing_protections.md b/skills/arcjet/references/choosing_protections.md new file mode 100644 index 0000000..464848c --- /dev/null +++ b/skills/arcjet/references/choosing_protections.md @@ -0,0 +1,61 @@ +# Choosing Protections + +Map the user's problem to the right Arcjet rules. Load this file when you need to pick rules for a specific concern (bot abuse, cost explosion, prompt injection, etc.) — the main SKILL.md only points here. + +## Automated traffic and bot abuse + +Automated clients — scrapers, data harvesters, and script-based attackers — treat AI features as free compute. Without bot protection, every request from a bot reaches your AI provider and inflates your costs. + +Arcjet bot detection runs inside the application, before the AI call, so denied requests never reach your provider. It classifies 600+ known bots across 25 categories, verifies legitimate bots (search engines, monitors), and detects emerging threats in real time. + +You configure bot rules in application code — not at the CDN layer — so you can apply different strategies per route and make decisions based on full application context (identity, subscription level, session state). + +**Rules:** `detectBot` (request-based only). Use `allow` for a safelist or `deny` to block specific categories — they're mutually exclusive. Combine with rate limiting for full traffic control. + +Bot rules can also be configured as remote rules via the CLI or MCP server — applied site-wide with no code changes or redeployment. Useful for blocking a newly-spotted bot category during an incident. + +## Cost explosion and budget control + +Automated traffic, user abuse, and prompt attacks inflate token and tool spend. Rate limiting enforces per-user token quotas to prevent cost explosions. + +**Rules:** `tokenBucket`, `fixedWindow`, `slidingWindow` (request-based and guard). + +Use token bucket for AI workloads where operations have variable cost — set `requested` per call to consume proportional tokens (1 for a lookup, 10 for an expensive generation). Fixed window gives a hard cap that resets at period boundaries. Sliding window provides smooth rate limiting without boundary bursts. + +For request-based protection, rate limits default to keying by IP. Use `characteristics: ["userId"]` to key by something else. For guard protection, you must always pass an explicit `key` (user ID, session ID, etc.) and a `bucket` name to avoid collisions. + +## Prompt injection attacks + +Jailbreaks, role-play escapes, and instruction overrides allow attackers to manipulate AI behavior. Arcjet scores incoming messages for injection patterns before they reach the model. + +**Rules:** `detectPromptInjection` (request-based and guard). Use on any untrusted text before it reaches a model or tool argument — and on tool call _results_ when the tool fetches content from untrusted sources. + +## Data loss prevention + +Sensitive data leaks into AI model context, logs, third-party tool calls, or model memory through unguarded inputs and outputs. Arcjet detects card numbers, email addresses, phone numbers, and custom patterns — entirely locally via WASM, with no data leaving your infrastructure. + +**Rules:** `sensitiveInfo` / `localDetectSensitiveInfo` (request-based and guard). Use to block PII from entering the system (users sending credit card numbers) or leaving it (tool outputs leaking email addresses). + +## Unauthorized tool invocation + +Agents invoke tools in ways they shouldn't — issuing refunds, accessing data, escalating privileges. The prompt can be benign; the tool call is catastrophic. + +**Rules:** Guard protection with per-tool rate limits and labels. Each tool call site gets its own `label` and rules, so you can enforce different budgets and detect abuse per operation. Combine with prompt injection detection on tool inputs. + +## Common web attacks + +SQLi, XSS, and other injection attacks targeting web endpoints. + +**Rules:** `shield` (request-based only). Zero config, no cost. Include on the shared client as a base rule unless the codebase has a different convention. + +## Signup abuse + +Credential stuffing, spam registrations, and disposable email abuse on signup/login forms. + +**Rules:** `validateEmail` + `protectSignup` (request-based only). Rejects disposable, no-MX, and invalid addresses. `protectSignup` combines bot detection + email validation + rate limiting in one rule. + +## IP-based filtering + +Block traffic by IP metadata — VPN, Tor, country, or specific IP ranges. + +**Rules:** `filter` (request-based only). Can also be configured as remote rules via CLI/MCP for immediate response to active attacks without redeployment. diff --git a/skills/arcjet/references/cli.md b/skills/arcjet/references/cli.md new file mode 100644 index 0000000..116ae0a --- /dev/null +++ b/skills/arcjet/references/cli.md @@ -0,0 +1,85 @@ +# Arcjet CLI + +The CLI is the preferred way to connect to the Arcjet platform from a terminal session. + +## Install + +The simplest way is `npx` (no install required): + +```bash +npx -y @arcjet/cli@latest +``` + +For frequent use, install the binary: + +```bash +# npm +npm install -g @arcjet/cli + +# Homebrew +brew install arcjet/tap/arcjet + +# Install script (macOS Apple Silicon/Intel, Linux x86_64/arm64) +curl -sSfL https://arcjet.com/cli/install.sh | bash +``` + +Or download the latest archive for your platform from https://github.com/arcjet/cli/releases, extract it, and place the `arcjet` binary somewhere on your `PATH`. Available for macOS (Apple Silicon, Intel), Linux (x86_64, arm64), and Windows (x86_64, arm64). + +Verify: `arcjet version` + +## Authentication + +Most users will not have `ARCJET_TOKEN` set in their shell. The default path is the browser-based device authorization flow, which works the same way as `gh auth login`, `fly auth login`, or `vercel login`: + +```bash +arcjet auth login +``` + +The CLI prints a one-time code and opens a URL. The user confirms the code in their browser, and the CLI stores the resulting credentials locally. Subsequent commands authenticate automatically. + +Check the current state: + +```bash +arcjet auth status +``` + +Sign out: + +```bash +arcjet auth logout +``` + +> **Note:** If `arcjet auth status` reports the user is not signed in, run `arcjet auth login` and wait for confirmation in the browser before continuing. Do not prompt the user for a token — the device flow is the expected path. + +## Agent Usage + +Two flags keep output predictable: + +- `--output json` — emit machine-readable JSON (default when stdout is not a TTY) +- `--fields ` — limit output to listed top-level keys (use aggressively to keep context small) + +```bash +arcjet teams list --output json --fields id,name +arcjet sites list --team-id team_01abc123 --output json --fields id,name +arcjet requests list --site-id site_01abc123 --output json --fields id,conclusion,path +``` + +## Common Workflows + +**Bootstrap a project:** `arcjet auth login` → `arcjet teams list` → `arcjet sites list --team-id ` (or `arcjet sites create`) → `arcjet sites get-key --site-id ` → write key to `.env` as `ARCJET_KEY`. + +**Investigate a request:** `arcjet requests list --site-id ` → `arcjet requests details --site-id --request-id ` → `arcjet requests explain --site-id --request-id ` + +**Daily security briefing:** `arcjet briefing --site-id ` + +**Manage remote rules:** `arcjet rules list` → `arcjet rules create` (DRY_RUN) → `arcjet analyze dry-run-impact` → `arcjet rules promote` (LIVE) + +## Command Reference + +Run `arcjet help` to see all available commands, or `arcjet help` for details on a specific command (e.g. `arcjet rules help`, `arcjet analyze help`). + +## Invariants + +- Always confirm with the user before write/delete operations. Mutating commands require `--confirm`. +- Site IDs: `site_` (TypeID format). Team IDs: `team_`. +- Commands and parameters are a stable API contract. diff --git a/skills/arcjet/references/guards_javascript.md b/skills/arcjet/references/guards_javascript.md new file mode 100644 index 0000000..5f5716c --- /dev/null +++ b/skills/arcjet/references/guards_javascript.md @@ -0,0 +1,175 @@ +# JavaScript/TypeScript Guard + +## What Guard Is + +Guard protects code paths that don't have an HTTP request — tool calls, agent loops, MCP handlers, queue consumers, background jobs. It's a separate SDK (`@arcjet/guard`) from the HTTP request SDKs (`@arcjet/node`, `@arcjet/next`, etc.) because there's no request object to inspect. Instead, you pass explicit context (labels, keys, text to scan) at each call site. + +## Installation + +Install with whichever package manager the project already uses (`npm install`, `pnpm add`, `yarn add`, `bun add`) — don't hand-edit `package.json`: + +```bash +npm install @arcjet/guard +``` + +Requires `@arcjet/guard` ≥ 1.4.0. The runtime minimums are stricter than the request adapters (which need only Node 20+): + +| Runtime | Minimum version | +| ------------------ | ------------------------ | +| Node.js | 22.18.0 | +| Bun | 1.3.0 | +| Deno | `stable` / `lts` | +| Cloudflare Workers | compat date `2025-09-01` | + +The correct transport is picked automatically via conditional exports (HTTP/2 on Node and Bun, fetch-based on Deno and Workers) — import from `@arcjet/guard` either way. If the project is on Node 20/21 or an older Bun/Workers compat date, warn the user and stop until the runtime is bumped. + +Read the installed package's types and doc comments for the full API surface. + +> _Runtime support last verified against `@arcjet/guard` v1.4.0 on **2026-05-20**. Before relying on these numbers, check the [Runtime support section](https://github.com/arcjet/arcjet-js/tree/main/arcjet-guard#runtime-support) of the current README — minimums tend to creep upward over time._ + +## Architecture: Why Things Go Where They Do + +### Client at module scope + +```typescript +import { launchArcjet } from "@arcjet/guard"; +const arcjet = launchArcjet({ key: process.env.ARCJET_KEY! }); +``` + +The client holds a persistent HTTP/2 connection to the Arcjet decision service. Creating it inside a function means a new connection per call — slow and wasteful. + +### Rules at module scope + +Rate limit state is tracked server-side by the combination of `bucket` and other configuration properties, so recreating rules per call won't break counting. However, defining rules at module scope is still best practice because: + +- It makes the per-rule result accessors (e.g. `userLimit.deniedResult(decision)`) work — you need a stable reference to call methods on. +- It avoids unnecessary object allocation on every invocation. +- It keeps rule configuration visible and centralized. + +```typescript +import { tokenBucket, detectPromptInjection } from "@arcjet/guard"; + +// WORKS but awkward — no stable reference for result inspection +function handleTool() { + const limit = tokenBucket({ /* config */ }); // hard to call limit.deniedResult() later +} + +// BETTER — declare rules at module scope, dynamically choose which to apply +const adminLimit = tokenBucket({ + label: "admin.tool-calls", + bucket: "admin-tools", + refillRate: 100, + intervalSeconds: 60, + maxTokens: 1000, +}); +const memberLimit = tokenBucket({ + label: "member.tool-calls", + bucket: "member-tools", + refillRate: 10, + intervalSeconds: 60, + maxTokens: 100, +}); +const piRule = detectPromptInjection(); + +function toolRules(userId: string, role: string, text: string) { + const limit = role === "admin" ? adminLimit : memberLimit; + return [ + limit({ key: userId, requested: 1 }), + piRule(text), + ]; +} +``` + +### guard() at the operation, with a hardcoded label + +Place `guard()` wherever you already know exactly what operation is happening. That's typically inside the specific tool/task function, but the dispatch arm right before the call works equally well — sometimes it gives cleaner error propagation: + +```typescript +// Option A: guard inside the tool function +async function getWeather(city: string, userId: string) { + const decision = await arcjet.guard({ + label: "tools.get-weather", + rules: [toolCallLimit({ key: userId, requested: 1 })], + metadata: { userId }, + }); + if (decision.conclusion === "DENY") throw new Error(decision.reason); + // ...do the work +} + +// Option B: guard at the dispatch site, right before calling the tool +switch (toolName) { + case "get_weather": { + const decision = await arcjet.guard({ + label: "tools.get-weather", + rules: [toolCallLimit({ key: userId, requested: 1 })], + metadata: { userId }, + }); + if (decision.conclusion === "DENY") throw new Error(decision.reason); + return getWeather(args.city); + } + // ... +} + +// Avoid: generic dispatcher with interpolated label +async function handleToolCall(name: string, args: Record, userId: string) { + const decision = await arcjet.guard({ label: `tools.${name}`, rules: [/* ... */] }); // 👎 +} +``` + +The `label` should be a hardcoded string — `"tools.get-weather"`, not `` `tools.${name}` ``. Hardcoded labels stay greppable, and the dashboard groups by them; interpolation produces a sea of distinct-looking calls instead of one bucket per operation. + +**Label naming rules (often surprising):** labels are validated server-side as slugs — **lowercase letters, digits, dash (`-`), and dot (`.`) only**, must start and end with a letter or digit, max 256 bytes. Underscores, uppercase, and forward slashes are rejected even though the `GuardOptions.label` TSDoc lists them as allowed. Use `tools.get-weather`, not `tools.get_weather`. Same rules apply to rate-limit `bucket` names. + +Pass `metadata` whenever you have useful auditing context (`{ userId, requestId }`) — it shows up in the dashboard alongside the decision and makes debugging much easier later. + +## Choosing a Rate Limit Strategy + +See the "Rate Limiting Strategies" section in the main skill for a comparison of token bucket vs fixed window vs sliding window. + +Key guard-specific notes: all rate limit rules require a `key` parameter at call time (user ID, session ID, API key) — without it, limits are global across all callers. They also need a `bucket` name to avoid collisions between different rules. + +**Picking a `key` when there's no user:** Some call sites have no per-user context — e.g. a stdio MCP server where the client is the only caller, or a single-tenant queue worker. Don't try to fake it by passing an empty string. Use whatever identifier actually matches the scope of the limit: + +- single-tenant worker → the deployment name or env (`process.env.HOSTNAME ?? "default"`) +- stdio MCP server → the MCP client/session id if exposed by the SDK, otherwise the process identity +- shared limit across all callers → a stable literal like `"global"`, and add a comment explaining why + The point is to be intentional. A wrong-but-explicit `key` is much easier to fix than a missing one. + +## Content Scanning Rules + +### Prompt injection detection + +Use `detectPromptInjection()` on any untrusted text before it reaches a model or is used as a tool argument. This catches jailbreaks, role-play escapes, and instruction overrides. Also useful on tool call _results_ when the tool fetches content from untrusted sources. + +### Sensitive information detection + +Use `localDetectSensitiveInfo()` to block PII from entering or leaving the system (e.g. users sending credit card numbers, or tool outputs leaking email addresses). The scan runs locally via WASM — raw text never leaves the SDK, which matters for compliance. + +## Decision Handling + +`decision.conclusion` is either `"ALLOW"` or `"DENY"`. Always check before proceeding. + +For useful error messages, branch on **which rule** denied — not just on `DENY`. Each rule defined at module scope exposes a `.deniedResult(decision)` accessor that returns rule-specific info (e.g. `resetAtUnixSeconds` for rate limits). Use this to give the caller something actionable: + +```typescript +if (decision.conclusion === "DENY") { + const rateLimited = toolCallLimit.deniedResult(decision); + if (rateLimited) { + throw new Error(`rate limited — retry after unix ${rateLimited.resetAtUnixSeconds}`); + } + if (decision.reason === "PROMPT_INJECTION") { + throw new Error("input flagged as prompt injection"); + } + throw new Error("blocked"); +} +``` + +`decision.reason` is a flat string when `conclusion === "DENY"` — one of `"RATE_LIMIT"`, `"PROMPT_INJECTION"`, `"SENSITIVE_INFO"`, `"CUSTOM"`, `"ERROR"`, `"NOT_RUN"`, `"UNKNOWN"`. (On ALLOW it's `undefined`.) Read the types on the decision object for the full structure. + +`decision.hasError()` means something went wrong during rule evaluation (service unreachable, rule execution failure, etc.) but the SDK failed open. Log it but don't block the user. + +## Key Patterns + +- Pass `signal` (an `AbortSignal`) on the `.guard()` call when one is available (e.g. from the caller or a timeout) so guard respects cancellation. `timeoutSeconds` is also available for a simple deadline. +- Use `metadata` for analytics/auditing context (user ID, session, etc.) — this appears in the dashboard. +- The `label` string should identify the operation (e.g. `"tools.get-weather"`, `"mcp.query-database"`) — it appears in the dashboard and helps you understand which operations are being rate limited or blocked. diff --git a/skills/arcjet/references/guards_python.md b/skills/arcjet/references/guards_python.md new file mode 100644 index 0000000..5c20a9b --- /dev/null +++ b/skills/arcjet/references/guards_python.md @@ -0,0 +1,163 @@ +# Python Guard + +## What Guard Is + +Guard protects code paths that don't have an HTTP request — tool calls, agent loops, queue consumers, background jobs. It's part of the `arcjet` package (≥ 0.7.0) but uses a different entry point (`arcjet.guard`) from the HTTP request protection (`arcjet`). There's no request object to inspect, so you pass explicit context (labels, keys, text to scan) at each call site. + +**Version compatibility:** Python ≥ 3.10 (same as the request SDK — they're shipped together in the `arcjet` package). If the project's Python is older, warn the user and stop. + +> _Version info last verified against `arcjet` v0.7.0 on **2026-05-20**. Before relying on these numbers, check the `requires-python` field in the current [`pyproject.toml`](https://github.com/arcjet/arcjet-py/blob/main/pyproject.toml) — minimums tend to creep upward over time._ + +## Installation + +Install with whichever package manager the project already uses (`pip install`, `uv add`, `poetry add`, etc.) — don't hand-edit `requirements.txt` with a guessed version (`arcjet>=1.0.0` doesn't exist; current is `>=0.7.0`): + +```bash +pip install arcjet +``` + +Guard is included in the `arcjet` package — no separate install. Read the installed package's types and docstrings for the full API surface. + +## Architecture: Why Things Go Where They Do + +### Client at module scope + +```python +import os +from arcjet.guard import launch_arcjet + +arcjet = launch_arcjet(key=os.environ["ARCJET_KEY"]) +``` + +Use `launch_arcjet` for async code, `launch_arcjet_sync` for sync. The client holds a persistent connection to the Arcjet decision service. Creating it inside a function means a new connection per call. + +### Rules at module scope + +Rate limit state is tracked server-side by the combination of `bucket` and other configuration properties, so recreating rules per call won't break counting. However, defining rules at module scope is still best practice because: + +- It makes the per-rule result accessors (e.g. `user_limit.denied_result(decision)`) work — you need a stable reference to call methods on. +- It avoids unnecessary object allocation on every invocation. +- It keeps rule configuration visible and centralized. + +```python +from arcjet.guard import TokenBucket, DetectPromptInjection + +# WORKS but awkward — no stable reference for result inspection +def handle_tool(): + limit = TokenBucket(...) # hard to call limit.denied_result() later + +# BETTER — declare rules at module scope, dynamically choose which to apply +admin_limit = TokenBucket( + label="admin.tool-calls", + bucket="admin-tools", + refill_rate=100, + interval_seconds=60, + max_tokens=1000, +) +member_limit = TokenBucket( + label="member.tool-calls", + bucket="member-tools", + refill_rate=10, + interval_seconds=60, + max_tokens=100, +) +pi_rule = DetectPromptInjection() + +def tool_rules(user_id: str, role: str, text: str): + limit = admin_limit if role == "admin" else member_limit + return [ + limit(key=user_id, requested=1), + pi_rule(text), + ] +``` + +### guard() at the operation, with a hardcoded label + +Place `guard()` wherever you already know exactly what operation is happening. That's typically inside the specific tool/task function, but the dispatch arm right before calling it works equally well — sometimes it gives cleaner error propagation: + +```python +# Option A: guard inside the tool function +async def get_weather(city: str, user_id: str) -> dict: + decision = await arcjet.guard( + label="tools.get-weather", + rules=[tool_call_limit(key=user_id, requested=1)], + metadata={"user_id": user_id}, + ) + if decision.conclusion == "DENY": + raise Exception(decision.reason) + # ...do the work + +# Option B: guard at the dispatch arm, right before the call +async def dispatch(task): + if task["type"] == "summarize": + decision = await arcjet.guard( + label="queue.summarize", + rules=[user_task_limit(key=task["user_id"], requested=3)], + metadata={"user_id": task["user_id"]}, + ) + if decision.conclusion == "DENY": + raise Exception(decision.reason) + return _summarize(task) + +# Avoid: generic dispatcher with interpolated label +async def handle_tool_call(name: str, args: dict, user_id: str): # 👎 + decision = await arcjet.guard(label=f"tools.{name}", rules=[...]) +``` + +The `label` should be a hardcoded string — `"tools.get-weather"`, not `f"tools.{name}"`. Hardcoded labels stay greppable, and the dashboard groups by them. + +**Label naming rules (often surprising):** labels are validated server-side as slugs — **lowercase letters, digits, dash (`-`), and dot (`.`) only**, must start and end with a letter or digit, max 256 bytes. Underscores, uppercase, and forward slashes are rejected even though some SDK TSDoc / docstring comments list them as allowed. Use `tools.get-weather`, not `tools.get_weather`. Same rules apply to rate-limit `bucket` names. + +Pass `metadata` whenever you have useful auditing context (`{"user_id": ..., "request_id": ...}`) — it shows up in the dashboard and makes debugging much easier later. + +## Choosing a Rate Limit Strategy + +See the "Rate Limiting Strategies" section in the main skill for a comparison of token bucket vs fixed window vs sliding window. + +Key guard-specific notes: all rate limit rules require a `key` parameter at call time (user ID, session ID) — without it, limits are global across all callers. They also need a `bucket` name to avoid collisions between different rules. + +**Picking a `key` when there's no user:** Some call sites have no per-user context — e.g. a single-tenant background worker. Don't fake it with an empty string. Use whatever identifier matches the scope (`os.environ.get("HOSTNAME", "default")`, deployment name, etc.) and add a short comment if it's deliberately global. + +## Content Scanning Rules + +### Prompt injection detection + +Use `DetectPromptInjection()` on any untrusted text before it reaches a model or is used as a tool argument. Also useful on tool call _results_ when the tool fetches content from untrusted sources. + +### Sensitive information detection + +Use `LocalDetectSensitiveInfo()` to block PII from entering or leaving the system (e.g. users sending credit card numbers, or tool outputs leaking email addresses). The scan runs locally — raw text never leaves the SDK, which matters for compliance. + +## Decision Handling + +`decision.conclusion` is either `"ALLOW"` or `"DENY"`. Always check before proceeding. + +For useful error messages, branch on **which rule** denied — not just on `DENY`. Each rule defined at module scope exposes a `.denied_result(decision)` accessor that returns rule-specific info (e.g. `reset_at_unix_seconds` for rate limits). Use this to give the caller something actionable: + +```python +if decision.conclusion == "DENY": + rate_limited = user_task_limit.denied_result(decision) + if rate_limited: + raise Exception(f"rate limited — retry after unix {rate_limited.reset_at_unix_seconds}") + if decision.reason == "PROMPT_INJECTION": + raise Exception("input flagged as prompt injection") + raise Exception("blocked") +``` + +`decision.reason` is a flat string — one of `"RATE_LIMIT"`, `"PROMPT_INJECTION"`, `"SENSITIVE_INFO"`, `"CUSTOM"`, `"ERROR"`, `"NOT_RUN"`, `"UNKNOWN"`. Read the types on the decision object for the full structure. + +`decision.has_error()` means something went wrong during rule evaluation (service unreachable, rule execution failure, etc.) but the SDK failed open. Log it but don't block the user. + +## Async vs Sync + +The package provides both variants: + +- `launch_arcjet` / `await arcjet.guard(...)` — async, use in `async def` functions +- `launch_arcjet_sync` / `arcjet.guard(...)` — sync, use in regular `def` functions + +**Pick the variant that matches the function you're protecting.** A FastAPI handler or an `AsyncOpenAI` agent loop is async — use `launch_arcjet`. A Celery task, a queue poller defined with `def`, or anything wrapped by a sync framework is sync — use `launch_arcjet_sync`. Mixing them produces "coroutine was never awaited" warnings or blocking calls inside an event loop. Both variants provide the same protection. + +## Key Patterns + +- Use `metadata` for analytics/auditing context (user ID, session, etc.) — this appears in the dashboard. +- The `label` string should identify the operation (e.g. `"tools.get-weather"`, `"queue.process-job"`) — it appears in the dashboard and helps you understand which operations are being limited or blocked. diff --git a/skills/arcjet/references/mcp.md b/skills/arcjet/references/mcp.md new file mode 100644 index 0000000..f7ded72 --- /dev/null +++ b/skills/arcjet/references/mcp.md @@ -0,0 +1,91 @@ +# Arcjet MCP Server + +The MCP server connects AI coding tools to the Arcjet API over HTTP with OAuth authentication. Use it when the CLI isn't available or the client has built-in MCP support. + +**Endpoint:** `https://api.arcjet.com/mcp` + +## Setup + +### VS Code with Copilot + +Add to `.vscode/mcp.json`: + +```json +{ + "servers": { + "arcjet": { + "type": "http", + "url": "https://api.arcjet.com/mcp" + } + } +} +``` + +Or via Command Palette: `MCP: Add Server` → HTTP → `https://api.arcjet.com/mcp` → name `Arcjet`. + +### Claude Code + +```bash +claude mcp add arcjet --transport http https://api.arcjet.com/mcp +``` + +### Cursor + +Add to `.cursor/mcp.json`: + +```json +{ + "mcpServers": { + "arcjet": { + "type": "streamable-http", + "url": "https://api.arcjet.com/mcp" + } + } +} +``` + +### Windsurf + +Add to `mcp_config.json`: + +```json +{ + "mcpServers": { + "arcjet": { + "serverUrl": "https://api.arcjet.com/mcp" + } + } +} +``` + +### Claude Desktop + +Settings → Connectors → Add custom connector → Name: `Arcjet`, URL: `https://api.arcjet.com/mcp` + +### ChatGPT + +Settings → Connectors → Add connection → URL: `https://api.arcjet.com/mcp` → OAuth + +## Authentication + +OAuth-based. On first connection, you'll be redirected to sign in with your Arcjet account. Subsequent calls authenticate automatically. + +## Available Tools + +Once connected, the MCP server exposes tools for managing teams, sites, keys, requests, decisions, traffic analysis, anomaly detection, IP investigation, security briefings, and remote rules. The agent can discover available tools through the MCP protocol directly. + +## Common Workflows + +**Bootstrap a project:** `list-teams` → `list-sites` → `get-site-key` → write to `.env` as `ARCJET_KEY` + +**Investigate suspicious traffic:** `analyze-traffic` → `list-requests` (filter DENY) → `investigate-ip` → `create-rule` (DRY_RUN) → `get-dry-run-impact` → `promote-rule` + +**Daily security briefing:** `get-security-briefing` + +**Add protection without redeploying:** `create-rule` (bot/filter in DRY_RUN) → `get-dry-run-impact` → `promote-rule` + +## Security Notes + +- Verify the endpoint is `https://api.arcjet.com/mcp` +- Enable confirmation prompts in your AI client for write operations +- Only connect from trusted AI clients diff --git a/skills/arcjet/references/requests_javascript.md b/skills/arcjet/references/requests_javascript.md new file mode 100644 index 0000000..ee64728 --- /dev/null +++ b/skills/arcjet/references/requests_javascript.md @@ -0,0 +1,275 @@ +# JavaScript/TypeScript Request Protection + +## What Request Protection Is + +Request protection inspects HTTP requests — headers, IP, body — to enforce security rules on API routes, form handlers, and server-rendered pages. Each web framework has a dedicated Arcjet adapter that knows how to extract the request metadata. + +## Installation + +Pick the adapter for the project's framework, then install it with whichever package manager the project already uses (`npm install`, `pnpm add`, `yarn add`, `bun add`). Don't hand-edit `package.json` — a typed version is usually stale, and the lockfile won't update. Read the installed package's types and doc comments for the full API surface. + +**Runtime baseline:** **Node.js ≥ 20**, **Bun ≥ 1.3.0**, **Deno** `stable` / `lts`. If the project is below any of these, the install will fail or runtime behavior will misbehave — bump the runtime first. + +> _Version info last verified against `@arcjet/*` v1.4.0 on **2026-05-20**. Numbers below may drift — before relying on them, check the current `package.json` of the relevant `@arcjet/*` package at https://github.com/arcjet/arcjet-js (or the latest release at https://github.com/arcjet/arcjet-js/releases). Minimums tend to creep upward over time._ + +| Framework | Package | Min framework version | +| ----------------- | --------------------------------------------------------- | ------------------------------------------------------------------------ | +| Next.js | `@arcjet/next` | Next.js ≥ 15 | +| Express / Node.js | `@arcjet/node` | Node ≥ 20 (no framework peer) | +| Fastify | `@arcjet/fastify` | Fastify ≥ 5 | +| NestJS | `@arcjet/nest` | `@nestjs/common` ^10 \|\| ^11 | +| SvelteKit | `@arcjet/sveltekit` | Svelte ^3.54 \|\| ^4 \|\| ^5 | +| Remix | `@arcjet/remix` | Remix v2 (v3 was renamed to React Router 7 — use `@arcjet/react-router`) | +| React Router | `@arcjet/react-router` | react-router ≥ 7 | +| Astro | `@arcjet/astro` | Astro ^5.9.3 \|\| ^6 | +| Nuxt | `@arcjet/nuxt` | `@nuxt/kit` ≥ 4, `@nuxt/schema` ≥ 4 | +| Bun | `@arcjet/bun` | Bun ≥ 1.3.0 | +| Deno | `@arcjet/deno` (install with `deno add npm:@arcjet/deno`) | Deno `stable` / `lts` | +| Hono | `@arcjet/node` (on Node) or `@arcjet/bun` (on Bun) | runtime-dependent (see Node/Bun rows) | + +If the project is below a listed minimum, warn the user and stop — installing anyway produces confusing errors. + +**Some frameworks don't fit the generic patterns below.** Check the [Framework-specific setup](#framework-specific-setup) section first: + +- **Astro, Nuxt, NestJS** — replace the "shared client file" pattern entirely (Astro integration / Nuxt module / NestJS DI). +- **Bun, Deno, Hono on Node** — use the shared client file as below, but with a runtime quirk (`aj.handler()` wrapping for Bun/Deno; `HttpBindings` type for Hono on Node). + +Everything else (Next.js, Express/Node, Fastify, SvelteKit, Remix, React Router, Hono on Bun) follows the generic patterns directly. + +## Architecture: Why Things Go Where They Do + +### Shared client file (standard pattern) + +Create a **separate file** (e.g. `src/lib/arcjet.ts` or `lib/arcjet.ts`) that exports the Arcjet instance. Do NOT define the client inline in route handlers — it should be importable from any route. + +Always include `shield({ mode: "LIVE" })` as a base rule, even when using combined rules like `protectSignup()`. Shield protects against common attacks (SQLi, XSS) and costs nothing to add. + +```typescript +// src/lib/arcjet.ts +import arcjet, { shield } from "@arcjet/next"; // or @arcjet/node, etc. + +export const aj = arcjet({ + key: process.env.ARCJET_KEY!, + rules: [shield({ mode: "LIVE" })], +}); +``` + +### withRule() for per-route rules + +Use `withRule()` to add route-specific rules without modifying the shared instance. This keeps the base protection (shield) everywhere while layering additional rules per endpoint. + +```typescript +import aj from "@/lib/arcjet"; +import { slidingWindow } from "@arcjet/next"; + +const protect = aj.withRule(slidingWindow({ mode: "LIVE", interval: 60, max: 100 })); +``` + +### protect() in route handlers, not middleware + +Call `protect()` inside each route handler, once per request. Don't call it in Express middleware (`app.use()`) or Next.js middleware — these run on every request including static assets, and you lose the ability to apply different rules to different routes. + +## Framework-specific setup + +Five frameworks don't fit the "shared client file" pattern above. Use the structure below for the affected framework, then read the installed package's types and README for the full API. + +### Astro + +Astro registers Arcjet as a build-time **integration** in `astro.config.mjs`. The configured client is exposed as a virtual module — there is no `lib/arcjet.ts` file and no `withRule()`. Rules are global to the integration; per-route variation isn't supported. + +```typescript +// astro.config.mjs +import arcjet, { shield } from "@arcjet/astro"; +import { defineConfig } from "astro/config"; + +export default defineConfig({ + integrations: [arcjet({ rules: [shield({ mode: "LIVE" })] })], +}); + +// src/pages/api/hello.ts +import aj from "arcjet:client"; +import type { APIRoute } from "astro"; + +export const GET: APIRoute = async ({ request }) => { + const decision = await aj.protect(request); + // ... +}; +``` + +### Nuxt + +Nuxt registers Arcjet as a Nuxt **module**. The key goes in `nuxt.config.ts`, not in the `arcjet()` call, and the SDK is imported from the auto-generated alias `#arcjet`. + +```typescript +// nuxt.config.ts +export default defineNuxtConfig({ + modules: ["@arcjet/nuxt"], + arcjet: { key: process.env.ARCJET_KEY }, +}); + +// server/routes/hello.get.ts +import arcjet, { shield } from "#arcjet"; + +const aj = arcjet({ rules: [shield({ mode: "LIVE" })] }); // no `key` — module provides it + +export default defineEventHandler(async (event) => { + const decision = await aj.protect(event); + // ... +}); +``` + +### NestJS + +NestJS uses dependency injection. Register `ArcjetModule.forRoot()` in your app module, then inject the client in controllers with `@InjectArcjet()`. + +```typescript +// app.module.ts +import { ArcjetModule, shield } from "@arcjet/nest"; + +@Module({ + imports: [ + ArcjetModule.forRoot({ + isGlobal: true, + key: process.env.ARCJET_KEY!, + rules: [shield({ mode: "LIVE" })], + }), + ], +}) +export class AppModule {} + +// app.controller.ts +import { ArcjetNest, InjectArcjet } from "@arcjet/nest"; + +@Controller() +export class AppController { + constructor(@InjectArcjet() private readonly arcjet: ArcjetNest) {} + + @Get("/") + async index(@Req() req: Request) { + const decision = await this.arcjet.protect(req); + // ... + } +} +``` + +### Bun and Deno + +Both expose `aj.handler()` to wrap the server's fetch handler. Wrapping is for accurate client IP detection — `protect()` still needs to be called inside. + +```typescript +// Bun +import arcjet, { shield } from "@arcjet/bun"; +import { env } from "bun"; + +const aj = arcjet({ key: env.ARCJET_KEY!, rules: [shield({ mode: "LIVE" })] }); + +Bun.serve({ + port: 3000, + fetch: aj.handler(async (req) => { + const decision = await aj.protect(req); + // ... + return new Response("ok"); + }), +}); + +// Deno +import arcjet, { shield } from "npm:@arcjet/deno"; + +const aj = arcjet({ key: Deno.env.get("ARCJET_KEY")!, rules: [shield({ mode: "LIVE" })] }); + +Deno.serve(aj.handler(async (request) => { + const decision = await aj.protect(request); + // ... + return new Response("ok"); +})); +``` + +On Deno, imports use the `npm:` prefix (`npm:@arcjet/deno`, `npm:@arcjet/inspect`). On Bun, env comes from `import { env } from "bun"` rather than `process.env`. + +### Hono + +Hono on **Bun** is straightforward — install `@arcjet/bun`, create the client per the standard pattern, and pass `c.req.raw` to `protect()`. + +Hono on **Node.js** needs the type-bindings dance so the underlying `IncomingMessage` is reachable. Install `@arcjet/node` and type the app with `HttpBindings` from `@hono/node-server`: + +```typescript +import arcjet, { shield } from "@arcjet/node"; +import { serve, type HttpBindings } from "@hono/node-server"; +import { Hono } from "hono"; + +const aj = arcjet({ key: process.env.ARCJET_KEY!, rules: [shield({ mode: "LIVE" })] }); +const app = new Hono<{ Bindings: HttpBindings }>(); + +app.get("/", async (c) => { + const decision = await aj.protect(c.env.incoming); + // ... +}); +``` + +Without the `Bindings` type, `c.env.incoming` won't typecheck. + +## Choosing Rules + +See the "Choosing the Right Rules" section in the main skill for rule selection guidance and rate limiting strategy comparisons. Key framework-specific notes: + +- **shield** — always include. No configuration needed. +- **detectBot** — use `allow` for a safelist or `deny` for specific categories. They're mutually exclusive. +- **Rate limits** — use `characteristics: ["userId"]` to key by something other than IP. +- **validateEmail** — for signup/login forms. +- **protectSignup** — combined bot + email + rate limit, purpose-built for registration flows. +- **sensitiveInfo** — blocks PII in request bodies. +- **detectPromptInjection** — for AI endpoints receiving user prompts. +- **filter** — block by IP metadata (VPN, Tor, country, IP range). + +## Framework-Specific protect() Calls + +The request object to pass differs by framework: + +| Framework | What to pass to `protect()` | +| ----------------------------------- | --------------------------------------------------------------------- | +| Express / Node.js | `req` (IncomingMessage) | +| Next.js App Router | `req` (Request) | +| Next.js Server Components / actions | `await request()` from `@arcjet/next` | +| Fastify | `request` (Fastify request, not raw Node) | +| NestJS | `req` (`@Req() req: Request`) | +| SvelteKit | `event` | +| Remix / React Router | `args` (the loader/action args) | +| Nuxt | `event` (H3 event) | +| Astro | `request` (the Web `Request`) | +| Hono on Node.js | `c.env.incoming` (requires `Hono<{ Bindings: HttpBindings }>`) | +| Hono on Bun | `c.req.raw` | +| Bun | `request` (Web `Request`), wrap `fetch` with `aj.handler()` | +| Deno | `request` (Web `Request`), wrap `Deno.serve` body with `aj.handler()` | + +`aj.handler()` on Bun and Deno wraps the user's fetch handler so Arcjet has access to the underlying socket / connection info for accurate IP detection — Bun and Deno don't expose that on the `Request` object alone. The wrapping is for IP detection only; you still need to call `aj.protect(request)` yourself inside the handler. + +## Decision Handling + +`decision.isDenied()` means a LIVE rule triggered a denial. Map denial reasons to HTTP status codes, but **only branch on reasons that produce a different response** — skip arms that would just return the same status as the default 403: + +- `decision.reason.isRateLimit()` → 429 +- `decision.reason.isEmail()` → 400 +- `decision.reason.isSensitiveInfo()` → 400 +- `decision.reason.isPromptInjection()` → 400 +- everything else (bot, shield, filter) → default 403 + +Writing an explicit `else if (reason.isShield())` arm that returns 403 just adds noise when the default already returns 403. + +`decision.isErrored()` means something went wrong during rule evaluation but the SDK failed open. Log it and allow the request. + +## Deprecations + +As of `@arcjet/*` 1.4.0, the request-based SDK carries a few deprecated bits. New code should avoid them; existing code that uses them should be migrated when convenient. + +- **`detectPromptInjection({ threshold })`** — the `threshold` option is no longer respected by the server and will be removed in a future release. Drop it from new configs; remove it from existing configs when touching them. Detection runs without it. +- **`PromptInjectionReason.score`** — the `score` field on the reason returned for prompt-injection denials is no longer populated by the server and will be removed. Don't read it; branch on `decision.reason.isPromptInjection()` instead. +- **`experimental_detectPromptInjection`** — the legacy `experimental_` alias is deprecated. Import `detectPromptInjection` directly from `@arcjet/node` / `@arcjet/next` / etc. +- **`ArcjetEdgeRuleReason`** — currently unused; can be ignored in reason-handling switches. + +> _Deprecations last verified against `@arcjet/*` v1.4.0 on **2026-05-20**. Before relying on the items above, grep the installed package for `@deprecated` markers — see [`protocol/index.ts`](https://github.com/arcjet/arcjet-js/blob/main/protocol/index.ts) and [`arcjet/index.ts`](https://github.com/arcjet/arcjet-js/blob/main/arcjet/index.ts)._ + +## Key Patterns + +- Rules that need extra input at protect() time: `tokenBucket` needs `{ requested: N }`, `validateEmail`/`protectSignup` needs `{ email }`, `sensitiveInfo` needs `{ sensitiveInfoValue }`, `detectPromptInjection` needs `{ detectPromptInjectionMessage }`. +- Every rule accepts `mode: "LIVE" | "DRY_RUN"`. Start with DRY_RUN to verify rules match expected traffic before enforcing. +- For existing projects, check for an existing Arcjet client file before creating a new one — extend with `withRule()` instead. diff --git a/skills/arcjet/references/requests_python.md b/skills/arcjet/references/requests_python.md new file mode 100644 index 0000000..c45f38f --- /dev/null +++ b/skills/arcjet/references/requests_python.md @@ -0,0 +1,136 @@ +# Python Request Protection + +## What Request Protection Is + +Request protection inspects HTTP requests — headers, IP, body — to enforce security rules on API routes and form handlers. Works with FastAPI (async) and Flask (sync). + +**Version compatibility:** + +- **Python:** ≥ 3.10 (declared in `pyproject.toml`). Older versions will fail to install — warn the user and stop. +- **FastAPI / Flask:** no formal peer dependency — the SDK adapts to whatever request shape is passed (ASGI scope dict, Flask/Werkzeug `Request`, Django `HttpRequest`, or a pre-built `RequestContext`). The SDK's own tests run against `fastapi==0.135.1` and `flask==3.1.3`; very old releases of either may not expose the expected request attributes. + +> _Version info last verified against `arcjet` v0.7.0 on **2026-05-20**. Before relying on these numbers, check the `requires-python` field in the current [`pyproject.toml`](https://github.com/arcjet/arcjet-py/blob/main/pyproject.toml) — minimums tend to creep upward over time._ + +## Installation + +Install with whichever package manager the project already uses (`pip install`, `uv add`, `poetry add`, etc.) — don't hand-edit `requirements.txt` with a guessed version like `arcjet>=1.0.0` (that release doesn't exist; current is `>=0.7.0`). + +```bash +pip install arcjet +``` + +Read the installed package's types and docstrings for the full API surface. + +## Architecture: Why Things Go Where They Do + +### Client(s) at module scope + +The Python SDK's `arcjet()` / `arcjet_sync()` constructor takes the full rule set at creation time — there is **no** `with_rule()` chain method on the resulting client (that pattern only exists in the JS SDKs). To apply different rules to different routes, create one client per rule set: + +```python +import os +from arcjet import BotCategory, Mode, arcjet, detect_bot, shield, sliding_window + +# Read endpoints: shield + bot detection + lenient rate limit +aj_read = arcjet( + key=os.environ["ARCJET_KEY"], + rules=[ + shield(mode=Mode.LIVE), + detect_bot(mode=Mode.LIVE, allow=[BotCategory.SEARCH_ENGINE]), + sliding_window(mode=Mode.LIVE, interval=60, max=100), + ], +) + +# Write endpoints: same plus a stricter limit +aj_write = arcjet( + key=os.environ["ARCJET_KEY"], + rules=[ + shield(mode=Mode.LIVE), + detect_bot(mode=Mode.LIVE, allow=[BotCategory.SEARCH_ENGINE]), + sliding_window(mode=Mode.LIVE, interval=60, max=15), + ], +) +``` + +For projects with multiple route files, put these clients in a separate `lib/arcjet.py` and import them. For single-file apps, define at the top of the file. Use `arcjet()` for async (FastAPI) and `arcjet_sync()` for sync (Flask). Create clients at module scope only — never inside a handler. + +If you only need one rule set across the whole app, a single client is fine. + +### protect() in route handlers + +Call `protect()` inside each route handler, once per request. Pass the framework's request object directly. + +## Choosing Rules + +See the "Choosing the Right Rules" section in the main skill for rule selection guidance and rate limiting strategy comparisons. Key framework-specific notes: + +- **shield** — always include. No configuration needed. +- **detect_bot** — `allow` and `deny` are mutually exclusive. +- **Rate limits** — use `characteristics` to key by something other than IP. +- **validate_email** — for signup/login forms. +- **detect_sensitive_info** — blocks PII in request bodies. +- **detect_prompt_injection** — for AI endpoints receiving user prompts. +- **filter_request** — block by IP metadata (VPN, Tor, country). + +## Framework-Specific protect() Calls + +### FastAPI (async) + +```python +from fastapi import Request, HTTPException + +@app.get("/api/items") +async def list_items(request: Request): + decision = await aj.protect(request) + if decision.is_denied(): + if decision.reason_v2.type == "RATE_LIMIT": + raise HTTPException(status_code=429, detail="Too many requests") + raise HTTPException(status_code=403, detail="Forbidden") + # proceed... +``` + +### Flask (sync) + +```python +from flask import request, jsonify + +@app.get("/api/items") +def list_items(): + decision = aj.protect(request) + if decision.is_denied(): + if decision.reason_v2.type == "RATE_LIMIT": + return jsonify(error="Too many requests"), 429 + return jsonify(error="Forbidden"), 403 + # proceed... +``` + +## Decision Handling + +`decision.is_denied()` means a LIVE rule triggered a denial. Map `decision.reason_v2.type` to HTTP status codes, but **only branch on reasons that produce a different response** — skip arms that would just return the same status as the default 403: + +- `"RATE_LIMIT"` → 429 +- `"EMAIL"` → 400 +- `"SENSITIVE_INFO"` → 400 +- `"PROMPT_INJECTION"` → 400 +- everything else (`"BOT"`, `"SHIELD"`, `"FILTER"`, fallback) → default 403 + +A branch that returns 403 for SHIELD when the default already returns 403 is dead code; drop it. + +`decision.is_error()` means something went wrong during rule evaluation but the SDK failed open. Log it and allow the request. + +## Deprecations + +As of `arcjet` 0.7.0, the request-based SDK carries a few deprecated bits. New code should avoid them; existing code in the project that uses them should be migrated when convenient. + +- **`decision.reason` / `result.reason` → use `decision.reason_v2` / `result.reason_v2`.** The legacy `reason` accessor returns a tagged-union helper (`reason.is_rate_limit()`, etc.) and is marked `@deprecated`. `reason_v2` returns a typed discriminated union — branch on `reason_v2.type` (`"RATE_LIMIT"`, `"BOT"`, etc.) and read typed fields directly (`reason_v2.remaining`, `reason_v2.spoofed`). A TODO in the SDK notes the name `reason_v2` is itself transitional — in a future major it's planned to fold back into `reason`, but until then `reason_v2` is the right call. +- **`detect_prompt_injection(threshold=...)`** — the `threshold` parameter is no longer respected by the server and will be removed. Drop it from new configs; remove it from existing configs when touching them. The detection runs without it. +- **`PromptInjectionReason.score`** — the `score` field on the reason returned for prompt-injection denials is no longer populated meaningfully and will be removed. Don't read it; rely on `reason_v2.type == "PROMPT_INJECTION"` instead. +- **`arcjet._decision.Reason`** — internal type; use `arcjet._dataclasses.Reason` (re-exported as `arcjet.Reason`) if you need the type annotation. Most callers won't touch this directly. + +> _Deprecations last verified against `arcjet` v0.7.0 on **2026-05-20**. Before relying on the items above, grep the installed package for new `@deprecated` markers — see [`src/arcjet/_decision.py`](https://github.com/arcjet/arcjet-py/blob/main/src/arcjet/_decision.py) and [`src/arcjet/_dataclasses.py`](https://github.com/arcjet/arcjet-py/blob/main/src/arcjet/_dataclasses.py)._ + +## Key Patterns + +- Rules that need extra input at protect() time: `token_bucket` needs `requested=N`, `validate_email` needs `email="..."`, `detect_sensitive_info` needs `sensitive_info_value="..."`, `detect_prompt_injection` needs `detect_prompt_injection_message="..."`. +- Every rule accepts `mode=Mode.LIVE` or `mode=Mode.DRY_RUN`. Start with DRY_RUN to verify rules match expected traffic. +- For existing projects, check for an existing Arcjet client before creating a new one — add the new rule to the existing client's `rules=[...]` list, or define a sibling client with the rules you need. diff --git a/skills/protect-route/SKILL.md b/skills/protect-route/SKILL.md index 8394f16..96a1233 100644 --- a/skills/protect-route/SKILL.md +++ b/skills/protect-route/SKILL.md @@ -1,17 +1,14 @@ --- name: protect-route license: Apache-2.0 -description: "Deprecated alias for add-request-protection. Add security protection to a server-side route or endpoint — rate limiting, bot detection, email validation, and abuse prevention. Prefer /arcjet:add-request-protection." +description: "Deprecated: use the `arcjet` skill instead. Adds security protection to a server-side route or endpoint — rate limiting, bot detection, email validation, and abuse prevention." metadata: author: arcjet internal: true --- -# Deprecated — Use `/arcjet:add-request-protection` +# Deprecated — Use `arcjet` -`/arcjet:protect-route` has been renamed to `/arcjet:add-request-protection`. The new skill includes the same route protection plus integrated CLI workflows for authentication, site/key setup, remote rule management, and traffic verification. +This skill has been replaced by the unified `arcjet` skill, which covers HTTP route protection (this skill's scope) plus non-HTTP code paths (tool calls, MCP handlers, queue workers) in a single skill. -## Instructions for the agent - -1. **Tell the user:** "`/arcjet:protect-route` is deprecated. Use `/arcjet:add-request-protection` instead — it has the same behavior plus integrated CLI setup and verification." -2. Then proceed by following the `/arcjet:add-request-protection` skill (`skills/add-request-protection/SKILL.md`) for the rest of the workflow. Do not duplicate its content here — read and follow that skill directly. +Please use the `arcjet` skill instead.