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.

Informações da origem

Repositório
arcjet/skills
Última atividade na origem
22 de setembro de 2026 às 13:41
Idioma detectado do SKILL.md
inglês
Estrelas
4
Forks
0

Opções de instalação

Por padrão, está selecionado o prompt que primeiro revisa a origem. Você pode mudar para um comando direto ou baixar uma cópia local.

Revise os arquivos de origem

Leia o SKILL.md e os arquivos complementares exibidos pelo SkillsMP antes de decidir se vai instalar.

Exibindo SKILL.md

SKILL.md
Instruções da origem · Visualização somente leitura
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.
Ver no GitHub