Skip to main content

integrate-arcjet-guard-cloudflare-think

Integrate Arcjet Guard into Cloudflare Think — delegate beforeToolCall to guardHooks so a DENY substitutes or blocks the tool, and read a caller-owned id via cloudflareThinkContext. Use when asked to add Arcjet to @cloudflare/think, Cloudflare Agents Think, rate limit those tools, screen inbound messages, or block prompt injection / PII. This is Cloudflare Think, not the Vercel AI SDK and not needsApproval HITL.

Quellinformationen

Repository
arcjet/skills
Letzte Quellaktivität
22. September 2026 um 13:41
Erkannte Sprache von SKILL.md
Englisch
Sterne
4
Forks
0

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
name
integrate-arcjet-guard-cloudflare-think
description
Integrate Arcjet Guard into Cloudflare Think — delegate beforeToolCall to guardHooks so a DENY substitutes or blocks the tool, and read a caller-owned id via cloudflareThinkContext. Use when asked to add Arcjet to @cloudflare/think, Cloudflare Agents Think, rate limit those tools, screen inbound messages, or block prompt injection / PII. This is Cloudflare Think, not the Vercel AI SDK and not needsApproval HITL.
license
Apache-2.0
compatibility
Requires official @cloudflare/think >=0.3.0 <1 as a peer of published @arcjet/guard. Install with npm install or pnpm add. Local Node.js follows the @arcjet/guard runtime floor (>=22.21.0 <23 || >=24.5.0). Cloudflare Workers need nodejs_compat and compat date 2025-09-01 or later (minimum, not an exact pin). Path is /v0. Do not use @arcjet/guard/vercel-ai/v7.
metadata
{"author":"arcjet","type":"core","library":"@arcjet/guard"}
# Integrate Arcjet Guard into Cloudflare Think `@arcjet/guard/cloudflare-think/v0` wraps the agent's existing Arcjet client. It never talks to the Arcjet API itself. Shared Guard fundamentals (client, rules, labels, decisions, capture, registration) live in [../arcjet/references/guards_javascript.md](../arcjet/references/guards_javascript.md). Load that reference for anything that is not Think-specific. Official `@cloudflare/think` `>=0.3.0 <1` only — that is the first release whose `beforeToolCall` returns a functional `ToolCallDecision` (`allow` / `block` / `substitute`) and wraps every server-side tool's `execute` so the hook runs first. There is no `/v1` until Think ships 1.x. This is **not** the Vercel AI SDK (`ai` / `@arcjet/guard/vercel-ai/v7`). Think owns `streamText` internally; do not also wrap its tools with the AI SDK mix-in. Not TanStack Start HTTP `protect()`. Not Cloudflare Workers HTTP `protect()` (`@arcjet/node` / request-based). Exports: `guardHooks`, `cloudflareThinkContext`. There is **no `guardTool`** and no `guardThink` mixin. Skip is the `beforeToolCall` return, not throw-from-execute. There is no `guardInbound` and no `guardApproval`. No unversioned `@arcjet/guard/cloudflare-think` alias. Two surfaces, one decision rule: - **Tool calls** → `guardHooks`. Returns a `{ beforeToolCall }` object the `Think` subclass **delegates to**. Delivery is a Think `ToolCallDecision`: **substitute** (`{ action: "substitute", output: ArcjetDenialResult }`) so the model sees the payload, or **block** (`{ action: "block", reason }`) so the model sees a reason string. `void` / `{ action: "allow" }` executes. Do not throw — a throw is a raw exception / `onChatError`, not a denial. - **Correlation** → `cloudflareThinkContext` reads a caller-owned id. It never mints. It never reads Durable Object ids, `toolCallId`, `requestId`, or `traceId`. Docs: https://docs.arcjet.com/guards/cloudflare-think/ Do not invent a second slug and do not overwrite any other `/guards/...` page. Example (the `main` tree 404s; no example PR is open, so cite this branch until `examples/cloudflare-think-agent` exists on `main`): [`examples/cloudflare-think-agent`](https://github.com/arcjet/examples/tree/david/cursor/cloudflare-think-agent-f8cc/examples/cloudflare-think-agent) (no verify fixture). Do not invent a second example name. ## The gate is `beforeToolCall` block / substitute Think wraps every server-side tool's `execute` and consults `beforeToolCall` first (subclass hook, then extensions). Returning `block` or `substitute` skips `execute`. Fail closed: always return one of those on error — do not return `void` (that executes the tool) and do not throw. Default DENY is **substitute** with `ArcjetDenialResult` so the model sees `{ arcjetDenied: true, … }`. `onDeny: "block"` uses `{ action: "block", reason }` instead and drops the structured fields. `onDeny: "block"` applies to **real DENY only**; unavailable still fail-closes with **substitute**. Core `guard()` still fails open (`hasFailedOpen()`). Client tools (no server `execute`) never enter this hook. Workspace tools that Think auto-merges do, if they have `execute`. ## `needsApproval` is not a policy gate AI SDK `needsApproval` / confirmation-style tools / human resume are human-in-the-loop. After a human yes, Guard still runs on the tool call. Same trap as TanStack `needsApproval`, Mastra `requireApproval`, Claude `canUseTool`, LangGraph `interrupt()`, OpenAI Agents `needsApproval`, Genkit `interrupt()`, and Google ADK `requireConfirmation`. There is no `guardApproval`. ## Screen inbound before `chat()` / `submitMessages()` There is no `guardInbound`. `beforeTurn` / `beforeStep` tune the model call; they are not this policy gate. Call `arcjet.guard()` in the application (or at the start of `beforeTurn` **and act on the decision**) before the turn starts. Core `guard()` fails open: `ALLOW` is not proof the rules ran. Gate on `decision.hasFailedOpen()` if this call site must fail closed; `guardHooks` already defaults to that. ## `actor` / `inputs` — omit and remote rules never fire A remote policy that declares `actor` or typed `inputs` only evaluates those values when this call sends them. Pass `actor` and `inputs` on `guardHooks` (and on inbound `arcjet.guard()`). Import `policyInput` from `@arcjet/guard`, not from the adapter path. Check installed types — do not pass fields a helper does not declare. - **Omit `actor` / `inputs`** and a remote policy that requires them never fires. The guard call still happens; the remote rules just have nothing to read. - **A resolver that throws fail-closes** under the default `onGuardError: "deny"` (unavailable substitute — never `void`). - **`actor` is the authenticated caller**, never a model-produced tool argument. `input` on the Think envelope is model-controlled. Derive `actor` from authenticated server-side context (a signed session, verified token, or Durable Object state you already trust) — not from `ctx.input`. `rules` / `action` / `metadata` see `{ toolName, input }`. `actor` / `inputs` resolvers also receive the Think `beforeToolCall` context as the second argument. ## Questions to ask the human first Ask only what you cannot infer from the code; suggest defaults. 1. Which tools are **risky** (external side effects, irreversible, spends money, sends messages)? Those are gated by `guardHooks`. Client tools with no `execute` are out of scope. 2. What **limits**? (e.g. "10 lookups/min per user" → `tokenBucket`.) 3. Who is the **user** for metadata — an opaque user/tenant ID (never PII)? Default: none. That same authenticated id is `actor`. Put the conversation / session id you already have on `guardHooks({ sessionId })`. That id is the correlation id, not the user. Do not use the Durable Object id or `toolCallId`. 4. Is an Arcjet outage unacceptable? Every helper defaults to `onGuardError: "deny"`. Ask explicitly about inbound screening before the turn: failing closed there means the agent does not answer, so `"allow"` is a routine and legitimate choice at that one call site. ## The things readers get wrong 1. **This is not the Vercel AI SDK.** Do not import `@arcjet/guard/vercel-ai/v7` and do not wrap Think tools with `guardTool` from that namespace. Think already owns `streamText`. 2. **There is no `guardInbound`.** Screen with core `guard()` before `chat()` / `submitMessages()`. 3. **`needsApproval` is HITL, not policy.** 4. **Fail closed = always return `block` or `substitute` on error.** `void` executes the tool. A throw is not a denial. Unavailable fail-closes with **substitute**, even when `onDeny: "block"`. 5. **Default DENY is substitute** (structured `ArcjetDenialResult`). `block` is the reason-string path and drops the fields. It applies to real DENY only. 6. **`guardHooks` is not a mixin.** Delegate `beforeToolCall` to `hooks.beforeToolCall(ctx)`. There is no `guardThink(Think, …)`. 7. **Correlation is read, never minted.** Never Durable Object id, never `toolCallId`, never `requestId`, never `traceId`. 8. **The import path is versioned.** `@arcjet/guard/cloudflare-think/v0` resolves; `@arcjet/guard/cloudflare-think` does not. 9. **Key rate limits on the authenticated caller**, not a model-supplied order id. Same for `actor`. 10. **Omit `actor` / `inputs` and remote rules never fire.** A resolver throw fail-closes. Import `policyInput` from `@arcjet/guard`. 11. **Do not hand-wrap every Think tool with raw `guard()`.** ## Step 1: Install and find the guard client Install published `@arcjet/guard` and the `@cloudflare/think` peer. Use the package manager the project already uses — do not pin a git SHA: ```bash npm install @arcjet/guard @cloudflare/think pnpm add @arcjet/guard @cloudflare/think ``` Peer range is `@cloudflare/think` `>=0.3.0 <1`. The versioned path `@arcjet/guard/cloudflare-think/v0` resolves; the unversioned `@arcjet/guard/cloudflare-think` path does not. If the agent has no guard client yet, launch one **once at module scope**: ```ts import { launchArcjet } from "@arcjet/guard"; export const arcjet = launchArcjet({ key: process.env.ARCJET_KEY! }); ``` ## Step 2: Gate tool calls — delegate `beforeToolCall` Pass the **current conversation's** authenticated caller into `guardHooks`. A Durable Object is often one-per-conversation — still do not use `this.ctx.id` / `this.name` as `actor` or correlation. ```ts import { Think } from "@cloudflare/think"; import { launchArcjet, policyInput, tokenBucket } from "@arcjet/guard"; import { guardHooks } from "@arcjet/guard/cloudflare-think/v0"; const arcjet = launchArcjet({ key: process.env.ARCJET_KEY! }); const lookupLimit = tokenBucket({ bucket: "lookups", refillRate: 10, intervalSeconds: 60, maxTokens: 10, }); // From auth middleware or Durable Object state you already trust. // Not this.ctx.id / this.name, and not a model-produced argument. const userId = authenticatedUserId; const conversationId = authenticatedConversationId; const hooks = guardHooks(arcjet, { action: ({ toolName }) => `${toolName}.invoked`, // Authenticated caller — never a model-produced order id / ctx.input. actor: userId, inputs: ({ toolName }) => ({ tool: policyInput.server.string(toolName), }), // Keyed on the authenticated caller, not the model-supplied order id. rules: () => [lookupLimit({ key: userId, requested: 1 })], sessionId: conversationId, onGuardError: "deny", // default DENY is substitute (ArcjetDenialResult). onDeny: "block" // uses { action: "block", reason } on real DENY only. Unavailable // still substitutes. }); export class SupportAgent extends Think<Env> { // needsApproval on a tool is HITL — not this policy gate override beforeToolCall = hooks.beforeToolCall; } ``` If the subclass already implements `beforeToolCall`, call `hooks.beforeToolCall(ctx)` and return its `ToolCallDecision`. Do not void past a DENY and do not also wrap with `@arcjet/guard/vercel-ai/v7`. Omit `rules` to submit none — the guard call still happens. ALLOW captures `outcome: "success"` when the policy lets the tool run, not when `execute` finishes. ## Step 3: Screen inbound before the turn ```ts import { detectPromptInjection, policyInput } from "@arcjet/guard"; import { cloudflareThinkContext } from "@arcjet/guard/cloudflare-think/v0"; const inbound = detectPromptInjection(); const decision = await arcjet.guard({ label: "message.received", actor: userId, inputs: { content: policyInput.server.string(userText) }, rules: [inbound(userText)], ...cloudflareThinkContext({ context: { sessionId: conversationId } }), }); if (decision.conclusion === "DENY") { throw new Error("message blocked"); } if (decision.hasFailedOpen()) { throw new Error("inbound guard unavailable"); } // Then chat() / submitMessages() / the WebSocket turn. ``` There is no `guardInbound`. `guard()` fails open — always check `hasFailedOpen()`. ## Step 4: Correlation `cloudflareThinkContext` reads a caller-owned id. Preference: `correlationId`, then `sessionId`, then `conversationId` on a caller-owned wrap (`cloudflareThinkContext({ context: appContext })`), then copies on a bare app object, then `guardHooks({ sessionId })` / `init.correlationId`. It never mints. It never reads the Durable Object id / `this.ctx.id` / `this.name`. It never reads `toolCallId`, `requestId`, `traceId`, or stream ids Think or the AI SDK mint. A `beforeToolCall` context that has `toolCallId` and `toolName` is Think's envelope — top-level `sessionId` on that object is ignored. Do not invent a correlation id per turn. If nothing valid remains, the call is uncorrelated rather than joined to a generated id. ## Verify the integration 1. `npm run typecheck` (or the project's type-check) passes. 2. Exercise inbound PI (before the turn, including `hasFailedOpen()`), a substitute-deny (model sees `ArcjetDenialResult`), a block-deny (reason string; real DENY only), unavailable + `onDeny: "block"` still substitute, void execute, no-throw, never-mint, omitted `actor` / `inputs` (remote rules silent), a resolver throw (fail-closed substitute), and fail-closed (an unreachable guard → substitute, never void). Confirm `needsApproval` is never treated as the gate and that tools are not also wrapped with `@arcjet/guard/vercel-ai/v7`. 3. Confirm in the Arcjet Console / CLI that decisions share the caller-owned session / conversation id — not a Durable Object id or `toolCallId` — and that `actor` is the authenticated caller. 4. Manual E2E with a real `ARCJET_KEY` is still-to-verify until you run it. Worked example: [`examples/cloudflare-think-agent`](https://github.com/arcjet/examples/tree/david/cursor/cloudflare-think-agent-f8cc/examples/cloudflare-think-agent) (no verify fixture; `main` does not have this tree yet). Do not invent a second example name. Do not add an example in this skills repo.
Auf GitHub ansehen