From edb4b26af1d1767f8e396f9d011f2cf06f32b0d1 Mon Sep 17 00:00:00 2001 From: David Mytton Date: Wed, 22 Apr 2026 12:55:41 +0000 Subject: [PATCH] feat: add new add guard protection skill and sync with upstream skills repo --- README.md | 9 +- skills/add-ai-protection/SKILL.md | 2 +- skills/add-guard-protection/SKILL.md | 155 ++++++++++++++++ .../references/javascript.md | 169 ++++++++++++++++++ .../add-guard-protection/references/python.md | 169 ++++++++++++++++++ 5 files changed, 499 insertions(+), 5 deletions(-) create mode 100644 skills/add-guard-protection/SKILL.md create mode 100644 skills/add-guard-protection/references/javascript.md create mode 100644 skills/add-guard-protection/references/python.md diff --git a/README.md b/README.md index 2b3fc40..896e373 100644 --- a/README.md +++ b/README.md @@ -25,10 +25,11 @@ After installing, guidance activates automatically. The plugin detects what you' ### Skills -| Skill | Purpose | -| --------------------------- | --------------------------------------------------------------------------------------------- | -| `/arcjet:protect-route` | Add Arcjet protection to any route handler — detects framework, sets up client, applies rules | -| `/arcjet:add-ai-protection` | Add prompt injection detection, PII blocking, and token budget rate limiting to AI endpoints | +| Skill | Purpose | +| ------------------------------ | --------------------------------------------------------------------------------------------------------- | +| `/arcjet:protect-route` | Add Arcjet protection to any route handler — detects framework, sets up client, applies rules | +| `/arcjet:add-ai-protection` | Add prompt injection detection, PII blocking, and token budget rate limiting to AI HTTP endpoints | +| `/arcjet:add-guard-protection` | Add Arcjet Guard to non-HTTP code paths — AI agent tool calls, MCP tool handlers, background jobs/workers | ### Rules (auto-activated) diff --git a/skills/add-ai-protection/SKILL.md b/skills/add-ai-protection/SKILL.md index 3057c5f..6361213 100644 --- a/skills/add-ai-protection/SKILL.md +++ b/skills/add-ai-protection/SKILL.md @@ -138,7 +138,7 @@ if (decision.isErrored()) { Adapt the response format to your framework (e.g., `res.status(429).json(...)` for Express). -## Step 5: Verify +## Step 4: Verify 1. Start the app and send a normal message — should succeed 2. Test prompt injection by sending something like "Ignore all previous instructions and..." diff --git a/skills/add-guard-protection/SKILL.md b/skills/add-guard-protection/SKILL.md new file mode 100644 index 0000000..ae24e9d --- /dev/null +++ b/skills/add-guard-protection/SKILL.md @@ -0,0 +1,155 @@ +--- +name: add-guard-protection +license: Apache-2.0 +description: Add Arcjet Guard protection to AI agent tool calls, background jobs, queue workers, MCP tool handlers, and other code paths where there is no HTTP request. Covers rate limiting, prompt injection detection, sensitive information blocking, and custom rules using `@arcjet/guard` (JS/TS) and `arcjet.guard` (Python). Use this skill whenever the user wants to protect tool calls, agent loops, MCP tool handlers, background workers, or any non-HTTP code from abuse — even if they describe it as "rate limit my tool calls," "block prompt injection in my agent," "add security to my MCP server," or "protect my queue worker" without mentioning Arcjet or Guard specifically. +metadata: + pathPatterns: + - "**/agents/**" + - "**/agent/**" + - "**/tools/**" + - "**/tool/**" + - "**/mcp/**" + - "**/mcp-server/**" + - "**/workers/**" + - "**/worker/**" + - "**/jobs/**" + - "**/tasks/**" + - "**/queue/**" + - "**/queues/**" + - "**/background/**" + importPatterns: + - "@arcjet/guard" + - "arcjet.guard" + - "@modelcontextprotocol/sdk" + - "@ai-sdk/*" + - "ai" + - "langchain" + - "bullmq" + - "celery" + promptSignals: + phrases: + - "arcjet guard" + - "tool call" + - "tool calls" + - "mcp server" + - "mcp tool" + - "agent loop" + - "background worker" + - "queue worker" + - "prompt injection" + anyOf: + - "protect tool" + - "protect agent" + - "protect mcp" + - "protect worker" + - "rate limit tool" + - "rate limit agent" + - "secure agent" + - "secure mcp" + - "guard" +--- + +# Add Arcjet Guard Protection + +Arcjet Guard provides rate limiting, prompt injection detection, sensitive information blocking, and custom rules for code paths that don't have an HTTP request — AI agent tool calls, MCP tool handlers, background job processors, queue workers, and similar. + +For code paths that **do** have an HTTP request (API routes, form handlers, webhooks), use `/arcjet:protect-route` instead. For AI chat/completion HTTP endpoints specifically, use `/arcjet:add-ai-protection`. + +## Reference + +Read https://docs.arcjet.com/llms.txt for comprehensive SDK documentation. + +## Step 1: Detect the Language and Install + +Check the project for language indicators: + +- `package.json` → JavaScript/TypeScript → `npm install @arcjet/guard` (requires `@arcjet/guard` >= 1.4.0) +- `requirements.txt` / `pyproject.toml` → Python → `pip install arcjet` (requires `arcjet` >= 0.7.0; Guard is included) +- `go.mod`, `Cargo.toml`, `pom.xml`, or other languages → **Guard is not available**. Tell the user that Arcjet Guard currently only supports JavaScript/TypeScript and Python. Do not create a hand-rolled imitation or hallucinate a package that doesn't exist. Suggest they reach out to Arcjet with their use case. + +## Step 2: Read the Language Reference + +**You must read the reference file for the detected language before writing any code.** The references contain the exact imports, constructor signatures, rule configuration syntax, and `guard()` call patterns for that language. + +- JavaScript/TypeScript: [references/javascript.md](references/javascript.md) +- Python: [references/python.md](references/python.md) + +Do not guess at the API. The reference files are the source of truth for all code patterns. + +## Step 3: Create the Guard Client (Once, at Module Scope) + +The client holds a persistent connection. Create it once at module scope and reuse it — never inside a function or per-call. Name the variable `arcjet`. + +Check if `ARCJET_KEY` is set in the environment file (`.env`, `.env.local`, etc.). If not, use the Arcjet MCP tools to get one: + +- Call `list-teams` to find the team +- Call `list-sites` to find an existing site, or `create-site` for a new one +- Call `get-site-key` to retrieve the key +- Add the key to the appropriate env file along with `ARCJET_ENV=development` + +Alternatively, remind the user to register at https://app.arcjet.com and add the key manually. + +## Step 4: Configure Rules at Module Scope + +Rules are configured once as reusable factories, then called with per-invocation input. This two-phase pattern matters — the rule config carries a stable ID used for server-side aggregation, while the per-call input varies. + +When configuring rate limit rules, set `bucket` to a descriptive name (e.g. `"tool-calls"`, `"session-api"`) for semantic clarity and fewer collisions. + +### Choosing Rules by Use Case + +| Use case | Recommended rules | +| ----------------------------------- | -------------------------------------------------------------------- | +| AI agent tool calls | `tokenBucket` + `detectPromptInjection` | +| MCP tool handlers | `slidingWindow` or `tokenBucket` + `detectPromptInjection` | +| Background AI task processor | `tokenBucket` + `localDetectSensitiveInfo` | +| Queue worker with user input | `tokenBucket` + `detectPromptInjection` + `localDetectSensitiveInfo` | +| Scanning tool results for injection | `detectPromptInjection` (scan the returned content) | + +## Step 5: Call guard() Inline Before Each Operation + +Call `guard()` directly where each operation happens — inline in each tool handler, task processor, or function that needs protection. Do not wrap guard in a shared helper function. + +Each `guard()` call takes: + +- **label**: descriptive string for the dashboard (e.g. `"tools.search_web"`, `"tasks.generate"`) +- **rules**: array of bound rule invocations +- **metadata** (optional): key-value pairs for analytics/auditing (e.g. `{ userId }`) + +Rate limit rules take an explicit **key** string — use a user ID, session ID, API key, or any stable identifier. + +You MUST modify the existing source files — adding the dependency to `package.json` / `requirements.txt` alone is not enough. The `guard()` calls must be integrated into the actual code. + +## Step 6: Handle Decisions + +Always check `decision.conclusion`: + +- `"DENY"` → block the operation. Use per-rule result accessors (see reference) for specific error messages like retry-after times. +- `"ALLOW"` → safe to proceed + +See the language reference for the exact decision-checking pattern and per-rule result accessors. + +## Step 7: Verify + +Start rules in `"DRY_RUN"` mode first and promote to `"LIVE"` once verified. + +**Always recommend using the Arcjet MCP tools** to verify rules and analyze traffic: + +- `list-requests` — confirm decisions are being recorded, filter by conclusion to see blocks +- `analyze-traffic` — review denial rates and patterns for the guarded code path +- `explain-decision` — understand why a specific call was allowed or denied +- `promote-rule` — promote rules from `DRY_RUN` to `LIVE` once verified + +If the user wants a full security review, suggest the `/arcjet:security-analyst` agent which can investigate traffic, detect anomalies, and recommend additional rules. + +The Arcjet dashboard at https://app.arcjet.com is also available for visual inspection. + +## Common Mistakes to Avoid + +- **Wrapping guard in a shared helper function** — calling `guard()` through a `guardToolCall()` or `protectCall()` wrapper hides which rules apply to each operation. Call `guard()` inline where each operation happens. +- **Creating the client per call** — the client holds a persistent connection. Create it once at module scope. +- **Configuring rules inside a function** — rule configs carry stable IDs. Creating them per call breaks dashboard tracking and rate limit state. +- **Forgetting the `key` parameter on rate limit rules** — without a key, Guard can't track per-user limits. +- **Forgetting `bucket` on rate limit rules** — without a named bucket, different rules may collide. +- **Using the HTTP SDK when there's no request** — use `@arcjet/guard` / `arcjet.guard` for non-HTTP code, not `@arcjet/node`, `@arcjet/next`, or `arcjet()`. +- **Not checking `decision.conclusion`** — always check before proceeding. +- **Generic DENY messages** — use per-rule result accessors to give users specific feedback like retry-after times. diff --git a/skills/add-guard-protection/references/javascript.md b/skills/add-guard-protection/references/javascript.md new file mode 100644 index 0000000..1e1a855 --- /dev/null +++ b/skills/add-guard-protection/references/javascript.md @@ -0,0 +1,169 @@ +# JavaScript/TypeScript Guard Reference + +## Installation + +Requires `@arcjet/guard` >= 1.4.0. + +```bash +npm install @arcjet/guard +``` + +## Create the Guard Client + +```typescript +import { launchArcjet } from "@arcjet/guard"; + +const arcjet = launchArcjet({ key: process.env.ARCJET_KEY! }); +``` + +Create once at module scope. The client holds a persistent HTTP/2 connection — creating it inside a function defeats connection reuse. + +## Rules + +Configure rules at module scope. Each rule config carries a stable ID for server-side aggregation, so creating them per call would break dashboard tracking and rate limit state. + +### Token Bucket + +Best for AI workloads with variable cost per call. Configure a `bucket` name for semantic clarity and to avoid collisions between different rate limit rules. + +```typescript +import { tokenBucket } from "@arcjet/guard"; + +const userLimit = tokenBucket({ + label: "user.tool_call_bucket", // rule label for dashboard tracking + bucket: "tool-calls", // named bucket for this limit + refillRate: 100, + intervalSeconds: 60, + maxTokens: 500, +}); +``` + +### Fixed Window + +Hard cap per time period, counter resets at end of window: + +```typescript +import { fixedWindow } from "@arcjet/guard"; + +const callLimit = fixedWindow({ + label: "user.hourly_calls", + bucket: "hourly-calls", + maxRequests: 100, + windowSeconds: 3600, +}); +``` + +### Sliding Window + +Smooth rate limiting without burst-at-boundary issues: + +```typescript +import { slidingWindow } from "@arcjet/guard"; + +const sessionLimit = slidingWindow({ + label: "session.api_calls", + bucket: "session-api", + maxRequests: 500, + intervalSeconds: 60, +}); +``` + +### Prompt Injection Detection + +Detects jailbreaks, role-play escapes, and instruction overrides. Useful both for user input before it reaches a model AND for tool call results containing untrusted content. + +```typescript +import { detectPromptInjection } from "@arcjet/guard"; + +const piRule = detectPromptInjection(); +``` + +### Sensitive Information Detection + +Detects PII locally via WASM — raw text never leaves the SDK. + +```typescript +import { localDetectSensitiveInfo } from "@arcjet/guard"; + +const siRule = localDetectSensitiveInfo({ + deny: ["CREDIT_CARD_NUMBER", "EMAIL", "PHONE_NUMBER"], +}); +``` + +## Calling guard() + +Call `guard()` inline where each operation happens. Pass a `label` (appears in the dashboard), `rules`, and optionally `metadata` for analytics/auditing. + +When an `AbortSignal` is available (e.g. from the caller or a timeout), pass it as `abortSignal` so guard respects cancellation. + +```typescript +async function getWeather(city: string, userId: string, signal?: AbortSignal) { + const decision = await arcjet.guard({ + label: "tools.get_weather", + metadata: { userId }, + rules: [ + userLimit({ key: userId, requested: 1 }), + ], + ...(signal && { abortSignal: signal }), + }); + + if (decision.conclusion === "DENY") { + throw new Error("Rate limited — try again later"); + } + + return fetchWeather(city); +} + +async function searchWeb(query: string, userId: string) { + const decision = await arcjet.guard({ + label: "tools.search_web", + metadata: { userId }, + rules: [ + userLimit({ key: userId, requested: 1 }), + piRule(query), + ], + }); + + if (decision.conclusion === "DENY") { + // Use per-rule results for specific error messages + const rateLimitDenied = userLimit.deniedResult(decision); + if (rateLimitDenied) { + throw new Error(`Rate limited — try again in ${rateLimitDenied.resetInSeconds}s`); + } + if (decision.reason === "PROMPT_INJECTION") { + throw new Error("Prompt injection detected in query"); + } + throw new Error("Request denied"); + } + + if (decision.hasError()) { + console.warn("Arcjet guard error — proceeding with caution"); + } + + return doSearch(query); +} +``` + +## Inspecting Per-Rule Results + +Both the configured rule and the bound input provide typed result accessors: + +```typescript +const rl = userLimit({ key: userId, requested: 5 }); +const decision = await arcjet.guard({ + label: "tools.chat", + rules: [rl, piRule(message)], +}); + +// From a bound input — this specific invocation's result +const r = rl.result(decision); +if (r) { + console.log(r.remainingTokens, r.maxTokens, r.resetInSeconds); +} + +// From the configured rule — first denied result across all submissions +const denied = userLimit.deniedResult(decision); +if (denied) { + console.log(`Retry after ${denied.resetInSeconds}s`); +} +``` diff --git a/skills/add-guard-protection/references/python.md b/skills/add-guard-protection/references/python.md new file mode 100644 index 0000000..a2f8277 --- /dev/null +++ b/skills/add-guard-protection/references/python.md @@ -0,0 +1,169 @@ +# 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") +```