バインディングで、Dynamic Worker がアクセスできる対象を制御します。Dynamic Worker を作成するときに、使えるリソースと操作を正確に決めます。
次のことができます。
- 各 Dynamic Worker に専用のリソースを渡す — KV 名前空間、R2 バケット、またはデータベースを分割し、各 Worker が自分のデータだけを見るようにします。
- 独自の機能を公開する — Dynamic Worker が呼べる独自のメソッドを定義します。チャットルームへの投稿、メール送信、内部サービスへのクエリなどです。インターフェイスは自分で設計し、Dynamic Worker はそのメソッドを呼ぶだけです。
- アクセスを制限し制御する — 呼び出しが基盤リソースに届く前に、検査、変換、または拒否できます。
カスタムバインディングでは、次を行います。
- ローダー Worker でバインディングの実装を定義する: メソッドを持つクラスを作成します。このクラスはローダー Worker で動くため、認証、ログ、顧客ごとのアクセス範囲をここで追加できます。
- Dynamic Worker にバインディングとして渡す: Dynamic Worker は実装を知らなくても、
this.env.CHAT_ROOM.post("Hello!")のようにメソッドを呼びます。
カスタムバインディングを作るには、ローダー Worker で WorkerEntrypoint クラス を実装してエクスポートします。このクラスに定義したメソッドが、Dynamic Worker から呼べるメソッドになります。
import { WorkerEntrypoint } from "cloudflare:workers";
export class ChatRoom extends WorkerEntrypoint {
async post(text: string): Promise<void> {
// Your implementation here
}
}ローダー Worker は、エクスポートしたクラスのインスタンス(スタブ)を作り、Dynamic Worker の env に渡します。
let chatRoom = ctx.exports.ChatRoom({ props: { roomName: "#bot-chat" } });
let worker = env.LOADER.load({
env: { CHAT_ROOM: chatRoom },
// ...
});Dynamic Worker から見ると、CHAT_ROOM は呼べるメソッドを持つ通常のバインディングです。
// Inside the Dynamic Worker
await this.env.CHAT_ROOM.post("Hello!");1 つのクラスを、複数の Dynamic Worker で使えます。ユーザーごとに別クラスを定義する代わりに、スタブ作成時に props を渡し、そのユーザー固有の情報を含めます。
// Same class, different props per user
let aliceRoom = ctx.exports.ChatRoom({ props: { roomName: "#alice", apiKey: ALICE_KEY } });
let bobRoom = ctx.exports.ChatRoom({ props: { roomName: "#bob", apiKey: BOB_KEY } });Dynamic Worker がバインディングのメソッドを呼ぶと、実際にはローダー Worker への呼び出しになり、メソッドはそこで実行されます。メソッド内では this.ctx.props で props を読めます。props にアクセスできるのはローダー Worker だけです。Dynamic Worker からは見えません。
export class ChatRoom extends WorkerEntrypoint<Cloudflare.Env, ChatRoomProps> {
async post(text: string): Promise<void> {
// Props are set when the stub is created — the Dynamic Worker never sees them
let roomName = this.ctx.props.roomName;
await postToChat(roomName, text);
}
}全体をまとめた例です。AI エージェントがチャットルームへ投稿できるプラットフォームを作るとします。各エージェントは割り当てられたルームにだけ投稿でき、認証に使う API キーは見えないようにします。
親 Worker で ChatRoom クラスを定義します。このクラスには post メソッドがあり、Dynamic Worker がこのバインディングで呼べるのはこのメソッドだけです。クラス内で、メッセージの送信先ルーム、使う API キー、メッセージに付ける名前を制御します。
import { WorkerEntrypoint } from "cloudflare:workers";
export class ChatRoom extends WorkerEntrypoint<Cloudflare.Env, ChatRoomProps> {
async post(text: string): Promise<void> {
let { apiKey, botName, roomName } = this.ctx.props;
// Prefix the message with the bot's name.
text = `[${botName}]: ${text}`;
// Send it to the chat service.
await postToChat(apiKey, roomName, text);
}
}
type ChatRoomProps = {
apiKey: string;
roomName: string;
botName: string;
};エクスポートする ChatRoom クラスは 1 つですが、作成するスタブごとに異なる props(ルーム名、API キー、ボット名)を持てます。props はスタブ作成時に設定され、Dynamic Worker からは見えません。
次に Dynamic Worker へ渡します。
// Create a stub scoped to a specific room.
let chatRoom = ctx.exports.ChatRoom({
props: {
apiKey,
roomName: "#bot-chat",
botName: "Robo",
},
});
let worker = env.LOADER.load({
env: {
CHAT_ROOM: chatRoom,
},
compatibilityDate: "$today",
mainModule: "index.js",
modules: {
"index.js": `
export class Agent extends WorkerEntrypoint {
async run() {
// This is all the Dynamic Worker sees.
await this.env.CHAT_ROOM.post("Hello!");
}
}
`,
},
globalOutbound: null,
});
return worker.getEntrypoint("Agent").run();エージェントは this.env.CHAT_ROOM.post("Hello!") を呼ぶだけです。別のルームへ投稿したり、API キーを見たり使ったり、メッセージに付くボット名を変えたりはできません。
AI エージェントがバインディングに対してコードを書くには、インターフェイスを知る必要があります。各メソッドを説明するドキュメントコメント付きの TypeScript 型宣言をエージェントに渡してください。最近の LLM は TypeScript をよく理解するため、JavaScript API を簡潔に説明する手段として適しています。エージェントがプレーンな JavaScript を書く場合でも有効です。
宣言と実装がずれないように、WorkerEntrypoint クラスは TypeScript の型を継承してください。
KV 名前空間や R2 バケットなどのリソースを Dynamic Worker に渡すには、リソースをローダー Worker にバインドし、それを包むカスタムバインディングを作ります。キーにプレフィックスを付け、公開するメソッドだけを定義することで、顧客ごとにアクセス範囲を制限できます。
まず KV 名前空間をローダー Worker にバインドします。次にローダー Worker で、その KV バインディングを使い、Dynamic Worker が呼べるメソッドを定義したクラスをエクスポートします。
import { WorkerEntrypoint } from "cloudflare:workers";
export class MyStorage extends WorkerEntrypoint<Cloudflare.Env, MyStorageProps> {
// Export this class from your loader Worker
// The Dynamic Worker will be able to call get() and put()
async get(key: string): Promise<string | null> {
// Prefix the key so this customer can only access their own data
return this.env.MY_KV.get(`${this.ctx.props.prefix}:${key}`);
}
async put(key: string, value: string): Promise<void> {
await this.env.MY_KV.put(`${this.ctx.props.prefix}:${key}`, value);
}
}
type MyStorageProps = {
prefix: string;
};次に、顧客固有のプレフィックス付きで Dynamic Worker に渡します。
// Create a stub scoped to this customer's prefix
let storage = ctx.exports.MyStorage({
props: { prefix: `customer-${customerId}` },
});
let worker = env.LOADER.load({
env: { STORAGE: storage },
// ...
});Dynamic Worker は、ほかのバインディングと同じように使います。
// Inside the Dynamic Worker, it just sees STORAGE with get and put
let value = await this.env.STORAGE.get("settings");
await this.env.STORAGE.put("settings", "dark-mode");同じパターンは、ローダー Worker がアクセスできる任意のリソース(R2 バケットや D1 データベース)に使えます。リソースをローダー Worker にバインドし、それを使うクラスをエクスポートして、スタブを Dynamic Worker に渡します。
各 Dynamic Worker に付随する永続ストレージについては、Durable Object Facets を参照してください。
カスタムバインディングは、能力ベース(capability-based)のセキュリティモデルに従います。Dynamic Worker は、明示的に渡したものにだけアクセスできます。対象のスタブを受け取っていなければ、アクセスできません。
これは Workers RPC(Cap'n Web ↗ とも呼ばれます)で実現されています。Cap'n Web は、オブジェクト参照をセキュリティ境界を越えて渡すために設計された RPC システムです。Dynamic Worker がスタブを受け取ると、そのオブジェクトのメソッドを呼べます。各呼び出しは、ローダー Worker 内の元オブジェクトへの RPC です。スタブにはグローバルな識別子がなく、偽造もできません。入手する方法は、受け取ることだけです。
能力ベースのセキュリティは、成功しているサンドボックスの設計に欠かせません。ただし、多くの場合は実装の詳細として隠されています。Android には Binder、Chrome には Mojo、Cloudflare Workers には Cap'n Web があります。Dynamic Workers はこの仕組みを開発者へ直接公開するため、独自の強固なサンドボックスを構築できます。