diff --git a/skills/cloudflare/PREREQUISITES.md b/skills/cloudflare/PREREQUISITES.md new file mode 100644 index 0000000..ee29d0a --- /dev/null +++ b/skills/cloudflare/PREREQUISITES.md @@ -0,0 +1,139 @@ +# Prerequisites for Adding a New Cloudflare Product Skill + +This document describes the research and source material needed before writing a skill reference for a new Cloudflare product. Following this process ensures the skill is accurate, consistent with existing references, and comprehensive enough to be useful. + +## 1. Gather Primary Sources + +Before writing anything, collect these materials for the product: + +### Official Documentation + +| Source | Where to find it | What it provides | +|--------|-------------------|------------------| +| **LLM reference** | `https://developers.cloudflare.com//llms-full.txt` | Condensed, structured overview of the full product — usually the single best starting point | +| **Developer docs** | `https://developers.cloudflare.com//` | Canonical reference for APIs, config, limits, pricing | +| **Getting started guide** | `https://developers.cloudflare.com//getting-started/` | Setup steps, minimal working examples | +| **Blog announcement** | `https://blog.cloudflare.com//` | Design rationale, performance claims, positioning vs alternatives | + +The LLM reference (`llms-full.txt`) is the highest-signal single source. Start there. Fall back to the full docs for details it doesn't cover. + +### Working Examples + +| Source | Where to find it | What it provides | +|--------|-------------------|------------------| +| **Official examples** | `https://github.com/cloudflare/agents/tree/main/examples/` (or product-specific repos) | Real, runnable code showing intended usage patterns | +| **Starter templates** | Linked from the getting started guide | Minimal scaffolding for common setups | + +**Read the actual source code of every relevant example.** GitHub tree pages only show file listings — you need to fetch each source file individually (via raw URLs or a clone). Examples reveal: +- Real `wrangler.jsonc` configurations (what bindings are actually needed) +- How the product combines with other Cloudflare products (AI, Durable Objects, etc.) +- Idiomatic patterns the docs may not spell out +- Edge cases handled in production-quality code + +### Upstream Repository + +If the product has an SDK or library (e.g. `@cloudflare/worker-bundler`, `@cloudflare/sandbox`): +- Check the npm package for current API surface +- Read the repo README and any inline type definitions +- Capture the current API shape and doc entry points; avoid pinning version-specific details unless they are required to use the product + +## 2. Understand the Product's Position + +Before writing, answer these questions: + +- **What existing products is this most similar to?** (e.g. Dynamic Workers vs Workers for Platforms vs Sandbox) +- **What's the key differentiator?** (runtime vs pre-deployed, isolates vs containers, etc.) +- **When should someone use this instead of the alternatives?** +- **What bindings/integrations does it need?** (worker_loaders, durable_objects, ai, etc.) + +This informs the README's comparison table and the SKILL.md decision tree entry. + +## 3. Follow the Standard 5-File Structure + +Every product reference in `skills/cloudflare/references/` uses this structure: + +``` +references// + README.md — Overview, architecture, quick start, comparison table + api.md — API reference: methods, types, parameters, return values + configuration.md — wrangler.jsonc setup, binding combinations, CLI commands + patterns.md — Common usage patterns with full code examples + gotchas.md — Errors, best practices, retrieval cues, starter links +``` + +### What goes in each file + +**README.md**: The entry point. Someone reading only this file should understand what the product is, when to use it, and how to get a minimal example running. Include: +- One-line description +- Comparison table vs similar products +- Architecture diagram or description +- Quick start (wrangler config + minimal code) +- Links to the other 4 files and related references + +**api.md**: Complete API surface. For each method/class/type: +- Signature and parameters +- Code example showing usage +- Notes on behavior (caching, async, error cases) +- For RPC/binding patterns, show both the definition and consumption sides + +**configuration.md**: Everything about setup and config. Include: +- The primary binding configuration (wrangler.jsonc and wrangler.toml) +- How it combines with other bindings (show real composite configs from examples) +- Supported languages/runtimes +- CLI commands relevant to this product +- Any bundling or build steps required + +**patterns.md**: Real-world usage patterns, each with complete runnable code. Source these from: +- Official examples (adapt, don't just copy) +- Patterns described in the docs/blog +- Common combinations with other products +Aim for 4-7 patterns ranging from basic to advanced. + +**gotchas.md**: Everything that trips people up. Include: +- Common errors with cause/solution pairs +- Best practices (do/don't format with code examples) +- Retrieval cues for pricing, limits, and plan availability, with links to the official docs instead of copied tables +- Links to starter templates and official resources + +## 4. Update the Skill Index + +After creating the reference directory, update `skills/cloudflare/SKILL.md`: + +1. **Decision tree**: Add a branch in the appropriate "I need to..." tree. Place it near similar products with a description that distinguishes it. + +2. **Product index table**: Add a row in the appropriate section (Compute & Runtime, Storage, AI, etc.). + +## 5. Cross-Reference Other Skills + +Check if existing skills should link to the new one: +- Products it's commonly used with (e.g. Dynamic Workers + Agents SDK, + Tail Workers) +- Products it's easily confused with (comparison in README helps) +- The main SKILL.md decision trees + +## 6. Verify Before Committing + +- [ ] All code examples use current API syntax (check against `llms-full.txt` and docs) +- [ ] `wrangler.jsonc` examples match real working configurations from official examples +- [ ] Pricing, limits, and plan availability link to official docs rather than copying values that may drift +- [ ] No broken internal links between the 5 files +- [ ] SKILL.md decision tree entry distinguishes this from similar products +- [ ] SKILL.md product index row added in the correct section +- [ ] README.md includes a retrieval-bias directive near the top of the file +- [ ] File structure matches `README.md`, `api.md`, `configuration.md`, `patterns.md`, `gotchas.md` exactly + +## Example: What Was Needed for Dynamic Workers + +| Source | URL | Key information extracted | +|--------|-----|--------------------------| +| LLM reference | `developers.cloudflare.com/dynamic-workers/llms-full.txt` | Full API surface, WorkerCode properties, retrieval entry points | +| Docs home | `developers.cloudflare.com/dynamic-workers/` | Architecture overview, use cases, security model | +| Getting started | `developers.cloudflare.com/dynamic-workers/getting-started/` | `worker_loaders` binding, `load()` vs `get()`, supported languages | +| Blog post | `blog.cloudflare.com/dynamic-workers/` | V8 isolate performance claims, helper libraries, design rationale | +| `dynamic-workers` example | `github.com/cloudflare/agents/.../dynamic-workers/` | Minimal `load()` pattern, basic wrangler config | +| `dynamic-workers-playground` example | `github.com/cloudflare/agents/.../dynamic-workers-playground/` | `get()` with content-hashed IDs, `@cloudflare/worker-bundler`, Tail Worker + DO logging pipeline, warmup trick | +| `codemode` example | `github.com/cloudflare/agents/.../codemode/` | `AIChatAgent` + `DynamicWorkerExecutor` integration, SQL-backed tools | +| `codemode-mcp` example | `github.com/cloudflare/agents/.../codemode-mcp/` | `codeMcpServer()` wrapper pattern | +| `codemode-mcp-openapi` example | `github.com/cloudflare/agents/.../codemode-mcp-openapi/` | `openApiMcpServer()` for REST API wrapping | +| `worker-bundler-playground` example | `github.com/cloudflare/agents/.../worker-bundler-playground/` | AI-generated app pattern, `createWorker()` with assets, DO persistence | + +Six examples were needed to cover the full range of patterns. The `llms-full.txt` provided the API reference backbone. The blog provided design rationale and helper-library context. The examples provided real wrangler configs and production patterns. diff --git a/skills/cloudflare/SKILL.md b/skills/cloudflare/SKILL.md index 870f9a2..3fdfa08 100644 --- a/skills/cloudflare/SKILL.md +++ b/skills/cloudflare/SKILL.md @@ -40,6 +40,7 @@ Need to run code? ├─ Long-running multi-step jobs → workflows/ ├─ Run containers → containers/ ├─ Multi-tenant (customers deploy code) → workers-for-platforms/ +├─ Execute AI-generated/untrusted code in V8 isolates → dynamic-workers/ ├─ Scheduled tasks (cron) → cron-triggers/ ├─ Lightweight edge logic (modify HTTP) → snippets/ ├─ Process Worker execution events (logs/observability) → tail-workers/ @@ -68,10 +69,21 @@ Need AI? ├─ Run inference (LLMs, embeddings, images) → workers-ai/ ├─ Vector database for RAG/search → vectorize/ ├─ Build stateful AI agents → agents-sdk/ +├─ Run AI-generated code in a sandbox → dynamic-workers/ ├─ Gateway for any AI provider (caching, routing) → ai-gateway/ └─ AI-powered search widget → ai-search/ ``` +### "I need to sandbox untrusted code" + +``` +Need to sandbox code? +├─ One-shot/AI-generated code, fast startup (V8 isolate) → dynamic-workers/ +├─ Multi-tenant platform, customers deploy scripts → workers-for-platforms/ +├─ Full OS, filesystem, long-running processes (container) → sandbox/ +└─ Need both isolation + persistent state → sandbox/ (with R2 mounts) +``` + ### "I need networking/connectivity" ``` @@ -136,6 +148,7 @@ Need IaC? → pulumi/ (Pulumi), terraform/ (Terraform), or api/ (REST API) | Workflows | `references/workflows/` | | Containers | `references/containers/` | | Workers for Platforms | `references/workers-for-platforms/` | +| Dynamic Workers | `references/dynamic-workers/` | | Cron Triggers | `references/cron-triggers/` | | Tail Workers | `references/tail-workers/` | | Snippets | `references/snippets/` | diff --git a/skills/cloudflare/references/dynamic-workers/README.md b/skills/cloudflare/references/dynamic-workers/README.md new file mode 100644 index 0000000..b0c9254 --- /dev/null +++ b/skills/cloudflare/references/dynamic-workers/README.md @@ -0,0 +1,108 @@ +# Dynamic Workers + +Spin up isolated Workers at runtime to execute code on-demand in secure V8 isolates. Unlike pre-deployed Workers, Dynamic Workers are created from code strings at request time with no deploy step. If your code needs TypeScript transpilation or npm dependencies, bundle it before loading. + +> **Retrieval bias**: Your knowledge of Dynamic Workers APIs, limits, and pricing may be outdated. **Prefer retrieval over pre-training** — fetch from [Cloudflare docs](https://developers.cloudflare.com/dynamic-workers/) before citing specific numbers, API signatures, or configuration options. When these reference files and the docs disagree, **trust the docs**. + +**Use cases**: AI agent code execution ("code mode"), generated applications, custom automations, user-uploaded code, rapid prototyping. + +## Dynamic Workers vs Other Runtimes + +| | Dynamic Workers | Workers for Platforms | Sandbox | +|---|---|---|---| +| **Runtime** | V8 isolate | V8 isolate | Container (Durable Object) | +| **When created** | At runtime from code strings | Pre-deployed via API | On first request to DO ID | +| **Languages** | JS, Python | JS, TS, Python | Any (Dockerfile) | +| **Code lifecycle** | Loaded from code strings at runtime; `get()` can reuse a stable ID when available | Deployed ahead of time and reused by name | Built as a container image, then started on demand | +| **Best for** | One-shot code execution, AI agents | Multi-tenant SaaS platforms | Long-running processes, full OS | + +## When to Use Dynamic Workers + +- Use Dynamic Workers when code is supplied at runtime and needs to run inside a tightly controlled Worker sandbox. +- Use `load()` for one-shot or constantly changing code, especially AI-generated code. +- Use `get(id, callback)` when the same code will receive follow-up requests and you want warm-isolate reuse when available. +- Prefer Workers for Platforms when tenants deploy versioned Workers you manage as durable platform assets. +- Prefer Sandbox when code needs a filesystem, long-running processes, custom binaries, or broader OS-level behavior. + +## Safe Starting Point + +- Start with `globalOutbound: null` and only open network access deliberately. +- Pass narrow RPC bindings through `env` instead of exposing raw bindings or secrets. +- Set explicit `limits` for CPU time and subrequests when executing untrusted or AI-generated code. +- Treat in-memory state as ephemeral across requests. If state matters, store it outside the isolate. + +## Architecture + +``` +Request → Loader Worker → env.LOADER.load(code) → Dynamic Worker isolate + → env.LOADER.get(id, cb) → Cached Dynamic Worker +``` + +- **Loader Worker**: Your deployed Worker with a `worker_loaders` binding +- **Dynamic Worker**: Ephemeral V8 isolate created from code you provide +- **Capability-based security**: Dynamic Workers only access what you pass via `env` (RPC stubs, not raw bindings) +- **Network control**: `globalOutbound` controls all egress (block, intercept, or inherit) + +## Two Loading Modes + +**`load(code)`** — Fresh isolate every time. Best for one-shot AI-generated code. + +**`get(id, callback)`** — Cached by ID across requests. Callback runs only when isolate isn't warm. Best for apps receiving repeated traffic. + +## Quick Start + +**wrangler.jsonc**: +```jsonc +{ + "name": "my-dynamic-worker", + "main": "src/index.ts", + "compatibility_date": "2026-04-22", // Use current date for new projects + "compatibility_flags": ["nodejs_compat"], + "worker_loaders": [{ "binding": "LOADER" }] +} +``` + +**src/index.ts**: +```typescript +export default { + async fetch(request: Request, env: Env): Promise { + const worker = env.LOADER.load({ + compatibilityDate: "2026-04-22", // Use a current compatibility date + mainModule: "worker.js", + modules: { + "worker.js": ` + export default { + fetch(request) { + return new Response("Hello from a dynamic Worker!"); + } + }; + ` + }, + globalOutbound: null, // Block all network access + limits: { cpuMs: 50, subRequests: 20 } + }); + + return worker.getEntrypoint().fetch(request); + } +} satisfies ExportedHandler; +``` + +## Core APIs + +- `env.LOADER.load(code)` → Create fresh Dynamic Worker +- `env.LOADER.get(id, callback)` → Load or reuse cached Dynamic Worker +- `worker.getEntrypoint()` → Access default export (fetch, RPC methods) +- `worker.getEntrypoint(name)` → Access named entrypoint + +## In This Reference +- [api.md](./api.md) — WorkerCode object, module types, RPC bindings, helper libraries +- [configuration.md](./configuration.md) — Wrangler config, bundling, observability setup +- [patterns.md](./patterns.md) — Code mode, credential injection, real-time logging, OpenAPI wrapping +- [gotchas.md](./gotchas.md) — Common errors, safe defaults, and live docs to retrieve pricing and limits + +## See Also +- [agents-sdk](../agents-sdk/) — Agents SDK (codemode, `createCodeTool()`, AI chat agents) +- [workers-for-platforms](../workers-for-platforms/) — Pre-deployed multi-tenant Workers +- [sandbox](../sandbox/) — Container-based isolated execution +- [workers](../workers/) — Standard Workers fundamentals +- [tail-workers](../tail-workers/) — Log consumption (used for Dynamic Worker observability) diff --git a/skills/cloudflare/references/dynamic-workers/api.md b/skills/cloudflare/references/dynamic-workers/api.md new file mode 100644 index 0000000..3f229da --- /dev/null +++ b/skills/cloudflare/references/dynamic-workers/api.md @@ -0,0 +1,286 @@ +# API Reference + +## Loading Dynamic Workers + +### `env.LOADER.load(code: WorkerCode): WorkerStub` + +Creates a fresh Dynamic Worker. No caching — each call creates a new isolate. + +```typescript +const worker = env.LOADER.load({ + compatibilityDate: "2026-04-22", // Use a current compatibility date + mainModule: "worker.js", + modules: { "worker.js": code }, + globalOutbound: null, + limits: { cpuMs: 50, subRequests: 20 } +}); + +const response = await worker.getEntrypoint().fetch(request); +``` + +### `env.LOADER.get(id: string, callback: () => Promise): WorkerStub` + +Loads or retrieves a cached Dynamic Worker by ID. The callback executes only if the Worker isn't already warm. Returns synchronously; requests queue if the Worker is still loading. + +```typescript +const worker = env.LOADER.get("hello-v1", async () => { + const code = await env.MY_CODE_STORAGE.get("hello-v1"); + return { + compatibilityDate: "2026-04-22", // Use a current compatibility date + mainModule: "index.js", + modules: { "index.js": code }, + globalOutbound: null + }; +}); +``` + +**Critical**: The callback must always return the same content for the same ID. Different code for the same ID breaks caching guarantees. + +### `WorkerStub.getEntrypoint(): object` + +Access the Dynamic Worker's `export default`. Returns an object you can call `fetch()` or RPC methods on. + +### `WorkerStub.getEntrypoint(name: string): object` + +Access a named entrypoint (a named `WorkerEntrypoint` export from the Dynamic Worker). + +## WorkerCode Object + +### Required Properties + +**`compatibilityDate`** (string) — Sets the Worker runtime version. Use a current compatibility date. + +**`mainModule`** (string) — Name of the entry module. Must exist as a key in `modules`. + +**`modules`** (Record) — Dictionary mapping filenames to code. String values require `.js` or `.py` extensions. Object values specify the module type: + +| Type | Syntax | Use | +|------|--------|-----| +| `{js: string}` | ES module (`import`/`export`) | Default for `.js` strings | +| `{cjs: string}` | CommonJS (`require()`) | Legacy modules | +| `{py: string}` | Python | Slower startup than JS | +| `{text: string}` | Importable string value | Config, templates | +| `{data: ArrayBuffer}` | Binary data | Wasm, images | +| `{json: object}` | JSON-serializable object | Structured config | + +### Optional Properties + +**`compatibilityFlags`** (string[]) — Compatibility flags augmenting the date. + +**`allowExperimental`** (boolean) — Permit experimental flags. Parent Worker must have the `"experimental"` flag set. + +**`globalOutbound`** (ServiceStub | null) — Controls network egress: +- **Omitted**: Inherits parent Worker's network access +- **`null`**: Blocks all `fetch()` and `connect()` (throws on attempt) +- **ServiceStub**: Routes all network requests through a custom gateway + +**`env`** (object) — Environment passed to the Dynamic Worker. Supports: +- Structured-clonable types (strings, numbers, objects, arrays) +- Service Bindings (RPC stubs from `ctx.exports`) +- Loopback bindings (the loader's own entrypoints) + +**`tails`** (ServiceStub[]) — Tail Workers that receive logs and metadata after execution. + +**`limits`** ({ cpuMs?: number; subRequests?: number }) — Optional per-worker limits for CPU time and subrequests. Use this when executing untrusted code so failures happen inside a bounded sandbox instead of consuming the parent Worker's full budget. + +Retrieve the current maximum values from the [Custom Limits docs](https://developers.cloudflare.com/dynamic-workers/usage/limits/) before citing specific numbers. + +## Per-Invocation Limits + +You can also apply stricter limits at call time: + +```typescript +const entrypoint = worker.getEntrypoint(null, { + limits: { cpuMs: 10, subRequests: 5 } +}); + +await entrypoint.fetch(request); +``` + +If limits are set both on the Worker code and at the entrypoint call, the lower limits apply. + +## Custom Bindings via RPC (Cap'n Web) + +Passing standard Workers bindings (KV, R2, D1, etc.) directly into a Dynamic Worker is [not currently supported](https://developers.cloudflare.com/dynamic-workers/usage/bindings/). Instead, create wrapper RPC interfaces using `WorkerEntrypoint` classes and pass them as stubs. + +This uses [Cap'n Web](https://developers.cloudflare.com/dynamic-workers/usage/bindings/) (Workers RPC) — a capability-based security model. Objects passed over RPC are stubs: if you haven't received a stub, you can't call the object. There's no global identifier to guess. This is why Dynamic Workers are secure by default — the sandbox can only access what you explicitly grant it, and you can narrow scope, filter requests, or inject credentials at the boundary. + +### Define in Loader Worker + +```typescript +import { WorkerEntrypoint } from "cloudflare:workers"; + +export class ChatRoom extends WorkerEntrypoint { + async post(text: string): Promise { + const { apiKey, botName, roomName } = this.ctx.props; + text = `[${botName}]: ${text}`; + await postToChat(apiKey, roomName, text); + } +} + +type ChatRoomProps = { + apiKey: string; + roomName: string; + botName: string; +}; +``` + +### Pass to Dynamic Worker + +```typescript +const chatRoom = ctx.exports.ChatRoom({ + props: { apiKey, botName: "Robo", roomName: "#bot-chat" } +}); + +const worker = env.LOADER.load({ + env: { CHAT_ROOM: chatRoom }, + compatibilityDate: "2026-04-22", // Use a current compatibility date + mainModule: "index.js", + modules: { "index.js": codeFromAgent }, + globalOutbound: null, + limits: { cpuMs: 50, subRequests: 20 } +}); + +return worker.getEntrypoint("Agent").run(); +``` + +The Dynamic Worker accesses `this.env.CHAT_ROOM.post(text)` without seeing secrets. + +## Network Access Control (globalOutbound) + +### Block All Egress + +```typescript +globalOutbound: null +// Any fetch() or connect() in the Dynamic Worker throws +``` + +### Intercept Requests via Gateway + +```typescript +export class HttpGateway extends WorkerEntrypoint { + async fetch(request: Request): Promise { + const url = new URL(request.url); + // Inspect, modify, block, or forward + return fetch(request); + } +} + +// In loader: +globalOutbound: ctx.exports.HttpGateway() +``` + +### Inject Credentials + +```typescript +export class HttpGateway extends WorkerEntrypoint { + async fetch(request: Request): Promise { + const url = new URL(request.url); + const headers = new Headers(request.headers); + + if (url.hostname === "api.example.com") { + headers.set("Authorization", `Bearer ${this.env.API_TOKEN}`); + headers.set("X-Tenant-Id", this.ctx.props.tenantId); + } + + return fetch(request, { headers }); + } +} + +// Pass with props: +globalOutbound: ctx.exports.HttpGateway({ props: { tenantId } }) +``` + +## Helper Libraries + +### `@cloudflare/worker-bundler` + +Bundles TypeScript and npm dependencies before passing to the loader. Handles transpilation, dependency resolution, and module output. + +```typescript +import { createWorker } from "@cloudflare/worker-bundler"; + +const worker = env.LOADER.get("my-worker", async () => { + const { mainModule, modules, wranglerConfig, warnings } = await createWorker({ + files: { + "src/index.ts": ` + import { Hono } from 'hono'; + const app = new Hono(); + app.get('/', (c) => c.text('Hello from Hono!')); + export default app; + `, + "package.json": JSON.stringify({ + dependencies: { hono: "^4" } + }) + }, + bundle: true, + minify: false + }); + + return { + mainModule, + modules: modules as Record, + compatibilityDate: wranglerConfig?.compatibilityDate ?? "2026-04-22", + compatibilityFlags: wranglerConfig?.compatibilityFlags ?? [] + }; +}); +``` + +### `@cloudflare/codemode` + +Replace individual tool calls with a single `code()` tool, so LLMs write and execute TypeScript that orchestrates multiple API calls in one pass. Provides `DynamicWorkerExecutor` and `createCodeTool()`. + +Key exports: +- `@cloudflare/codemode` — `DynamicWorkerExecutor` class +- `@cloudflare/codemode/ai` — `createCodeTool()` function +- `@cloudflare/codemode/mcp` — `codeMcpServer()` and `openApiMcpServer()` wrappers + +The `DynamicWorkerExecutor` implements the generic [`Executor` interface](https://developers.cloudflare.com/agents/api-reference/codemode/) — you can implement your own `Executor` to run code in a different sandbox (e.g., containers, external services) while reusing `createCodeTool()` and the MCP wrappers. + +See the [Code Mode documentation](https://developers.cloudflare.com/agents/api-reference/codemode/) for full API reference. + +### `@cloudflare/shell` + +Give your agent a virtual filesystem inside a Dynamic Worker with persistent storage backed by SQLite and R2. + +See [npm package](https://www.npmjs.com/package/@cloudflare/shell) for current API surface. + +## Tail Workers (Observability) + +Attach Tail Workers via the `tails` property to capture `console.log()`, exceptions, and request metadata. + +### Define Tail Worker + +```typescript +import { WorkerEntrypoint } from "cloudflare:workers"; + +export class DynamicWorkerTail extends WorkerEntrypoint { + async tail(events: TraceItem[]) { + for (const event of events) { + for (const log of event.logs) { + console.log({ + source: "dynamic-worker-tail", + workerId: this.ctx.props.workerId, + level: log.level, + message: log.message + }); + } + } + } +} +``` + +### Attach to Dynamic Worker + +```typescript +const worker = env.LOADER.get(workerId, () => ({ + mainModule: "index.js", + modules: { "index.js": code }, + compatibilityDate: "2026-04-22", // Use a current compatibility date + tails: [ + ctx.exports.DynamicWorkerTail({ props: { workerId } }) + ] +})); +``` + +Tail Workers run asynchronously after the response is sent — they cannot affect the request-response cycle. diff --git a/skills/cloudflare/references/dynamic-workers/configuration.md b/skills/cloudflare/references/dynamic-workers/configuration.md new file mode 100644 index 0000000..ca6bcaa --- /dev/null +++ b/skills/cloudflare/references/dynamic-workers/configuration.md @@ -0,0 +1,185 @@ +# Configuration + +## Worker Loader Binding + +The `worker_loaders` binding gives your Worker access to the Dynamic Worker Loader API. + +**wrangler.jsonc**: +```jsonc +{ + "name": "my-loader", + "main": "src/index.ts", + "compatibility_date": "2026-04-22", // Use current date for new projects + "compatibility_flags": ["nodejs_compat"], + "worker_loaders": [{ "binding": "LOADER" }] +} +``` + +**wrangler.toml**: +```toml +[[worker_loaders]] +binding = "LOADER" +``` + +Access via `env.LOADER` in your Worker code. + +## Combining with Other Bindings + +Dynamic Workers are typically used alongside other Cloudflare bindings in the **loader** Worker. The Dynamic Worker itself does not receive these bindings directly — you wrap them in RPC entrypoints (see [api.md](./api.md)). + +### With Durable Objects (for logging) + +```jsonc +{ + "worker_loaders": [{ "binding": "LOADER" }], + "durable_objects": { + "bindings": [{ "class_name": "LogSession", "name": "LogSession" }] + }, + "migrations": [{ "tag": "v1", "new_classes": ["LogSession"] }] +} +``` + +### With AI Binding (for code generation) + +```jsonc +{ + "worker_loaders": [{ "binding": "LOADER" }], + "ai": { "binding": "AI" } +} +``` + +### With AI + Durable Objects + Assets (full-stack playground) + +```jsonc +{ + "worker_loaders": [{ "binding": "LOADER" }], + "ai": { "binding": "AI" }, + "durable_objects": { + "bindings": [{ "class_name": "WorkerPlayground", "name": "WorkerPlayground" }] + }, + "migrations": [{ "tag": "v1", "new_sqlite_classes": ["WorkerPlayground"] }], + "assets": { + "directory": "public", + "not_found_handling": "single-page-application", + "run_worker_first": ["/api/*"] + } +} +``` + +## Observability + +Enable Workers Logs to see output from both the loader and Dynamic Workers. + +**wrangler.jsonc**: +```jsonc +{ + "observability": { + "enabled": true, + "head_sampling_rate": 1 + } +} +``` + +**wrangler.toml**: +```toml +[observability] +enabled = true +head_sampling_rate = 1 +``` + +For Tail Worker setup and the `tails` property, see [api.md — Tail Workers](./api.md#tail-workers-observability). For the real-time log streaming pattern using Durable Objects, see [patterns.md — Real-Time Log Streaming](./patterns.md#real-time-log-streaming). + +## Custom Limits + +Set explicit limits when running untrusted or AI-generated code. This keeps each invocation bounded even if the generated code loops, fans out, or behaves unexpectedly. + +```typescript +const worker = env.LOADER.load({ + compatibilityDate: "2026-04-22", // Use a current compatibility date + mainModule: "worker.js", + modules: { "worker.js": code }, + globalOutbound: null, + limits: { cpuMs: 50, subRequests: 20 } +}); + +const entrypoint = worker.getEntrypoint(null, { + limits: { cpuMs: 10, subRequests: 5 } +}); +``` + +Retrieve current maximum values and behavior from the [Custom Limits docs](https://developers.cloudflare.com/dynamic-workers/usage/limits/) before citing specific ceilings. + +## Supported Languages + +| Language | Module Type | Notes | +|----------|-------------|-------| +| JavaScript (ES modules) | `{js: string}` or `.js` string | Recommended. Fastest startup. | +| JavaScript (CommonJS) | `{cjs: string}` | Use for `require()`-style modules | +| Python | `{py: string}` or `.py` string | Significantly slower startup than JS | +| TypeScript | Requires bundling | Use `@cloudflare/worker-bundler` to compile before loading | + +### Non-Code Module Types + +You can also include non-code modules in the `modules` object (see [api.md — WorkerCode](./api.md#workercode-object)): + +| Type | Syntax | Use | +|------|--------|-----| +| `{text: string}` | Importable string value | Config files, templates | +| `{data: ArrayBuffer}` | Importable binary data | Wasm modules, images | +| `{json: object}` | Importable JSON-serializable object | Structured config, manifests | + +### TypeScript and npm Dependencies + +TypeScript cannot be passed directly to the loader. Use `@cloudflare/worker-bundler` to transpile and resolve dependencies at runtime: + +```typescript +import { createWorker } from "@cloudflare/worker-bundler"; + +const { mainModule, modules } = await createWorker({ + files: { + "src/index.ts": typescriptCode, + "package.json": JSON.stringify({ + dependencies: { hono: "^4" } + }) + }, + bundle: true, + minify: false +}); + +const worker = env.LOADER.load({ + mainModule, + modules: modules as Record, + compatibilityDate: "2026-04-22" // Use a current compatibility date +}); +``` + +`createWorker()` returns (based on [official examples](https://github.com/cloudflare/agents/tree/main/examples/dynamic-workers-playground)): +- `mainModule`: Entry point filename +- `modules`: Bundled module map +- `wranglerConfig`: Parsed config from the files (if a `wrangler.jsonc` was included) +- `warnings`: Build warnings + +See the [@cloudflare/worker-bundler npm package](https://www.npmjs.com/package/@cloudflare/worker-bundler) for the latest API surface. + +## Compatibility Date and Flags + +```typescript +env.LOADER.load({ + compatibilityDate: "2026-04-22", // Required. Use current date for new projects. + compatibilityFlags: ["nodejs_compat"], // Optional + allowExperimental: true, // Optional — parent must have "experimental" flag + // ... +}); +``` + +## CLI Commands + +```bash +wrangler dev # Local development (uses local runtime) +wrangler deploy # Deploy loader Worker +wrangler tail # Stream real-time logs +``` + +Dynamic Workers are created at runtime — there are no separate deploy or management commands for them. + +**Local development**: `wrangler dev` runs the loader Worker locally. Dynamic Workers execute in the local runtime during development. Behavior may differ from production — test with `wrangler deploy` to a staging Worker before relying on production-only features. Check the [official docs](https://developers.cloudflare.com/dynamic-workers/) for current local dev support status. diff --git a/skills/cloudflare/references/dynamic-workers/gotchas.md b/skills/cloudflare/references/dynamic-workers/gotchas.md new file mode 100644 index 0000000..8eba1a8 --- /dev/null +++ b/skills/cloudflare/references/dynamic-workers/gotchas.md @@ -0,0 +1,149 @@ +# Gotchas & Best Practices + +Retrieve current pricing, plan availability, and platform limits from the official docs before citing specific numbers. Those details change more often than the behavioral guidance in this file. + +## Common Errors + +### Dynamic Worker returns an error or empty response + +**Cause**: `mainModule` doesn't match a key in `modules`, or the module doesn't have a default export with `fetch()`. +**Solution**: Ensure `mainModule` is an exact key in the `modules` object and the code exports a `fetch` handler. + +```typescript +// ❌ BAD: mainModule doesn't match +env.LOADER.load({ + mainModule: "index.js", + modules: { "worker.js": code } // Key mismatch +}); + +// ✅ GOOD: mainModule matches modules key +env.LOADER.load({ + mainModule: "worker.js", + modules: { "worker.js": code } +}); +``` + +### `fetch()` or `connect()` throws an exception in Dynamic Worker + +**Cause**: `globalOutbound` is set to `null`, which blocks all outbound network access. Any `fetch()` or `connect()` call will throw. +**Solution**: If the Dynamic Worker needs network access, either omit `globalOutbound` (inherits parent's access) or provide a gateway `ServiceStub`. + +### Callback returning different code for the same ID + +**Cause**: Using `get(id, callback)` where the callback returns different content across invocations for the same ID. +**Solution**: The callback must always return identical content for a given ID. Use content-hashed IDs if code varies: + +```typescript +// ❌ BAD: Same ID, potentially different code +env.LOADER.get("my-worker", async () => { + const code = await fetchLatestCode(); // May change! + return { modules: { "index.js": code }, /* ... */ }; +}); + +// ✅ GOOD: ID derived from content hash +const hash = await hashCode(code); +env.LOADER.get(`worker-${hash}`, async () => { + return { modules: { "index.js": code }, /* ... */ }; +}); +``` + +### Standard bindings (KV, R2, D1) not working in Dynamic Worker + +**Cause**: Passing standard Workers bindings directly via `env` is [not currently supported](https://developers.cloudflare.com/dynamic-workers/usage/bindings/). Dynamic Workers use capability-based security via Workers RPC. +**Solution**: Create a wrapper RPC interface using `WorkerEntrypoint` classes and pass as stubs (see [api.md](./api.md)). This also lets you narrow scope, filter requests, etc. + +### Props not serializable + +**Cause**: Passing functions or non-clonable objects via `ctx.props`. +**Solution**: `ctx.props` values must be structured-clonable (strings, numbers, plain objects, arrays). No functions, classes, or circular references. + +### Python code runs slowly + +**Cause**: Python isolates have significantly slower startup than JavaScript. +**Solution**: Use JavaScript or TypeScript (via bundler) for AI-generated code. Reserve Python for cases where it's strictly necessary. + +### Tail Worker logs not appearing + +**Cause**: Tail Workers run asynchronously after the response is sent. If you return immediately, logs may not have arrived yet. +**Solution**: Use the Durable Object log session pattern (see [patterns.md](./patterns.md)) to wait for logs before responding, or accept that logs arrive asynchronously. + +### RPC methods executing in different isolates + +**Cause**: There is [no guarantee](https://developers.cloudflare.com/dynamic-workers/api-reference/) that two requests go to the same isolate, even with the same `WorkerStub` or the same ID via `get()`. Only stubs returned from the same RPC method call share a session. +**Solution**: Do not rely on in-memory state persisting across requests. Pass state explicitly (via RPC arguments, Durable Objects, or KV). `get()` with a stable ID improves the *likelihood* of hitting a warm isolate but does not guarantee it. + +## Best Practices + +### Use `load()` for one-shot, `get()` for repeated + +```typescript +// One-shot AI code execution — use load() +const worker = env.LOADER.load({ /* ... */ }); + +// App receiving multiple requests — use get() with stable ID +const worker = env.LOADER.get("app-v1", async () => { /* ... */ }); +``` + +### Content-hash your worker IDs + +Derive IDs from the code content so identical code reuses the same warm isolate: + +```typescript +async function workerId(files: Record): Promise { + const sorted = Object.entries(files).sort(); + const digest = await crypto.subtle.digest( + "SHA-256", + new TextEncoder().encode(JSON.stringify(sorted)) + ); + return "worker-" + Array.from(new Uint8Array(digest), (b) => + b.toString(16).padStart(2, "0") + ).join("").slice(0, 16); +} +``` + +### Block network by default + +Use `globalOutbound: null` unless the Dynamic Worker genuinely needs network access. When it does, use a gateway to filter and inject credentials rather than passing raw tokens. + +### Set explicit runtime limits + +Use `limits` to bound CPU time and subrequests for untrusted or AI-generated code. Choose values that fit the task rather than inheriting the parent Worker's full budget. + +```typescript +const worker = env.LOADER.load({ + compatibilityDate: "2026-04-22", // Use a current compatibility date + mainModule: "worker.js", + modules: { "worker.js": code }, + globalOutbound: null, + limits: { cpuMs: 50, subRequests: 20 } +}); +``` + +## Retrieve Current Pricing and Limits + +- Use the [Pricing docs](https://developers.cloudflare.com/dynamic-workers/pricing/) for current plan availability, billing dimensions, and whether any pricing component is active yet. +- Use the [Custom Limits docs](https://developers.cloudflare.com/dynamic-workers/usage/limits/) for current limit controls and ceilings. +- Use the [Workers platform limits docs](https://developers.cloudflare.com/workers/platform/limits/) when you need current runtime ceilings. +- Prefer describing cost behavior qualitatively in the skill: `load()` creates fresh Workers, while `get()` can reuse a stable ID and usually fits repeated traffic better. + +## Starter Templates + +- [Dynamic Workers Starter](https://github.com/cloudflare/agents/tree/main/examples/dynamic-workers) — Minimal `load()` example +- [Dynamic Workers Playground](https://github.com/cloudflare/agents/tree/main/examples/dynamic-workers-playground) — Full IDE with bundling, caching, and real-time logs +- [Codemode](https://github.com/cloudflare/agents/tree/main/examples/codemode) — AI agent code execution with tools +- [Codemode MCP](https://github.com/cloudflare/agents/tree/main/examples/codemode-mcp) — Wrap MCP server into single code tool +- [Codemode MCP OpenAPI](https://github.com/cloudflare/agents/tree/main/examples/codemode-mcp-openapi) — OpenAPI spec → MCP code tool +- [Worker Bundler Playground](https://github.com/cloudflare/agents/tree/main/examples/worker-bundler-playground) — AI-generated full-stack apps + +## Resources + +- [Official Docs](https://developers.cloudflare.com/dynamic-workers/) +- [Getting Started](https://developers.cloudflare.com/dynamic-workers/getting-started/) +- [Bindings (Cap'n Web)](https://developers.cloudflare.com/dynamic-workers/usage/bindings/) +- [Custom Limits](https://developers.cloudflare.com/dynamic-workers/usage/limits/) +- [Egress Control](https://developers.cloudflare.com/dynamic-workers/usage/egress-control/) +- [Observability](https://developers.cloudflare.com/dynamic-workers/usage/observability/) +- [Pricing](https://developers.cloudflare.com/dynamic-workers/pricing/) +- [API Reference](https://developers.cloudflare.com/dynamic-workers/api-reference/) +- [Blog Post](https://blog.cloudflare.com/dynamic-workers/) +- [LLM Reference](https://developers.cloudflare.com/dynamic-workers/llms-full.txt) diff --git a/skills/cloudflare/references/dynamic-workers/patterns.md b/skills/cloudflare/references/dynamic-workers/patterns.md new file mode 100644 index 0000000..19622b0 --- /dev/null +++ b/skills/cloudflare/references/dynamic-workers/patterns.md @@ -0,0 +1,277 @@ +# Common Patterns + +## Basic Code Execution + +The simplest pattern: accept code, execute it, return the result. + +```typescript +const DEFAULT_CODE = `export default { + fetch() { + return new Response("Hello from a dynamic Worker!"); + }, +};`; + +export default { + async fetch(request: Request, env: Env): Promise { + if (request.method === "POST") { + const { code } = await request.json<{ code?: string }>(); + + try { + const worker = env.LOADER.load({ + compatibilityDate: "2026-04-22", // Use a current compatibility date + mainModule: "worker.js", + modules: { "worker.js": code?.trim() || DEFAULT_CODE }, + globalOutbound: null, + limits: { cpuMs: 50, subRequests: 20 } + }); + + const result = await worker.getEntrypoint().fetch(new Request("https://worker/")); + const text = await result.text(); + return Response.json({ ok: true, status: result.status, output: text }); + } catch (err) { + return Response.json( + { ok: false, error: err instanceof Error ? err.message : String(err) }, + { status: 400 } + ); + } + } + + return new Response("Not found", { status: 404 }); + } +} satisfies ExportedHandler; +``` + +## AI Agent Code Mode + +Instead of calling tools one at a time, the LLM writes code that calls multiple tools programmatically. + +Uses `@cloudflare/codemode` with `DynamicWorkerExecutor` to combine tools into a single `codemode` tool: + +```typescript +import { Agent } from "agents"; +import { createCodeTool } from "@cloudflare/codemode/ai"; +import { DynamicWorkerExecutor } from "@cloudflare/codemode"; +import { streamText, convertToModelMessages, stepCountIs } from "ai"; + +export class MyAgent extends Agent { + async onChatMessage() { + const executor = new DynamicWorkerExecutor({ + loader: this.env.LOADER + }); + + const codemode = createCodeTool({ + tools: getMyTools(this.sql), + executor + }); + + const result = streamText({ + model, + system: "You are a helpful assistant.", + messages: await convertToModelMessages(this.state.messages), + tools: { codemode }, + stopWhen: stepCountIs(10) + }); + + // Stream response back to client... + } +} +``` + +The agent generates a single TypeScript function that chains multiple tool calls, rather than making sequential individual tool calls. + +## Wrapping MCP Servers (codeMcpServer) + +Collapse any MCP server's tool list into a single `code` tool with `codeMcpServer`: + +```typescript +import { codeMcpServer } from "@cloudflare/codemode/mcp"; +import { DynamicWorkerExecutor } from "@cloudflare/codemode"; + +const executor = new DynamicWorkerExecutor({ loader: env.LOADER }); + +// Wrap an existing MCP server — all its tools become +// typed methods the LLM can call from generated code +const server = await codeMcpServer({ server: upstreamMcp, executor }); +``` + +## OpenAPI Spec → MCP Code Tool + +Turn any REST API into a pair of MCP tools (`search` + `execute`) using `openApiMcpServer`. The host-side `request` handler keeps authentication out of the sandbox: + +```typescript +import { openApiMcpServer } from "@cloudflare/codemode/mcp"; +import { DynamicWorkerExecutor } from "@cloudflare/codemode"; + +const executor = new DynamicWorkerExecutor({ loader: env.LOADER }); + +const server = openApiMcpServer({ + spec: openApiSpec, + executor, + request: async ({ method, path, query, body }) => { + // Runs on the host — add auth headers here + const res = await fetch(`https://api.example.com${path}`, { + method, + headers: { Authorization: `Bearer ${token}` }, + body: body ? JSON.stringify(body) : undefined, + }); + return res.json(); + }, +}); +``` + +This keeps authentication and request policy on the host side while the sandbox executes the generated orchestration code. + +## Bundled Playground with Warm Caching + +Use content-hashed IDs with `get()` for efficient caching, and `@cloudflare/worker-bundler` for TypeScript/npm support: + +```typescript +import { createWorker } from "@cloudflare/worker-bundler"; + +type LoaderCtx = ExecutionContext & { + exports: { + DynamicWorkerTail(options: { props: { workerId: string } }): ServiceStub; + }; +}; + +async function createWorkerId(files: Record): Promise { + const payload = JSON.stringify(Object.entries(files).sort()); + const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(payload)); + const hash = Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")) + .join("") + .slice(0, 16); + return `worker-${hash}`; +} + +export default { + async fetch(request: Request, env: Env, ctx: LoaderCtx) { + const { files } = await request.json<{ files: Record }>(); + const workerId = await createWorkerId(files); + + const worker = env.LOADER.get(workerId, async () => { + const { mainModule, modules, wranglerConfig } = await createWorker({ + files, bundle: true, minify: false + }); + + return { + mainModule, + modules: modules as Record, + compatibilityDate: wranglerConfig?.compatibilityDate ?? "2026-04-22", + globalOutbound: null, + limits: { cpuMs: 50, subRequests: 20 }, + tails: [ + ctx.exports.DynamicWorkerTail({ props: { workerId } }) + ] + }; + }); + + return worker.getEntrypoint().fetch(request); + } +}; +``` + +Same code + same hash = same ID = warm cache hit. Different code = different hash = new isolate. + +## Real-Time Log Streaming + +Use a Durable Object to collect Tail Worker logs and return them synchronously with the response: + +```typescript +import { WorkerEntrypoint } from "cloudflare:workers"; +import { DurableObject, RpcTarget } from "cloudflare:workers"; + +// 1. Log waiter collects events +export class LogWaiter extends RpcTarget { + private logs: unknown[] = []; + private resolve?: (logs: unknown[]) => void; + + push(entries: unknown[]) { + this.logs.push(...entries); + this.resolve?.(this.logs); + } + + async getLogs(timeoutMs: number): Promise { + if (this.logs.length > 0) return this.logs; + return new Promise((resolve) => { + this.resolve = resolve; + setTimeout(() => resolve(this.logs), timeoutMs); + }); + } +} + +// 2. Durable Object holds the waiter +export class LogSession extends DurableObject { + private waiter = new LogWaiter(); + + async waitForLogs() { return this.waiter; } + async pushLogs(entries: unknown[]) { this.waiter.push(entries); } +} + +// 3. Tail Worker forwards logs to the DO +export class DynamicWorkerTail extends WorkerEntrypoint { + async tail(events: TraceItem[]) { + const entries = events.flatMap((e) => + e.logs.map((log) => ({ + workerId: this.ctx.props.workerId, + level: log.level, + message: log.message + })) + ); + + const session = this.env.LogSession.getByName(this.ctx.props.workerId); + await session.pushLogs(entries); + } +} +``` + +**Usage in loader**: + +```typescript +const logSession = ctx.exports.LogSession.getByName(workerId); +const logWaiter = await logSession.waitForLogs(); + +const response = await worker.getEntrypoint().fetch(request); + +const logs = await logWaiter.getLogs(1000); // Wait up to 1s for logs +return Response.json({ response: await response.text(), logs }); +``` + +## Capability-Based Security + +Pass only the capabilities the Dynamic Worker needs. Hide secrets, restrict access: + +```typescript +// Loader defines narrow interfaces +export class OrderReader extends WorkerEntrypoint { + async listRecentOrders(customerId: string): Promise { + return this.env.DB + .prepare( + "SELECT id, total, created_at FROM orders WHERE customer_id = ? ORDER BY created_at DESC LIMIT 20" + ) + .bind(customerId) + .all().results; + } +} + +export class HttpGateway extends WorkerEntrypoint { + async fetch(request: Request): Promise { + const url = new URL(request.url); + // Only allow specific hosts + const allowed = ["api.example.com", "data.example.com"]; + if (!allowed.includes(url.hostname)) { + return new Response("Blocked", { status: 403 }); + } + return fetch(request); + } +} + +// Dynamic Worker receives constrained capabilities +const worker = env.LOADER.load({ + env: { + ORDERS: ctx.exports.OrderReader(), + // No direct KV, R2, or D1 access + }, + globalOutbound: ctx.exports.HttpGateway(), // Filtered egress + // ... +}); +``` diff --git a/skills/cloudflare/references/sandbox/README.md b/skills/cloudflare/references/sandbox/README.md index 638f4e9..7dd6ccf 100644 --- a/skills/cloudflare/references/sandbox/README.md +++ b/skills/cloudflare/references/sandbox/README.md @@ -94,3 +94,4 @@ EXPOSE 8080 3000 # Required for wrangler dev - [durable-objects](../durable-objects/) - Sandbox runs on DO infrastructure - [containers](../containers/) - Container runtime fundamentals - [workers](../workers/) - Entry point for sandbox requests +- [dynamic-workers](../dynamic-workers/) - Lighter-weight alternative (V8 isolates, no container/filesystem) diff --git a/skills/cloudflare/references/tail-workers/patterns.md b/skills/cloudflare/references/tail-workers/patterns.md index a696ec2..593d161 100644 --- a/skills/cloudflare/references/tail-workers/patterns.md +++ b/skills/cloudflare/references/tail-workers/patterns.md @@ -175,6 +175,10 @@ See durable-objects skill for full implementation. Dynamic dispatch sends TWO events per request. Filter by `scriptName` to distinguish dispatch vs user Worker events. +### Dynamic Workers + +Dynamic Workers emit tail events via the `tails` property in WorkerCode. See [dynamic-workers patterns](../dynamic-workers/patterns.md) for the real-time log streaming pattern. + ### Error Handling Always wrap external calls. See gotchas.md for fallback storage pattern. diff --git a/skills/cloudflare/references/workers-for-platforms/README.md b/skills/cloudflare/references/workers-for-platforms/README.md index 7bfd9b7..ff673ec 100644 --- a/skills/cloudflare/references/workers-for-platforms/README.md +++ b/skills/cloudflare/references/workers-for-platforms/README.md @@ -84,6 +84,7 @@ Worker mode? ## See Also - [workers](../workers/) - Core Workers runtime documentation - [durable-objects](../durable-objects/) - Stateful multi-tenant patterns -- [sandbox](../sandbox/) - Alternative for untrusted code execution +- [sandbox](../sandbox/) - Alternative for untrusted code execution (containers) +- [dynamic-workers](../dynamic-workers/) - Alternative for AI/one-shot code execution (V8 isolates, no deploy step) - [Reference Architecture: Programmable Platforms](https://developers.cloudflare.com/reference-architecture/diagrams/serverless/programmable-platforms/) - [Reference Architecture: AI Vibe Coding Platform](https://developers.cloudflare.com/reference-architecture/diagrams/ai/ai-vibe-coding-platform/)