integrate-arcjet-guard-cloudflare-think について
このスキルは、Arcjet GuardをCloudflare Thinkに統合し、サーバー側ツール呼び出しの制御と受信メッセージの検査を行う方法を案内します。指定されたThinkのバージョン範囲を使う開発者向けに、拒否動作、呼び出し元の識別、相関IDの扱いを説明します。
主な機能
- 01`beforeToolCall`でサーバー側ツールを制御
- 02ブロックと代替応答による拒否に対応
- 03ターン開始前に受信メッセージを検査
- 04認証済みの呼び出し元とポリシー入力を渡す
- 05呼び出し元が用意した相関IDを参照
活用シーン
- 01リスクのあるツール呼び出しを拒否または代替
- 02エージェントのターン前にユーザーメッセージを検査
- 03呼び出し元と入力を使ってリモートルールを適用
- 04Guardの判定をセッションコンテキストに関連付け
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ arcjet/ skills --skill integrate-arcjet-guard-cloudflare-thinkインストール後、Claude Code で/integrate-arcjet-guard-cloudflare-thinkを入力して読み込まれたか確認します。
詳細
Cloudflare Think に Arcjet Guard を統合する
@arcjet/guard/cloudflare-think/v0 は、エージェントが既に使用している Arcjet クライアントをラップします。Arcjet API と直接通信することはありません。共有の Guard の基本機能(クライアント、ルール、ラベル、判定、キャプチャ、登録)については、
../arcjet/references/guards_javascript.md を参照してください。Think 固有ではない内容については、このリファレンスを読み込んでください。
公式の @cloudflare/think >=0.3.0 <1 のみが対象です。これは、beforeToolCall が機能する ToolCallDecision(allow / block / substitute)を返し、すべてのサーバー側ツールの execute をラップしてフックを先に実行する最初のリリースです。Think が 1.x をリリースするまでは /v1 はありません。これは Vercel AI SDK(ai / @arcjet/guard/vercel-ai/v7)ではありません。Think は内部で streamText を管理するため、AI SDK のミックスインでツールを重ねてラップしないでください。TanStack Start の HTTP protect() でもありません。Cloudflare Workers の HTTP protect()(@arcjet/node/リクエストベース)でもありません。
エクスポート:guardHooks、cloudflareThinkContext。guardTool も guardThink ミックスインもありません。スキップするには beforeToolCall の戻り値を使います。execute から例外をスローする方法ではありません。guardInbound も guardApproval もありません。バージョン指定のない @arcjet/guard/cloudflare-think エイリアスもありません。
2 つの用途、1 つの判定ルール:
- ツール呼び出し →
guardHooks。Thinkのサブクラスが委譲する{ beforeToolCall }オブジェクトを返します。Think のToolCallDecisionを返します。モデルにペイロードを見せるには substitute({ action: "substitute", output: ArcjetDenialResult })、モデルに理由文字列を見せるには block({ action: "block", reason })を使います。void/{ action: "allow" }の場合は実行されます。例外をスローしないでください。スローすると拒否ではなく、未加工の例外/onChatErrorになります。 - 相関付け →
cloudflareThinkContextは呼び出し元が所有する ID を読み取ります。ID を生成することはありません。Durable Object の ID、toolCallId、requestId、traceIdは読み取りません。
ドキュメント:https://docs.arcjet.com/guards/cloudflare-think/
2 つ目のスラッグを作成したり、他の /guards/... ページを上書きしたりしないでください。例(main ツリーは 404 になり、例の PR もまだないため、examples/cloudflare-think-agent が main に存在するまでは、このブランチを引用してください):
examples/cloudflare-think-agent
(検証用フィクスチャはありません)。2 つ目の例の名前を作らないでください。
ゲートは beforeToolCall の block/substitute
Think はすべてのサーバー側ツールの execute をラップし、最初に beforeToolCall(サブクラスのフック、その後に拡張機能)を呼び出します。block または substitute を返すと、execute はスキップされます。フェイルクローズにしてください。エラー時には必ずいずれかを返してください。void(ツールが実行されます)を返したり、例外をスローしたりしないでください。デフォルトの DENY は、モデルに { arcjetDenied: true, … } を見せる ArcjetDenialResult を使った substitute です。onDeny: "block" では代わりに { action: "block", reason } を使い、構造化フィールドは破棄されます。onDeny: "block" が適用されるのは 実際の DENY のみです。利用不可の場合もフェイルクローズし、substitute を使います。コアの guard() は引き続きフェイルオープンします(hasFailedOpen())。
クライアントツール(サーバー側の execute がないツール)は、このフックに入りません。Think が自動マージするワークスペースツールは、execute があればこのフックに入ります。
needsApproval はポリシーゲートではありません
AI SDK の needsApproval、確認形式のツール、人間による再開は、ヒューマン・イン・ザ・ループの仕組みです。人間が承認した後も、Guard はツール呼び出しに対して実行されます。TanStack の needsApproval、Mastra の requireApproval、Claude の canUseTool、LangGraph の interrupt()、OpenAI Agents の needsApproval、Genkit の interrupt()、Google ADK の requireConfirmation と同じ落とし穴があります。guardApproval はありません。
chat() / submitMessages() の前に受信内容をスクリーニングする
guardInbound はありません。beforeTurn / beforeStep はモデル呼び出しを調整するものであり、このポリシーゲートではありません。ターン開始前に、アプリケーション内で(または beforeTurn の冒頭で判定に基づいて対応したうえで)arcjet.guard() を呼び出してください。コアの guard() はフェイルオープンです。ALLOW はルールが実行された証拠ではありません。この呼び出し箇所でフェイルクローズが必要な場合は decision.hasFailedOpen() に基づいてゲートしてください。guardHooks はすでにこれをデフォルトにしています。
actor / inputs を省略すると、リモートルールは一切実行されない
actor または型付き inputs を宣言するリモートポリシーは、この呼び出しでそれらの値を送信した場合にのみ評価されます。guardHooks(および受信時の arcjet.guard())に actor と inputs を渡してください。policyInput はアダプターパスではなく、@arcjet/guard からインポートしてください。インストール済みの型を確認し、ヘルパーで宣言されていないフィールドは渡さないでください。
actor/inputsを省略すると、それらを必要とするリモートポリシーは一切実行されません。ガード呼び出し自体は行われますが、リモートルールが読み取るものがありません。- リゾルバーが例外をスローすると、デフォルトの
onGuardError: "deny"ではフェイルクローズします(代替値を使ってください。決してvoidにしないでください)。 actorは認証済みの呼び出し元であり、モデルが生成したツール引数ではありません。Think エンベロープのinputはモデルが制御します。actorは、すでに信頼しているサーバー側の認証済みコンテキスト(署名付きセッション、検証済みトークン、または Durable Object の状態など)から導出してください。ctx.inputから導出してはいけません。
rules / action / metadata には { toolName, input } が渡されます。actor / inputs リゾルバーには、2 番目の引数として Think の beforeToolCall コンテキストも渡されます。
まず人に確認する質問
コードから推測できないことだけを質問し、デフォルト値を提案してください。
- リスクのあるツールはどれですか(外部への副作用、取り消し不能、金銭の消費、メッセージの送信など)?これらは
guardHooksでゲートします。executeのないクライアントツールは対象外です。 - 制限は何ですか?(例:「ユーザーあたり毎分 10 回の検索」→
tokenBucket。) - メタデータに使用するユーザーは誰ですか。不透明なユーザー ID/テナント ID(個人を特定できる情報は使わない)ですか?デフォルトはなしです。その同じ認証済み ID が
actorになります。すでにある会話 ID/セッション ID をguardHooks({ sessionId })に渡してください。この ID は相関 ID であり、ユーザーではありません。Durable Object ID やtoolCallIdは使わないでください。 - Arcjet の停止は許容できませんか?すべてのヘルパーはデフォルトで
onGuardError: "deny"です。ターン前の受信内容のスクリーニングについては、明示的に確認してください。そこでフェイルクローズするとエージェントは応答しなくなるため、その呼び出し箇所だけ"allow"を選ぶのは一般的で妥当な選択です。
読者が間違えやすい点
- これは Vercel AI SDK ではありません。
@arcjet/guard/vercel-ai/v7をインポートしたり、その名前空間のguardToolで Think ツールをラップしたりしないでください。streamTextは Think がすでに所有しています。 guardInboundはありません。chat()/submitMessages()の前にコアのguard()でスクリーニングしてください。needsApprovalは HITL であり、ポリシーではありません。- フェイルクローズとは、エラー時に必ず
blockまたはsubstituteを返すことです。voidはツールを実行します。例外をスローしても拒否にはなりません。利用不可の場合は、onDeny: "block"であっても substitute でフェイルクローズします。 - デフォルトの DENY は substitute(構造化された
ArcjetDenialResult)です。blockは理由文字列の経路で、フィールドが失われます。これは実際の DENY にのみ適用されます。 guardHooksはミックスインではありません。beforeToolCallをhooks.beforeToolCall(ctx)に委譲してください。guardThink(Think, …)はありません。- 相関 ID は読み取るものであり、新たに作るものではありません。 Durable Object ID、
toolCallId、requestId、traceIdは決して使わないでください。 - インポートパスにはバージョンが含まれます。
@arcjet/guard/cloudflare-think/v0は解決されますが、@arcjet/guard/cloudflare-thinkは解決されません。 - レート制限のキーには、モデルが指定した注文 ID ではなく、認証済みの呼び出し元を使ってください。
actorも同様です。 actor/inputsを省略すると、リモートルールは一切実行されません。 リゾルバーが例外をスローするとフェイルクローズします。policyInputは@arcjet/guardからインポートしてください。- Think の各ツールを、素の
guard()で個別にラップしないでください。
手順 1:guard クライアントをインストールして取得する
公開済みの @arcjet/guard と、ピア依存関係の @cloudflare/think をインストールします。
プロジェクトですでに使用しているパッケージマネージャーを使ってください。git SHA は固定しないでください。
npm install @arcjet/guard @cloudflare/think
pnpm add @arcjet/guard @cloudflare/think
ピア依存関係の範囲は @cloudflare/think >=0.3.0 <1 です。バージョン付きのパス
@arcjet/guard/cloudflare-think/v0 は解決されますが、バージョンなしの
@arcjet/guard/cloudflare-think パスは解決されません。エージェントに
guard クライアントがまだない場合は、モジュールスコープで 一度だけ 起動します。
import { launchArcjet } from "@arcjet/guard";
export const arcjet = launchArcjet({ key: process.env.ARCJET_KEY! });
手順 2:ツール呼び出しをゲートする — beforeToolCall に委譲する
guardHooks に、現在の会話の 認証済み呼び出し元を渡します。
Durable Object は会話ごとに 1 つであることが多いですが、それでも
this.ctx.id / this.name を actor や相関情報として使用しないでください。
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;
}
サブクラスがすでに beforeToolCall を実装している場合は、
hooks.beforeToolCall(ctx) を呼び出し、その ToolCallDecision を返します。
DENY を無視して処理を続けたり、@arcjet/guard/vercel-ai/v7 でもラップしたりしないでください。
rules を省略すると、ルールは送信されません。それでも guard の呼び出しは行われます。ポリシーによってツールの実行が許可されたときに ALLOW は outcome: "success" を記録します。execute の完了時ではありません。
手順 3:ターンの開始前に受信内容をスクリーニングする
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.
guardInbound はありません。guard() はフェイルオープンするため、必ず
hasFailedOpen() を確認してください。
手順 4:相関情報
cloudflareThinkContext は、呼び出し元が所有する ID を読み取ります。優先順位は次のとおりです。
correlationId、次に sessionId、次に呼び出し元が所有するラッパー上の conversationId(cloudflareThinkContext({ context: appContext }))、
次にベアなアプリオブジェクト上のコピー、次に guardHooks({ sessionId }) /
init.correlationId です。ID を生成することはありません。Durable
Object の ID / this.ctx.id / this.name は読み取りません。Think や AI SDK が生成する
toolCallId、requestId、traceId、ストリーム ID も読み取りません。toolCallId と toolName を持つ
beforeToolCall コンテキストは、Think のエンベロープです。そのオブジェクトのトップレベルの sessionId は無視されます。
ターンごとに相関 ID を作らないでください。有効な ID が何も残らない場合、その呼び出しは相関なしとなり、生成された ID に結び付けられることはありません。
統合を検証する
npm run typecheck(またはプロジェクトの型チェック)が成功することを確認します。- 受信 PI(ターン開始前に実行し、
hasFailedOpen()も確認)、代替 DENY(モデルがArcjetDenialResultを受け取る)、ブロック DENY(理由文字列。実際の DENY のみ)、利用不可時にonDeny: "block"でも代替されること、executeの void、 例外をスローしないこと、ID を決して生成しないこと、省略されたactor/inputs(リモートルールは無音)、リゾルバーの例外(フェイルクローズの代替処理)、フェイルクローズ(到達不能な guard → 代替処理、決して void にしない)をテストします。needsApprovalがゲートとして扱われないこと、ツールが@arcjet/guard/vercel-ai/v7でもラップされていないことを確認します。 - Arcjet Console / CLI で、判定に呼び出し元が所有するセッション ID / 会話 ID が使われていること(Durable Object の ID や
toolCallIdではないこと)、およびactorが認証済み呼び出し元であることを確認します。 - 実際の
ARCJET_KEYを使用する手動 E2E は、実行するまで未検証です。
実装例:
examples/cloudflare-think-agent
(検証用フィクスチャはありません。main にはまだこのツリーがありません)。別の例の名前を作らないでください。この skills
リポジトリに例を追加しないでください。