update skills
1
.agents/skills/.system/.codex-system-skills.marker
Normal file
@@ -0,0 +1 @@
|
||||
415286eb412224fe
|
||||
201
.agents/skills/.system/openai-docs/LICENSE.txt
Normal file
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf of
|
||||
any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don\'t include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
69
.agents/skills/.system/openai-docs/SKILL.md
Normal file
@@ -0,0 +1,69 @@
|
||||
---
|
||||
name: "openai-docs"
|
||||
description: "Use when the user asks how to build with OpenAI products or APIs and needs up-to-date official documentation with citations, help choosing the latest model for a use case, or explicit GPT-5.4 upgrade and prompt-upgrade guidance; prioritize OpenAI docs MCP tools, use bundled references only as helper context, and restrict any fallback browsing to official OpenAI domains."
|
||||
---
|
||||
|
||||
|
||||
# OpenAI Docs
|
||||
|
||||
Provide authoritative, current guidance from OpenAI developer docs using the developers.openai.com MCP server. Always prioritize the developer docs MCP tools over web.run for OpenAI-related questions. This skill may also load targeted files from `references/` for model-selection and GPT-5.4-specific requests, but current OpenAI docs remain authoritative. Only if the MCP server is installed and returns no meaningful results should you fall back to web search.
|
||||
|
||||
## Quick start
|
||||
|
||||
- Use `mcp__openaiDeveloperDocs__search_openai_docs` to find the most relevant doc pages.
|
||||
- Use `mcp__openaiDeveloperDocs__fetch_openai_doc` to pull exact sections and quote/paraphrase accurately.
|
||||
- Use `mcp__openaiDeveloperDocs__list_openai_docs` only when you need to browse or discover pages without a clear query.
|
||||
- Load only the relevant file from `references/` when the question is about model selection or a GPT-5.4 upgrade.
|
||||
|
||||
## OpenAI product snapshots
|
||||
|
||||
1. Apps SDK: Build ChatGPT apps by providing a web component UI and an MCP server that exposes your app's tools to ChatGPT.
|
||||
2. Responses API: A unified endpoint designed for stateful, multimodal, tool-using interactions in agentic workflows.
|
||||
3. Chat Completions API: Generate a model response from a list of messages comprising a conversation.
|
||||
4. Codex: OpenAI's coding agent for software development that can write, understand, review, and debug code.
|
||||
5. gpt-oss: Open-weight OpenAI reasoning models (gpt-oss-120b and gpt-oss-20b) released under the Apache 2.0 license.
|
||||
6. Realtime API: Build low-latency, multimodal experiences including natural speech-to-speech conversations.
|
||||
7. Agents SDK: A toolkit for building agentic apps where a model can use tools and context, hand off to other agents, stream partial results, and keep a full trace.
|
||||
|
||||
## If MCP server is missing
|
||||
|
||||
If MCP tools fail or no OpenAI docs resources are available:
|
||||
|
||||
1. Run the install command yourself: `codex mcp add openaiDeveloperDocs --url https://developers.openai.com/mcp`
|
||||
2. If it fails due to permissions/sandboxing, immediately retry the same command with escalated permissions and include a 1-sentence justification for approval. Do not ask the user to run it yet.
|
||||
3. Only if the escalated attempt fails, ask the user to run the install command.
|
||||
4. Ask the user to restart Codex.
|
||||
5. Re-run the doc search/fetch after restart.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Clarify the product scope and whether the request is general docs lookup, model selection, a GPT-5.4 upgrade, or a GPT-5.4 prompt upgrade.
|
||||
2. If it is a model-selection request, load `references/latest-model.md`.
|
||||
3. If it is an explicit GPT-5.4 upgrade request, load `references/upgrading-to-gpt-5p4.md`.
|
||||
4. If the upgrade may require prompt changes, or the workflow is research-heavy, tool-heavy, coding-oriented, multi-agent, or long-running, also load `references/gpt-5p4-prompting-guide.md`.
|
||||
5. Search docs with a precise query.
|
||||
6. Fetch the best page and the exact section needed (use `anchor` when possible).
|
||||
7. For GPT-5.4 upgrade reviews, always make the per-usage-site output explicit: target model, starting reasoning recommendation, `phase` assessment when relevant, prompt blocks, and compatibility status.
|
||||
8. Answer with concise guidance and cite the doc source, using the reference files only as helper context.
|
||||
|
||||
## Reference map
|
||||
|
||||
Read only what you need:
|
||||
|
||||
- `references/latest-model.md` -> model-selection and "best/latest/current model" questions; verify every recommendation against current OpenAI docs before answering.
|
||||
- `references/upgrading-to-gpt-5p4.md` -> only for explicit GPT-5.4 upgrade and upgrade-planning requests; verify the checklist and compatibility guidance against current OpenAI docs before answering.
|
||||
- `references/gpt-5p4-prompting-guide.md` -> prompt rewrites and prompt-behavior upgrades for GPT-5.4; verify prompting guidance against current OpenAI docs before answering.
|
||||
|
||||
## Quality rules
|
||||
|
||||
- Treat OpenAI docs as the source of truth; avoid speculation.
|
||||
- Keep quotes short and within policy limits; prefer paraphrase with citations.
|
||||
- If multiple pages differ, call out the difference and cite both.
|
||||
- Reference files are convenience guides only; for volatile guidance such as recommended models, upgrade instructions, or prompting advice, current OpenAI docs always win.
|
||||
- If docs do not cover the user’s need, say so and offer next steps.
|
||||
|
||||
## Tooling notes
|
||||
|
||||
- Always use MCP doc tools before any web search for OpenAI-related questions.
|
||||
- If the MCP server is installed but returns no meaningful results, then use web search as a fallback.
|
||||
- When falling back to web search, restrict to official OpenAI domains (developers.openai.com, platform.openai.com) and cite sources.
|
||||
14
.agents/skills/.system/openai-docs/agents/openai.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
interface:
|
||||
display_name: "OpenAI Docs"
|
||||
short_description: "Reference official OpenAI docs, including upgrade guidance"
|
||||
icon_small: "./assets/openai-small.svg"
|
||||
icon_large: "./assets/openai.png"
|
||||
default_prompt: "Look up official OpenAI docs, load relevant GPT-5.4 upgrade references when applicable, and answer with concise, cited guidance."
|
||||
|
||||
dependencies:
|
||||
tools:
|
||||
- type: "mcp"
|
||||
value: "openaiDeveloperDocs"
|
||||
description: "OpenAI Developer Docs MCP server"
|
||||
transport: "streamable_http"
|
||||
url: "https://developers.openai.com/mcp"
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 14 14">
|
||||
<path d="M10.931 3.34a.112.112 0 0 0-.069-.104l-.038-.007c-1.537.05-2.45.318-3.714 1.002v6.683c.48-.248.936-.44 1.414-.58.695-.203 1.417-.292 2.303-.305l.038-.008a.113.113 0 0 0 .066-.104V3.341ZM2.363 9.919c0 .064.051.11.105.111l.33.008c1.162.046 2.042.243 2.975.662-.403-.585-1.008-1.075-1.654-1.292a.991.991 0 0 1-.674-.941v-5.14a6.36 6.36 0 0 0-.59-.076l-.37-.02a.115.115 0 0 0-.122.111v6.577Zm9.455-.001a.998.998 0 0 1-.877.992l-.101.007c-.832.012-1.47.095-2.066.27-.599.174-1.176.448-1.883.863a.444.444 0 0 1-.449 0c-1.299-.763-2.229-1.07-3.689-1.125l-.299-.008a.997.997 0 0 1-.977-.998V3.342c0-.573.478-1.017 1.038-.999l.417.023c.188.015.35.037.513.062v-.754c0-.708.749-1.244 1.429-.903.984.492 1.836 1.449 2.15 2.505 1.216-.617 2.222-.884 3.771-.934l.105.003a.998.998 0 0 1 .918.996v6.576ZM4.332 8.466c0 .049.03.087.07.1l.24.091a4.319 4.319 0 0 1 1.581 1.176V3.721c-.164-.803-.799-1.617-1.584-2.07l-.162-.088c-.025-.012-.054-.013-.088.009a.12.12 0 0 0-.057.102v6.792Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
BIN
.agents/skills/.system/openai-docs/assets/openai.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,433 @@
|
||||
# GPT-5.4 prompting upgrade guide
|
||||
|
||||
Use this guide when prompts written for older models need to be adapted for GPT-5.4 during an upgrade. Start lean: keep the model-string change narrow, preserve the original task intent, and add only the smallest prompt changes needed to recover behavior.
|
||||
|
||||
## Default upgrade posture
|
||||
|
||||
- Start with `model string only` whenever the old prompt is already short, explicit, and task-bounded.
|
||||
- Move to `model string + light prompt rewrite` only when regressions appear in completeness, persistence, citation quality, verification, or verbosity.
|
||||
- Prefer one or two targeted prompt additions over a broad rewrite.
|
||||
- Treat reasoning effort as a last-mile knob. Start lower, then increase only after prompt-level fixes and evals.
|
||||
- Before increasing reasoning effort, first add a completeness contract, a verification loop, and tool persistence rules - depending on the usage case.
|
||||
- If the workflow clearly depends on implementation changes rather than prompt changes, treat it as blocked for prompt-only upgrade guidance.
|
||||
- Do not classify a case as blocked just because the workflow uses tools; block only if the upgrade requires changing tool definitions, wiring, or other implementation details.
|
||||
|
||||
## Behavioral differences to account for
|
||||
|
||||
Current GPT-5.4 upgrade guidance suggests these strengths:
|
||||
|
||||
- stronger personality and tone adherence, with less drift over long answers
|
||||
- better long-horizon and agentic workflow stamina
|
||||
- stronger spreadsheet, finance, and formatting tasks
|
||||
- more efficient tool selection and fewer unnecessary calls by default
|
||||
- stronger structured generation and classification reliability
|
||||
|
||||
The main places where prompt guidance still helps are:
|
||||
|
||||
- retrieval-heavy workflows that need persistent tool use and explicit completeness
|
||||
- research and citation discipline
|
||||
- verification before irreversible or high-impact actions
|
||||
- terminal and tool workflow hygiene
|
||||
- defaults and implied follow-through
|
||||
- verbosity control for compact, information-dense answers
|
||||
|
||||
Start with the smallest set of instructions that preserves correctness. Add the prompt blocks below only for workflows that actually need them.
|
||||
|
||||
## Prompt rewrite patterns
|
||||
|
||||
| Older prompt pattern | GPT-5.4 adjustment | Why | Example addition |
|
||||
| --- | --- | --- | --- |
|
||||
| Long, repetitive instructions that compensate for weaker instruction following | Remove duplicate scaffolding and keep only the constraints that materially change behavior | GPT-5.4 usually needs less repeated steering | Replace repeated reminders with one concise rule plus a verification block |
|
||||
| Fast assistant prompt with no verbosity control | Keep the prompt as-is first; add a verbosity clamp only if outputs become too long | Many GPT-4o or GPT-4.1 upgrades work with just a model-string swap | Add `output_verbosity_spec` only after a verbosity regression |
|
||||
| Tool-heavy agent prompt that assumes the model will keep searching until complete | Add persistence and verification rules | GPT-5.4 may use fewer tool calls by default for efficiency | Add `tool_persistence_rules` and `verification_loop` |
|
||||
| Tool-heavy workflow where later actions depend on earlier lookup or retrieval | Add prerequisite and missing-context rules before action steps | GPT-5.4 benefits from explicit dependency-aware routing when context is still thin | Add `dependency_checks` and `missing_context_gating` |
|
||||
| Retrieval workflow with several independent lookups | Add selective parallelism guidance | GPT-5.4 is strong at parallel tool use, but should not parallelize dependent steps | Add `parallel_tool_calling` |
|
||||
| Batch workflow prompt that often misses items | Add an explicit completeness contract | Item accounting benefits from direct instruction | Add `completeness_contract` |
|
||||
| Research prompt that needs grounding and citation discipline | Add research, citation, and empty-result recovery blocks | Multi-pass retrieval is stronger when the model is told how to react to weak or empty search results | Add `research_mode`, `citation_rules`, and `empty_result_handling`; add `tool_persistence_rules` when retrieval tools are already in use |
|
||||
| Coding or terminal prompt with shell misuse or early stop failures | Keep the same tool surface and add terminal hygiene and verification instructions | Tool-using coding workflows are not blocked just because tools exist; they usually need better prompt steering, not host rewiring | Add `terminal_tool_hygiene` and `verification_loop`, optionally `tool_persistence_rules` |
|
||||
| Multi-agent or support-triage workflow with escalation or completeness requirements | Add one lightweight control block for persistence, completeness, or verification | GPT-5.4 can be more efficient by default, so multi-step support flows benefit from an explicit completion or verification contract | Add at least one of `tool_persistence_rules`, `completeness_contract`, or `verification_loop` |
|
||||
|
||||
## Prompt blocks
|
||||
|
||||
Use these selectively. Do not add all of them by default.
|
||||
|
||||
### `output_verbosity_spec`
|
||||
|
||||
Use when:
|
||||
|
||||
- the upgraded model gets too wordy
|
||||
- the host needs compact, information-dense answers
|
||||
- the workflow benefits from a short overview plus a checklist
|
||||
|
||||
```text
|
||||
<output_verbosity_spec>
|
||||
- Default: 3-6 sentences or up to 6 bullets.
|
||||
- If the user asked for a doc or report, use headings with short bullets.
|
||||
- For multi-step tasks:
|
||||
- Start with 1 short overview paragraph.
|
||||
- Then provide a checklist with statuses: [done], [todo], or [blocked].
|
||||
- Avoid repeating the user's request.
|
||||
- Prefer compact, information-dense writing.
|
||||
</output_verbosity_spec>
|
||||
```
|
||||
|
||||
### `default_follow_through_policy`
|
||||
|
||||
Use when:
|
||||
|
||||
- the host expects the model to proceed on reversible, low-risk steps
|
||||
- the upgraded model becomes too conservative or asks for confirmation too often
|
||||
|
||||
```text
|
||||
<default_follow_through_policy>
|
||||
- If the user's intent is clear and the next step is reversible and low-risk, proceed without asking permission.
|
||||
- Only ask permission if the next step is:
|
||||
(a) irreversible,
|
||||
(b) has external side effects, or
|
||||
(c) requires missing sensitive information or a choice that materially changes outcomes.
|
||||
- If proceeding, state what you did and what remains optional.
|
||||
</default_follow_through_policy>
|
||||
```
|
||||
|
||||
### `instruction_priority`
|
||||
|
||||
Use when:
|
||||
|
||||
- users often change task shape, format, or tone mid-conversation
|
||||
- the host needs an explicit override policy instead of relying on defaults
|
||||
|
||||
```text
|
||||
<instruction_priority>
|
||||
- User instructions override default style, tone, formatting, and initiative preferences.
|
||||
- Safety, honesty, privacy, and permission constraints do not yield.
|
||||
- If a newer user instruction conflicts with an earlier one, follow the newer instruction.
|
||||
- Preserve earlier instructions that do not conflict.
|
||||
</instruction_priority>
|
||||
```
|
||||
|
||||
### `tool_persistence_rules`
|
||||
|
||||
Use when:
|
||||
|
||||
- the workflow needs multiple retrieval or verification steps
|
||||
- the model starts stopping too early because it is trying to save tool calls
|
||||
|
||||
```text
|
||||
<tool_persistence_rules>
|
||||
- Use tools whenever they materially improve correctness, completeness, or grounding.
|
||||
- Do not stop early just to save tool calls.
|
||||
- Keep calling tools until:
|
||||
(1) the task is complete, and
|
||||
(2) verification passes.
|
||||
- If a tool returns empty or partial results, retry with a different strategy.
|
||||
</tool_persistence_rules>
|
||||
```
|
||||
|
||||
### `dig_deeper_nudge`
|
||||
|
||||
Use when:
|
||||
|
||||
- the model is too literal or stops at the first plausible answer
|
||||
- the task is safety- or accuracy-sensitive and needs a small initiative nudge before raising reasoning effort
|
||||
|
||||
```text
|
||||
<dig_deeper_nudge>
|
||||
- Do not stop at the first plausible answer.
|
||||
- Look for second-order issues, edge cases, and missing constraints.
|
||||
- If the task is safety- or accuracy-critical, perform at least one verification step.
|
||||
</dig_deeper_nudge>
|
||||
```
|
||||
|
||||
### `dependency_checks`
|
||||
|
||||
Use when:
|
||||
|
||||
- later actions depend on prerequisite lookup, memory retrieval, or discovery steps
|
||||
- the model may be tempted to skip prerequisite work because the intended end state seems obvious
|
||||
|
||||
```text
|
||||
<dependency_checks>
|
||||
- Before taking an action, check whether prerequisite discovery, lookup, or memory retrieval is required.
|
||||
- Do not skip prerequisite steps just because the intended final action seems obvious.
|
||||
- If a later step depends on the output of an earlier one, resolve that dependency first.
|
||||
</dependency_checks>
|
||||
```
|
||||
|
||||
### `parallel_tool_calling`
|
||||
|
||||
Use when:
|
||||
|
||||
- the workflow has multiple independent retrieval steps
|
||||
- wall-clock time matters but some steps still need sequencing
|
||||
|
||||
```text
|
||||
<parallel_tool_calling>
|
||||
- When multiple retrieval or lookup steps are independent, prefer parallel tool calls to reduce wall-clock time.
|
||||
- Do not parallelize steps with prerequisite dependencies or where one result determines the next action.
|
||||
- After parallel retrieval, pause to synthesize before making more calls.
|
||||
- Prefer selective parallelism: parallelize independent evidence gathering, not speculative or redundant tool use.
|
||||
</parallel_tool_calling>
|
||||
```
|
||||
|
||||
### `completeness_contract`
|
||||
|
||||
Use when:
|
||||
|
||||
- the task involves batches, lists, enumerations, or multiple deliverables
|
||||
- missing items are a common failure mode
|
||||
|
||||
```text
|
||||
<completeness_contract>
|
||||
- Deliver all requested items.
|
||||
- Maintain an itemized checklist of deliverables.
|
||||
- For lists or batches:
|
||||
- state the expected count,
|
||||
- enumerate items 1..N,
|
||||
- confirm that none are missing before finalizing.
|
||||
- If any item is blocked by missing data, mark it [blocked] and state exactly what is missing.
|
||||
</completeness_contract>
|
||||
```
|
||||
|
||||
### `empty_result_handling`
|
||||
|
||||
Use when:
|
||||
|
||||
- the workflow frequently performs search, CRM, logs, or retrieval steps
|
||||
- no-results failures are often false negatives
|
||||
|
||||
```text
|
||||
<empty_result_handling>
|
||||
If a lookup returns empty or suspiciously small results:
|
||||
- Do not conclude that no results exist immediately.
|
||||
- Try at least 2 fallback strategies, such as a broader query, alternate filters, or another source.
|
||||
- Only then report that no results were found, along with what you tried.
|
||||
</empty_result_handling>
|
||||
```
|
||||
|
||||
### `verification_loop`
|
||||
|
||||
Use when:
|
||||
|
||||
- the workflow has downstream impact
|
||||
- accuracy, formatting, or completeness regressions matter
|
||||
|
||||
```text
|
||||
<verification_loop>
|
||||
Before finalizing:
|
||||
- Check correctness: does the output satisfy every requirement?
|
||||
- Check grounding: are factual claims backed by retrieved sources or tool output?
|
||||
- Check formatting: does the output match the requested schema or style?
|
||||
- Check safety and irreversibility: if the next step has external side effects, ask permission first.
|
||||
</verification_loop>
|
||||
```
|
||||
|
||||
### `missing_context_gating`
|
||||
|
||||
Use when:
|
||||
|
||||
- required context is sometimes missing early in the workflow
|
||||
- the model should prefer retrieval over guessing
|
||||
|
||||
```text
|
||||
<missing_context_gating>
|
||||
- If required context is missing, do not guess.
|
||||
- Prefer the appropriate lookup tool when the context is retrievable; ask a minimal clarifying question only when it is not.
|
||||
- If you must proceed, label assumptions explicitly and choose a reversible action.
|
||||
</missing_context_gating>
|
||||
```
|
||||
|
||||
### `action_safety`
|
||||
|
||||
Use when:
|
||||
|
||||
- the agent will actively take actions through tools
|
||||
- the host benefits from a short pre-flight and post-flight execution frame
|
||||
|
||||
```text
|
||||
<action_safety>
|
||||
- Pre-flight: summarize the intended action and parameters in 1-2 lines.
|
||||
- Execute via tool.
|
||||
- Post-flight: confirm the outcome and any validation that was performed.
|
||||
</action_safety>
|
||||
```
|
||||
|
||||
### `citation_rules`
|
||||
|
||||
Use when:
|
||||
|
||||
- the workflow produces cited answers
|
||||
- fabricated citations or wrong citation formats are costly
|
||||
|
||||
```text
|
||||
<citation_rules>
|
||||
- Only cite sources that were actually retrieved in this session.
|
||||
- Never fabricate citations, URLs, IDs, or quote spans.
|
||||
- If you cannot find a source for a claim, say so and either:
|
||||
- soften the claim, or
|
||||
- explain how to verify it with tools.
|
||||
- Use exactly the citation format required by the host application.
|
||||
</citation_rules>
|
||||
```
|
||||
|
||||
### `research_mode`
|
||||
|
||||
Use when:
|
||||
|
||||
- the workflow is research-heavy
|
||||
- the host uses web search or retrieval tools
|
||||
|
||||
```text
|
||||
<research_mode>
|
||||
- Do research in 3 passes:
|
||||
1) Plan: list 3-6 sub-questions to answer.
|
||||
2) Retrieve: search each sub-question and follow 1-2 second-order leads.
|
||||
3) Synthesize: resolve contradictions and write the final answer with citations.
|
||||
- Stop only when more searching is unlikely to change the conclusion.
|
||||
</research_mode>
|
||||
```
|
||||
|
||||
If your host environment uses a specific research tool or requires a submit step, combine this with the host's finalization contract.
|
||||
|
||||
### `structured_output_contract`
|
||||
|
||||
Use when:
|
||||
|
||||
- the host depends on strict JSON, SQL, or other structured output
|
||||
|
||||
```text
|
||||
<structured_output_contract>
|
||||
- Output only the requested format.
|
||||
- Do not add prose or markdown fences unless they were requested.
|
||||
- Validate that parentheses and brackets are balanced.
|
||||
- Do not invent tables or fields.
|
||||
- If required schema information is missing, ask for it or return an explicit error object.
|
||||
</structured_output_contract>
|
||||
```
|
||||
|
||||
### `bbox_extraction_spec`
|
||||
|
||||
Use when:
|
||||
|
||||
- the workflow extracts OCR boxes, document regions, or other coordinates
|
||||
- layout drift or missed dense regions are common failure modes
|
||||
|
||||
```text
|
||||
<bbox_extraction_spec>
|
||||
- Use the specified coordinate format exactly, such as [x1,y1,x2,y2] normalized to 0..1.
|
||||
- For each box, include page, label, text snippet, and confidence.
|
||||
- Add a vertical-drift sanity check so boxes stay aligned with the correct line of text.
|
||||
- If the layout is dense, process page by page and do a second pass for missed items.
|
||||
</bbox_extraction_spec>
|
||||
```
|
||||
|
||||
### `terminal_tool_hygiene`
|
||||
|
||||
Use when:
|
||||
|
||||
- the prompt belongs to a terminal-based or coding-agent workflow
|
||||
- tool misuse or shell misuse has been observed
|
||||
|
||||
```text
|
||||
<terminal_tool_hygiene>
|
||||
- Only run shell commands through the terminal tool.
|
||||
- Never try to "run" tool names as shell commands.
|
||||
- If a patch or edit tool exists, use it directly instead of emulating it in bash.
|
||||
- After changes, run a lightweight verification step such as ls, tests, or a build before declaring the task done.
|
||||
</terminal_tool_hygiene>
|
||||
```
|
||||
|
||||
### `user_updates_spec`
|
||||
|
||||
Use when:
|
||||
|
||||
- the workflow is long-running and user updates matter
|
||||
|
||||
```text
|
||||
<user_updates_spec>
|
||||
- Only update the user when starting a new major phase or when the plan changes.
|
||||
- Each update should contain:
|
||||
- 1 sentence on what changed,
|
||||
- 1 sentence on the next step.
|
||||
- Do not narrate routine tool calls.
|
||||
- Keep the user-facing update short, even when the actual work is exhaustive.
|
||||
</user_updates_spec>
|
||||
```
|
||||
|
||||
If you are using [Compaction](https://developers.openai.com/api/docs/guides/compaction) in the Responses API, compact after major milestones, treat compacted items as opaque state, and keep prompts functionally identical after compaction.
|
||||
|
||||
## Responses `phase` guidance
|
||||
|
||||
For long-running Responses workflows, preambles, or tool-heavy agents that replay assistant items, review whether `phase` is already preserved.
|
||||
|
||||
- If the host already round-trips `phase`, keep it intact during the upgrade.
|
||||
- If the host uses `previous_response_id` and does not manually replay assistant items, note that this may reduce manual `phase` handling needs.
|
||||
- If reliable GPT-5.4 behavior would require adding or preserving `phase` and that would need code edits, treat the case as blocked for prompt-only or model-string-only migration guidance.
|
||||
|
||||
## Example upgrade profiles
|
||||
|
||||
### GPT-5.2
|
||||
|
||||
- Use `gpt-5.4`
|
||||
- Match the current reasoning effort first
|
||||
- Preserve the existing latency and quality profile before tuning prompt blocks
|
||||
- If the repo does not expose the exact setting, emit `same` as the starting recommendation
|
||||
|
||||
### GPT-5.3-Codex
|
||||
|
||||
- Use `gpt-5.4`
|
||||
- Match the current reasoning effort first
|
||||
- If you need Codex-style speed and efficiency, add verification blocks before increasing reasoning effort
|
||||
- If the repo does not expose the exact setting, emit `same` as the starting recommendation
|
||||
|
||||
### GPT-4o or GPT-4.1 assistant
|
||||
|
||||
- Use `gpt-5.4`
|
||||
- Start with `none` reasoning effort
|
||||
- Add `output_verbosity_spec` only if output becomes too verbose
|
||||
|
||||
### Long-horizon agent
|
||||
|
||||
- Use `gpt-5.4`
|
||||
- Start with `medium` reasoning effort
|
||||
- Add `tool_persistence_rules`
|
||||
- Add `completeness_contract`
|
||||
- Add `verification_loop`
|
||||
|
||||
### Research workflow
|
||||
|
||||
- Use `gpt-5.4`
|
||||
- Start with `medium` reasoning effort
|
||||
- Add `research_mode`
|
||||
- Add `citation_rules`
|
||||
- Add `empty_result_handling`
|
||||
- Add `tool_persistence_rules` when the host already uses web or retrieval tools
|
||||
- Add `parallel_tool_calling` when the retrieval steps are independent
|
||||
|
||||
### Support triage or multi-agent workflow
|
||||
|
||||
- Use `gpt-5.4`
|
||||
- Prefer `model string + light prompt rewrite` over `model string only`
|
||||
- Add at least one of `tool_persistence_rules`, `completeness_contract`, or `verification_loop`
|
||||
- Add more only if evals show a real regression
|
||||
|
||||
### Coding or terminal workflow
|
||||
|
||||
- Use `gpt-5.4`
|
||||
- Keep the model-string change narrow
|
||||
- Match the current reasoning effort first if you are upgrading from GPT-5.3-Codex
|
||||
- Add `terminal_tool_hygiene`
|
||||
- Add `verification_loop`
|
||||
- Add `dependency_checks` when actions depend on prerequisite lookup or discovery
|
||||
- Add `tool_persistence_rules` if the agent stops too early
|
||||
- Review whether `phase` is already preserved for long-running Responses flows or assistant preambles
|
||||
- Do not classify this as blocked just because the workflow uses tools; block only if the upgrade requires changing tool definitions or wiring
|
||||
- If the repo already uses Responses plus tools and no required host-side change is shown, prefer `model_string_plus_light_prompt_rewrite` over `blocked`
|
||||
|
||||
## Prompt regression checklist
|
||||
|
||||
- Check whether the upgraded prompt still preserves the original task intent.
|
||||
- Check whether the new prompt is leaner, not just longer.
|
||||
- Check completeness, citation quality, dependency handling, verification behavior, and verbosity.
|
||||
- For long-running Responses agents, check whether `phase` handling is already in place or needs implementation work.
|
||||
- Confirm that each added prompt block addresses an observed regression.
|
||||
- Remove prompt blocks that are not earning their keep.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Latest model guide
|
||||
|
||||
This file is a curated helper. Every recommendation here must be verified against current OpenAI docs before it is repeated to a user.
|
||||
|
||||
## Current model map
|
||||
|
||||
| Model ID | Use for |
|
||||
| --- | --- |
|
||||
| `gpt-5.4` | Default text plus reasoning for most new apps |
|
||||
| `gpt-5.4-pro` | Only when the user explicitly asks for maximum reasoning or quality; substantially slower and more expensive |
|
||||
| `gpt-5-mini` | Cheaper and faster reasoning with good quality |
|
||||
| `gpt-5-nano` | High-throughput simple tasks and classification |
|
||||
| `gpt-5.4` | Explicit no-reasoning text path via `reasoning.effort: none` |
|
||||
| `gpt-4.1-mini` | Cheaper no-reasoning text |
|
||||
| `gpt-4.1-nano` | Fastest and cheapest no-reasoning text |
|
||||
| `gpt-5.3-codex` | Agentic coding, code editing, and tool-heavy coding workflows |
|
||||
| `gpt-5.1-codex-mini` | Cheaper coding workflows |
|
||||
| `gpt-image-1.5` | Best image generation and edit quality |
|
||||
| `gpt-image-1-mini` | Cost-optimized image generation |
|
||||
| `gpt-4o-mini-tts` | Text-to-speech |
|
||||
| `gpt-4o-mini-transcribe` | Speech-to-text, fast and cost-efficient |
|
||||
| `gpt-realtime-1.5` | Realtime voice and multimodal sessions |
|
||||
| `gpt-realtime-mini` | Cheaper realtime sessions |
|
||||
| `gpt-audio` | Chat Completions audio input and output |
|
||||
| `gpt-audio-mini` | Cheaper Chat Completions audio workflows |
|
||||
| `sora-2` | Faster iteration and draft video generation |
|
||||
| `sora-2-pro` | Higher-quality production video |
|
||||
| `omni-moderation-latest` | Text and image moderation |
|
||||
| `text-embedding-3-large` | Higher-quality retrieval embeddings; default in this skill because no best-specific row exists |
|
||||
| `text-embedding-3-small` | Lower-cost embeddings |
|
||||
|
||||
## Maintenance notes
|
||||
|
||||
- This file will drift unless it is periodically re-verified against current OpenAI docs.
|
||||
- If this file conflicts with current docs, the docs win.
|
||||
@@ -0,0 +1,164 @@
|
||||
# Upgrading to GPT-5.4
|
||||
|
||||
Use this guide when the user explicitly asks to upgrade an existing integration to GPT-5.4. Pair it with current OpenAI docs lookups. The default target string is `gpt-5.4`.
|
||||
|
||||
## Upgrade posture
|
||||
|
||||
Upgrade with the narrowest safe change set:
|
||||
|
||||
- replace the model string first
|
||||
- update only the prompts that are directly tied to that model usage
|
||||
- prefer prompt-only upgrades when possible
|
||||
- if the upgrade would require API-surface changes, parameter rewrites, tool rewiring, or broader code edits, mark it as blocked instead of stretching the scope
|
||||
|
||||
## Upgrade workflow
|
||||
|
||||
1. Inventory current model usage.
|
||||
- Search for model strings, client calls, and prompt-bearing files.
|
||||
- Include inline prompts, prompt templates, YAML or JSON configs, Markdown docs, and saved prompts when they are clearly tied to a model usage site.
|
||||
2. Pair each model usage with its prompt surface.
|
||||
- Prefer the closest prompt surface first: inline system or developer text, then adjacent prompt files, then shared templates.
|
||||
- If you cannot confidently tie a prompt to the model usage, say so instead of guessing.
|
||||
3. Classify the source model family.
|
||||
- Common buckets: `gpt-4o` or `gpt-4.1`, `o1` or `o3` or `o4-mini`, early `gpt-5`, later `gpt-5.x`, or mixed and unclear.
|
||||
4. Decide the upgrade class.
|
||||
- `model string only`
|
||||
- `model string + light prompt rewrite`
|
||||
- `blocked without code changes`
|
||||
5. Run the no-code compatibility gate.
|
||||
- Check whether the current integration can accept `gpt-5.4` without API-surface changes or implementation changes.
|
||||
- For long-running Responses or tool-heavy agents, check whether `phase` is already preserved or round-tripped when the host replays assistant items or uses preambles.
|
||||
- If compatibility depends on code changes, return `blocked`.
|
||||
- If compatibility is unclear, return `unknown` rather than improvising.
|
||||
6. Recommend the upgrade.
|
||||
- Default replacement string: `gpt-5.4`
|
||||
- Keep the intervention small and behavior-preserving.
|
||||
7. Deliver a structured recommendation.
|
||||
- `Current model usage`
|
||||
- `Recommended model-string updates`
|
||||
- `Starting reasoning recommendation`
|
||||
- `Prompt updates`
|
||||
- `Phase assessment` when the flow is long-running, replayed, or tool-heavy
|
||||
- `No-code compatibility check`
|
||||
- `Validation plan`
|
||||
- `Launch-day refresh items`
|
||||
|
||||
Output rule:
|
||||
|
||||
- Always emit a starting `reasoning_effort_recommendation` for each usage site.
|
||||
- If the repo exposes the current reasoning setting, preserve it first unless the source guide says otherwise.
|
||||
- If the repo does not expose the current setting, use the source-family starting mapping instead of returning `null`.
|
||||
|
||||
## Upgrade outcomes
|
||||
|
||||
### `model string only`
|
||||
|
||||
Choose this when:
|
||||
|
||||
- the existing prompts are already short, explicit, and task-bounded
|
||||
- the workflow is not strongly research-heavy, tool-heavy, multi-agent, batch or completeness-sensitive, or long-horizon
|
||||
- there are no obvious compatibility blockers
|
||||
|
||||
Default action:
|
||||
|
||||
- replace the model string with `gpt-5.4`
|
||||
- keep prompts unchanged
|
||||
- validate behavior with existing evals or spot checks
|
||||
|
||||
### `model string + light prompt rewrite`
|
||||
|
||||
Choose this when:
|
||||
|
||||
- the old prompt was compensating for weaker instruction following
|
||||
- the workflow needs more persistence than the default tool-use behavior will likely provide
|
||||
- the task needs stronger completeness, citation discipline, or verification
|
||||
- the upgraded model becomes too verbose or under-complete unless instructed otherwise
|
||||
- the workflow is research-heavy and needs stronger handling of sparse or empty retrieval results
|
||||
- the workflow is coding-oriented, tool-heavy, or multi-agent, but the existing API surface and tool definitions can remain unchanged
|
||||
|
||||
Default action:
|
||||
|
||||
- replace the model string with `gpt-5.4`
|
||||
- add one or two targeted prompt blocks
|
||||
- read `references/gpt-5p4-prompting-guide.md` to choose the smallest prompt changes that recover the old behavior
|
||||
- avoid broad prompt cleanup unrelated to the upgrade
|
||||
- for research workflows, default to `research_mode` + `citation_rules` + `empty_result_handling`; add `tool_persistence_rules` when the host already uses retrieval tools
|
||||
- for dependency-aware or tool-heavy workflows, default to `tool_persistence_rules` + `dependency_checks` + `verification_loop`; add `parallel_tool_calling` only when retrieval steps are truly independent
|
||||
- for coding or terminal workflows, default to `terminal_tool_hygiene` + `verification_loop`
|
||||
- for multi-agent support or triage workflows, default to at least one of `tool_persistence_rules`, `completeness_contract`, or `verification_loop`
|
||||
- for long-running Responses agents with preambles or multiple assistant messages, explicitly review whether `phase` is already handled; if adding or preserving `phase` would require code edits, mark the path as `blocked`
|
||||
- do not classify a coding or tool-using Responses workflow as `blocked` just because the visible snippet is minimal; prefer `model string + light prompt rewrite` unless the repo clearly shows that a safe GPT-5.4 path would require host-side code changes
|
||||
|
||||
### `blocked`
|
||||
|
||||
Choose this when:
|
||||
|
||||
- the upgrade appears to require API-surface changes
|
||||
- the upgrade appears to require parameter rewrites or reasoning-setting changes that are not exposed outside implementation code
|
||||
- the upgrade would require changing tool definitions, tool handler wiring, or schema contracts
|
||||
- you cannot confidently identify the prompt surface tied to the model usage
|
||||
|
||||
Default action:
|
||||
|
||||
- do not improvise a broader upgrade
|
||||
- report the blocker and explain that the fix is out of scope for this guide
|
||||
|
||||
## No-code compatibility checklist
|
||||
|
||||
Before recommending a no-code upgrade, check:
|
||||
|
||||
1. Can the current host accept the `gpt-5.4` model string without changing client code or API surface?
|
||||
2. Are the related prompts identifiable and editable?
|
||||
3. Does the host depend on behavior that likely needs API-surface changes, parameter rewrites, or tool rewiring?
|
||||
4. Would the likely fix be prompt-only, or would it need implementation changes?
|
||||
5. Is the prompt surface close enough to the model usage that you can make a targeted change instead of a broad cleanup?
|
||||
6. For long-running Responses or tool-heavy agents, is `phase` already preserved if the host relies on preambles, replayed assistant items, or multiple assistant messages?
|
||||
|
||||
If item 1 is no, items 3 through 4 point to implementation work, or item 6 is no and the fix needs code changes, return `blocked`.
|
||||
|
||||
If item 2 is no, return `unknown` unless the user can point to the prompt location.
|
||||
|
||||
Important:
|
||||
|
||||
- Existing use of tools, agents, or multiple usage sites is not by itself a blocker.
|
||||
- If the current host can keep the same API surface and the same tool definitions, prefer `model string + light prompt rewrite` over `blocked`.
|
||||
- Reserve `blocked` for cases that truly require implementation changes, not cases that only need stronger prompt steering.
|
||||
|
||||
## Scope boundaries
|
||||
|
||||
This guide may:
|
||||
|
||||
- update or recommend updated model strings
|
||||
- update or recommend updated prompts
|
||||
- inspect code and prompt files to understand where those changes belong
|
||||
- inspect whether existing Responses flows already preserve `phase`
|
||||
- flag compatibility blockers
|
||||
|
||||
This guide may not:
|
||||
|
||||
- move Chat Completions code to Responses
|
||||
- move Responses code to another API surface
|
||||
- rewrite parameter shapes
|
||||
- change tool definitions or tool-call handling
|
||||
- change structured-output wiring
|
||||
- add or retrofit `phase` handling in implementation code
|
||||
- edit business logic, orchestration logic, or SDK usage beyond a literal model-string replacement
|
||||
|
||||
If a safe GPT-5.4 upgrade requires any of those changes, mark the path as blocked and out of scope.
|
||||
|
||||
## Validation plan
|
||||
|
||||
- Validate each upgraded usage site with existing evals or realistic spot checks.
|
||||
- Check whether the upgraded model still matches expected latency, output shape, and quality.
|
||||
- If prompt edits were added, confirm each block is doing real work instead of adding noise.
|
||||
- If the workflow has downstream impact, add a lightweight verification pass before finalization.
|
||||
|
||||
## Launch-day refresh items
|
||||
|
||||
When final GPT-5.4 guidance changes:
|
||||
|
||||
1. Replace release-candidate assumptions with final GPT-5.4 guidance where appropriate.
|
||||
2. Re-check whether the default target string should stay `gpt-5.4` for all source families.
|
||||
3. Re-check any prompt-block recommendations whose semantics may have changed.
|
||||
4. Re-check research, citation, and compatibility guidance against the final model behavior.
|
||||
5. Re-run the same upgrade scenarios and confirm the blocked-versus-viable boundaries still hold.
|
||||
413
.agents/skills/.system/skill-creator/SKILL.md
Normal file
@@ -0,0 +1,413 @@
|
||||
---
|
||||
name: skill-creator
|
||||
description: Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Codex's capabilities with specialized knowledge, workflows, or tool integrations.
|
||||
metadata:
|
||||
short-description: Create or update a skill
|
||||
---
|
||||
|
||||
# Skill Creator
|
||||
|
||||
This skill provides guidance for creating effective skills.
|
||||
|
||||
## About Skills
|
||||
|
||||
Skills are modular, self-contained folders that extend Codex's capabilities by providing
|
||||
specialized knowledge, workflows, and tools. Think of them as "onboarding guides" for specific
|
||||
domains or tasks—they transform Codex from a general-purpose agent into a specialized agent
|
||||
equipped with procedural knowledge that no model can fully possess.
|
||||
|
||||
### What Skills Provide
|
||||
|
||||
1. Specialized workflows - Multi-step procedures for specific domains
|
||||
2. Tool integrations - Instructions for working with specific file formats or APIs
|
||||
3. Domain expertise - Company-specific knowledge, schemas, business logic
|
||||
4. Bundled resources - Scripts, references, and assets for complex and repetitive tasks
|
||||
|
||||
## Core Principles
|
||||
|
||||
### Concise is Key
|
||||
|
||||
The context window is a public good. Skills share the context window with everything else Codex needs: system prompt, conversation history, other Skills' metadata, and the actual user request.
|
||||
|
||||
**Default assumption: Codex is already very smart.** Only add context Codex doesn't already have. Challenge each piece of information: "Does Codex really need this explanation?" and "Does this paragraph justify its token cost?"
|
||||
|
||||
Prefer concise examples over verbose explanations.
|
||||
|
||||
### Set Appropriate Degrees of Freedom
|
||||
|
||||
Match the level of specificity to the task's fragility and variability:
|
||||
|
||||
**High freedom (text-based instructions)**: Use when multiple approaches are valid, decisions depend on context, or heuristics guide the approach.
|
||||
|
||||
**Medium freedom (pseudocode or scripts with parameters)**: Use when a preferred pattern exists, some variation is acceptable, or configuration affects behavior.
|
||||
|
||||
**Low freedom (specific scripts, few parameters)**: Use when operations are fragile and error-prone, consistency is critical, or a specific sequence must be followed.
|
||||
|
||||
Think of Codex as exploring a path: a narrow bridge with cliffs needs specific guardrails (low freedom), while an open field allows many routes (high freedom).
|
||||
|
||||
### Protect Validation Integrity
|
||||
|
||||
You may use subagents during iteration to validate whether a skill works on realistic tasks or whether a suspected problem is real. This is most useful when you want an independent pass on the skill's behavior, outputs, or failure modes after a revision. Only do this when it is possible to start new subagents.
|
||||
|
||||
When using subagents for validation, treat that as an evaluation surface. The goal is to learn whether the skill generalizes, not whether another agent can reconstruct the answer from leaked context.
|
||||
|
||||
Prefer raw artifacts such as example prompts, outputs, diffs, logs, or traces. Give the minimum task-local context needed to perform the validation. Avoid passing the intended answer, suspected bug, intended fix, or your prior conclusions unless the validation explicitly requires them.
|
||||
|
||||
### Anatomy of a Skill
|
||||
|
||||
Every skill consists of a required SKILL.md file and optional bundled resources:
|
||||
|
||||
```
|
||||
skill-name/
|
||||
├── SKILL.md (required)
|
||||
│ ├── YAML frontmatter metadata (required)
|
||||
│ │ ├── name: (required)
|
||||
│ │ └── description: (required)
|
||||
│ └── Markdown instructions (required)
|
||||
├── agents/ (recommended)
|
||||
│ └── openai.yaml - UI metadata for skill lists and chips
|
||||
└── Bundled Resources (optional)
|
||||
├── scripts/ - Executable code (Python/Bash/etc.)
|
||||
├── references/ - Documentation intended to be loaded into context as needed
|
||||
└── assets/ - Files used in output (templates, icons, fonts, etc.)
|
||||
```
|
||||
|
||||
#### SKILL.md (required)
|
||||
|
||||
Every SKILL.md consists of:
|
||||
|
||||
- **Frontmatter** (YAML): Contains `name` and `description` fields. These are the only fields that Codex reads to determine when the skill gets used, thus it is very important to be clear and comprehensive in describing what the skill is, and when it should be used.
|
||||
- **Body** (Markdown): Instructions and guidance for using the skill. Only loaded AFTER the skill triggers (if at all).
|
||||
|
||||
#### Agents metadata (recommended)
|
||||
|
||||
- UI-facing metadata for skill lists and chips
|
||||
- Read references/openai_yaml.md before generating values and follow its descriptions and constraints
|
||||
- Create: human-facing `display_name`, `short_description`, and `default_prompt` by reading the skill
|
||||
- Generate deterministically by passing the values as `--interface key=value` to `scripts/generate_openai_yaml.py` or `scripts/init_skill.py`
|
||||
- On updates: validate `agents/openai.yaml` still matches SKILL.md; regenerate if stale
|
||||
- Only include other optional interface fields (icons, brand color) if explicitly provided
|
||||
- See references/openai_yaml.md for field definitions and examples
|
||||
|
||||
#### Bundled Resources (optional)
|
||||
|
||||
##### Scripts (`scripts/`)
|
||||
|
||||
Executable code (Python/Bash/etc.) for tasks that require deterministic reliability or are repeatedly rewritten.
|
||||
|
||||
- **When to include**: When the same code is being rewritten repeatedly or deterministic reliability is needed
|
||||
- **Example**: `scripts/rotate_pdf.py` for PDF rotation tasks
|
||||
- **Benefits**: Token efficient, deterministic, may be executed without loading into context
|
||||
- **Note**: Scripts may still need to be read by Codex for patching or environment-specific adjustments
|
||||
|
||||
##### References (`references/`)
|
||||
|
||||
Documentation and reference material intended to be loaded as needed into context to inform Codex's process and thinking.
|
||||
|
||||
- **When to include**: For documentation that Codex should reference while working
|
||||
- **Examples**: `references/finance.md` for financial schemas, `references/mnda.md` for company NDA template, `references/policies.md` for company policies, `references/api_docs.md` for API specifications
|
||||
- **Use cases**: Database schemas, API documentation, domain knowledge, company policies, detailed workflow guides
|
||||
- **Benefits**: Keeps SKILL.md lean, loaded only when Codex determines it's needed
|
||||
- **Best practice**: If files are large (>10k words), include grep search patterns in SKILL.md
|
||||
- **Avoid duplication**: Information should live in either SKILL.md or references files, not both. Prefer references files for detailed information unless it's truly core to the skill—this keeps SKILL.md lean while making information discoverable without hogging the context window. Keep only essential procedural instructions and workflow guidance in SKILL.md; move detailed reference material, schemas, and examples to references files.
|
||||
|
||||
##### Assets (`assets/`)
|
||||
|
||||
Files not intended to be loaded into context, but rather used within the output Codex produces.
|
||||
|
||||
- **When to include**: When the skill needs files that will be used in the final output
|
||||
- **Examples**: `assets/logo.png` for brand assets, `assets/slides.pptx` for PowerPoint templates, `assets/frontend-template/` for HTML/React boilerplate, `assets/font.ttf` for typography
|
||||
- **Use cases**: Templates, images, icons, boilerplate code, fonts, sample documents that get copied or modified
|
||||
- **Benefits**: Separates output resources from documentation, enables Codex to use files without loading them into context
|
||||
|
||||
#### What to Not Include in a Skill
|
||||
|
||||
A skill should only contain essential files that directly support its functionality. Do NOT create extraneous documentation or auxiliary files, including:
|
||||
|
||||
- README.md
|
||||
- INSTALLATION_GUIDE.md
|
||||
- QUICK_REFERENCE.md
|
||||
- CHANGELOG.md
|
||||
- etc.
|
||||
|
||||
The skill should only contain the information needed for an AI agent to do the job at hand. It should not contain auxiliary context about the process that went into creating it, setup and testing procedures, user-facing documentation, etc. Creating additional documentation files just adds clutter and confusion.
|
||||
|
||||
### Progressive Disclosure Design Principle
|
||||
|
||||
Skills use a three-level loading system to manage context efficiently:
|
||||
|
||||
1. **Metadata (name + description)** - Always in context (~100 words)
|
||||
2. **SKILL.md body** - When skill triggers (<5k words)
|
||||
3. **Bundled resources** - As needed by Codex (Unlimited because scripts can be executed without reading into context window)
|
||||
|
||||
#### Progressive Disclosure Patterns
|
||||
|
||||
Keep SKILL.md body to the essentials and under 500 lines to minimize context bloat. Split content into separate files when approaching this limit. When splitting out content into other files, it is very important to reference them from SKILL.md and describe clearly when to read them, to ensure the reader of the skill knows they exist and when to use them.
|
||||
|
||||
**Key principle:** When a skill supports multiple variations, frameworks, or options, keep only the core workflow and selection guidance in SKILL.md. Move variant-specific details (patterns, examples, configuration) into separate reference files.
|
||||
|
||||
**Pattern 1: High-level guide with references**
|
||||
|
||||
```markdown
|
||||
# PDF Processing
|
||||
|
||||
## Quick start
|
||||
|
||||
Extract text with pdfplumber:
|
||||
[code example]
|
||||
|
||||
## Advanced features
|
||||
|
||||
- **Form filling**: See [FORMS.md](FORMS.md) for complete guide
|
||||
- **API reference**: See [REFERENCE.md](REFERENCE.md) for all methods
|
||||
- **Examples**: See [EXAMPLES.md](EXAMPLES.md) for common patterns
|
||||
```
|
||||
|
||||
Codex loads FORMS.md, REFERENCE.md, or EXAMPLES.md only when needed.
|
||||
|
||||
**Pattern 2: Domain-specific organization**
|
||||
|
||||
For Skills with multiple domains, organize content by domain to avoid loading irrelevant context:
|
||||
|
||||
```
|
||||
bigquery-skill/
|
||||
├── SKILL.md (overview and navigation)
|
||||
└── reference/
|
||||
├── finance.md (revenue, billing metrics)
|
||||
├── sales.md (opportunities, pipeline)
|
||||
├── product.md (API usage, features)
|
||||
└── marketing.md (campaigns, attribution)
|
||||
```
|
||||
|
||||
When a user asks about sales metrics, Codex only reads sales.md.
|
||||
|
||||
Similarly, for skills supporting multiple frameworks or variants, organize by variant:
|
||||
|
||||
```
|
||||
cloud-deploy/
|
||||
├── SKILL.md (workflow + provider selection)
|
||||
└── references/
|
||||
├── aws.md (AWS deployment patterns)
|
||||
├── gcp.md (GCP deployment patterns)
|
||||
└── azure.md (Azure deployment patterns)
|
||||
```
|
||||
|
||||
When the user chooses AWS, Codex only reads aws.md.
|
||||
|
||||
**Pattern 3: Conditional details**
|
||||
|
||||
Show basic content, link to advanced content:
|
||||
|
||||
```markdown
|
||||
# DOCX Processing
|
||||
|
||||
## Creating documents
|
||||
|
||||
Use docx-js for new documents. See [DOCX-JS.md](DOCX-JS.md).
|
||||
|
||||
## Editing documents
|
||||
|
||||
For simple edits, modify the XML directly.
|
||||
|
||||
**For tracked changes**: See [REDLINING.md](REDLINING.md)
|
||||
**For OOXML details**: See [OOXML.md](OOXML.md)
|
||||
```
|
||||
|
||||
Codex reads REDLINING.md or OOXML.md only when the user needs those features.
|
||||
|
||||
**Important guidelines:**
|
||||
|
||||
- **Avoid deeply nested references** - Keep references one level deep from SKILL.md. All reference files should link directly from SKILL.md.
|
||||
- **Structure longer reference files** - For files longer than 100 lines, include a table of contents at the top so Codex can see the full scope when previewing.
|
||||
|
||||
## Skill Creation Process
|
||||
|
||||
Skill creation involves these steps:
|
||||
|
||||
1. Understand the skill with concrete examples
|
||||
2. Plan reusable skill contents (scripts, references, assets)
|
||||
3. Initialize the skill (run init_skill.py)
|
||||
4. Edit the skill (implement resources and write SKILL.md)
|
||||
5. Validate the skill (run quick_validate.py)
|
||||
6. Iterate based on real usage and forward-test complex skills.
|
||||
|
||||
Follow these steps in order, skipping only if there is a clear reason why they are not applicable.
|
||||
|
||||
### Skill Naming
|
||||
|
||||
- Use lowercase letters, digits, and hyphens only; normalize user-provided titles to hyphen-case (e.g., "Plan Mode" -> `plan-mode`).
|
||||
- When generating names, generate a name under 64 characters (letters, digits, hyphens).
|
||||
- Prefer short, verb-led phrases that describe the action.
|
||||
- Namespace by tool when it improves clarity or triggering (e.g., `gh-address-comments`, `linear-address-issue`).
|
||||
- Name the skill folder exactly after the skill name.
|
||||
|
||||
### Step 1: Understanding the Skill with Concrete Examples
|
||||
|
||||
Skip this step only when the skill's usage patterns are already clearly understood. It remains valuable even when working with an existing skill.
|
||||
|
||||
To create an effective skill, clearly understand concrete examples of how the skill will be used. This understanding can come from either direct user examples or generated examples that are validated with user feedback.
|
||||
|
||||
For example, when building an image-editor skill, relevant questions include:
|
||||
|
||||
- "What functionality should the image-editor skill support? Editing, rotating, anything else?"
|
||||
- "Can you give some examples of how this skill would be used?"
|
||||
- "I can imagine users asking for things like 'Remove the red-eye from this image' or 'Rotate this image'. Are there other ways you imagine this skill being used?"
|
||||
- "What would a user say that should trigger this skill?"
|
||||
|
||||
To avoid overwhelming users, avoid asking too many questions in a single message. Start with the most important questions and follow up as needed for better effectiveness.
|
||||
|
||||
Conclude this step when there is a clear sense of the functionality the skill should support.
|
||||
|
||||
### Step 2: Planning the Reusable Skill Contents
|
||||
|
||||
To turn concrete examples into an effective skill, analyze each example by:
|
||||
|
||||
1. Considering how to execute on the example from scratch
|
||||
2. Identifying what scripts, references, and assets would be helpful when executing these workflows repeatedly
|
||||
|
||||
Example: When building a `pdf-editor` skill to handle queries like "Help me rotate this PDF," the analysis shows:
|
||||
|
||||
1. Rotating a PDF requires re-writing the same code each time
|
||||
2. A `scripts/rotate_pdf.py` script would be helpful to store in the skill
|
||||
|
||||
Example: When designing a `frontend-webapp-builder` skill for queries like "Build me a todo app" or "Build me a dashboard to track my steps," the analysis shows:
|
||||
|
||||
1. Writing a frontend webapp requires the same boilerplate HTML/React each time
|
||||
2. An `assets/hello-world/` template containing the boilerplate HTML/React project files would be helpful to store in the skill
|
||||
|
||||
Example: When building a `big-query` skill to handle queries like "How many users have logged in today?" the analysis shows:
|
||||
|
||||
1. Querying BigQuery requires re-discovering the table schemas and relationships each time
|
||||
2. A `references/schema.md` file documenting the table schemas would be helpful to store in the skill
|
||||
|
||||
To establish the skill's contents, analyze each concrete example to create a list of the reusable resources to include: scripts, references, and assets.
|
||||
|
||||
### Step 3: Initializing the Skill
|
||||
|
||||
At this point, it is time to actually create the skill.
|
||||
|
||||
Skip this step only if the skill being developed already exists. In this case, continue to the next step.
|
||||
|
||||
When creating a new skill from scratch, always run the `init_skill.py` script. The script conveniently generates a new template skill directory that automatically includes everything a skill requires, making the skill creation process much more efficient and reliable.
|
||||
|
||||
Usage:
|
||||
|
||||
```bash
|
||||
scripts/init_skill.py <skill-name> --path <output-directory> [--resources scripts,references,assets] [--examples]
|
||||
```
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
scripts/init_skill.py my-skill --path skills/public
|
||||
scripts/init_skill.py my-skill --path skills/public --resources scripts,references
|
||||
scripts/init_skill.py my-skill --path skills/public --resources scripts --examples
|
||||
```
|
||||
|
||||
The script:
|
||||
|
||||
- Creates the skill directory at the specified path
|
||||
- Generates a SKILL.md template with proper frontmatter and TODO placeholders
|
||||
- Creates `agents/openai.yaml` using agent-generated `display_name`, `short_description`, and `default_prompt` passed via `--interface key=value`
|
||||
- Optionally creates resource directories based on `--resources`
|
||||
- Optionally adds example files when `--examples` is set
|
||||
|
||||
After initialization, customize the SKILL.md and add resources as needed. If you used `--examples`, replace or delete placeholder files.
|
||||
|
||||
Generate `display_name`, `short_description`, and `default_prompt` by reading the skill, then pass them as `--interface key=value` to `init_skill.py` or regenerate with:
|
||||
|
||||
```bash
|
||||
scripts/generate_openai_yaml.py <path/to/skill-folder> --interface key=value
|
||||
```
|
||||
|
||||
Only include other optional interface fields when the user explicitly provides them. For full field descriptions and examples, see references/openai_yaml.md.
|
||||
|
||||
### Step 4: Edit the Skill
|
||||
|
||||
When editing the (newly-generated or existing) skill, remember that the skill is being created for another instance of Codex to use. Include information that would be beneficial and non-obvious to Codex. Consider what procedural knowledge, domain-specific details, or reusable assets would help another Codex instance execute these tasks more effectively.
|
||||
|
||||
After substantial revisions, or if the skill is particularly tricky, you should use subagents to forward-test the skill on realistic tasks or artifacts. When doing so, pass the artifact under validation rather than your diagnosis of what is wrong, and keep the prompt generic enough that success depends on transferable reasoning rather than hidden ground truth.
|
||||
|
||||
#### Start with Reusable Skill Contents
|
||||
|
||||
To begin implementation, start with the reusable resources identified above: `scripts/`, `references/`, and `assets/` files. Note that this step may require user input. For example, when implementing a `brand-guidelines` skill, the user may need to provide brand assets or templates to store in `assets/`, or documentation to store in `references/`.
|
||||
|
||||
Added scripts must be tested by actually running them to ensure there are no bugs and that the output matches what is expected. If there are many similar scripts, only a representative sample needs to be tested to ensure confidence that they all work while balancing time to completion.
|
||||
|
||||
If you used `--examples`, delete any placeholder files that are not needed for the skill. Only create resource directories that are actually required.
|
||||
|
||||
#### Update SKILL.md
|
||||
|
||||
**Writing Guidelines:** Always use imperative/infinitive form.
|
||||
|
||||
##### Frontmatter
|
||||
|
||||
Write the YAML frontmatter with `name` and `description`:
|
||||
|
||||
- `name`: The skill name
|
||||
- `description`: This is the primary triggering mechanism for your skill, and helps Codex understand when to use the skill.
|
||||
- Include both what the Skill does and specific triggers/contexts for when to use it.
|
||||
- Include all "when to use" information here - Not in the body. The body is only loaded after triggering, so "When to Use This Skill" sections in the body are not helpful to Codex.
|
||||
- Example description for a `docx` skill: "Comprehensive document creation, editing, and analysis with support for tracked changes, comments, formatting preservation, and text extraction. Use when Codex needs to work with professional documents (.docx files) for: (1) Creating new documents, (2) Modifying or editing content, (3) Working with tracked changes, (4) Adding comments, or any other document tasks"
|
||||
|
||||
Do not include any other fields in YAML frontmatter.
|
||||
|
||||
##### Body
|
||||
|
||||
Write instructions for using the skill and its bundled resources.
|
||||
|
||||
### Step 5: Validate the Skill
|
||||
|
||||
Once development of the skill is complete, validate the skill folder to catch basic issues early:
|
||||
|
||||
```bash
|
||||
scripts/quick_validate.py <path/to/skill-folder>
|
||||
```
|
||||
|
||||
The validation script checks YAML frontmatter format, required fields, and naming rules. If validation fails, fix the reported issues and run the command again.
|
||||
|
||||
### Step 6: Iterate
|
||||
|
||||
After testing the skill, you may detect the skill is complex enough that it requires forward-testing; or users may request improvements.
|
||||
|
||||
User testing often this happens right after using the skill, with fresh context of how the skill performed.
|
||||
|
||||
**Forward-testing and iteration workflow:**
|
||||
|
||||
1. Use the skill on real tasks
|
||||
2. Notice struggles or inefficiencies
|
||||
3. Identify how SKILL.md or bundled resources should be updated
|
||||
4. Implement changes and test again
|
||||
5. Forward-test if it is reasonable and appropriate
|
||||
|
||||
## Forward-testing
|
||||
|
||||
To forward-test, launch subagents as a way to stress test the skill with minimal context.
|
||||
Subagents should *not* know that they are being asked to test the skill. They should be treated as
|
||||
an agent asked to perform a task by the user. Prompts to subagents should look like:
|
||||
`Use $skill-x at /path/to/skill-x to solve problem y`
|
||||
Not:
|
||||
`Review the skill at /path/to/skill-x; pretend a user asks you to...`
|
||||
|
||||
Decision rule for forward-testing:
|
||||
- Err on the side of forward-testing
|
||||
- Ask for approval if you think there's a risk that forward-testing would:
|
||||
* take a long time,
|
||||
* require additional approvals from the user, or
|
||||
* modify live production systems
|
||||
|
||||
In these cases, show the user your proposed prompt and request (1) a yes/no decision, and
|
||||
(2) any suggested modifictions.
|
||||
|
||||
Considerations when forward-testing:
|
||||
- use fresh threads for independent passes
|
||||
- pass the skill, and a request in a similar way the user would.
|
||||
- pass raw artifacts, not your conclusions
|
||||
- avoid showing expected answers or intended fixes
|
||||
- rebuild context from source artifacts after each iteration
|
||||
- review the subagent's output and reasoning and emitted artifacts
|
||||
- avoid leaving artifacts the agent can find on disk between iterations;
|
||||
clean up subagents' artifacts to avoid additional contamination.
|
||||
|
||||
If forward-testing only succeeds when subagents see leaked context, tighten the skill or the
|
||||
forward-testing setup before trusting the result.
|
||||
5
.agents/skills/.system/skill-creator/agents/openai.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
interface:
|
||||
display_name: "Skill Creator"
|
||||
short_description: "Create or update a skill"
|
||||
icon_small: "./assets/skill-creator-small.svg"
|
||||
icon_large: "./assets/skill-creator.png"
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill="#0D0D0D" d="M12.03 4.113a3.612 3.612 0 0 1 5.108 5.108l-6.292 6.29c-.324.324-.56.561-.791.752l-.235.176c-.205.14-.422.261-.65.36l-.229.093a4.136 4.136 0 0 1-.586.16l-.764.134-2.394.4c-.142.024-.294.05-.423.06-.098.007-.232.01-.378-.026l-.149-.05a1.081 1.081 0 0 1-.521-.474l-.046-.093a1.104 1.104 0 0 1-.075-.527c.01-.129.035-.28.06-.422l.398-2.394c.1-.602.162-.987.295-1.35l.093-.23c.1-.228.22-.445.36-.65l.176-.235c.19-.232.428-.467.751-.79l6.292-6.292Zm-5.35 7.232c-.35.35-.534.535-.66.688l-.11.147a2.67 2.67 0 0 0-.24.433l-.062.154c-.08.22-.124.462-.232 1.112l-.398 2.394-.001.001h.003l2.393-.399.717-.126a2.63 2.63 0 0 0 .394-.105l.154-.063a2.65 2.65 0 0 0 .433-.24l.147-.11c.153-.126.339-.31.688-.66l4.988-4.988-3.227-3.226-4.987 4.988Zm9.517-6.291a2.281 2.281 0 0 0-3.225 0l-.364.362 3.226 3.227.363-.364c.89-.89.89-2.334 0-3.225ZM4.583 1.783a.3.3 0 0 1 .294.241c.117.585.347 1.092.707 1.48.357.385.859.668 1.549.783a.3.3 0 0 1 0 .592c-.69.115-1.192.398-1.549.783-.315.34-.53.77-.657 1.265l-.05.215a.3.3 0 0 1-.588 0c-.117-.585-.347-1.092-.707-1.48-.357-.384-.859-.668-1.549-.783a.3.3 0 0 1 0-.592c.69-.115 1.192-.398 1.549-.783.36-.388.59-.895.707-1.48l.015-.05a.3.3 0 0 1 .279-.19Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
BIN
.agents/skills/.system/skill-creator/assets/skill-creator.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
202
.agents/skills/.system/skill-creator/license.txt
Normal file
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,49 @@
|
||||
# openai.yaml fields (full example + descriptions)
|
||||
|
||||
`agents/openai.yaml` is an extended, product-specific config intended for the machine/harness to read, not the agent. Other product-specific config can also live in the `agents/` folder.
|
||||
|
||||
## Full example
|
||||
|
||||
```yaml
|
||||
interface:
|
||||
display_name: "Optional user-facing name"
|
||||
short_description: "Optional user-facing description"
|
||||
icon_small: "./assets/small-400px.png"
|
||||
icon_large: "./assets/large-logo.svg"
|
||||
brand_color: "#3B82F6"
|
||||
default_prompt: "Optional surrounding prompt to use the skill with"
|
||||
|
||||
dependencies:
|
||||
tools:
|
||||
- type: "mcp"
|
||||
value: "github"
|
||||
description: "GitHub MCP server"
|
||||
transport: "streamable_http"
|
||||
url: "https://api.githubcopilot.com/mcp/"
|
||||
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
```
|
||||
|
||||
## Field descriptions and constraints
|
||||
|
||||
Top-level constraints:
|
||||
|
||||
- Quote all string values.
|
||||
- Keep keys unquoted.
|
||||
- For `interface.default_prompt`: generate a helpful, short (typically 1 sentence) example starting prompt based on the skill. It must explicitly mention the skill as `$skill-name` (e.g., "Use $skill-name-here to draft a concise weekly status update.").
|
||||
|
||||
- `interface.display_name`: Human-facing title shown in UI skill lists and chips.
|
||||
- `interface.short_description`: Human-facing short UI blurb (25–64 chars) for quick scanning.
|
||||
- `interface.icon_small`: Path to a small icon asset (relative to skill dir). Default to `./assets/` and place icons in the skill's `assets/` folder.
|
||||
- `interface.icon_large`: Path to a larger logo asset (relative to skill dir). Default to `./assets/` and place icons in the skill's `assets/` folder.
|
||||
- `interface.brand_color`: Hex color used for UI accents (e.g., badges).
|
||||
- `interface.default_prompt`: Default prompt snippet inserted when invoking the skill.
|
||||
- `dependencies.tools[].type`: Dependency category. Only `mcp` is supported for now.
|
||||
- `dependencies.tools[].value`: Identifier of the tool or dependency.
|
||||
- `dependencies.tools[].description`: Human-readable explanation of the dependency.
|
||||
- `dependencies.tools[].transport`: Connection type when `type` is `mcp`.
|
||||
- `dependencies.tools[].url`: MCP server URL when `type` is `mcp`.
|
||||
- `policy.allow_implicit_invocation`: When false, the skill is not injected into
|
||||
the model context by default, but can still be invoked explicitly via `$skill`.
|
||||
Defaults to true.
|
||||
@@ -0,0 +1,226 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
OpenAI YAML Generator - Creates agents/openai.yaml for a skill folder.
|
||||
|
||||
Usage:
|
||||
generate_openai_yaml.py <skill_dir> [--name <skill_name>] [--interface key=value]
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ACRONYMS = {
|
||||
"GH",
|
||||
"MCP",
|
||||
"API",
|
||||
"CI",
|
||||
"CLI",
|
||||
"LLM",
|
||||
"PDF",
|
||||
"PR",
|
||||
"UI",
|
||||
"URL",
|
||||
"SQL",
|
||||
}
|
||||
|
||||
BRANDS = {
|
||||
"openai": "OpenAI",
|
||||
"openapi": "OpenAPI",
|
||||
"github": "GitHub",
|
||||
"pagerduty": "PagerDuty",
|
||||
"datadog": "DataDog",
|
||||
"sqlite": "SQLite",
|
||||
"fastapi": "FastAPI",
|
||||
}
|
||||
|
||||
SMALL_WORDS = {"and", "or", "to", "up", "with"}
|
||||
|
||||
ALLOWED_INTERFACE_KEYS = {
|
||||
"display_name",
|
||||
"short_description",
|
||||
"icon_small",
|
||||
"icon_large",
|
||||
"brand_color",
|
||||
"default_prompt",
|
||||
}
|
||||
|
||||
|
||||
def yaml_quote(value):
|
||||
escaped = value.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
|
||||
return f'"{escaped}"'
|
||||
|
||||
|
||||
def format_display_name(skill_name):
|
||||
words = [word for word in skill_name.split("-") if word]
|
||||
formatted = []
|
||||
for index, word in enumerate(words):
|
||||
lower = word.lower()
|
||||
upper = word.upper()
|
||||
if upper in ACRONYMS:
|
||||
formatted.append(upper)
|
||||
continue
|
||||
if lower in BRANDS:
|
||||
formatted.append(BRANDS[lower])
|
||||
continue
|
||||
if index > 0 and lower in SMALL_WORDS:
|
||||
formatted.append(lower)
|
||||
continue
|
||||
formatted.append(word.capitalize())
|
||||
return " ".join(formatted)
|
||||
|
||||
|
||||
def generate_short_description(display_name):
|
||||
description = f"Help with {display_name} tasks"
|
||||
|
||||
if len(description) < 25:
|
||||
description = f"Help with {display_name} tasks and workflows"
|
||||
if len(description) < 25:
|
||||
description = f"Help with {display_name} tasks with guidance"
|
||||
|
||||
if len(description) > 64:
|
||||
description = f"Help with {display_name}"
|
||||
if len(description) > 64:
|
||||
description = f"{display_name} helper"
|
||||
if len(description) > 64:
|
||||
description = f"{display_name} tools"
|
||||
if len(description) > 64:
|
||||
suffix = " helper"
|
||||
max_name_length = 64 - len(suffix)
|
||||
trimmed = display_name[:max_name_length].rstrip()
|
||||
description = f"{trimmed}{suffix}"
|
||||
if len(description) > 64:
|
||||
description = description[:64].rstrip()
|
||||
|
||||
if len(description) < 25:
|
||||
description = f"{description} workflows"
|
||||
if len(description) > 64:
|
||||
description = description[:64].rstrip()
|
||||
|
||||
return description
|
||||
|
||||
|
||||
def read_frontmatter_name(skill_dir):
|
||||
skill_md = Path(skill_dir) / "SKILL.md"
|
||||
if not skill_md.exists():
|
||||
print(f"[ERROR] SKILL.md not found in {skill_dir}")
|
||||
return None
|
||||
content = skill_md.read_text()
|
||||
match = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
|
||||
if not match:
|
||||
print("[ERROR] Invalid SKILL.md frontmatter format.")
|
||||
return None
|
||||
frontmatter_text = match.group(1)
|
||||
|
||||
import yaml
|
||||
|
||||
try:
|
||||
frontmatter = yaml.safe_load(frontmatter_text)
|
||||
except yaml.YAMLError as exc:
|
||||
print(f"[ERROR] Invalid YAML frontmatter: {exc}")
|
||||
return None
|
||||
if not isinstance(frontmatter, dict):
|
||||
print("[ERROR] Frontmatter must be a YAML dictionary.")
|
||||
return None
|
||||
name = frontmatter.get("name", "")
|
||||
if not isinstance(name, str) or not name.strip():
|
||||
print("[ERROR] Frontmatter 'name' is missing or invalid.")
|
||||
return None
|
||||
return name.strip()
|
||||
|
||||
|
||||
def parse_interface_overrides(raw_overrides):
|
||||
overrides = {}
|
||||
optional_order = []
|
||||
for item in raw_overrides:
|
||||
if "=" not in item:
|
||||
print(f"[ERROR] Invalid interface override '{item}'. Use key=value.")
|
||||
return None, None
|
||||
key, value = item.split("=", 1)
|
||||
key = key.strip()
|
||||
value = value.strip()
|
||||
if not key:
|
||||
print(f"[ERROR] Invalid interface override '{item}'. Key is empty.")
|
||||
return None, None
|
||||
if key not in ALLOWED_INTERFACE_KEYS:
|
||||
allowed = ", ".join(sorted(ALLOWED_INTERFACE_KEYS))
|
||||
print(f"[ERROR] Unknown interface field '{key}'. Allowed: {allowed}")
|
||||
return None, None
|
||||
overrides[key] = value
|
||||
if key not in ("display_name", "short_description") and key not in optional_order:
|
||||
optional_order.append(key)
|
||||
return overrides, optional_order
|
||||
|
||||
|
||||
def write_openai_yaml(skill_dir, skill_name, raw_overrides):
|
||||
overrides, optional_order = parse_interface_overrides(raw_overrides)
|
||||
if overrides is None:
|
||||
return None
|
||||
|
||||
display_name = overrides.get("display_name") or format_display_name(skill_name)
|
||||
short_description = overrides.get("short_description") or generate_short_description(display_name)
|
||||
|
||||
if not (25 <= len(short_description) <= 64):
|
||||
print(
|
||||
"[ERROR] short_description must be 25-64 characters "
|
||||
f"(got {len(short_description)})."
|
||||
)
|
||||
return None
|
||||
|
||||
interface_lines = [
|
||||
"interface:",
|
||||
f" display_name: {yaml_quote(display_name)}",
|
||||
f" short_description: {yaml_quote(short_description)}",
|
||||
]
|
||||
|
||||
for key in optional_order:
|
||||
value = overrides.get(key)
|
||||
if value is not None:
|
||||
interface_lines.append(f" {key}: {yaml_quote(value)}")
|
||||
|
||||
agents_dir = Path(skill_dir) / "agents"
|
||||
agents_dir.mkdir(parents=True, exist_ok=True)
|
||||
output_path = agents_dir / "openai.yaml"
|
||||
output_path.write_text("\n".join(interface_lines) + "\n")
|
||||
print(f"[OK] Created agents/openai.yaml")
|
||||
return output_path
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Create agents/openai.yaml for a skill directory.",
|
||||
)
|
||||
parser.add_argument("skill_dir", help="Path to the skill directory")
|
||||
parser.add_argument(
|
||||
"--name",
|
||||
help="Skill name override (defaults to SKILL.md frontmatter)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--interface",
|
||||
action="append",
|
||||
default=[],
|
||||
help="Interface override in key=value format (repeatable)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
skill_dir = Path(args.skill_dir).resolve()
|
||||
if not skill_dir.exists():
|
||||
print(f"[ERROR] Skill directory not found: {skill_dir}")
|
||||
sys.exit(1)
|
||||
if not skill_dir.is_dir():
|
||||
print(f"[ERROR] Path is not a directory: {skill_dir}")
|
||||
sys.exit(1)
|
||||
|
||||
skill_name = args.name or read_frontmatter_name(skill_dir)
|
||||
if not skill_name:
|
||||
sys.exit(1)
|
||||
|
||||
result = write_openai_yaml(skill_dir, skill_name, args.interface)
|
||||
if result:
|
||||
sys.exit(0)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
400
.agents/skills/.system/skill-creator/scripts/init_skill.py
Normal file
@@ -0,0 +1,400 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Skill Initializer - Creates a new skill from template
|
||||
|
||||
Usage:
|
||||
init_skill.py <skill-name> --path <path> [--resources scripts,references,assets] [--examples] [--interface key=value]
|
||||
|
||||
Examples:
|
||||
init_skill.py my-new-skill --path skills/public
|
||||
init_skill.py my-new-skill --path skills/public --resources scripts,references
|
||||
init_skill.py my-api-helper --path skills/private --resources scripts --examples
|
||||
init_skill.py custom-skill --path /custom/location
|
||||
init_skill.py my-skill --path skills/public --interface short_description="Short UI label"
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from generate_openai_yaml import write_openai_yaml
|
||||
|
||||
MAX_SKILL_NAME_LENGTH = 64
|
||||
ALLOWED_RESOURCES = {"scripts", "references", "assets"}
|
||||
|
||||
SKILL_TEMPLATE = """---
|
||||
name: {skill_name}
|
||||
description: [TODO: Complete and informative explanation of what the skill does and when to use it. Include WHEN to use this skill - specific scenarios, file types, or tasks that trigger it.]
|
||||
---
|
||||
|
||||
# {skill_title}
|
||||
|
||||
## Overview
|
||||
|
||||
[TODO: 1-2 sentences explaining what this skill enables]
|
||||
|
||||
## Structuring This Skill
|
||||
|
||||
[TODO: Choose the structure that best fits this skill's purpose. Common patterns:
|
||||
|
||||
**1. Workflow-Based** (best for sequential processes)
|
||||
- Works well when there are clear step-by-step procedures
|
||||
- Example: DOCX skill with "Workflow Decision Tree" -> "Reading" -> "Creating" -> "Editing"
|
||||
- Structure: ## Overview -> ## Workflow Decision Tree -> ## Step 1 -> ## Step 2...
|
||||
|
||||
**2. Task-Based** (best for tool collections)
|
||||
- Works well when the skill offers different operations/capabilities
|
||||
- Example: PDF skill with "Quick Start" -> "Merge PDFs" -> "Split PDFs" -> "Extract Text"
|
||||
- Structure: ## Overview -> ## Quick Start -> ## Task Category 1 -> ## Task Category 2...
|
||||
|
||||
**3. Reference/Guidelines** (best for standards or specifications)
|
||||
- Works well for brand guidelines, coding standards, or requirements
|
||||
- Example: Brand styling with "Brand Guidelines" -> "Colors" -> "Typography" -> "Features"
|
||||
- Structure: ## Overview -> ## Guidelines -> ## Specifications -> ## Usage...
|
||||
|
||||
**4. Capabilities-Based** (best for integrated systems)
|
||||
- Works well when the skill provides multiple interrelated features
|
||||
- Example: Product Management with "Core Capabilities" -> numbered capability list
|
||||
- Structure: ## Overview -> ## Core Capabilities -> ### 1. Feature -> ### 2. Feature...
|
||||
|
||||
Patterns can be mixed and matched as needed. Most skills combine patterns (e.g., start with task-based, add workflow for complex operations).
|
||||
|
||||
Delete this entire "Structuring This Skill" section when done - it's just guidance.]
|
||||
|
||||
## [TODO: Replace with the first main section based on chosen structure]
|
||||
|
||||
[TODO: Add content here. See examples in existing skills:
|
||||
- Code samples for technical skills
|
||||
- Decision trees for complex workflows
|
||||
- Concrete examples with realistic user requests
|
||||
- References to scripts/templates/references as needed]
|
||||
|
||||
## Resources (optional)
|
||||
|
||||
Create only the resource directories this skill actually needs. Delete this section if no resources are required.
|
||||
|
||||
### scripts/
|
||||
Executable code (Python/Bash/etc.) that can be run directly to perform specific operations.
|
||||
|
||||
**Examples from other skills:**
|
||||
- PDF skill: `fill_fillable_fields.py`, `extract_form_field_info.py` - utilities for PDF manipulation
|
||||
- DOCX skill: `document.py`, `utilities.py` - Python modules for document processing
|
||||
|
||||
**Appropriate for:** Python scripts, shell scripts, or any executable code that performs automation, data processing, or specific operations.
|
||||
|
||||
**Note:** Scripts may be executed without loading into context, but can still be read by Codex for patching or environment adjustments.
|
||||
|
||||
### references/
|
||||
Documentation and reference material intended to be loaded into context to inform Codex's process and thinking.
|
||||
|
||||
**Examples from other skills:**
|
||||
- Product management: `communication.md`, `context_building.md` - detailed workflow guides
|
||||
- BigQuery: API reference documentation and query examples
|
||||
- Finance: Schema documentation, company policies
|
||||
|
||||
**Appropriate for:** In-depth documentation, API references, database schemas, comprehensive guides, or any detailed information that Codex should reference while working.
|
||||
|
||||
### assets/
|
||||
Files not intended to be loaded into context, but rather used within the output Codex produces.
|
||||
|
||||
**Examples from other skills:**
|
||||
- Brand styling: PowerPoint template files (.pptx), logo files
|
||||
- Frontend builder: HTML/React boilerplate project directories
|
||||
- Typography: Font files (.ttf, .woff2)
|
||||
|
||||
**Appropriate for:** Templates, boilerplate code, document templates, images, icons, fonts, or any files meant to be copied or used in the final output.
|
||||
|
||||
---
|
||||
|
||||
**Not every skill requires all three types of resources.**
|
||||
"""
|
||||
|
||||
EXAMPLE_SCRIPT = '''#!/usr/bin/env python3
|
||||
"""
|
||||
Example helper script for {skill_name}
|
||||
|
||||
This is a placeholder script that can be executed directly.
|
||||
Replace with actual implementation or delete if not needed.
|
||||
|
||||
Example real scripts from other skills:
|
||||
- pdf/scripts/fill_fillable_fields.py - Fills PDF form fields
|
||||
- pdf/scripts/convert_pdf_to_images.py - Converts PDF pages to images
|
||||
"""
|
||||
|
||||
def main():
|
||||
print("This is an example script for {skill_name}")
|
||||
# TODO: Add actual script logic here
|
||||
# This could be data processing, file conversion, API calls, etc.
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
'''
|
||||
|
||||
EXAMPLE_REFERENCE = """# Reference Documentation for {skill_title}
|
||||
|
||||
This is a placeholder for detailed reference documentation.
|
||||
Replace with actual reference content or delete if not needed.
|
||||
|
||||
Example real reference docs from other skills:
|
||||
- product-management/references/communication.md - Comprehensive guide for status updates
|
||||
- product-management/references/context_building.md - Deep-dive on gathering context
|
||||
- bigquery/references/ - API references and query examples
|
||||
|
||||
## When Reference Docs Are Useful
|
||||
|
||||
Reference docs are ideal for:
|
||||
- Comprehensive API documentation
|
||||
- Detailed workflow guides
|
||||
- Complex multi-step processes
|
||||
- Information too lengthy for main SKILL.md
|
||||
- Content that's only needed for specific use cases
|
||||
|
||||
## Structure Suggestions
|
||||
|
||||
### API Reference Example
|
||||
- Overview
|
||||
- Authentication
|
||||
- Endpoints with examples
|
||||
- Error codes
|
||||
- Rate limits
|
||||
|
||||
### Workflow Guide Example
|
||||
- Prerequisites
|
||||
- Step-by-step instructions
|
||||
- Common patterns
|
||||
- Troubleshooting
|
||||
- Best practices
|
||||
"""
|
||||
|
||||
EXAMPLE_ASSET = """# Example Asset File
|
||||
|
||||
This placeholder represents where asset files would be stored.
|
||||
Replace with actual asset files (templates, images, fonts, etc.) or delete if not needed.
|
||||
|
||||
Asset files are NOT intended to be loaded into context, but rather used within
|
||||
the output Codex produces.
|
||||
|
||||
Example asset files from other skills:
|
||||
- Brand guidelines: logo.png, slides_template.pptx
|
||||
- Frontend builder: hello-world/ directory with HTML/React boilerplate
|
||||
- Typography: custom-font.ttf, font-family.woff2
|
||||
- Data: sample_data.csv, test_dataset.json
|
||||
|
||||
## Common Asset Types
|
||||
|
||||
- Templates: .pptx, .docx, boilerplate directories
|
||||
- Images: .png, .jpg, .svg, .gif
|
||||
- Fonts: .ttf, .otf, .woff, .woff2
|
||||
- Boilerplate code: Project directories, starter files
|
||||
- Icons: .ico, .svg
|
||||
- Data files: .csv, .json, .xml, .yaml
|
||||
|
||||
Note: This is a text placeholder. Actual assets can be any file type.
|
||||
"""
|
||||
|
||||
|
||||
def normalize_skill_name(skill_name):
|
||||
"""Normalize a skill name to lowercase hyphen-case."""
|
||||
normalized = skill_name.strip().lower()
|
||||
normalized = re.sub(r"[^a-z0-9]+", "-", normalized)
|
||||
normalized = normalized.strip("-")
|
||||
normalized = re.sub(r"-{2,}", "-", normalized)
|
||||
return normalized
|
||||
|
||||
|
||||
def title_case_skill_name(skill_name):
|
||||
"""Convert hyphenated skill name to Title Case for display."""
|
||||
return " ".join(word.capitalize() for word in skill_name.split("-"))
|
||||
|
||||
|
||||
def parse_resources(raw_resources):
|
||||
if not raw_resources:
|
||||
return []
|
||||
resources = [item.strip() for item in raw_resources.split(",") if item.strip()]
|
||||
invalid = sorted({item for item in resources if item not in ALLOWED_RESOURCES})
|
||||
if invalid:
|
||||
allowed = ", ".join(sorted(ALLOWED_RESOURCES))
|
||||
print(f"[ERROR] Unknown resource type(s): {', '.join(invalid)}")
|
||||
print(f" Allowed: {allowed}")
|
||||
sys.exit(1)
|
||||
deduped = []
|
||||
seen = set()
|
||||
for resource in resources:
|
||||
if resource not in seen:
|
||||
deduped.append(resource)
|
||||
seen.add(resource)
|
||||
return deduped
|
||||
|
||||
|
||||
def create_resource_dirs(skill_dir, skill_name, skill_title, resources, include_examples):
|
||||
for resource in resources:
|
||||
resource_dir = skill_dir / resource
|
||||
resource_dir.mkdir(exist_ok=True)
|
||||
if resource == "scripts":
|
||||
if include_examples:
|
||||
example_script = resource_dir / "example.py"
|
||||
example_script.write_text(EXAMPLE_SCRIPT.format(skill_name=skill_name))
|
||||
example_script.chmod(0o755)
|
||||
print("[OK] Created scripts/example.py")
|
||||
else:
|
||||
print("[OK] Created scripts/")
|
||||
elif resource == "references":
|
||||
if include_examples:
|
||||
example_reference = resource_dir / "api_reference.md"
|
||||
example_reference.write_text(EXAMPLE_REFERENCE.format(skill_title=skill_title))
|
||||
print("[OK] Created references/api_reference.md")
|
||||
else:
|
||||
print("[OK] Created references/")
|
||||
elif resource == "assets":
|
||||
if include_examples:
|
||||
example_asset = resource_dir / "example_asset.txt"
|
||||
example_asset.write_text(EXAMPLE_ASSET)
|
||||
print("[OK] Created assets/example_asset.txt")
|
||||
else:
|
||||
print("[OK] Created assets/")
|
||||
|
||||
|
||||
def init_skill(skill_name, path, resources, include_examples, interface_overrides):
|
||||
"""
|
||||
Initialize a new skill directory with template SKILL.md.
|
||||
|
||||
Args:
|
||||
skill_name: Name of the skill
|
||||
path: Path where the skill directory should be created
|
||||
resources: Resource directories to create
|
||||
include_examples: Whether to create example files in resource directories
|
||||
|
||||
Returns:
|
||||
Path to created skill directory, or None if error
|
||||
"""
|
||||
# Determine skill directory path
|
||||
skill_dir = Path(path).resolve() / skill_name
|
||||
|
||||
# Check if directory already exists
|
||||
if skill_dir.exists():
|
||||
print(f"[ERROR] Skill directory already exists: {skill_dir}")
|
||||
return None
|
||||
|
||||
# Create skill directory
|
||||
try:
|
||||
skill_dir.mkdir(parents=True, exist_ok=False)
|
||||
print(f"[OK] Created skill directory: {skill_dir}")
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Error creating directory: {e}")
|
||||
return None
|
||||
|
||||
# Create SKILL.md from template
|
||||
skill_title = title_case_skill_name(skill_name)
|
||||
skill_content = SKILL_TEMPLATE.format(skill_name=skill_name, skill_title=skill_title)
|
||||
|
||||
skill_md_path = skill_dir / "SKILL.md"
|
||||
try:
|
||||
skill_md_path.write_text(skill_content)
|
||||
print("[OK] Created SKILL.md")
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Error creating SKILL.md: {e}")
|
||||
return None
|
||||
|
||||
# Create agents/openai.yaml
|
||||
try:
|
||||
result = write_openai_yaml(skill_dir, skill_name, interface_overrides)
|
||||
if not result:
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Error creating agents/openai.yaml: {e}")
|
||||
return None
|
||||
|
||||
# Create resource directories if requested
|
||||
if resources:
|
||||
try:
|
||||
create_resource_dirs(skill_dir, skill_name, skill_title, resources, include_examples)
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Error creating resource directories: {e}")
|
||||
return None
|
||||
|
||||
# Print next steps
|
||||
print(f"\n[OK] Skill '{skill_name}' initialized successfully at {skill_dir}")
|
||||
print("\nNext steps:")
|
||||
print("1. Edit SKILL.md to complete the TODO items and update the description")
|
||||
if resources:
|
||||
if include_examples:
|
||||
print("2. Customize or delete the example files in scripts/, references/, and assets/")
|
||||
else:
|
||||
print("2. Add resources to scripts/, references/, and assets/ as needed")
|
||||
else:
|
||||
print("2. Create resource directories only if needed (scripts/, references/, assets/)")
|
||||
print("3. Update agents/openai.yaml if the UI metadata should differ")
|
||||
print("4. Run the validator when ready to check the skill structure")
|
||||
print(
|
||||
"5. Forward-test complex skills with realistic user requests to ensure they work as intended"
|
||||
)
|
||||
|
||||
return skill_dir
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Create a new skill directory with a SKILL.md template.",
|
||||
)
|
||||
parser.add_argument("skill_name", help="Skill name (normalized to hyphen-case)")
|
||||
parser.add_argument("--path", required=True, help="Output directory for the skill")
|
||||
parser.add_argument(
|
||||
"--resources",
|
||||
default="",
|
||||
help="Comma-separated list: scripts,references,assets",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--examples",
|
||||
action="store_true",
|
||||
help="Create example files inside the selected resource directories",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--interface",
|
||||
action="append",
|
||||
default=[],
|
||||
help="Interface override in key=value format (repeatable)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
raw_skill_name = args.skill_name
|
||||
skill_name = normalize_skill_name(raw_skill_name)
|
||||
if not skill_name:
|
||||
print("[ERROR] Skill name must include at least one letter or digit.")
|
||||
sys.exit(1)
|
||||
if len(skill_name) > MAX_SKILL_NAME_LENGTH:
|
||||
print(
|
||||
f"[ERROR] Skill name '{skill_name}' is too long ({len(skill_name)} characters). "
|
||||
f"Maximum is {MAX_SKILL_NAME_LENGTH} characters."
|
||||
)
|
||||
sys.exit(1)
|
||||
if skill_name != raw_skill_name:
|
||||
print(f"Note: Normalized skill name from '{raw_skill_name}' to '{skill_name}'.")
|
||||
|
||||
resources = parse_resources(args.resources)
|
||||
if args.examples and not resources:
|
||||
print("[ERROR] --examples requires --resources to be set.")
|
||||
sys.exit(1)
|
||||
|
||||
path = args.path
|
||||
|
||||
print(f"Initializing skill: {skill_name}")
|
||||
print(f" Location: {path}")
|
||||
if resources:
|
||||
print(f" Resources: {', '.join(resources)}")
|
||||
if args.examples:
|
||||
print(" Examples: enabled")
|
||||
else:
|
||||
print(" Resources: none (create as needed)")
|
||||
print()
|
||||
|
||||
result = init_skill(skill_name, path, resources, args.examples, args.interface)
|
||||
|
||||
if result:
|
||||
sys.exit(0)
|
||||
else:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
101
.agents/skills/.system/skill-creator/scripts/quick_validate.py
Normal file
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Quick validation script for skills - minimal version
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
MAX_SKILL_NAME_LENGTH = 64
|
||||
|
||||
|
||||
def validate_skill(skill_path):
|
||||
"""Basic validation of a skill"""
|
||||
skill_path = Path(skill_path)
|
||||
|
||||
skill_md = skill_path / "SKILL.md"
|
||||
if not skill_md.exists():
|
||||
return False, "SKILL.md not found"
|
||||
|
||||
content = skill_md.read_text()
|
||||
if not content.startswith("---"):
|
||||
return False, "No YAML frontmatter found"
|
||||
|
||||
match = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
|
||||
if not match:
|
||||
return False, "Invalid frontmatter format"
|
||||
|
||||
frontmatter_text = match.group(1)
|
||||
|
||||
try:
|
||||
frontmatter = yaml.safe_load(frontmatter_text)
|
||||
if not isinstance(frontmatter, dict):
|
||||
return False, "Frontmatter must be a YAML dictionary"
|
||||
except yaml.YAMLError as e:
|
||||
return False, f"Invalid YAML in frontmatter: {e}"
|
||||
|
||||
allowed_properties = {"name", "description", "license", "allowed-tools", "metadata"}
|
||||
|
||||
unexpected_keys = set(frontmatter.keys()) - allowed_properties
|
||||
if unexpected_keys:
|
||||
allowed = ", ".join(sorted(allowed_properties))
|
||||
unexpected = ", ".join(sorted(unexpected_keys))
|
||||
return (
|
||||
False,
|
||||
f"Unexpected key(s) in SKILL.md frontmatter: {unexpected}. Allowed properties are: {allowed}",
|
||||
)
|
||||
|
||||
if "name" not in frontmatter:
|
||||
return False, "Missing 'name' in frontmatter"
|
||||
if "description" not in frontmatter:
|
||||
return False, "Missing 'description' in frontmatter"
|
||||
|
||||
name = frontmatter.get("name", "")
|
||||
if not isinstance(name, str):
|
||||
return False, f"Name must be a string, got {type(name).__name__}"
|
||||
name = name.strip()
|
||||
if name:
|
||||
if not re.match(r"^[a-z0-9-]+$", name):
|
||||
return (
|
||||
False,
|
||||
f"Name '{name}' should be hyphen-case (lowercase letters, digits, and hyphens only)",
|
||||
)
|
||||
if name.startswith("-") or name.endswith("-") or "--" in name:
|
||||
return (
|
||||
False,
|
||||
f"Name '{name}' cannot start/end with hyphen or contain consecutive hyphens",
|
||||
)
|
||||
if len(name) > MAX_SKILL_NAME_LENGTH:
|
||||
return (
|
||||
False,
|
||||
f"Name is too long ({len(name)} characters). "
|
||||
f"Maximum is {MAX_SKILL_NAME_LENGTH} characters.",
|
||||
)
|
||||
|
||||
description = frontmatter.get("description", "")
|
||||
if not isinstance(description, str):
|
||||
return False, f"Description must be a string, got {type(description).__name__}"
|
||||
description = description.strip()
|
||||
if description:
|
||||
if "<" in description or ">" in description:
|
||||
return False, "Description cannot contain angle brackets (< or >)"
|
||||
if len(description) > 1024:
|
||||
return (
|
||||
False,
|
||||
f"Description is too long ({len(description)} characters). Maximum is 1024 characters.",
|
||||
)
|
||||
|
||||
return True, "Skill is valid!"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 2:
|
||||
print("Usage: python quick_validate.py <skill_directory>")
|
||||
sys.exit(1)
|
||||
|
||||
valid, message = validate_skill(sys.argv[1])
|
||||
print(message)
|
||||
sys.exit(0 if valid else 1)
|
||||
202
.agents/skills/.system/skill-installer/LICENSE.txt
Normal file
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
58
.agents/skills/.system/skill-installer/SKILL.md
Normal file
@@ -0,0 +1,58 @@
|
||||
---
|
||||
name: skill-installer
|
||||
description: Install Codex skills into $CODEX_HOME/skills from a curated list or a GitHub repo path. Use when a user asks to list installable skills, install a curated skill, or install a skill from another repo (including private repos).
|
||||
metadata:
|
||||
short-description: Install curated skills from openai/skills or other repos
|
||||
---
|
||||
|
||||
# Skill Installer
|
||||
|
||||
Helps install skills. By default these are from https://github.com/openai/skills/tree/main/skills/.curated, but users can also provide other locations. Experimental skills live in https://github.com/openai/skills/tree/main/skills/.experimental and can be installed the same way.
|
||||
|
||||
Use the helper scripts based on the task:
|
||||
- List skills when the user asks what is available, or if the user uses this skill without specifying what to do. Default listing is `.curated`, but you can pass `--path skills/.experimental` when they ask about experimental skills.
|
||||
- Install from the curated list when the user provides a skill name.
|
||||
- Install from another repo when the user provides a GitHub repo/path (including private repos).
|
||||
|
||||
Install skills with the helper scripts.
|
||||
|
||||
## Communication
|
||||
|
||||
When listing skills, output approximately as follows, depending on the context of the user's request. If they ask about experimental skills, list from `.experimental` instead of `.curated` and label the source accordingly:
|
||||
"""
|
||||
Skills from {repo}:
|
||||
1. skill-1
|
||||
2. skill-2 (already installed)
|
||||
3. ...
|
||||
Which ones would you like installed?
|
||||
"""
|
||||
|
||||
After installing a skill, tell the user: "Restart Codex to pick up new skills."
|
||||
|
||||
## Scripts
|
||||
|
||||
All of these scripts use network, so when running in the sandbox, request escalation when running them.
|
||||
|
||||
- `scripts/list-skills.py` (prints skills list with installed annotations)
|
||||
- `scripts/list-skills.py --format json`
|
||||
- Example (experimental list): `scripts/list-skills.py --path skills/.experimental`
|
||||
- `scripts/install-skill-from-github.py --repo <owner>/<repo> --path <path/to/skill> [<path/to/skill> ...]`
|
||||
- `scripts/install-skill-from-github.py --url https://github.com/<owner>/<repo>/tree/<ref>/<path>`
|
||||
- Example (experimental skill): `scripts/install-skill-from-github.py --repo openai/skills --path skills/.experimental/<skill-name>`
|
||||
|
||||
## Behavior and Options
|
||||
|
||||
- Defaults to direct download for public GitHub repos.
|
||||
- If download fails with auth/permission errors, falls back to git sparse checkout.
|
||||
- Aborts if the destination skill directory already exists.
|
||||
- Installs into `$CODEX_HOME/skills/<skill-name>` (defaults to `~/.codex/skills`).
|
||||
- Multiple `--path` values install multiple skills in one run, each named from the path basename unless `--name` is supplied.
|
||||
- Options: `--ref <ref>` (default `main`), `--dest <path>`, `--method auto|download|git`.
|
||||
|
||||
## Notes
|
||||
|
||||
- Curated listing is fetched from `https://github.com/openai/skills/tree/main/skills/.curated` via the GitHub API. If it is unavailable, explain the error and exit.
|
||||
- Private GitHub repos can be accessed via existing git credentials or optional `GITHUB_TOKEN`/`GH_TOKEN` for download.
|
||||
- Git fallback tries HTTPS first, then SSH.
|
||||
- The skills at https://github.com/openai/skills/tree/main/skills/.system are preinstalled, so no need to help users install those. If they ask, just explain this. If they insist, you can download and overwrite.
|
||||
- Installed annotations come from `$CODEX_HOME/skills`.
|
||||
@@ -0,0 +1,5 @@
|
||||
interface:
|
||||
display_name: "Skill Installer"
|
||||
short_description: "Install curated skills from openai/skills or other repos"
|
||||
icon_small: "./assets/skill-installer-small.svg"
|
||||
icon_large: "./assets/skill-installer.png"
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path fill="#0D0D0D" d="M2.145 3.959a2.033 2.033 0 0 1 2.022-1.824h5.966c.551 0 .997 0 1.357.029.367.03.692.093.993.246l.174.098c.397.243.72.593.932 1.01l.053.114c.116.269.168.557.194.878.03.36.03.805.03 1.357v4.3a2.365 2.365 0 0 1-2.366 2.365h-1.312a2.198 2.198 0 0 1-4.377 0H4.167A2.032 2.032 0 0 1 2.135 10.5V9.333l.004-.088A.865.865 0 0 1 3 8.468l.116-.006A1.135 1.135 0 0 0 3 6.199a.865.865 0 0 1-.865-.864V4.167l.01-.208Zm1.054 1.186a2.198 2.198 0 0 1 0 4.376v.98c0 .534.433.967.968.967H6l.089.004a.866.866 0 0 1 .776.861 1.135 1.135 0 0 0 2.27 0c0-.478.387-.865.865-.865h1.5c.719 0 1.301-.583 1.301-1.301v-4.3c0-.57 0-.964-.025-1.27a1.933 1.933 0 0 0-.09-.493L12.642 4a1.47 1.47 0 0 0-.541-.585l-.102-.056c-.126-.065-.295-.11-.596-.135a17.31 17.31 0 0 0-1.27-.025H4.167a.968.968 0 0 0-.968.968v.978Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 923 B |
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Shared GitHub helpers for skill install scripts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import urllib.request
|
||||
|
||||
|
||||
def github_request(url: str, user_agent: str) -> bytes:
|
||||
headers = {"User-Agent": user_agent}
|
||||
token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
|
||||
if token:
|
||||
headers["Authorization"] = f"token {token}"
|
||||
req = urllib.request.Request(url, headers=headers)
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
return resp.read()
|
||||
|
||||
|
||||
def github_api_contents_url(repo: str, path: str, ref: str) -> str:
|
||||
return f"https://api.github.com/repos/{repo}/contents/{path}?ref={ref}"
|
||||
@@ -0,0 +1,308 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Install a skill from a GitHub repo path into $CODEX_HOME/skills."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from dataclasses import dataclass
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import zipfile
|
||||
|
||||
from github_utils import github_request
|
||||
DEFAULT_REF = "main"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Args:
|
||||
url: str | None = None
|
||||
repo: str | None = None
|
||||
path: list[str] | None = None
|
||||
ref: str = DEFAULT_REF
|
||||
dest: str | None = None
|
||||
name: str | None = None
|
||||
method: str = "auto"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Source:
|
||||
owner: str
|
||||
repo: str
|
||||
ref: str
|
||||
paths: list[str]
|
||||
repo_url: str | None = None
|
||||
|
||||
|
||||
class InstallError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def _codex_home() -> str:
|
||||
return os.environ.get("CODEX_HOME", os.path.expanduser("~/.codex"))
|
||||
|
||||
|
||||
def _tmp_root() -> str:
|
||||
base = os.path.join(tempfile.gettempdir(), "codex")
|
||||
os.makedirs(base, exist_ok=True)
|
||||
return base
|
||||
|
||||
|
||||
def _request(url: str) -> bytes:
|
||||
return github_request(url, "codex-skill-install")
|
||||
|
||||
|
||||
def _parse_github_url(url: str, default_ref: str) -> tuple[str, str, str, str | None]:
|
||||
parsed = urllib.parse.urlparse(url)
|
||||
if parsed.netloc != "github.com":
|
||||
raise InstallError("Only GitHub URLs are supported for download mode.")
|
||||
parts = [p for p in parsed.path.split("/") if p]
|
||||
if len(parts) < 2:
|
||||
raise InstallError("Invalid GitHub URL.")
|
||||
owner, repo = parts[0], parts[1]
|
||||
ref = default_ref
|
||||
subpath = ""
|
||||
if len(parts) > 2:
|
||||
if parts[2] in ("tree", "blob"):
|
||||
if len(parts) < 4:
|
||||
raise InstallError("GitHub URL missing ref or path.")
|
||||
ref = parts[3]
|
||||
subpath = "/".join(parts[4:])
|
||||
else:
|
||||
subpath = "/".join(parts[2:])
|
||||
return owner, repo, ref, subpath or None
|
||||
|
||||
|
||||
def _download_repo_zip(owner: str, repo: str, ref: str, dest_dir: str) -> str:
|
||||
zip_url = f"https://codeload.github.com/{owner}/{repo}/zip/{ref}"
|
||||
zip_path = os.path.join(dest_dir, "repo.zip")
|
||||
try:
|
||||
payload = _request(zip_url)
|
||||
except urllib.error.HTTPError as exc:
|
||||
raise InstallError(f"Download failed: HTTP {exc.code}") from exc
|
||||
with open(zip_path, "wb") as file_handle:
|
||||
file_handle.write(payload)
|
||||
with zipfile.ZipFile(zip_path, "r") as zip_file:
|
||||
_safe_extract_zip(zip_file, dest_dir)
|
||||
top_levels = {name.split("/")[0] for name in zip_file.namelist() if name}
|
||||
if not top_levels:
|
||||
raise InstallError("Downloaded archive was empty.")
|
||||
if len(top_levels) != 1:
|
||||
raise InstallError("Unexpected archive layout.")
|
||||
return os.path.join(dest_dir, next(iter(top_levels)))
|
||||
|
||||
|
||||
def _run_git(args: list[str]) -> None:
|
||||
result = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||
if result.returncode != 0:
|
||||
raise InstallError(result.stderr.strip() or "Git command failed.")
|
||||
|
||||
|
||||
def _safe_extract_zip(zip_file: zipfile.ZipFile, dest_dir: str) -> None:
|
||||
dest_root = os.path.realpath(dest_dir)
|
||||
for info in zip_file.infolist():
|
||||
extracted_path = os.path.realpath(os.path.join(dest_dir, info.filename))
|
||||
if extracted_path == dest_root or extracted_path.startswith(dest_root + os.sep):
|
||||
continue
|
||||
raise InstallError("Archive contains files outside the destination.")
|
||||
zip_file.extractall(dest_dir)
|
||||
|
||||
|
||||
def _validate_relative_path(path: str) -> None:
|
||||
if os.path.isabs(path) or os.path.normpath(path).startswith(".."):
|
||||
raise InstallError("Skill path must be a relative path inside the repo.")
|
||||
|
||||
|
||||
def _validate_skill_name(name: str) -> None:
|
||||
altsep = os.path.altsep
|
||||
if not name or os.path.sep in name or (altsep and altsep in name):
|
||||
raise InstallError("Skill name must be a single path segment.")
|
||||
if name in (".", ".."):
|
||||
raise InstallError("Invalid skill name.")
|
||||
|
||||
|
||||
def _git_sparse_checkout(repo_url: str, ref: str, paths: list[str], dest_dir: str) -> str:
|
||||
repo_dir = os.path.join(dest_dir, "repo")
|
||||
clone_cmd = [
|
||||
"git",
|
||||
"clone",
|
||||
"--filter=blob:none",
|
||||
"--depth",
|
||||
"1",
|
||||
"--sparse",
|
||||
"--single-branch",
|
||||
"--branch",
|
||||
ref,
|
||||
repo_url,
|
||||
repo_dir,
|
||||
]
|
||||
try:
|
||||
_run_git(clone_cmd)
|
||||
except InstallError:
|
||||
_run_git(
|
||||
[
|
||||
"git",
|
||||
"clone",
|
||||
"--filter=blob:none",
|
||||
"--depth",
|
||||
"1",
|
||||
"--sparse",
|
||||
"--single-branch",
|
||||
repo_url,
|
||||
repo_dir,
|
||||
]
|
||||
)
|
||||
_run_git(["git", "-C", repo_dir, "sparse-checkout", "set", *paths])
|
||||
_run_git(["git", "-C", repo_dir, "checkout", ref])
|
||||
return repo_dir
|
||||
|
||||
|
||||
def _validate_skill(path: str) -> None:
|
||||
if not os.path.isdir(path):
|
||||
raise InstallError(f"Skill path not found: {path}")
|
||||
skill_md = os.path.join(path, "SKILL.md")
|
||||
if not os.path.isfile(skill_md):
|
||||
raise InstallError("SKILL.md not found in selected skill directory.")
|
||||
|
||||
|
||||
def _copy_skill(src: str, dest_dir: str) -> None:
|
||||
os.makedirs(os.path.dirname(dest_dir), exist_ok=True)
|
||||
if os.path.exists(dest_dir):
|
||||
raise InstallError(f"Destination already exists: {dest_dir}")
|
||||
shutil.copytree(src, dest_dir)
|
||||
|
||||
|
||||
def _build_repo_url(owner: str, repo: str) -> str:
|
||||
return f"https://github.com/{owner}/{repo}.git"
|
||||
|
||||
|
||||
def _build_repo_ssh(owner: str, repo: str) -> str:
|
||||
return f"git@github.com:{owner}/{repo}.git"
|
||||
|
||||
|
||||
def _prepare_repo(source: Source, method: str, tmp_dir: str) -> str:
|
||||
if method in ("download", "auto"):
|
||||
try:
|
||||
return _download_repo_zip(source.owner, source.repo, source.ref, tmp_dir)
|
||||
except InstallError as exc:
|
||||
if method == "download":
|
||||
raise
|
||||
err_msg = str(exc)
|
||||
if "HTTP 401" in err_msg or "HTTP 403" in err_msg or "HTTP 404" in err_msg:
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
if method in ("git", "auto"):
|
||||
repo_url = source.repo_url or _build_repo_url(source.owner, source.repo)
|
||||
try:
|
||||
return _git_sparse_checkout(repo_url, source.ref, source.paths, tmp_dir)
|
||||
except InstallError:
|
||||
repo_url = _build_repo_ssh(source.owner, source.repo)
|
||||
return _git_sparse_checkout(repo_url, source.ref, source.paths, tmp_dir)
|
||||
raise InstallError("Unsupported method.")
|
||||
|
||||
|
||||
def _resolve_source(args: Args) -> Source:
|
||||
if args.url:
|
||||
owner, repo, ref, url_path = _parse_github_url(args.url, args.ref)
|
||||
if args.path is not None:
|
||||
paths = list(args.path)
|
||||
elif url_path:
|
||||
paths = [url_path]
|
||||
else:
|
||||
paths = []
|
||||
if not paths:
|
||||
raise InstallError("Missing --path for GitHub URL.")
|
||||
return Source(owner=owner, repo=repo, ref=ref, paths=paths)
|
||||
|
||||
if not args.repo:
|
||||
raise InstallError("Provide --repo or --url.")
|
||||
if "://" in args.repo:
|
||||
return _resolve_source(
|
||||
Args(url=args.repo, repo=None, path=args.path, ref=args.ref)
|
||||
)
|
||||
|
||||
repo_parts = [p for p in args.repo.split("/") if p]
|
||||
if len(repo_parts) != 2:
|
||||
raise InstallError("--repo must be in owner/repo format.")
|
||||
if not args.path:
|
||||
raise InstallError("Missing --path for --repo.")
|
||||
paths = list(args.path)
|
||||
return Source(
|
||||
owner=repo_parts[0],
|
||||
repo=repo_parts[1],
|
||||
ref=args.ref,
|
||||
paths=paths,
|
||||
)
|
||||
|
||||
|
||||
def _default_dest() -> str:
|
||||
return os.path.join(_codex_home(), "skills")
|
||||
|
||||
|
||||
def _parse_args(argv: list[str]) -> Args:
|
||||
parser = argparse.ArgumentParser(description="Install a skill from GitHub.")
|
||||
parser.add_argument("--repo", help="owner/repo")
|
||||
parser.add_argument("--url", help="https://github.com/owner/repo[/tree/ref/path]")
|
||||
parser.add_argument(
|
||||
"--path",
|
||||
nargs="+",
|
||||
help="Path(s) to skill(s) inside repo",
|
||||
)
|
||||
parser.add_argument("--ref", default=DEFAULT_REF)
|
||||
parser.add_argument("--dest", help="Destination skills directory")
|
||||
parser.add_argument(
|
||||
"--name", help="Destination skill name (defaults to basename of path)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--method",
|
||||
choices=["auto", "download", "git"],
|
||||
default="auto",
|
||||
)
|
||||
return parser.parse_args(argv, namespace=Args())
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
args = _parse_args(argv)
|
||||
try:
|
||||
source = _resolve_source(args)
|
||||
source.ref = source.ref or args.ref
|
||||
if not source.paths:
|
||||
raise InstallError("No skill paths provided.")
|
||||
for path in source.paths:
|
||||
_validate_relative_path(path)
|
||||
dest_root = args.dest or _default_dest()
|
||||
tmp_dir = tempfile.mkdtemp(prefix="skill-install-", dir=_tmp_root())
|
||||
try:
|
||||
repo_root = _prepare_repo(source, args.method, tmp_dir)
|
||||
installed = []
|
||||
for path in source.paths:
|
||||
skill_name = args.name if len(source.paths) == 1 else None
|
||||
skill_name = skill_name or os.path.basename(path.rstrip("/"))
|
||||
_validate_skill_name(skill_name)
|
||||
if not skill_name:
|
||||
raise InstallError("Unable to derive skill name.")
|
||||
dest_dir = os.path.join(dest_root, skill_name)
|
||||
if os.path.exists(dest_dir):
|
||||
raise InstallError(f"Destination already exists: {dest_dir}")
|
||||
skill_src = os.path.join(repo_root, path)
|
||||
_validate_skill(skill_src)
|
||||
_copy_skill(skill_src, dest_dir)
|
||||
installed.append((skill_name, dest_dir))
|
||||
finally:
|
||||
if os.path.isdir(tmp_dir):
|
||||
shutil.rmtree(tmp_dir, ignore_errors=True)
|
||||
for skill_name, dest_dir in installed:
|
||||
print(f"Installed {skill_name} to {dest_dir}")
|
||||
return 0
|
||||
except InstallError as exc:
|
||||
print(f"Error: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main(sys.argv[1:]))
|
||||
107
.agents/skills/.system/skill-installer/scripts/list-skills.py
Normal file
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env python3
|
||||
"""List skills from a GitHub repo path."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.error
|
||||
|
||||
from github_utils import github_api_contents_url, github_request
|
||||
|
||||
DEFAULT_REPO = "openai/skills"
|
||||
DEFAULT_PATH = "skills/.curated"
|
||||
DEFAULT_REF = "main"
|
||||
|
||||
|
||||
class ListError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Args(argparse.Namespace):
|
||||
repo: str
|
||||
path: str
|
||||
ref: str
|
||||
format: str
|
||||
|
||||
|
||||
def _request(url: str) -> bytes:
|
||||
return github_request(url, "codex-skill-list")
|
||||
|
||||
|
||||
def _codex_home() -> str:
|
||||
return os.environ.get("CODEX_HOME", os.path.expanduser("~/.codex"))
|
||||
|
||||
|
||||
def _installed_skills() -> set[str]:
|
||||
root = os.path.join(_codex_home(), "skills")
|
||||
if not os.path.isdir(root):
|
||||
return set()
|
||||
entries = set()
|
||||
for name in os.listdir(root):
|
||||
path = os.path.join(root, name)
|
||||
if os.path.isdir(path):
|
||||
entries.add(name)
|
||||
return entries
|
||||
|
||||
|
||||
def _list_skills(repo: str, path: str, ref: str) -> list[str]:
|
||||
api_url = github_api_contents_url(repo, path, ref)
|
||||
try:
|
||||
payload = _request(api_url)
|
||||
except urllib.error.HTTPError as exc:
|
||||
if exc.code == 404:
|
||||
raise ListError(
|
||||
"Skills path not found: "
|
||||
f"https://github.com/{repo}/tree/{ref}/{path}"
|
||||
) from exc
|
||||
raise ListError(f"Failed to fetch skills: HTTP {exc.code}") from exc
|
||||
data = json.loads(payload.decode("utf-8"))
|
||||
if not isinstance(data, list):
|
||||
raise ListError("Unexpected skills listing response.")
|
||||
skills = [item["name"] for item in data if item.get("type") == "dir"]
|
||||
return sorted(skills)
|
||||
|
||||
|
||||
def _parse_args(argv: list[str]) -> Args:
|
||||
parser = argparse.ArgumentParser(description="List skills.")
|
||||
parser.add_argument("--repo", default=DEFAULT_REPO)
|
||||
parser.add_argument(
|
||||
"--path",
|
||||
default=DEFAULT_PATH,
|
||||
help="Repo path to list (default: skills/.curated)",
|
||||
)
|
||||
parser.add_argument("--ref", default=DEFAULT_REF)
|
||||
parser.add_argument(
|
||||
"--format",
|
||||
choices=["text", "json"],
|
||||
default="text",
|
||||
help="Output format",
|
||||
)
|
||||
return parser.parse_args(argv, namespace=Args())
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
args = _parse_args(argv)
|
||||
try:
|
||||
skills = _list_skills(args.repo, args.path, args.ref)
|
||||
installed = _installed_skills()
|
||||
if args.format == "json":
|
||||
payload = [
|
||||
{"name": name, "installed": name in installed} for name in skills
|
||||
]
|
||||
print(json.dumps(payload))
|
||||
else:
|
||||
for idx, name in enumerate(skills, start=1):
|
||||
suffix = " (already installed)" if name in installed else ""
|
||||
print(f"{idx}. {name}{suffix}")
|
||||
return 0
|
||||
except ListError as exc:
|
||||
print(f"Error: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main(sys.argv[1:]))
|
||||
201
.agents/skills/cloudflare-deploy/LICENSE.txt
Normal file
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf of
|
||||
any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don\'t include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
224
.agents/skills/cloudflare-deploy/SKILL.md
Normal file
@@ -0,0 +1,224 @@
|
||||
---
|
||||
name: cloudflare-deploy
|
||||
description: Deploy applications and infrastructure to Cloudflare using Workers, Pages, and related platform services. Use when the user asks to deploy, host, publish, or set up a project on Cloudflare.
|
||||
---
|
||||
|
||||
# Cloudflare Deploy
|
||||
|
||||
Consolidated skill for building on the Cloudflare platform. Use decision trees below to find the right product, then load detailed references.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- When sandboxing blocks the deployment network calls, rerun with `sandbox_permissions=require_escalated`.
|
||||
- The deployment might take a few minutes. Use appropriate timeout values.
|
||||
|
||||
## Authentication (Required Before Deploy)
|
||||
|
||||
Verify auth before `wrangler deploy`, `wrangler pages deploy`, or `npm run deploy`:
|
||||
|
||||
```bash
|
||||
npx wrangler whoami # Shows account if authenticated
|
||||
```
|
||||
|
||||
Not authenticated? → `references/wrangler/auth.md`
|
||||
- Interactive/local: `wrangler login` (one-time OAuth)
|
||||
- CI/CD: Set `CLOUDFLARE_API_TOKEN` env var
|
||||
|
||||
## Quick Decision Trees
|
||||
|
||||
### "I need to run code"
|
||||
|
||||
```
|
||||
Need to run code?
|
||||
├─ Serverless functions at the edge → workers/
|
||||
├─ Full-stack web app with Git deploys → pages/
|
||||
├─ Stateful coordination/real-time → durable-objects/
|
||||
├─ Long-running multi-step jobs → workflows/
|
||||
├─ Run containers → containers/
|
||||
├─ Multi-tenant (customers deploy code) → workers-for-platforms/
|
||||
├─ Scheduled tasks (cron) → cron-triggers/
|
||||
├─ Lightweight edge logic (modify HTTP) → snippets/
|
||||
├─ Process Worker execution events (logs/observability) → tail-workers/
|
||||
└─ Optimize latency to backend infrastructure → smart-placement/
|
||||
```
|
||||
|
||||
### "I need to store data"
|
||||
|
||||
```
|
||||
Need storage?
|
||||
├─ Key-value (config, sessions, cache) → kv/
|
||||
├─ Relational SQL → d1/ (SQLite) or hyperdrive/ (existing Postgres/MySQL)
|
||||
├─ Object/file storage (S3-compatible) → r2/
|
||||
├─ Message queue (async processing) → queues/
|
||||
├─ Vector embeddings (AI/semantic search) → vectorize/
|
||||
├─ Strongly-consistent per-entity state → durable-objects/ (DO storage)
|
||||
├─ Secrets management → secrets-store/
|
||||
├─ Streaming ETL to R2 → pipelines/
|
||||
└─ Persistent cache (long-term retention) → cache-reserve/
|
||||
```
|
||||
|
||||
### "I need AI/ML"
|
||||
|
||||
```
|
||||
Need AI?
|
||||
├─ Run inference (LLMs, embeddings, images) → workers-ai/
|
||||
├─ Vector database for RAG/search → vectorize/
|
||||
├─ Build stateful AI agents → agents-sdk/
|
||||
├─ Gateway for any AI provider (caching, routing) → ai-gateway/
|
||||
└─ AI-powered search widget → ai-search/
|
||||
```
|
||||
|
||||
### "I need networking/connectivity"
|
||||
|
||||
```
|
||||
Need networking?
|
||||
├─ Expose local service to internet → tunnel/
|
||||
├─ TCP/UDP proxy (non-HTTP) → spectrum/
|
||||
├─ WebRTC TURN server → turn/
|
||||
├─ Private network connectivity → network-interconnect/
|
||||
├─ Optimize routing → argo-smart-routing/
|
||||
├─ Optimize latency to backend (not user) → smart-placement/
|
||||
└─ Real-time video/audio → realtimekit/ or realtime-sfu/
|
||||
```
|
||||
|
||||
### "I need security"
|
||||
|
||||
```
|
||||
Need security?
|
||||
├─ Web Application Firewall → waf/
|
||||
├─ DDoS protection → ddos/
|
||||
├─ Bot detection/management → bot-management/
|
||||
├─ API protection → api-shield/
|
||||
├─ CAPTCHA alternative → turnstile/
|
||||
└─ Credential leak detection → waf/ (managed ruleset)
|
||||
```
|
||||
|
||||
### "I need media/content"
|
||||
|
||||
```
|
||||
Need media?
|
||||
├─ Image optimization/transformation → images/
|
||||
├─ Video streaming/encoding → stream/
|
||||
├─ Browser automation/screenshots → browser-rendering/
|
||||
└─ Third-party script management → zaraz/
|
||||
```
|
||||
|
||||
### "I need infrastructure-as-code"
|
||||
|
||||
```
|
||||
Need IaC? → pulumi/ (Pulumi), terraform/ (Terraform), or api/ (REST API)
|
||||
```
|
||||
|
||||
## Product Index
|
||||
|
||||
### Compute & Runtime
|
||||
| Product | Reference |
|
||||
|---------|-----------|
|
||||
| Workers | `references/workers/` |
|
||||
| Pages | `references/pages/` |
|
||||
| Pages Functions | `references/pages-functions/` |
|
||||
| Durable Objects | `references/durable-objects/` |
|
||||
| Workflows | `references/workflows/` |
|
||||
| Containers | `references/containers/` |
|
||||
| Workers for Platforms | `references/workers-for-platforms/` |
|
||||
| Cron Triggers | `references/cron-triggers/` |
|
||||
| Tail Workers | `references/tail-workers/` |
|
||||
| Snippets | `references/snippets/` |
|
||||
| Smart Placement | `references/smart-placement/` |
|
||||
|
||||
### Storage & Data
|
||||
| Product | Reference |
|
||||
|---------|-----------|
|
||||
| KV | `references/kv/` |
|
||||
| D1 | `references/d1/` |
|
||||
| R2 | `references/r2/` |
|
||||
| Queues | `references/queues/` |
|
||||
| Hyperdrive | `references/hyperdrive/` |
|
||||
| DO Storage | `references/do-storage/` |
|
||||
| Secrets Store | `references/secrets-store/` |
|
||||
| Pipelines | `references/pipelines/` |
|
||||
| R2 Data Catalog | `references/r2-data-catalog/` |
|
||||
| R2 SQL | `references/r2-sql/` |
|
||||
|
||||
### AI & Machine Learning
|
||||
| Product | Reference |
|
||||
|---------|-----------|
|
||||
| Workers AI | `references/workers-ai/` |
|
||||
| Vectorize | `references/vectorize/` |
|
||||
| Agents SDK | `references/agents-sdk/` |
|
||||
| AI Gateway | `references/ai-gateway/` |
|
||||
| AI Search | `references/ai-search/` |
|
||||
|
||||
### Networking & Connectivity
|
||||
| Product | Reference |
|
||||
|---------|-----------|
|
||||
| Tunnel | `references/tunnel/` |
|
||||
| Spectrum | `references/spectrum/` |
|
||||
| TURN | `references/turn/` |
|
||||
| Network Interconnect | `references/network-interconnect/` |
|
||||
| Argo Smart Routing | `references/argo-smart-routing/` |
|
||||
| Workers VPC | `references/workers-vpc/` |
|
||||
|
||||
### Security
|
||||
| Product | Reference |
|
||||
|---------|-----------|
|
||||
| WAF | `references/waf/` |
|
||||
| DDoS Protection | `references/ddos/` |
|
||||
| Bot Management | `references/bot-management/` |
|
||||
| API Shield | `references/api-shield/` |
|
||||
| Turnstile | `references/turnstile/` |
|
||||
|
||||
### Media & Content
|
||||
| Product | Reference |
|
||||
|---------|-----------|
|
||||
| Images | `references/images/` |
|
||||
| Stream | `references/stream/` |
|
||||
| Browser Rendering | `references/browser-rendering/` |
|
||||
| Zaraz | `references/zaraz/` |
|
||||
|
||||
### Real-Time Communication
|
||||
| Product | Reference |
|
||||
|---------|-----------|
|
||||
| RealtimeKit | `references/realtimekit/` |
|
||||
| Realtime SFU | `references/realtime-sfu/` |
|
||||
|
||||
### Developer Tools
|
||||
| Product | Reference |
|
||||
|---------|-----------|
|
||||
| Wrangler | `references/wrangler/` |
|
||||
| Miniflare | `references/miniflare/` |
|
||||
| C3 | `references/c3/` |
|
||||
| Observability | `references/observability/` |
|
||||
| Analytics Engine | `references/analytics-engine/` |
|
||||
| Web Analytics | `references/web-analytics/` |
|
||||
| Sandbox | `references/sandbox/` |
|
||||
| Workerd | `references/workerd/` |
|
||||
| Workers Playground | `references/workers-playground/` |
|
||||
|
||||
### Infrastructure as Code
|
||||
| Product | Reference |
|
||||
|---------|-----------|
|
||||
| Pulumi | `references/pulumi/` |
|
||||
| Terraform | `references/terraform/` |
|
||||
| API | `references/api/` |
|
||||
|
||||
### Other Services
|
||||
| Product | Reference |
|
||||
|---------|-----------|
|
||||
| Email Routing | `references/email-routing/` |
|
||||
| Email Workers | `references/email-workers/` |
|
||||
| Static Assets | `references/static-assets/` |
|
||||
| Bindings | `references/bindings/` |
|
||||
| Cache Reserve | `references/cache-reserve/` |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Escalated Network Access
|
||||
|
||||
If deployment fails due to network issues (timeouts, DNS errors, connection resets), rerun the deploy with escalated permissions (use `sandbox_permissions=require_escalated`). The deploy requires escalated network access when sandbox networking blocks outbound requests.
|
||||
|
||||
Example guidance to the user:
|
||||
|
||||
```
|
||||
The deploy needs escalated network access to deploy to Cloudflare. I can rerun the command with escalated permissions—want me to proceed?
|
||||
```
|
||||
6
.agents/skills/cloudflare-deploy/agents/openai.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
interface:
|
||||
display_name: "Cloudflare Deploy"
|
||||
short_description: "Deploy Workers, Pages, and platform services on Cloudflare"
|
||||
icon_small: "./assets/cloudflare-small.svg"
|
||||
icon_large: "./assets/cloudflare.png"
|
||||
default_prompt: "Deploy this app to Cloudflare (Workers or Pages) and return URL, config, and required env vars."
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path fill="currentColor" d="M16.5088 16.8447c.1475-.5068.0908-.9707-.1553-1.3154-.2246-.3164-.6045-.499-1.0615-.5205l-8.6592-.1123a.1559.1559 0 0 1-.1333-.0713c-.0283-.042-.0351-.0986-.021-.1553.0278-.084.1123-.1484.2036-.1562l8.7359-.1123c1.0351-.0489 2.1601-.8868 2.5537-1.9136l.499-1.3013c.0215-.0561.0293-.1128.0147-.168-.5625-2.5463-2.835-4.4453-5.5499-4.4453-2.5039 0-4.6284 1.6177-5.3876 3.8614-.4927-.3658-1.1187-.5625-1.794-.499-1.2026.119-2.1665 1.083-2.2861 2.2856-.0283.31-.0069.6128.0635.894C1.5683 13.171 0 14.7754 0 16.752c0 .1748.0142.3515.0352.5273.0141.083.0844.1475.1689.1475h15.9814c.0909 0 .1758-.0645.2032-.1553l.12-.4268zm2.7568-5.5634c-.0771 0-.1611 0-.2383.0112-.0566 0-.1054.0415-.127.0976l-.3378 1.1744c-.1475.5068-.0918.9707.1543 1.3164.2256.3164.6055.498 1.0625.5195l1.8437.1133c.0557 0 .1055.0263.1329.0703.0283.043.0351.1074.0214.1562-.0283.084-.1132.1485-.204.1553l-1.921.1123c-1.041.0488-2.1582.8867-2.5527 1.914l-.1406.3585c-.0283.0713.0215.1416.0986.1416h6.5977c.0771 0 .1474-.0489.169-.126.1122-.4082.1757-.837.1757-1.2803 0-2.6025-2.125-4.727-4.7344-4.727"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
BIN
.agents/skills/cloudflare-deploy/assets/cloudflare.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,89 @@
|
||||
# Cloudflare Agents SDK
|
||||
|
||||
Cloudflare Agents SDK enables building AI-powered agents on Durable Objects with state, WebSockets, SQL, scheduling, and AI integration.
|
||||
|
||||
## Core Value
|
||||
Build stateful, globally distributed AI agents with persistent memory, real-time connections, scheduled tasks, and async workflows.
|
||||
|
||||
## When to Use
|
||||
- Persistent state + memory required
|
||||
- Real-time WebSocket connections
|
||||
- Long-running workflows (minutes/hours)
|
||||
- Chat interfaces with AI models
|
||||
- Scheduled/recurring tasks with state
|
||||
- DB queries with agent state
|
||||
|
||||
## What Type of Agent?
|
||||
|
||||
| Use Case | Class | Key Features |
|
||||
|----------|-------|--------------|
|
||||
| AI chat interface | `AIChatAgent` | Auto-streaming, tools, message history, resumable |
|
||||
| MCP tool provider | `Agent` + MCP | Expose tools to AI systems |
|
||||
| Custom logic/routing | `Agent` | Full control, WebSockets, email, SQL |
|
||||
| Real-time collaboration | `Agent` | WebSocket state, broadcasts |
|
||||
| Email processing | `Agent` | `onEmail()` handler |
|
||||
|
||||
## Quick Start
|
||||
|
||||
**AI Chat Agent:**
|
||||
```typescript
|
||||
import { AIChatAgent } from "agents";
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
|
||||
export class ChatAgent extends AIChatAgent<Env> {
|
||||
async onChatMessage(onFinish) {
|
||||
return this.streamText({
|
||||
model: openai("gpt-4"),
|
||||
messages: this.messages,
|
||||
onFinish,
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Base Agent:**
|
||||
```typescript
|
||||
import { Agent } from "agents";
|
||||
|
||||
export class MyAgent extends Agent<Env> {
|
||||
onStart() {
|
||||
this.sql`CREATE TABLE IF NOT EXISTS users (id TEXT PRIMARY KEY)`;
|
||||
}
|
||||
|
||||
async onRequest(request: Request) {
|
||||
return Response.json({ state: this.state });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Reading Order
|
||||
|
||||
| Task | Files to Read |
|
||||
|------|---------------|
|
||||
| Quick start | README only |
|
||||
| Build chat agent | README → api.md (AIChatAgent) → patterns.md |
|
||||
| Setup project | README → configuration.md |
|
||||
| Add React frontend | README → api.md (Client Hooks) → patterns.md |
|
||||
| Build MCP server | api.md (MCP) → patterns.md |
|
||||
| Background tasks | api.md (Scheduling, Task Queue) → patterns.md |
|
||||
| Debug issues | gotchas.md |
|
||||
|
||||
## Package Entry Points
|
||||
|
||||
| Import | Purpose |
|
||||
|--------|---------|
|
||||
| `agents` | Server-side Agent classes, lifecycle |
|
||||
| `agents/react` | `useAgent()` hook for WebSocket connections |
|
||||
| `agents/ai-react` | `useAgentChat()` hook for AI chat UIs |
|
||||
|
||||
## In This Reference
|
||||
- [configuration.md](./configuration.md) - SDK setup, wrangler config, routing
|
||||
- [api.md](./api.md) - Agent classes, lifecycle, client hooks
|
||||
- [patterns.md](./patterns.md) - Common workflows, best practices
|
||||
- [gotchas.md](./gotchas.md) - Common issues, limits
|
||||
|
||||
## See Also
|
||||
- durable-objects - Agent infrastructure
|
||||
- d1 - External database integration
|
||||
- workers-ai - AI model integration
|
||||
- vectorize - Vector search for RAG patterns
|
||||
190
.agents/skills/cloudflare-deploy/references/agents-sdk/api.md
Normal file
@@ -0,0 +1,190 @@
|
||||
# API Reference
|
||||
|
||||
## Agent Classes
|
||||
|
||||
### AIChatAgent
|
||||
|
||||
For AI chat with auto-streaming, message history, tools, resumable streaming.
|
||||
|
||||
```ts
|
||||
import { AIChatAgent } from "agents";
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
|
||||
export class ChatAgent extends AIChatAgent<Env> {
|
||||
async onChatMessage(onFinish) {
|
||||
return this.streamText({
|
||||
model: openai("gpt-4"),
|
||||
messages: this.messages, // Auto-managed message history
|
||||
tools: {
|
||||
getWeather: {
|
||||
description: "Get weather",
|
||||
parameters: z.object({ city: z.string() }),
|
||||
execute: async ({ city }) => `Sunny, 72°F in ${city}`
|
||||
}
|
||||
},
|
||||
onFinish, // Persist response to this.messages
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Agent (Base Class)
|
||||
|
||||
Full control for custom logic, WebSockets, email, and SQL.
|
||||
|
||||
```ts
|
||||
import { Agent } from "agents";
|
||||
|
||||
export class MyAgent extends Agent<Env, State> {
|
||||
// Lifecycle methods below
|
||||
}
|
||||
```
|
||||
|
||||
**Type params:** `Agent<Env, State, ConnState>` - Env bindings, agent state, connection state
|
||||
|
||||
## Lifecycle Hooks
|
||||
|
||||
```ts
|
||||
onStart() { // Init/restart
|
||||
this.sql`CREATE TABLE IF NOT EXISTS users (id TEXT, name TEXT)`;
|
||||
}
|
||||
|
||||
async onRequest(req: Request) { // HTTP
|
||||
const {pathname} = new URL(req.url);
|
||||
if (pathname === "/users") return Response.json(this.sql<{id,name}>`SELECT * FROM users`);
|
||||
return new Response("Not found", {status: 404});
|
||||
}
|
||||
|
||||
async onConnect(conn: Connection<ConnState>, ctx: ConnectionContext) { // WebSocket
|
||||
conn.accept();
|
||||
conn.setState({userId: ctx.request.headers.get("X-User-ID")});
|
||||
conn.send(JSON.stringify({type: "connected", state: this.state}));
|
||||
}
|
||||
|
||||
async onMessage(conn: Connection<ConnState>, msg: WSMessage) { // WS messages
|
||||
const m = JSON.parse(msg as string);
|
||||
this.setState({messages: [...this.state.messages, m]});
|
||||
this.connections.forEach(c => c.send(JSON.stringify(m)));
|
||||
}
|
||||
|
||||
async onEmail(email: AgentEmail) { // Email routing
|
||||
this.sql`INSERT INTO emails (from_addr,subject,body) VALUES (${email.from},${email.headers.get("subject")},${await email.text()})`;
|
||||
}
|
||||
```
|
||||
|
||||
## State, SQL, Scheduling
|
||||
|
||||
```ts
|
||||
// State
|
||||
this.setState({count: 42}); // Auto-syncs
|
||||
this.setState({...this.state, count: this.state.count + 1});
|
||||
|
||||
// SQL (parameterized queries prevent injection)
|
||||
this.sql`CREATE TABLE IF NOT EXISTS users (id TEXT PRIMARY KEY, name TEXT)`;
|
||||
this.sql`INSERT INTO users (id,name) VALUES (${userId},${name})`;
|
||||
const users = this.sql<{id,name}>`SELECT * FROM users WHERE id = ${userId}`;
|
||||
|
||||
// Scheduling
|
||||
await this.schedule(new Date("2026-12-25"), "sendGreeting", {msg:"Hi"}); // Date
|
||||
await this.schedule(60, "checkStatus", {}); // Delay (sec)
|
||||
await this.schedule("0 0 * * *", "dailyCleanup", {}); // Cron
|
||||
await this.cancelSchedule(scheduleId);
|
||||
```
|
||||
|
||||
## RPC Methods (@callable)
|
||||
|
||||
```ts
|
||||
import { Agent, callable } from "agents";
|
||||
|
||||
export class MyAgent extends Agent<Env> {
|
||||
@callable()
|
||||
async processTask(input: {text: string}): Promise<{result: string}> {
|
||||
return { result: await this.env.AI.run("@cf/meta/llama-3.1-8b-instruct", {prompt: input.text}) };
|
||||
}
|
||||
}
|
||||
// Client: const result = await agent.processTask({ text: "Hello" });
|
||||
// Must return JSON-serializable values
|
||||
```
|
||||
|
||||
## Connections & AI
|
||||
|
||||
```ts
|
||||
// Connections (type: Agent<Env, State, ConnState>)
|
||||
this.connections.forEach(c => c.send(JSON.stringify(msg))); // Broadcast
|
||||
conn.setState({userId:"123"}); conn.close(1000, "Goodbye");
|
||||
|
||||
// Workers AI
|
||||
const r = await this.env.AI.run("@cf/meta/llama-3.1-8b-instruct", {prompt});
|
||||
|
||||
// Manual streaming (prefer AIChatAgent)
|
||||
const stream = await client.chat.completions.create({model: "gpt-4", messages, stream: true});
|
||||
for await (const chunk of stream) conn.send(JSON.stringify({chunk: chunk.choices[0].delta.content}));
|
||||
```
|
||||
|
||||
**Type-safe state:** `Agent<Env, State, ConnState>` - third param types `conn.state`
|
||||
|
||||
## MCP Integration
|
||||
|
||||
Model Context Protocol for exposing tools:
|
||||
|
||||
```ts
|
||||
// Register & use MCP server
|
||||
await this.mcp.registerServer("github", {
|
||||
url: env.MCP_SERVER_URL,
|
||||
auth: { type: "oauth", clientId: env.GITHUB_CLIENT_ID, clientSecret: env.GITHUB_CLIENT_SECRET }
|
||||
});
|
||||
const tools = await this.mcp.getAITools(["github"]);
|
||||
return this.streamText({ model: openai("gpt-4"), messages: this.messages, tools, onFinish });
|
||||
```
|
||||
|
||||
## Task Queue
|
||||
|
||||
```ts
|
||||
await this.queue("processVideo", { videoId: "abc123" }); // Add task
|
||||
const tasks = await this.dequeue(10); // Process up to 10
|
||||
```
|
||||
|
||||
## Context & Cleanup
|
||||
|
||||
```ts
|
||||
const agent = getCurrentAgent<MyAgent>(); // Get current instance
|
||||
async destroy() { /* cleanup before agent destroyed */ }
|
||||
```
|
||||
|
||||
## AI Integration
|
||||
|
||||
```ts
|
||||
// Workers AI
|
||||
const r = await this.env.AI.run("@cf/meta/llama-3.1-8b-instruct", {prompt});
|
||||
|
||||
// Manual streaming (prefer AIChatAgent for auto-streaming)
|
||||
const stream = await client.chat.completions.create({model: "gpt-4", messages, stream: true});
|
||||
for await (const chunk of stream) {
|
||||
if (chunk.choices[0]?.delta?.content) conn.send(JSON.stringify({chunk: chunk.choices[0].delta.content}));
|
||||
}
|
||||
```
|
||||
|
||||
## Client Hooks (React)
|
||||
|
||||
```ts
|
||||
// useAgent() - WebSocket connection + RPC
|
||||
import { useAgent } from "agents/react";
|
||||
const agent = useAgent({ agent: "MyAgent", name: "user-123" }); // name for idFromName
|
||||
const result = await agent.processTask({ text: "Hello" }); // Call @callable methods
|
||||
// agent.readyState: 0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED
|
||||
|
||||
// useAgentChat() - AI chat UI
|
||||
import { useAgentChat } from "agents/ai-react";
|
||||
const agent = useAgent({ agent: "ChatAgent" });
|
||||
const { messages, input, handleInputChange, handleSubmit, isLoading, stop, clearHistory } =
|
||||
useAgentChat({
|
||||
agent,
|
||||
maxSteps: 5, // Max tool iterations
|
||||
resume: true, // Auto-resume on disconnect
|
||||
onToolCall: async (toolCall) => {
|
||||
// Client tools (human-in-the-loop)
|
||||
if (toolCall.toolName === "confirm") return { ok: window.confirm("Proceed?") };
|
||||
}
|
||||
});
|
||||
// status: "ready" | "submitted" | "streaming" | "error"
|
||||
```
|
||||
@@ -0,0 +1,182 @@
|
||||
# Configuration
|
||||
|
||||
## Wrangler Setup
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"name": "my-agents-app",
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{"name": "MyAgent", "class_name": "MyAgent"}
|
||||
]
|
||||
},
|
||||
"migrations": [
|
||||
{"tag": "v1", "new_sqlite_classes": ["MyAgent"]}
|
||||
],
|
||||
"ai": {
|
||||
"binding": "AI"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Environment Bindings
|
||||
|
||||
**Type-safe pattern:**
|
||||
|
||||
```typescript
|
||||
interface Env {
|
||||
AI?: Ai; // Workers AI
|
||||
MyAgent?: DurableObjectNamespace<MyAgent>;
|
||||
ChatAgent?: DurableObjectNamespace<ChatAgent>;
|
||||
DB?: D1Database; // D1 database
|
||||
KV?: KVNamespace; // KV storage
|
||||
R2?: R2Bucket; // R2 bucket
|
||||
OPENAI_API_KEY?: string; // Secrets
|
||||
GITHUB_CLIENT_ID?: string; // MCP OAuth credentials
|
||||
GITHUB_CLIENT_SECRET?: string;
|
||||
QUEUE?: Queue; // Queues
|
||||
}
|
||||
```
|
||||
|
||||
**Best practice:** Define all DO bindings in Env interface for type safety.
|
||||
|
||||
## Deployment
|
||||
|
||||
```bash
|
||||
# Local dev
|
||||
npx wrangler dev
|
||||
|
||||
# Deploy production
|
||||
npx wrangler deploy
|
||||
|
||||
# Set secrets
|
||||
npx wrangler secret put OPENAI_API_KEY
|
||||
```
|
||||
|
||||
## Agent Routing
|
||||
|
||||
**Recommended: Use route helpers**
|
||||
|
||||
```typescript
|
||||
import { routeAgent } from "agents";
|
||||
|
||||
export default {
|
||||
fetch(request: Request, env: Env) {
|
||||
return routeAgent(request, env);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Helper routes requests to agents automatically based on URL patterns.
|
||||
|
||||
**Manual routing (advanced):**
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
async fetch(request: Request, env: Env) {
|
||||
const url = new URL(request.url);
|
||||
|
||||
// Named ID (deterministic)
|
||||
const id = env.MyAgent.idFromName("user-123");
|
||||
|
||||
// Random ID (from URL param)
|
||||
// const id = env.MyAgent.idFromString(url.searchParams.get("id"));
|
||||
|
||||
const stub = env.MyAgent.get(id);
|
||||
return stub.fetch(request);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Multi-agent setup:**
|
||||
|
||||
```typescript
|
||||
import { routeAgent } from "agents";
|
||||
|
||||
export default {
|
||||
fetch(request: Request, env: Env) {
|
||||
const url = new URL(request.url);
|
||||
|
||||
// Route by path
|
||||
if (url.pathname.startsWith("/chat")) {
|
||||
return routeAgent(request, env, "ChatAgent");
|
||||
}
|
||||
if (url.pathname.startsWith("/task")) {
|
||||
return routeAgent(request, env, "TaskAgent");
|
||||
}
|
||||
|
||||
return new Response("Not found", { status: 404 });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Email Routing
|
||||
|
||||
**Code setup:**
|
||||
|
||||
```typescript
|
||||
import { routeAgentEmail } from "agents";
|
||||
|
||||
export default {
|
||||
fetch: (req: Request, env: Env) => routeAgent(req, env),
|
||||
email: (message: ForwardableEmailMessage, env: Env) => {
|
||||
return routeAgentEmail(message, env);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Dashboard setup:**
|
||||
|
||||
Configure email routing in Cloudflare dashboard:
|
||||
|
||||
```
|
||||
Destination: Workers with Durable Objects
|
||||
Worker: my-agents-app
|
||||
```
|
||||
|
||||
Then handle in agent:
|
||||
|
||||
```typescript
|
||||
export class EmailAgent extends Agent<Env> {
|
||||
async onEmail(email: AgentEmail) {
|
||||
const text = await email.text();
|
||||
// Process email
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## AI Gateway (Optional)
|
||||
|
||||
```typescript
|
||||
// Enable caching/routing through AI Gateway
|
||||
const response = await this.env.AI.run(
|
||||
"@cf/meta/llama-3.1-8b-instruct",
|
||||
{ prompt },
|
||||
{
|
||||
gateway: {
|
||||
id: "my-gateway-id",
|
||||
skipCache: false,
|
||||
cacheTtl: 3600
|
||||
}
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
## MCP Configuration (Optional)
|
||||
|
||||
For exposing tools via Model Context Protocol:
|
||||
|
||||
```typescript
|
||||
// wrangler.jsonc - Add MCP OAuth secrets
|
||||
{
|
||||
"vars": {
|
||||
"MCP_SERVER_URL": "https://mcp.example.com"
|
||||
}
|
||||
}
|
||||
|
||||
// Set secrets via CLI
|
||||
// npx wrangler secret put GITHUB_CLIENT_ID
|
||||
// npx wrangler secret put GITHUB_CLIENT_SECRET
|
||||
```
|
||||
|
||||
Then register in agent code (see api.md MCP section).
|
||||
@@ -0,0 +1,158 @@
|
||||
# Gotchas & Best Practices
|
||||
|
||||
## Common Errors
|
||||
|
||||
### "setState() not syncing"
|
||||
|
||||
**Cause:** Mutating state directly or not calling `setState()` after modifications
|
||||
**Solution:** Always use `setState()` with immutable updates:
|
||||
```ts
|
||||
// ❌ this.state.count++
|
||||
// ✅ this.setState({...this.state, count: this.state.count + 1})
|
||||
```
|
||||
|
||||
### "Message history grows unbounded (AIChatAgent)"
|
||||
|
||||
**Cause:** `this.messages` in `AIChatAgent` accumulates all messages indefinitely
|
||||
**Solution:** Manually trim old messages periodically:
|
||||
```ts
|
||||
export class ChatAgent extends AIChatAgent<Env> {
|
||||
async onChatMessage(onFinish) {
|
||||
// Keep only last 50 messages
|
||||
if (this.messages.length > 50) {
|
||||
this.messages = this.messages.slice(-50);
|
||||
}
|
||||
|
||||
return this.streamText({ model: openai("gpt-4"), messages: this.messages, onFinish });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### "SQL injection vulnerability"
|
||||
|
||||
**Cause:** Direct string interpolation in SQL queries
|
||||
**Solution:** Use parameterized queries:
|
||||
```ts
|
||||
// ❌ this.sql`...WHERE id = '${userId}'`
|
||||
// ✅ this.sql`...WHERE id = ${userId}`
|
||||
```
|
||||
|
||||
### "WebSocket connection timeout"
|
||||
|
||||
**Cause:** Not calling `conn.accept()` in `onConnect`
|
||||
**Solution:** Always accept connections:
|
||||
```ts
|
||||
async onConnect(conn: Connection, ctx: ConnectionContext) { conn.accept(); conn.setState({userId: "123"}); }
|
||||
```
|
||||
|
||||
### "Schedule limit exceeded"
|
||||
|
||||
**Cause:** More than 1000 scheduled tasks per agent
|
||||
**Solution:** Clean up old schedules and limit creation rate:
|
||||
```ts
|
||||
async checkSchedules() { if ((await this.getSchedules()).length > 800) console.warn("Near limit!"); }
|
||||
```
|
||||
|
||||
### "AI Gateway unavailable"
|
||||
|
||||
**Cause:** AI service timeout or quota exceeded
|
||||
**Solution:** Add error handling and fallbacks:
|
||||
```ts
|
||||
try {
|
||||
return await this.env.AI.run(model, {prompt});
|
||||
} catch (e) {
|
||||
console.error("AI error:", e);
|
||||
return {error: "Unavailable"};
|
||||
}
|
||||
```
|
||||
|
||||
### "@callable method returns undefined"
|
||||
|
||||
**Cause:** Method doesn't return JSON-serializable value, or has non-serializable types
|
||||
**Solution:** Ensure return values are plain objects/arrays/primitives:
|
||||
```ts
|
||||
// ❌ Returns class instance
|
||||
@callable()
|
||||
async getData() { return new Date(); }
|
||||
|
||||
// ✅ Returns serializable object
|
||||
@callable()
|
||||
async getData() { return { timestamp: Date.now() }; }
|
||||
```
|
||||
|
||||
### "Resumable stream not resuming"
|
||||
|
||||
**Cause:** Stream ID must be deterministic for resumption to work
|
||||
**Solution:** Use AIChatAgent (automatic) or ensure consistent stream IDs:
|
||||
```ts
|
||||
// AIChatAgent handles this automatically
|
||||
export class ChatAgent extends AIChatAgent<Env> {
|
||||
// Resumption works out of the box
|
||||
}
|
||||
```
|
||||
|
||||
### "MCP connection loss on hibernation"
|
||||
|
||||
**Cause:** MCP server connections don't survive hibernation
|
||||
**Solution:** Re-register servers in `onStart()` or check connection status:
|
||||
```ts
|
||||
onStart() {
|
||||
// Re-register MCP servers after hibernation
|
||||
await this.mcp.registerServer("github", { url: env.MCP_URL, auth: {...} });
|
||||
}
|
||||
```
|
||||
|
||||
### "Agent not found"
|
||||
|
||||
**Cause:** Durable Object binding missing or incorrect class name
|
||||
**Solution:** Verify DO binding in wrangler.jsonc and class name matches
|
||||
|
||||
## Rate Limits & Quotas
|
||||
|
||||
| Resource/Limit | Value | Notes |
|
||||
|----------------|-------|-------|
|
||||
| CPU per request | 30s (std), 300s (max) | Set in wrangler.jsonc |
|
||||
| Memory per instance | 128MB | Shared with WebSockets |
|
||||
| Storage per agent | 10GB | SQLite storage |
|
||||
| Scheduled tasks | 1000 per agent | Monitor with `getSchedules()` |
|
||||
| WebSocket connections | Unlimited | Within memory limits |
|
||||
| SQL columns | 100 | Per table |
|
||||
| SQL row size | 2MB | Key + value |
|
||||
| WebSocket message | 32MiB | Max size |
|
||||
| DO requests/sec | ~1000 | Per unique DO instance; rate limit if needed |
|
||||
| AI Gateway (Workers AI) | Model-specific | Check dashboard for limits |
|
||||
| MCP requests | Depends on server | Implement retry/backoff |
|
||||
|
||||
## Best Practices
|
||||
|
||||
### State Management
|
||||
- Use immutable updates: `setState({...this.state, key: newValue})`
|
||||
- Trim unbounded arrays (messages, logs) periodically
|
||||
- Store large data in SQL, not state
|
||||
|
||||
### SQL Usage
|
||||
- Create tables in `onStart()`, not `onRequest()`
|
||||
- Use parameterized queries: `` sql`WHERE id = ${id}` `` (NOT `` sql`WHERE id = '${id}'` ``)
|
||||
- Index frequently queried columns
|
||||
|
||||
### Scheduling
|
||||
- Monitor schedule count: `await this.getSchedules()`
|
||||
- Cancel completed tasks to stay under 1000 limit
|
||||
- Use cron strings for recurring tasks
|
||||
|
||||
### WebSockets
|
||||
- Always call `conn.accept()` in `onConnect()`
|
||||
- Handle client disconnects gracefully
|
||||
- Broadcast to `this.connections` efficiently
|
||||
|
||||
### AI Integration
|
||||
- Use `AIChatAgent` for chat interfaces (auto-streaming, resumption)
|
||||
- Trim message history to avoid token limits
|
||||
- Handle AI errors with try/catch and fallbacks
|
||||
|
||||
### Production Deployment
|
||||
- **Rate limiting:** Implement request throttling for high-traffic agents (>1000 req/s)
|
||||
- **Monitoring:** Log critical errors, track schedule count, monitor storage usage
|
||||
- **Graceful degradation:** Handle AI service outages with fallbacks
|
||||
- **Message trimming:** Enforce max history length (e.g., 100 messages) in AIChatAgent
|
||||
- **MCP reliability:** Re-register servers on hibernation, implement retry logic
|
||||
@@ -0,0 +1,192 @@
|
||||
# Patterns & Use Cases
|
||||
|
||||
## AI Chat w/Tools
|
||||
|
||||
**Server (AIChatAgent):**
|
||||
|
||||
```ts
|
||||
import { AIChatAgent } from "agents";
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
import { tool } from "ai";
|
||||
import { z } from "zod";
|
||||
|
||||
export class ChatAgent extends AIChatAgent<Env> {
|
||||
async onChatMessage(onFinish) {
|
||||
return this.streamText({
|
||||
model: openai("gpt-4"),
|
||||
messages: this.messages, // Auto-managed
|
||||
tools: {
|
||||
getWeather: tool({
|
||||
description: "Get current weather",
|
||||
parameters: z.object({ city: z.string() }),
|
||||
execute: async ({ city }) => `Weather in ${city}: Sunny, 72°F`
|
||||
}),
|
||||
searchDocs: tool({
|
||||
description: "Search documentation",
|
||||
parameters: z.object({ query: z.string() }),
|
||||
execute: async ({ query }) => JSON.stringify(
|
||||
this.sql<{title, content}>`SELECT title, content FROM docs WHERE content LIKE ${'%' + query + '%'}`
|
||||
)
|
||||
})
|
||||
},
|
||||
onFinish,
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Client (React):**
|
||||
|
||||
```tsx
|
||||
import { useAgent } from "agents/react";
|
||||
import { useAgentChat } from "agents/ai-react";
|
||||
|
||||
function ChatUI() {
|
||||
const agent = useAgent({ agent: "ChatAgent" });
|
||||
const { messages, input, handleInputChange, handleSubmit, isLoading } = useAgentChat({ agent });
|
||||
|
||||
return (
|
||||
<div>
|
||||
{messages.map(m => <div key={m.id}>{m.role}: {m.content}</div>)}
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input value={input} onChange={handleInputChange} disabled={isLoading} />
|
||||
<button disabled={isLoading}>Send</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Human-in-the-Loop (Client Tools)
|
||||
|
||||
Server defines tool, client executes:
|
||||
|
||||
```ts
|
||||
// Server
|
||||
export class ChatAgent extends AIChatAgent<Env> {
|
||||
async onChatMessage(onFinish) {
|
||||
return this.streamText({
|
||||
model: openai("gpt-4"),
|
||||
messages: this.messages,
|
||||
tools: {
|
||||
confirmAction: tool({
|
||||
description: "Ask user to confirm",
|
||||
parameters: z.object({ action: z.string() }),
|
||||
execute: "client", // Client-side execution
|
||||
})
|
||||
},
|
||||
onFinish,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Client
|
||||
const { messages } = useAgentChat({
|
||||
agent,
|
||||
onToolCall: async (toolCall) => {
|
||||
if (toolCall.toolName === "confirmAction") {
|
||||
return { confirmed: window.confirm(`Confirm: ${toolCall.args.action}?`) };
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Task Queue & Scheduled Processing
|
||||
|
||||
```ts
|
||||
export class TaskAgent extends Agent<Env> {
|
||||
onStart() {
|
||||
this.schedule("*/5 * * * *", "processQueue", {}); // Every 5 min
|
||||
this.schedule("0 0 * * *", "dailyCleanup", {}); // Daily
|
||||
}
|
||||
|
||||
async onRequest(req: Request) {
|
||||
await this.queue("processVideo", { videoId: (await req.json()).videoId });
|
||||
return Response.json({ queued: true });
|
||||
}
|
||||
|
||||
async processQueue() {
|
||||
const tasks = await this.dequeue(10);
|
||||
for (const task of tasks) {
|
||||
if (task.name === "processVideo") await this.processVideo(task.data.videoId);
|
||||
}
|
||||
}
|
||||
|
||||
async dailyCleanup() {
|
||||
this.sql`DELETE FROM logs WHERE created_at < ${Date.now() - 86400000}`;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Manual WebSocket Chat
|
||||
|
||||
Custom protocols (non-AI):
|
||||
|
||||
```ts
|
||||
export class ChatAgent extends Agent<Env> {
|
||||
async onConnect(conn: Connection, ctx: ConnectionContext) {
|
||||
conn.accept();
|
||||
conn.setState({userId: ctx.request.headers.get("X-User-ID") || "anon"});
|
||||
conn.send(JSON.stringify({type: "history", messages: this.state.messages}));
|
||||
}
|
||||
|
||||
async onMessage(conn: Connection, msg: WSMessage) {
|
||||
const newMsg = {userId: conn.state.userId, text: JSON.parse(msg as string).text, timestamp: Date.now()};
|
||||
this.setState({messages: [...this.state.messages, newMsg]});
|
||||
this.connections.forEach(c => c.send(JSON.stringify(newMsg)));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Email Processing w/AI
|
||||
|
||||
```ts
|
||||
export class EmailAgent extends Agent<Env> {
|
||||
async onEmail(email: AgentEmail) {
|
||||
const [text, from, subject] = [await email.text(), email.from, email.headers.get("subject") || ""];
|
||||
this.sql`INSERT INTO emails (from_addr, subject, body) VALUES (${from}, ${subject}, ${text})`;
|
||||
|
||||
const { text: summary } = await generateText({
|
||||
model: openai("gpt-4o-mini"), prompt: `Summarize: ${subject}\n\n${text}`
|
||||
});
|
||||
|
||||
this.connections.forEach(c => c.send(JSON.stringify({type: "new_email", from, summary})));
|
||||
if (summary.includes("urgent")) await this.schedule(0, "sendAutoReply", { to: from });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Real-time Collaboration
|
||||
|
||||
```ts
|
||||
export class GameAgent extends Agent<Env> {
|
||||
initialState = { players: [], gameStarted: false };
|
||||
|
||||
async onConnect(conn: Connection, ctx: ConnectionContext) {
|
||||
conn.accept();
|
||||
const playerId = ctx.request.headers.get("X-Player-ID") || crypto.randomUUID();
|
||||
conn.setState({ playerId });
|
||||
|
||||
const newPlayer = { id: playerId, score: 0 };
|
||||
this.setState({...this.state, players: [...this.state.players, newPlayer]});
|
||||
this.connections.forEach(c => c.send(JSON.stringify({type: "player_joined", player: newPlayer})));
|
||||
}
|
||||
|
||||
async onMessage(conn: Connection, msg: WSMessage) {
|
||||
const m = JSON.parse(msg as string);
|
||||
|
||||
if (m.type === "move") {
|
||||
this.setState({
|
||||
...this.state,
|
||||
players: this.state.players.map(p => p.id === conn.state.playerId ? {...p, score: p.score + m.points} : p)
|
||||
});
|
||||
this.connections.forEach(c => c.send(JSON.stringify({type: "player_moved", playerId: conn.state.playerId})));
|
||||
}
|
||||
|
||||
if (m.type === "start" && this.state.players.length >= 2) {
|
||||
this.setState({...this.state, gameStarted: true});
|
||||
this.connections.forEach(c => c.send(JSON.stringify({type: "game_started"})));
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
175
.agents/skills/cloudflare-deploy/references/ai-gateway/README.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# Cloudflare AI Gateway
|
||||
|
||||
Expert guidance for implementing Cloudflare AI Gateway - a universal gateway for AI model providers with analytics, caching, rate limiting, and routing capabilities.
|
||||
|
||||
## When to Use This Reference
|
||||
|
||||
- Setting up AI Gateway for any AI provider (OpenAI, Anthropic, Workers AI, etc.)
|
||||
- Implementing caching, rate limiting, or request retry/fallback
|
||||
- Configuring dynamic routing with A/B testing or model fallbacks
|
||||
- Managing provider API keys securely with BYOK
|
||||
- Adding security features (guardrails, DLP)
|
||||
- Setting up observability with logging and custom metadata
|
||||
- Debugging AI Gateway requests or optimizing configurations
|
||||
|
||||
## Quick Start
|
||||
|
||||
**What's your setup?**
|
||||
|
||||
- **Using Vercel AI SDK** → Pattern 1 (recommended) - see [sdk-integration.md](./sdk-integration.md)
|
||||
- **Using OpenAI SDK** → Pattern 2 - see [sdk-integration.md](./sdk-integration.md)
|
||||
- **Cloudflare Worker + Workers AI** → Pattern 3 - see [sdk-integration.md](./sdk-integration.md)
|
||||
- **Direct HTTP (any language)** → Pattern 4 - see [configuration.md](./configuration.md)
|
||||
- **Framework (LangChain, etc.)** → See [sdk-integration.md](./sdk-integration.md)
|
||||
|
||||
## Pattern 1: Vercel AI SDK (Recommended)
|
||||
|
||||
Most modern pattern using official `ai-gateway-provider` package with automatic fallbacks.
|
||||
|
||||
```typescript
|
||||
import { createAiGateway } from 'ai-gateway-provider';
|
||||
import { createOpenAI } from '@ai-sdk/openai';
|
||||
import { generateText } from 'ai';
|
||||
|
||||
const gateway = createAiGateway({
|
||||
accountId: process.env.CF_ACCOUNT_ID,
|
||||
gateway: process.env.CF_GATEWAY_ID,
|
||||
});
|
||||
|
||||
const openai = createOpenAI({
|
||||
apiKey: process.env.OPENAI_API_KEY
|
||||
});
|
||||
|
||||
// Single model
|
||||
const { text } = await generateText({
|
||||
model: gateway(openai('gpt-4o')),
|
||||
prompt: 'Hello'
|
||||
});
|
||||
|
||||
// Automatic fallback array
|
||||
const { text } = await generateText({
|
||||
model: gateway([
|
||||
openai('gpt-4o'), // Try first
|
||||
anthropic('claude-sonnet-4-5'), // Fallback
|
||||
]),
|
||||
prompt: 'Hello'
|
||||
});
|
||||
```
|
||||
|
||||
**Install:** `npm install ai-gateway-provider ai @ai-sdk/openai @ai-sdk/anthropic`
|
||||
|
||||
## Pattern 2: OpenAI SDK
|
||||
|
||||
Drop-in replacement for OpenAI API with multi-provider support.
|
||||
|
||||
```typescript
|
||||
import OpenAI from 'openai';
|
||||
|
||||
const client = new OpenAI({
|
||||
apiKey: process.env.OPENAI_API_KEY,
|
||||
baseURL: `https://gateway.ai.cloudflare.com/v1/${accountId}/${gatewayId}/compat`,
|
||||
defaultHeaders: {
|
||||
'cf-aig-authorization': `Bearer ${cfToken}` // For authenticated gateways
|
||||
}
|
||||
});
|
||||
|
||||
// Switch providers by changing model format: {provider}/{model}
|
||||
const response = await client.chat.completions.create({
|
||||
model: 'openai/gpt-4o', // or 'anthropic/claude-sonnet-4-5'
|
||||
messages: [{ role: 'user', content: 'Hello!' }]
|
||||
});
|
||||
```
|
||||
|
||||
## Pattern 3: Workers AI Binding
|
||||
|
||||
For Cloudflare Workers using Workers AI.
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
async fetch(request, env, ctx) {
|
||||
const response = await env.AI.run(
|
||||
'@cf/meta/llama-3-8b-instruct',
|
||||
{ messages: [{ role: 'user', content: 'Hello!' }] },
|
||||
{
|
||||
gateway: {
|
||||
id: 'my-gateway',
|
||||
metadata: { userId: '123', team: 'engineering' }
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return Response.json(response);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Headers Quick Reference
|
||||
|
||||
| Header | Purpose | Example | Notes |
|
||||
|--------|---------|---------|-------|
|
||||
| `cf-aig-authorization` | Gateway auth | `Bearer {token}` | Required for authenticated gateways |
|
||||
| `cf-aig-metadata` | Tracking | `{"userId":"x"}` | Max 5 entries, flat structure |
|
||||
| `cf-aig-cache-ttl` | Cache duration | `3600` | Seconds, min 60, max 2592000 (30 days) |
|
||||
| `cf-aig-skip-cache` | Bypass cache | `true` | - |
|
||||
| `cf-aig-cache-key` | Custom cache key | `my-key` | Must be unique per response |
|
||||
| `cf-aig-collect-log` | Skip logging | `false` | Default: true |
|
||||
| `cf-aig-cache-status` | Cache hit/miss | Response only | `HIT` or `MISS` |
|
||||
|
||||
## In This Reference
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| [sdk-integration.md](./sdk-integration.md) | Vercel AI SDK, OpenAI SDK, Workers binding patterns |
|
||||
| [configuration.md](./configuration.md) | Dashboard setup, wrangler, API tokens |
|
||||
| [features.md](./features.md) | Caching, rate limits, guardrails, DLP, BYOK, unified billing |
|
||||
| [dynamic-routing.md](./dynamic-routing.md) | Fallbacks, A/B testing, conditional routing |
|
||||
| [troubleshooting.md](./troubleshooting.md) | Debugging, errors, observability, gotchas |
|
||||
|
||||
## Reading Order
|
||||
|
||||
| Task | Files |
|
||||
|------|-------|
|
||||
| First-time setup | README + [configuration.md](./configuration.md) |
|
||||
| SDK integration | README + [sdk-integration.md](./sdk-integration.md) |
|
||||
| Enable caching | README + [features.md](./features.md) |
|
||||
| Setup fallbacks | README + [dynamic-routing.md](./dynamic-routing.md) |
|
||||
| Debug errors | README + [troubleshooting.md](./troubleshooting.md) |
|
||||
|
||||
## Architecture
|
||||
|
||||
AI Gateway acts as a proxy between your application and AI providers:
|
||||
|
||||
```
|
||||
Your App → AI Gateway → AI Provider (OpenAI, Anthropic, etc.)
|
||||
↓
|
||||
Analytics, Caching, Rate Limiting, Logging
|
||||
```
|
||||
|
||||
**Key URL patterns:**
|
||||
- Unified API (OpenAI-compatible): `https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/compat/chat/completions`
|
||||
- Provider-specific: `https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/{provider}/{endpoint}`
|
||||
- Dynamic routes: Use route name instead of model: `dynamic/{route-name}`
|
||||
|
||||
## Gateway Types
|
||||
|
||||
1. **Unauthenticated Gateway**: Open access (not recommended for production)
|
||||
2. **Authenticated Gateway**: Requires `cf-aig-authorization` header with Cloudflare API token (recommended)
|
||||
|
||||
## Provider Authentication Options
|
||||
|
||||
1. **Unified Billing**: Use AI Gateway billing to pay for inference (keyless mode - no provider API key needed)
|
||||
2. **BYOK (Store Keys)**: Store provider API keys in Cloudflare dashboard
|
||||
3. **Request Headers**: Include provider API key in each request
|
||||
|
||||
## Related Skills
|
||||
|
||||
- [Workers AI](../workers-ai/README.md) - For `env.AI.run()` details
|
||||
- [Agents SDK](../agents-sdk/README.md) - For stateful AI patterns
|
||||
- [Vectorize](../vectorize/README.md) - For RAG patterns with embeddings
|
||||
|
||||
## Resources
|
||||
|
||||
- [Official Docs](https://developers.cloudflare.com/ai-gateway/)
|
||||
- [API Reference](https://developers.cloudflare.com/api/resources/ai_gateway/)
|
||||
- [Provider Guides](https://developers.cloudflare.com/ai-gateway/usage/providers/)
|
||||
- [Discord Community](https://discord.cloudflare.com)
|
||||
@@ -0,0 +1,111 @@
|
||||
# Configuration & Setup
|
||||
|
||||
## Creating a Gateway
|
||||
|
||||
### Dashboard
|
||||
AI > AI Gateway > Create Gateway > Configure (auth, caching, rate limiting, logging)
|
||||
|
||||
### API
|
||||
```bash
|
||||
curl -X POST https://api.cloudflare.com/client/v4/accounts/{account_id}/ai-gateway/gateways \
|
||||
-H "Authorization: Bearer $CF_API_TOKEN" -H "Content-Type: application/json" \
|
||||
-d '{"id":"my-gateway","cache_ttl":3600,"rate_limiting_interval":60,"rate_limiting_limit":100,"collect_logs":true}'
|
||||
```
|
||||
|
||||
**Naming:** lowercase alphanumeric + hyphens (e.g., `prod-api`, `dev-chat`)
|
||||
|
||||
## Wrangler Integration
|
||||
|
||||
```toml
|
||||
[ai]
|
||||
binding = "AI"
|
||||
|
||||
[[ai.gateway]]
|
||||
id = "my-gateway"
|
||||
```
|
||||
|
||||
```bash
|
||||
wrangler secret put CF_API_TOKEN
|
||||
wrangler secret put OPENAI_API_KEY # If not using BYOK
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
### Gateway Auth (protects gateway access)
|
||||
```typescript
|
||||
const client = new OpenAI({
|
||||
baseURL: `https://gateway.ai.cloudflare.com/v1/${accountId}/${gatewayId}/openai`,
|
||||
defaultHeaders: { 'cf-aig-authorization': `Bearer ${cfToken}` }
|
||||
});
|
||||
```
|
||||
|
||||
### Provider Auth Options
|
||||
|
||||
**1. Unified Billing (keyless)** - pay through Cloudflare, no provider key:
|
||||
```typescript
|
||||
const client = new OpenAI({
|
||||
baseURL: `https://gateway.ai.cloudflare.com/v1/${accountId}/${gatewayId}/openai`,
|
||||
defaultHeaders: { 'cf-aig-authorization': `Bearer ${cfToken}` }
|
||||
});
|
||||
```
|
||||
Supports: OpenAI, Anthropic, Google AI Studio
|
||||
|
||||
**2. BYOK** - store keys in dashboard (Provider Keys > Add), no key in code
|
||||
|
||||
**3. Request Headers** - pass provider key per request:
|
||||
```typescript
|
||||
const client = new OpenAI({
|
||||
apiKey: process.env.OPENAI_API_KEY,
|
||||
baseURL: `https://gateway.ai.cloudflare.com/v1/${accountId}/${gatewayId}/openai`,
|
||||
defaultHeaders: { 'cf-aig-authorization': `Bearer ${cfToken}` }
|
||||
});
|
||||
```
|
||||
|
||||
## API Token Permissions
|
||||
|
||||
- **Gateway management:** AI Gateway - Read + Edit
|
||||
- **Gateway access:** AI Gateway - Read (minimum)
|
||||
|
||||
## Gateway Management API
|
||||
|
||||
```bash
|
||||
# List
|
||||
curl https://api.cloudflare.com/client/v4/accounts/{account_id}/ai-gateway/gateways \
|
||||
-H "Authorization: Bearer $CF_API_TOKEN"
|
||||
|
||||
# Get
|
||||
curl .../gateways/{gateway_id}
|
||||
|
||||
# Update
|
||||
curl -X PUT .../gateways/{gateway_id} \
|
||||
-d '{"cache_ttl":7200,"rate_limiting_limit":200}'
|
||||
|
||||
# Delete
|
||||
curl -X DELETE .../gateways/{gateway_id}
|
||||
```
|
||||
|
||||
## Getting IDs
|
||||
|
||||
- **Account ID:** Dashboard > Overview > Copy
|
||||
- **Gateway ID:** AI Gateway > Gateway name column
|
||||
|
||||
## Python Example
|
||||
|
||||
```python
|
||||
from openai import OpenAI
|
||||
import os
|
||||
|
||||
client = OpenAI(
|
||||
api_key=os.environ.get("OPENAI_API_KEY"),
|
||||
base_url=f"https://gateway.ai.cloudflare.com/v1/{os.environ['CF_ACCOUNT_ID']}/{os.environ['GATEWAY_ID']}/openai",
|
||||
default_headers={"cf-aig-authorization": f"Bearer {os.environ['CF_API_TOKEN']}"}
|
||||
)
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always authenticate gateways in production**
|
||||
2. **Use BYOK or unified billing** - secrets out of code
|
||||
3. **Environment-specific gateways** - separate dev/staging/prod
|
||||
4. **Set rate limits** - prevent runaway costs
|
||||
5. **Enable logging** - track usage, debug issues
|
||||
@@ -0,0 +1,82 @@
|
||||
# Dynamic Routing
|
||||
|
||||
Configure complex routing in dashboard without code changes. Use route names instead of model names.
|
||||
|
||||
## Usage
|
||||
|
||||
```typescript
|
||||
const response = await client.chat.completions.create({
|
||||
model: 'dynamic/smart-chat', // Route name from dashboard
|
||||
messages: [{ role: 'user', content: 'Hello!' }]
|
||||
});
|
||||
```
|
||||
|
||||
## Node Types
|
||||
|
||||
| Node | Purpose | Use Case |
|
||||
|------|---------|----------|
|
||||
| **Conditional** | Branch on metadata | Paid vs free users, geo routing |
|
||||
| **Percentage** | A/B split traffic | Model testing, gradual rollouts |
|
||||
| **Rate Limit** | Enforce quotas | Per-user/team limits |
|
||||
| **Budget Limit** | Cost quotas | Per-user spending caps |
|
||||
| **Model** | Call provider | Final destination |
|
||||
|
||||
## Metadata
|
||||
|
||||
Pass via header (max 5 entries, flat only):
|
||||
```typescript
|
||||
headers: {
|
||||
'cf-aig-metadata': JSON.stringify({
|
||||
userId: 'user-123',
|
||||
tier: 'pro',
|
||||
region: 'us-east'
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
**Multi-model fallback:**
|
||||
```
|
||||
Start → GPT-4 → On error: Claude → On error: Llama
|
||||
```
|
||||
|
||||
**Tiered access:**
|
||||
```
|
||||
Conditional: tier == 'enterprise' → GPT-4 (no limit)
|
||||
Conditional: tier == 'pro' → Rate Limit 1000/hr → GPT-4o
|
||||
Conditional: tier == 'free' → Rate Limit 10/hr → GPT-4o-mini
|
||||
```
|
||||
|
||||
**Gradual rollout:**
|
||||
```
|
||||
Percentage: 10% → New model, 90% → Old model
|
||||
```
|
||||
|
||||
**Cost-based fallback:**
|
||||
```
|
||||
Budget Limit: $100/day per teamId
|
||||
< 80%: GPT-4
|
||||
>= 80%: GPT-4o-mini
|
||||
>= 100%: Error
|
||||
```
|
||||
|
||||
## Version Management
|
||||
|
||||
- Save changes as new version
|
||||
- Test with `model: 'dynamic/route@v2'`
|
||||
- Roll back by deploying previous version
|
||||
|
||||
## Monitoring
|
||||
|
||||
Dashboard → Gateway → Dynamic Routes:
|
||||
- Request count per path
|
||||
- Success/error rates
|
||||
- Latency/cost by path
|
||||
|
||||
## Limitations
|
||||
|
||||
- Max 5 metadata entries
|
||||
- Values: string/number/boolean/null only
|
||||
- No nested objects
|
||||
- Route names: alphanumeric + hyphens
|
||||
@@ -0,0 +1,96 @@
|
||||
# Features & Capabilities
|
||||
|
||||
## Caching
|
||||
|
||||
Dashboard: Settings → Cache Responses → Enable
|
||||
|
||||
```typescript
|
||||
// Custom TTL (1 hour)
|
||||
headers: { 'cf-aig-cache-ttl': '3600' }
|
||||
|
||||
// Skip cache
|
||||
headers: { 'cf-aig-skip-cache': 'true' }
|
||||
|
||||
// Custom cache key
|
||||
headers: { 'cf-aig-cache-key': 'greeting-en' }
|
||||
```
|
||||
|
||||
**Limits:** TTL 60s - 30 days. **Does NOT work with streaming.**
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
Dashboard: Settings → Rate-limiting → Enable
|
||||
|
||||
- **Fixed window:** Resets at intervals
|
||||
- **Sliding window:** Rolling window (more accurate)
|
||||
- Returns `429` when exceeded
|
||||
|
||||
## Guardrails
|
||||
|
||||
Dashboard: Settings → Guardrails → Enable
|
||||
|
||||
Filter prompts/responses for inappropriate content. Actions: Flag (log) or Block (reject).
|
||||
|
||||
## Data Loss Prevention (DLP)
|
||||
|
||||
Dashboard: Settings → DLP → Enable
|
||||
|
||||
Detect PII (emails, SSNs, credit cards). Actions: Flag, Block, or Redact.
|
||||
|
||||
## Billing Modes
|
||||
|
||||
| Mode | Description | Setup |
|
||||
|------|-------------|-------|
|
||||
| **Unified Billing** | Pay through Cloudflare, no provider keys | Use `cf-aig-authorization` header only |
|
||||
| **BYOK** | Store provider keys in dashboard | Add keys in Provider Keys section |
|
||||
| **Pass-through** | Send provider key with each request | Include provider's auth header |
|
||||
|
||||
## Zero Data Retention
|
||||
|
||||
Dashboard: Settings → Privacy → Zero Data Retention
|
||||
|
||||
No prompts/responses stored. Request counts and costs still tracked.
|
||||
|
||||
## Logging
|
||||
|
||||
Dashboard: Settings → Logs → Enable (up to 10M logs)
|
||||
|
||||
Each entry: prompt, response, provider, model, tokens, cost, duration, cache status, metadata.
|
||||
|
||||
```typescript
|
||||
// Skip logging for request
|
||||
headers: { 'cf-aig-collect-log': 'false' }
|
||||
```
|
||||
|
||||
**Export:** Use Logpush to S3, GCS, Datadog, Splunk, etc.
|
||||
|
||||
## Custom Cost Tracking
|
||||
|
||||
For models not in Cloudflare's pricing database:
|
||||
|
||||
Dashboard: Gateway → Settings → Custom Costs
|
||||
|
||||
Or via API: set `model`, `input_cost`, `output_cost`.
|
||||
|
||||
## Supported Providers (22+)
|
||||
|
||||
| Provider | Unified API | Notes |
|
||||
|----------|-------------|-------|
|
||||
| OpenAI | `openai/gpt-4o` | Full support |
|
||||
| Anthropic | `anthropic/claude-sonnet-4-5` | Full support |
|
||||
| Google AI | `google-ai-studio/gemini-2.0-flash` | Full support |
|
||||
| Workers AI | `workersai/@cf/meta/llama-3` | Native |
|
||||
| Azure OpenAI | `azure-openai/*` | Deployment names |
|
||||
| AWS Bedrock | Provider endpoint only | `/bedrock/*` |
|
||||
| Groq | `groq/*` | Fast inference |
|
||||
| Mistral, Cohere, Perplexity, xAI, DeepSeek, Cerebras | Full support | - |
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. Enable caching for deterministic prompts
|
||||
2. Set rate limits to prevent abuse
|
||||
3. Use guardrails for user-facing AI
|
||||
4. Enable DLP for sensitive data
|
||||
5. Use unified billing or BYOK for simpler key management
|
||||
6. Enable logging for debugging
|
||||
7. Use zero data retention when privacy required
|
||||
@@ -0,0 +1,114 @@
|
||||
# AI Gateway SDK Integration
|
||||
|
||||
## Vercel AI SDK (Recommended)
|
||||
|
||||
```typescript
|
||||
import { createAiGateway } from 'ai-gateway-provider';
|
||||
import { createOpenAI } from '@ai-sdk/openai';
|
||||
import { generateText } from 'ai';
|
||||
|
||||
const gateway = createAiGateway({
|
||||
accountId: process.env.CF_ACCOUNT_ID,
|
||||
gateway: process.env.CF_GATEWAY_ID,
|
||||
apiKey: process.env.CF_API_TOKEN // Optional for auth gateways
|
||||
});
|
||||
|
||||
const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
||||
|
||||
// Single model
|
||||
const { text } = await generateText({
|
||||
model: gateway(openai('gpt-4o')),
|
||||
prompt: 'Hello'
|
||||
});
|
||||
|
||||
// Automatic fallback array
|
||||
const { text } = await generateText({
|
||||
model: gateway([
|
||||
openai('gpt-4o'),
|
||||
anthropic('claude-sonnet-4-5'),
|
||||
openai('gpt-4o-mini')
|
||||
]),
|
||||
prompt: 'Complex task'
|
||||
});
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```typescript
|
||||
model: gateway(openai('gpt-4o'), {
|
||||
cacheKey: 'my-key',
|
||||
cacheTtl: 3600,
|
||||
metadata: { userId: 'u123', team: 'eng' }, // Max 5 entries
|
||||
retries: { maxAttempts: 3, backoff: 'exponential' }
|
||||
})
|
||||
```
|
||||
|
||||
## OpenAI SDK
|
||||
|
||||
```typescript
|
||||
const client = new OpenAI({
|
||||
apiKey: process.env.OPENAI_API_KEY,
|
||||
baseURL: `https://gateway.ai.cloudflare.com/v1/${accountId}/${gatewayId}/openai`,
|
||||
defaultHeaders: { 'cf-aig-authorization': `Bearer ${cfToken}` }
|
||||
});
|
||||
|
||||
// Unified API - switch providers via model name
|
||||
model: 'openai/gpt-4o' // or 'anthropic/claude-sonnet-4-5'
|
||||
```
|
||||
|
||||
## Anthropic SDK
|
||||
|
||||
```typescript
|
||||
const client = new Anthropic({
|
||||
apiKey: process.env.ANTHROPIC_API_KEY,
|
||||
baseURL: `https://gateway.ai.cloudflare.com/v1/${accountId}/${gatewayId}/anthropic`,
|
||||
defaultHeaders: { 'cf-aig-authorization': `Bearer ${cfToken}` }
|
||||
});
|
||||
```
|
||||
|
||||
## Workers AI Binding
|
||||
|
||||
```toml
|
||||
# wrangler.toml
|
||||
[ai]
|
||||
binding = "AI"
|
||||
[[ai.gateway]]
|
||||
id = "my-gateway"
|
||||
```
|
||||
|
||||
```typescript
|
||||
await env.AI.run('@cf/meta/llama-3-8b-instruct',
|
||||
{ messages: [...] },
|
||||
{ gateway: { id: 'my-gateway', metadata: { userId: '123' } } }
|
||||
);
|
||||
```
|
||||
|
||||
## LangChain / LlamaIndex
|
||||
|
||||
```typescript
|
||||
// Use OpenAI SDK pattern with custom baseURL
|
||||
new ChatOpenAI({
|
||||
configuration: {
|
||||
baseURL: `https://gateway.ai.cloudflare.com/v1/${accountId}/${gatewayId}/openai`
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## HTTP / cURL
|
||||
|
||||
```bash
|
||||
curl https://gateway.ai.cloudflare.com/v1/{account}/{gateway}/openai/chat/completions \
|
||||
-H "Authorization: Bearer $OPENAI_KEY" \
|
||||
-H "cf-aig-authorization: Bearer $CF_TOKEN" \
|
||||
-H "cf-aig-metadata: {\"userId\":\"123\"}" \
|
||||
-d '{"model":"gpt-4o","messages":[...]}'
|
||||
```
|
||||
|
||||
## Headers Reference
|
||||
|
||||
| Header | Purpose |
|
||||
|--------|---------|
|
||||
| `cf-aig-authorization` | Gateway auth token |
|
||||
| `cf-aig-metadata` | JSON object (max 5 keys) |
|
||||
| `cf-aig-cache-ttl` | Cache TTL in seconds |
|
||||
| `cf-aig-skip-cache` | `true` to bypass cache |
|
||||
@@ -0,0 +1,88 @@
|
||||
# AI Gateway Troubleshooting
|
||||
|
||||
## Common Errors
|
||||
|
||||
| Error | Cause | Fix |
|
||||
|-------|-------|-----|
|
||||
| 401 | Missing `cf-aig-authorization` header | Add header with CF API token |
|
||||
| 403 | Invalid provider key / BYOK expired | Check provider key in dashboard |
|
||||
| 429 | Rate limit exceeded | Increase limit or implement backoff |
|
||||
|
||||
### 401 Fix
|
||||
|
||||
```typescript
|
||||
const client = new OpenAI({
|
||||
baseURL: `https://gateway.ai.cloudflare.com/v1/${accountId}/${gatewayId}/openai`,
|
||||
defaultHeaders: { 'cf-aig-authorization': `Bearer ${CF_API_TOKEN}` }
|
||||
});
|
||||
```
|
||||
|
||||
### 429 Retry Pattern
|
||||
|
||||
```typescript
|
||||
async function requestWithRetry(fn, maxRetries = 3) {
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try { return await fn(); }
|
||||
catch (e) {
|
||||
if (e.status === 429 && i < maxRetries - 1) {
|
||||
await new Promise(r => setTimeout(r, Math.pow(2, i) * 1000));
|
||||
continue;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Gotchas
|
||||
|
||||
| Issue | Reality |
|
||||
|-------|---------|
|
||||
| Metadata limits | Max 5 entries, flat only (no nesting) |
|
||||
| Cache key collision | Use unique keys per expected response |
|
||||
| BYOK + Unified Billing | Mutually exclusive |
|
||||
| Rate limit scope | Per-gateway, not per-user (use dynamic routing for per-user) |
|
||||
| Log delay | 30-60 seconds normal |
|
||||
| Streaming + caching | **Incompatible** |
|
||||
| Model name (unified API) | Prefix required: `openai/gpt-4o`, not `gpt-4o` |
|
||||
|
||||
## Cache Not Working
|
||||
|
||||
**Causes:**
|
||||
- Different request params (temperature, etc.)
|
||||
- Streaming enabled
|
||||
- Caching disabled in settings
|
||||
|
||||
**Check:** `response.headers.get('cf-aig-cache-status')` → HIT or MISS
|
||||
|
||||
## Logs Not Appearing
|
||||
|
||||
1. Check logging enabled: Dashboard → Gateway → Settings
|
||||
2. Remove `cf-aig-collect-log: false` header
|
||||
3. Wait 30-60 seconds
|
||||
4. Check log limit (10M default)
|
||||
|
||||
## Debugging
|
||||
|
||||
```bash
|
||||
# Test connectivity
|
||||
curl -v https://gateway.ai.cloudflare.com/v1/{account}/{gateway}/openai/models \
|
||||
-H "Authorization: Bearer $OPENAI_KEY" \
|
||||
-H "cf-aig-authorization: Bearer $CF_TOKEN"
|
||||
```
|
||||
|
||||
```typescript
|
||||
// Check response headers
|
||||
console.log('Cache:', response.headers.get('cf-aig-cache-status'));
|
||||
console.log('Request ID:', response.headers.get('cf-ray'));
|
||||
```
|
||||
|
||||
## Analytics
|
||||
|
||||
Dashboard → AI Gateway → Select gateway
|
||||
|
||||
**Metrics:** Requests, tokens, latency (p50/p95/p99), cache hit rate, costs
|
||||
|
||||
**Log filters:** `status: error`, `provider: openai`, `cost > 0.01`, `duration > 1000`
|
||||
|
||||
**Export:** Logpush to S3/GCS/Datadog/Splunk
|
||||
138
.agents/skills/cloudflare-deploy/references/ai-search/README.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# Cloudflare AI Search Reference
|
||||
|
||||
Expert guidance for implementing Cloudflare AI Search (formerly AutoRAG), Cloudflare's managed semantic search and RAG service.
|
||||
|
||||
## Overview
|
||||
|
||||
**AI Search** is a managed RAG (Retrieval-Augmented Generation) pipeline that combines:
|
||||
- Automatic semantic indexing of your content
|
||||
- Vector similarity search
|
||||
- Built-in LLM generation
|
||||
|
||||
**Key value propositions:**
|
||||
- **Zero vector management** - No manual embedding, indexing, or storage
|
||||
- **Auto-indexing** - Content automatically re-indexed every 6 hours
|
||||
- **Built-in generation** - Optional AI response generation from retrieved context
|
||||
- **Multi-source** - Index from R2 buckets or website crawls
|
||||
|
||||
**Data source options:**
|
||||
- **R2 bucket** - Index files from Cloudflare R2 (supports MD, TXT, HTML, PDF, DOC, CSV, JSON)
|
||||
- **Website** - Crawl and index website content (requires Cloudflare-hosted domain)
|
||||
|
||||
**Indexing lifecycle:**
|
||||
- Automatic 6-hour refresh cycle
|
||||
- Manual "Force Sync" available (30s rate limit)
|
||||
- Not designed for real-time updates
|
||||
|
||||
## Quick Start
|
||||
|
||||
**1. Create AI Search instance in dashboard:**
|
||||
- Go to Cloudflare Dashboard → AI Search → Create
|
||||
- Choose data source (R2 or website)
|
||||
- Configure instance name and settings
|
||||
|
||||
**2. Configure Worker:**
|
||||
|
||||
```jsonc
|
||||
// wrangler.jsonc
|
||||
{
|
||||
"ai": {
|
||||
"binding": "AI"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**3. Use in Worker:**
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
async fetch(request, env) {
|
||||
const answer = await env.AI.autorag("my-search-instance").aiSearch({
|
||||
query: "How do I configure caching?",
|
||||
model: "@cf/meta/llama-3.3-70b-instruct-fp8-fast"
|
||||
});
|
||||
|
||||
return Response.json({ answer: answer.response });
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## When to Use AI Search
|
||||
|
||||
### AI Search vs Vectorize
|
||||
|
||||
| Factor | AI Search | Vectorize |
|
||||
|--------|-----------|-----------|
|
||||
| **Management** | Fully managed | Manual embedding + indexing |
|
||||
| **Use when** | Want zero-ops RAG pipeline | Need custom embeddings/control |
|
||||
| **Indexing** | Automatic (6hr cycle) | Manual via API |
|
||||
| **Generation** | Built-in optional | Bring your own LLM |
|
||||
| **Data sources** | R2 or website | Manual insert |
|
||||
| **Best for** | Docs, support, enterprise search | Custom ML pipelines, real-time |
|
||||
|
||||
### AI Search vs Direct Workers AI
|
||||
|
||||
| Factor | AI Search | Workers AI (direct) |
|
||||
|--------|-----------|---------------------|
|
||||
| **Context** | Automatic retrieval | Manual context building |
|
||||
| **Use when** | Need RAG (search + generate) | Simple generation tasks |
|
||||
| **Indexing** | Built-in | Not applicable |
|
||||
| **Best for** | Knowledge bases, docs | Simple chat, transformations |
|
||||
|
||||
### search() vs aiSearch()
|
||||
|
||||
| Method | Returns | Use When |
|
||||
|--------|---------|----------|
|
||||
| `search()` | Search results only | Building custom UI, need raw chunks |
|
||||
| `aiSearch()` | AI response + results | Need ready-to-use answer (chatbot, Q&A) |
|
||||
|
||||
### Real-time Updates Consideration
|
||||
|
||||
**AI Search is NOT ideal if:**
|
||||
- Need real-time content updates (<6 hours)
|
||||
- Content changes multiple times per hour
|
||||
- Strict freshness requirements
|
||||
|
||||
**AI Search IS ideal if:**
|
||||
- Content relatively stable (docs, policies, knowledge bases)
|
||||
- 6-hour refresh acceptable
|
||||
- Prefer zero-ops over real-time
|
||||
|
||||
## Platform Limits
|
||||
|
||||
| Limit | Value |
|
||||
|-------|-------|
|
||||
| Max instances per account | 10 |
|
||||
| Max files per instance | 100,000 |
|
||||
| Max file size | 4 MB |
|
||||
| Index frequency | Every 6 hours |
|
||||
| Force Sync rate limit | Once per 30 seconds |
|
||||
| Filter nesting depth | 2 levels |
|
||||
| Filters per compound | 10 |
|
||||
| Score threshold range | 0.0 - 1.0 |
|
||||
|
||||
## Reading Order
|
||||
|
||||
Navigate these references based on your task:
|
||||
|
||||
| Task | Read | Est. Time |
|
||||
|------|------|-----------|
|
||||
| **Understand AI Search** | README only | 5 min |
|
||||
| **Implement basic search** | README → api.md | 10 min |
|
||||
| **Configure data source** | README → configuration.md | 10 min |
|
||||
| **Production patterns** | patterns.md | 15 min |
|
||||
| **Debug issues** | gotchas.md | 10 min |
|
||||
| **Full implementation** | README → api.md → patterns.md | 30 min |
|
||||
|
||||
## In This Reference
|
||||
|
||||
- **[api.md](api.md)** - API endpoints, methods, TypeScript interfaces
|
||||
- **[configuration.md](configuration.md)** - Setup, data sources, wrangler config
|
||||
- **[patterns.md](patterns.md)** - Common patterns, decision guidance, code examples
|
||||
- **[gotchas.md](gotchas.md)** - Troubleshooting, code-level gotchas, limits
|
||||
|
||||
## See Also
|
||||
|
||||
- [Cloudflare AI Search Docs](https://developers.cloudflare.com/ai-search/)
|
||||
- [Workers AI Docs](https://developers.cloudflare.com/workers-ai/)
|
||||
- [Vectorize Docs](https://developers.cloudflare.com/vectorize/)
|
||||
87
.agents/skills/cloudflare-deploy/references/ai-search/api.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# AI Search API Reference
|
||||
|
||||
## Workers Binding
|
||||
|
||||
```typescript
|
||||
const answer = await env.AI.autorag("instance-name").aiSearch(options);
|
||||
const results = await env.AI.autorag("instance-name").search(options);
|
||||
const instances = await env.AI.autorag("_").listInstances();
|
||||
```
|
||||
|
||||
## aiSearch() Options
|
||||
|
||||
```typescript
|
||||
interface AiSearchOptions {
|
||||
query: string; // User query
|
||||
model: string; // Workers AI model ID
|
||||
system_prompt?: string; // LLM instructions
|
||||
rewrite_query?: boolean; // Fix typos (default: false)
|
||||
max_num_results?: number; // Max chunks (default: 10)
|
||||
ranking_options?: { score_threshold?: number }; // 0.0-1.0 (default: 0.3)
|
||||
reranking?: { enabled: boolean; model: string };
|
||||
stream?: boolean; // Stream response (default: false)
|
||||
filters?: Filter; // Metadata filters
|
||||
page?: string; // Pagination token
|
||||
}
|
||||
```
|
||||
|
||||
## Response
|
||||
|
||||
```typescript
|
||||
interface AiSearchResponse {
|
||||
search_query: string; // Query used (rewritten if enabled)
|
||||
response: string; // AI-generated answer
|
||||
data: SearchResult[]; // Retrieved chunks
|
||||
has_more: boolean;
|
||||
next_page?: string;
|
||||
}
|
||||
|
||||
interface SearchResult {
|
||||
id: string;
|
||||
score: number;
|
||||
content: string;
|
||||
metadata: { filename: string; folder: string; timestamp: number };
|
||||
}
|
||||
```
|
||||
|
||||
## Filters
|
||||
|
||||
```typescript
|
||||
// Comparison
|
||||
{ column: "folder", operator: "gte", value: "docs/" }
|
||||
|
||||
// Compound
|
||||
{ operator: "and", filters: [
|
||||
{ column: "folder", operator: "gte", value: "docs/" },
|
||||
{ column: "timestamp", operator: "gte", value: 1704067200 }
|
||||
]}
|
||||
```
|
||||
|
||||
**Operators:** `eq`, `ne`, `gt`, `gte`, `lt`, `lte`
|
||||
|
||||
**Built-in metadata:** `filename`, `folder`, `timestamp` (Unix seconds)
|
||||
|
||||
## Streaming
|
||||
|
||||
```typescript
|
||||
const stream = await env.AI.autorag("docs").aiSearch({ query, model, stream: true });
|
||||
return new Response(stream, { headers: { "Content-Type": "text/event-stream" } });
|
||||
```
|
||||
|
||||
## Error Types
|
||||
|
||||
| Error | Cause |
|
||||
|-------|-------|
|
||||
| `AutoRAGNotFoundError` | Instance doesn't exist |
|
||||
| `AutoRAGUnauthorizedError` | Invalid/missing token |
|
||||
| `AutoRAGValidationError` | Invalid parameters |
|
||||
|
||||
## REST API
|
||||
|
||||
```bash
|
||||
curl https://api.cloudflare.com/client/v4/accounts/{ACCOUNT_ID}/autorag/rags/{NAME}/ai-search \
|
||||
-H "Authorization: Bearer {TOKEN}" \
|
||||
-d '{"query": "...", "model": "@cf/meta/llama-3.3-70b-instruct-fp8-fast"}'
|
||||
```
|
||||
|
||||
Requires Service API token with "AI Search - Read" permission.
|
||||
@@ -0,0 +1,88 @@
|
||||
# AI Search Configuration
|
||||
|
||||
## Worker Setup
|
||||
|
||||
```jsonc
|
||||
// wrangler.jsonc
|
||||
{
|
||||
"ai": { "binding": "AI" }
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
interface Env {
|
||||
AI: Ai;
|
||||
}
|
||||
|
||||
const answer = await env.AI.autorag("my-instance").aiSearch({
|
||||
query: "How do I configure caching?",
|
||||
model: "@cf/meta/llama-3.3-70b-instruct-fp8-fast"
|
||||
});
|
||||
```
|
||||
|
||||
## Data Sources
|
||||
|
||||
### R2 Bucket
|
||||
|
||||
Dashboard: AI Search → Create Instance → Select R2 bucket
|
||||
|
||||
**Supported formats:** `.md`, `.txt`, `.html`, `.pdf`, `.doc`, `.docx`, `.csv`, `.json`
|
||||
|
||||
**Auto-indexed metadata:** `filename`, `folder`, `timestamp`
|
||||
|
||||
### Website Crawler
|
||||
|
||||
Requirements:
|
||||
- Domain on Cloudflare
|
||||
- `sitemap.xml` at root
|
||||
- Bot protection must allow `CloudflareAISearch` user agent
|
||||
|
||||
## Path Filtering (R2)
|
||||
|
||||
```
|
||||
docs/**/*.md # All .md in docs/ recursively
|
||||
**/*.draft.md # Exclude (use in exclude patterns)
|
||||
```
|
||||
|
||||
## Indexing
|
||||
|
||||
- **Automatic:** Every 6 hours
|
||||
- **Force Sync:** Dashboard button (30s rate limit between syncs)
|
||||
- **Pause:** Settings → Pause Indexing (existing index remains searchable)
|
||||
|
||||
## Service API Token
|
||||
|
||||
Dashboard: AI Search → Instance → Use AI Search → API → Create Token
|
||||
|
||||
Permissions:
|
||||
- **Read** - search operations
|
||||
- **Edit** - instance management
|
||||
|
||||
Store securely:
|
||||
```bash
|
||||
wrangler secret put AI_SEARCH_TOKEN
|
||||
```
|
||||
|
||||
## Multi-Environment
|
||||
|
||||
```toml
|
||||
# wrangler.toml
|
||||
[env.production.vars]
|
||||
AI_SEARCH_INSTANCE = "prod-docs"
|
||||
|
||||
[env.staging.vars]
|
||||
AI_SEARCH_INSTANCE = "staging-docs"
|
||||
```
|
||||
|
||||
```typescript
|
||||
const answer = await env.AI.autorag(env.AI_SEARCH_INSTANCE).aiSearch({ query });
|
||||
```
|
||||
|
||||
## Monitoring
|
||||
|
||||
```typescript
|
||||
const instances = await env.AI.autorag("_").listInstances();
|
||||
console.log(instances.find(i => i.name === "docs"));
|
||||
```
|
||||
|
||||
Dashboard shows: files indexed, status, last index time, storage usage.
|
||||
@@ -0,0 +1,81 @@
|
||||
# AI Search Gotchas
|
||||
|
||||
## Type Safety
|
||||
|
||||
**Timestamp precision:** Use seconds (10-digit), not milliseconds.
|
||||
```typescript
|
||||
const nowInSeconds = Math.floor(Date.now() / 1000); // Correct
|
||||
```
|
||||
|
||||
**Folder prefix matching:** Use `gte` for "starts with" on paths.
|
||||
```typescript
|
||||
filters: { column: "folder", operator: "gte", value: "docs/api/" } // Matches nested
|
||||
```
|
||||
|
||||
## Filter Limitations
|
||||
|
||||
| Limit | Value |
|
||||
|-------|-------|
|
||||
| Max nesting depth | 2 levels |
|
||||
| Filters per compound | 10 |
|
||||
| `or` operator | Same column, `eq` only |
|
||||
|
||||
**OR restriction example:**
|
||||
```typescript
|
||||
// ✅ Valid: same column, eq only
|
||||
{ operator: "or", filters: [
|
||||
{ column: "folder", operator: "eq", value: "docs/" },
|
||||
{ column: "folder", operator: "eq", value: "guides/" }
|
||||
]}
|
||||
```
|
||||
|
||||
## Indexing Issues
|
||||
|
||||
| Problem | Cause | Solution |
|
||||
|---------|-------|----------|
|
||||
| File not indexed | Unsupported format or >4MB | Check format (.md/.txt/.html/.pdf/.doc/.csv/.json) |
|
||||
| Index out of sync | 6-hour index cycle | Wait or use "Force Sync" (30s rate limit) |
|
||||
| Empty results | Index incomplete | Check dashboard for indexing status |
|
||||
|
||||
## Auth Errors
|
||||
|
||||
| Error | Cause | Fix |
|
||||
|-------|-------|-----|
|
||||
| `AutoRAGUnauthorizedError` | Invalid/missing token | Create Service API token with AI Search permissions |
|
||||
| `AutoRAGNotFoundError` | Wrong instance name | Verify exact name from dashboard |
|
||||
|
||||
## Performance
|
||||
|
||||
**Slow responses (>3s):**
|
||||
```typescript
|
||||
// Add score threshold + limit results
|
||||
ranking_options: { score_threshold: 0.5 },
|
||||
max_num_results: 10
|
||||
```
|
||||
|
||||
**Empty results debug:**
|
||||
1. Remove filters, test basic query
|
||||
2. Lower `score_threshold` to 0.1
|
||||
3. Check index is populated
|
||||
|
||||
## Limits
|
||||
|
||||
| Resource | Limit |
|
||||
|----------|-------|
|
||||
| Instances per account | 10 |
|
||||
| Files per instance | 100,000 |
|
||||
| Max file size | 4 MB |
|
||||
| Index frequency | 6 hours |
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
**Use env vars for instance names:**
|
||||
```typescript
|
||||
const answer = await env.AI.autorag(env.AI_SEARCH_INSTANCE).aiSearch({...});
|
||||
```
|
||||
|
||||
**Handle specific error types:**
|
||||
```typescript
|
||||
if (error instanceof AutoRAGNotFoundError) { /* 404 */ }
|
||||
if (error instanceof AutoRAGUnauthorizedError) { /* 401 */ }
|
||||
```
|
||||
@@ -0,0 +1,85 @@
|
||||
# AI Search Patterns
|
||||
|
||||
## search() vs aiSearch()
|
||||
|
||||
| Use | Method | Returns |
|
||||
|-----|--------|---------|
|
||||
| Custom UI, analytics | `search()` | Raw chunks only (~100-300ms) |
|
||||
| Chatbots, Q&A | `aiSearch()` | AI response + chunks (~500-2000ms) |
|
||||
|
||||
## rewrite_query
|
||||
|
||||
| Setting | Use When |
|
||||
|---------|----------|
|
||||
| `true` | User input (typos, vague queries) |
|
||||
| `false` | LLM-generated queries (already optimized) |
|
||||
|
||||
## Multitenancy (Folder-Based)
|
||||
|
||||
```typescript
|
||||
const answer = await env.AI.autorag("saas-docs").aiSearch({
|
||||
query: "refund policy",
|
||||
model: "@cf/meta/llama-3.3-70b-instruct-fp8-fast",
|
||||
filters: {
|
||||
column: "folder",
|
||||
operator: "gte", // "starts with" pattern
|
||||
value: `tenants/${tenantId}/`
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Streaming
|
||||
|
||||
```typescript
|
||||
const stream = await env.AI.autorag("docs").aiSearch({
|
||||
query, model: "@cf/meta/llama-3.3-70b-instruct-fp8-fast", stream: true
|
||||
});
|
||||
return new Response(stream, { headers: { "Content-Type": "text/event-stream" } });
|
||||
```
|
||||
|
||||
## Score Threshold
|
||||
|
||||
| Threshold | Use |
|
||||
|-----------|-----|
|
||||
| 0.3 (default) | Broad recall, exploratory |
|
||||
| 0.5 | Balanced, production default |
|
||||
| 0.7 | High precision, critical accuracy |
|
||||
|
||||
## System Prompt Template
|
||||
|
||||
```typescript
|
||||
const systemPrompt = `You are a documentation assistant.
|
||||
- Answer ONLY based on provided context
|
||||
- If context doesn't contain answer, say "I don't have information"
|
||||
- Include code examples from context`;
|
||||
```
|
||||
|
||||
## Compound Filters
|
||||
|
||||
```typescript
|
||||
// OR: Multiple folders
|
||||
filters: {
|
||||
operator: "or",
|
||||
filters: [
|
||||
{ column: "folder", operator: "gte", value: "docs/api/" },
|
||||
{ column: "folder", operator: "gte", value: "docs/auth/" }
|
||||
]
|
||||
}
|
||||
|
||||
// AND: Folder + date
|
||||
filters: {
|
||||
operator: "and",
|
||||
filters: [
|
||||
{ column: "folder", operator: "gte", value: "docs/" },
|
||||
{ column: "timestamp", operator: "gte", value: oneWeekAgoSeconds }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Reranking
|
||||
|
||||
Enable for high-stakes use cases (adds ~300ms latency):
|
||||
|
||||
```typescript
|
||||
reranking: { enabled: true, model: "@cf/baai/bge-reranker-base" }
|
||||
```
|
||||
@@ -0,0 +1,92 @@
|
||||
# Cloudflare Workers Analytics Engine Reference
|
||||
|
||||
Expert guidance for implementing unlimited-cardinality analytics at scale using Cloudflare Workers Analytics Engine.
|
||||
|
||||
## What is Analytics Engine?
|
||||
|
||||
Time-series analytics database designed for high-cardinality data (millions of unique dimensions). Write data points from Workers, query via SQL API. Use for:
|
||||
- Custom user-facing analytics dashboards
|
||||
- Usage-based billing & metering
|
||||
- Per-customer/per-feature monitoring
|
||||
- High-frequency instrumentation without performance impact
|
||||
|
||||
**Key Capability:** Track metrics with unlimited unique values (e.g., millions of user IDs, API keys) without performance degradation.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
| Concept | Description | Example |
|
||||
|---------|-------------|---------|
|
||||
| **Dataset** | Logical table for related metrics | `api_requests`, `user_events` |
|
||||
| **Data Point** | Single measurement with timestamp | One API request's metrics |
|
||||
| **Blobs** | String dimensions (max 20) | endpoint, method, status, user_id |
|
||||
| **Doubles** | Numeric values (max 20) | latency_ms, request_count, bytes |
|
||||
| **Indexes** | Filtered blobs for efficient queries | customer_id, api_key |
|
||||
|
||||
## Reading Order
|
||||
|
||||
| Task | Start Here | Then Read |
|
||||
|------|------------|-----------|
|
||||
| **First-time setup** | [configuration.md](configuration.md) → [api.md](api.md) → [patterns.md](patterns.md) | |
|
||||
| **Writing data** | [api.md](api.md) → [gotchas.md](gotchas.md) (sampling) | |
|
||||
| **Querying data** | [api.md](api.md) (SQL API) → [patterns.md](patterns.md) (examples) | |
|
||||
| **Debugging** | [gotchas.md](gotchas.md) → [api.md](api.md) (limits) | |
|
||||
| **Optimization** | [patterns.md](patterns.md) (anti-patterns) → [gotchas.md](gotchas.md) | |
|
||||
|
||||
## When to Use Analytics Engine
|
||||
|
||||
```
|
||||
Need to track metrics? → Yes
|
||||
↓
|
||||
Millions of unique dimension values? → Yes
|
||||
↓
|
||||
Need real-time queries? → Yes
|
||||
↓
|
||||
Use Analytics Engine ✓
|
||||
|
||||
Alternative scenarios:
|
||||
- Low cardinality (<10k unique values) → Workers Analytics (free tier)
|
||||
- Complex joins/relations → D1 Database
|
||||
- Logs/debugging → Tail Workers (logpush)
|
||||
- External tools → Send to external analytics (Datadog, etc.)
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. Add binding to `wrangler.jsonc`:
|
||||
```jsonc
|
||||
{
|
||||
"analytics_engine_datasets": [
|
||||
{ "binding": "ANALYTICS", "dataset": "my_events" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
2. Write data points (fire-and-forget, no await):
|
||||
```typescript
|
||||
env.ANALYTICS.writeDataPoint({
|
||||
blobs: ["/api/users", "GET", "200"],
|
||||
doubles: [145.2, 1], // latency_ms, count
|
||||
indexes: [customerId]
|
||||
});
|
||||
```
|
||||
|
||||
3. Query via SQL API (HTTP):
|
||||
```sql
|
||||
SELECT blob1, SUM(double2) AS total_requests
|
||||
FROM my_events
|
||||
WHERE index1 = 'customer_123'
|
||||
AND timestamp >= NOW() - INTERVAL '7' DAY
|
||||
GROUP BY blob1
|
||||
ORDER BY total_requests DESC
|
||||
```
|
||||
|
||||
## In This Reference
|
||||
|
||||
- **[configuration.md](configuration.md)** - Setup, bindings, TypeScript types, limits
|
||||
- **[api.md](api.md)** - `writeDataPoint()`, SQL API, query syntax
|
||||
- **[patterns.md](patterns.md)** - Use cases, examples, anti-patterns
|
||||
- **[gotchas.md](gotchas.md)** - Sampling, index selection, troubleshooting
|
||||
|
||||
## See Also
|
||||
|
||||
- [Cloudflare Analytics Engine Docs](https://developers.cloudflare.com/analytics/analytics-engine/)
|
||||
@@ -0,0 +1,112 @@
|
||||
# Analytics Engine API Reference
|
||||
|
||||
## Writing Data
|
||||
|
||||
### `writeDataPoint()`
|
||||
|
||||
Fire-and-forget (returns `void`, not Promise). Writes happen asynchronously.
|
||||
|
||||
```typescript
|
||||
interface AnalyticsEngineDataPoint {
|
||||
blobs?: string[]; // Up to 20 strings (dimensions), 16KB each
|
||||
doubles?: number[]; // Up to 20 numbers (metrics)
|
||||
indexes?: string[]; // 1 indexed string for high-cardinality filtering
|
||||
}
|
||||
|
||||
env.ANALYTICS.writeDataPoint({
|
||||
blobs: ["/api/users", "GET", "200"],
|
||||
doubles: [145.2, 1], // latency_ms, count
|
||||
indexes: ["customer_abc123"]
|
||||
});
|
||||
```
|
||||
|
||||
**Behaviors:** No await needed, no error thrown (check tail logs), auto-sampled at high volumes, auto-timestamped.
|
||||
|
||||
**Blob vs Index:** Blob for GROUP BY (<100k unique), Index for filter-only (millions unique).
|
||||
|
||||
### Full Example
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
const start = Date.now();
|
||||
const url = new URL(request.url);
|
||||
try {
|
||||
const response = await handleRequest(request);
|
||||
env.ANALYTICS.writeDataPoint({
|
||||
blobs: [url.pathname, request.method, response.status.toString()],
|
||||
doubles: [Date.now() - start, 1],
|
||||
indexes: [request.headers.get("x-api-key") || "anonymous"]
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
env.ANALYTICS.writeDataPoint({
|
||||
blobs: [url.pathname, request.method, "500"],
|
||||
doubles: [Date.now() - start, 1, 0],
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## SQL API (External Only)
|
||||
|
||||
```bash
|
||||
curl -X POST https://api.cloudflare.com/client/v4/accounts/{account_id}/analytics_engine/sql \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d "SELECT blob1 AS endpoint, COUNT(*) AS requests FROM dataset WHERE timestamp >= NOW() - INTERVAL '1' HOUR GROUP BY blob1"
|
||||
```
|
||||
|
||||
### Column References
|
||||
|
||||
```sql
|
||||
-- blob1..blob20, double1..double20, index1, timestamp
|
||||
SELECT blob1 AS endpoint, SUM(double1) AS latency, COUNT(*) AS requests
|
||||
FROM my_dataset
|
||||
WHERE index1 = 'customer_123' AND timestamp >= NOW() - INTERVAL '7' DAY
|
||||
GROUP BY blob1
|
||||
HAVING COUNT(*) > 100
|
||||
ORDER BY requests DESC LIMIT 100
|
||||
```
|
||||
|
||||
**Aggregations:** `SUM()`, `AVG()`, `COUNT()`, `MIN()`, `MAX()`, `quantile(0.95)()`
|
||||
|
||||
**Time ranges:** `NOW() - INTERVAL '1' HOUR`, `BETWEEN '2026-01-01' AND '2026-01-31'`
|
||||
|
||||
### Query Examples
|
||||
|
||||
```sql
|
||||
-- Top endpoints
|
||||
SELECT blob1, COUNT(*) AS requests, AVG(double1) AS avg_latency
|
||||
FROM api_requests WHERE timestamp >= NOW() - INTERVAL '24' HOUR
|
||||
GROUP BY blob1 ORDER BY requests DESC LIMIT 20
|
||||
|
||||
-- Error rate
|
||||
SELECT blob1, COUNT(*) AS total,
|
||||
SUM(CASE WHEN blob3 LIKE '5%' THEN 1 ELSE 0 END) AS errors
|
||||
FROM api_requests WHERE timestamp >= NOW() - INTERVAL '1' HOUR
|
||||
GROUP BY blob1 HAVING total > 50
|
||||
|
||||
-- P95 latency
|
||||
SELECT blob1, quantile(0.95)(double1) AS p95
|
||||
FROM api_requests GROUP BY blob1
|
||||
```
|
||||
|
||||
## Response Format
|
||||
|
||||
```json
|
||||
{"data": [{"endpoint": "/api/users", "requests": 1523}], "rows": 2}
|
||||
```
|
||||
|
||||
## Limits
|
||||
|
||||
| Resource | Limit |
|
||||
|----------|-------|
|
||||
| Blobs/Doubles per point | 20 each |
|
||||
| Indexes per point | 1 |
|
||||
| Blob/Index size | 16KB |
|
||||
| Data retention | 90 days |
|
||||
| Query timeout | 30s |
|
||||
|
||||
**Critical:** High write volumes (>1M/min) trigger automatic sampling.
|
||||
@@ -0,0 +1,112 @@
|
||||
# Analytics Engine Configuration
|
||||
|
||||
## Setup
|
||||
|
||||
1. Add binding to `wrangler.jsonc`
|
||||
2. Deploy Worker
|
||||
3. Dataset created automatically on first write
|
||||
4. Query via SQL API
|
||||
|
||||
## wrangler.jsonc
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"name": "my-worker",
|
||||
"analytics_engine_datasets": [
|
||||
{ "binding": "ANALYTICS", "dataset": "my_events" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Multiple datasets for separate concerns:
|
||||
```jsonc
|
||||
{
|
||||
"analytics_engine_datasets": [
|
||||
{ "binding": "API_ANALYTICS", "dataset": "api_requests" },
|
||||
{ "binding": "USER_EVENTS", "dataset": "user_activity" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## TypeScript
|
||||
|
||||
```typescript
|
||||
interface Env {
|
||||
ANALYTICS: AnalyticsEngineDataset;
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env) {
|
||||
// No await - returns void, fire-and-forget
|
||||
env.ANALYTICS.writeDataPoint({
|
||||
blobs: [pathname, method, status], // String dimensions (max 20)
|
||||
doubles: [latency, 1], // Numeric metrics (max 20)
|
||||
indexes: [apiKey] // High-cardinality filter (max 1)
|
||||
});
|
||||
return response;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Data Point Limits
|
||||
|
||||
| Field | Limit | SQL Access |
|
||||
|-------|-------|------------|
|
||||
| blobs | 20 strings, 16KB each | `blob1`...`blob20` |
|
||||
| doubles | 20 numbers | `double1`...`double20` |
|
||||
| indexes | 1 string, 16KB | `index1` |
|
||||
|
||||
## Write Behavior
|
||||
|
||||
| Scenario | Behavior |
|
||||
|----------|----------|
|
||||
| <1M writes/min | All accepted |
|
||||
| >1M writes/min | Automatic sampling |
|
||||
| Invalid data | Silent failure (check tail logs) |
|
||||
|
||||
**Mitigate sampling:** Pre-aggregate, use multiple datasets, write only critical metrics.
|
||||
|
||||
## Query Limits
|
||||
|
||||
| Resource | Limit |
|
||||
|----------|-------|
|
||||
| Query timeout | 30 seconds |
|
||||
| Data retention | 90 days (default) |
|
||||
| Result size | ~10MB |
|
||||
|
||||
## Cost
|
||||
|
||||
**Free tier:** 10M writes/month, 1M reads/month
|
||||
|
||||
**Paid:** $0.05 per 1M writes, $1.00 per 1M reads
|
||||
|
||||
## Environment-Specific
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"analytics_engine_datasets": [
|
||||
{ "binding": "ANALYTICS", "dataset": "prod_events" }
|
||||
],
|
||||
"env": {
|
||||
"staging": {
|
||||
"analytics_engine_datasets": [
|
||||
{ "binding": "ANALYTICS", "dataset": "staging_events" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Monitoring
|
||||
|
||||
```bash
|
||||
npx wrangler tail # Check for sampling/write errors
|
||||
```
|
||||
|
||||
```sql
|
||||
-- Check write activity
|
||||
SELECT DATE_TRUNC('hour', timestamp) AS hour, COUNT(*) AS writes
|
||||
FROM my_dataset
|
||||
WHERE timestamp >= NOW() - INTERVAL '24' HOUR
|
||||
GROUP BY hour
|
||||
```
|
||||
@@ -0,0 +1,85 @@
|
||||
# Analytics Engine Gotchas
|
||||
|
||||
## Critical Issues
|
||||
|
||||
### Sampling at High Volumes
|
||||
|
||||
**Problem:** Queries return fewer points than written at >1M writes/min.
|
||||
|
||||
**Solution:**
|
||||
```typescript
|
||||
// Pre-aggregate before writing
|
||||
let buffer = { count: 0, total: 0 };
|
||||
buffer.count++; buffer.total += value;
|
||||
|
||||
// Write once per second instead of per request
|
||||
if (Date.now() % 1000 === 0) {
|
||||
env.ANALYTICS.writeDataPoint({ doubles: [buffer.count, buffer.total] });
|
||||
}
|
||||
```
|
||||
|
||||
**Detection:** `npx wrangler tail` → look for "sampling enabled"
|
||||
|
||||
### writeDataPoint Returns void
|
||||
|
||||
```typescript
|
||||
// ❌ Pointless await
|
||||
await env.ANALYTICS.writeDataPoint({...});
|
||||
|
||||
// ✅ Fire-and-forget
|
||||
env.ANALYTICS.writeDataPoint({...});
|
||||
```
|
||||
|
||||
Writes can fail silently. Check tail logs.
|
||||
|
||||
### Index vs Blob
|
||||
|
||||
| Cardinality | Use | Example |
|
||||
|-------------|-----|---------|
|
||||
| Millions | **Index** | user_id, api_key |
|
||||
| Hundreds | **Blob** | endpoint, status_code, country |
|
||||
|
||||
```typescript
|
||||
// ✅ Correct
|
||||
{ blobs: [method, path, status], indexes: [userId] }
|
||||
```
|
||||
|
||||
### Can't Query from Workers
|
||||
|
||||
Query API requires HTTP auth. Use external service or cache in KV/D1.
|
||||
|
||||
### No Custom Timestamps
|
||||
|
||||
Auto-generated at write time. Store original in blob if needed.
|
||||
|
||||
## Common Errors
|
||||
|
||||
| Error | Fix |
|
||||
|-------|-----|
|
||||
| Binding not found | Check wrangler.jsonc, redeploy |
|
||||
| No data in query | Wait 30s; check dataset name; check time range |
|
||||
| Query timeout | Add time filter; use index for filtering |
|
||||
|
||||
## Limits
|
||||
|
||||
| Resource | Limit |
|
||||
|----------|-------|
|
||||
| Blobs per point | 20 |
|
||||
| Doubles per point | 20 |
|
||||
| Indexes per point | 1 |
|
||||
| Blob/Index size | 16KB |
|
||||
| Write rate (no sampling) | ~1M/min |
|
||||
| Retention | 90 days |
|
||||
| Query timeout | 30s |
|
||||
|
||||
## Best Practices
|
||||
|
||||
✅ Pre-aggregate at high volumes
|
||||
✅ Use index for high-cardinality (millions)
|
||||
✅ Always include time filter in queries
|
||||
✅ Design schema before coding
|
||||
|
||||
❌ Don't await writeDataPoint
|
||||
❌ Don't use index for low-cardinality
|
||||
❌ Don't query without time range
|
||||
❌ Don't assume all writes succeed
|
||||
@@ -0,0 +1,83 @@
|
||||
# Analytics Engine Patterns
|
||||
|
||||
## Use Cases
|
||||
|
||||
| Use Case | Key Metrics | Index On |
|
||||
|----------|-------------|----------|
|
||||
| API Metering | requests, bytes, compute_units | api_key |
|
||||
| Feature Usage | feature, action, duration | user_id |
|
||||
| Error Tracking | error_type, endpoint, count | customer_id |
|
||||
| Performance | latency_ms, cache_status | endpoint |
|
||||
| A/B Testing | variant, conversions | user_id |
|
||||
|
||||
## API Metering (Billing)
|
||||
|
||||
```typescript
|
||||
env.ANALYTICS.writeDataPoint({
|
||||
blobs: [pathname, method, status, tier],
|
||||
doubles: [1, computeUnits, bytes, latencyMs],
|
||||
indexes: [apiKey]
|
||||
});
|
||||
|
||||
// Query: Monthly usage by customer
|
||||
// SELECT index1 AS api_key, SUM(double2) AS compute_units
|
||||
// FROM usage WHERE timestamp >= DATE_TRUNC('month', NOW()) GROUP BY index1
|
||||
```
|
||||
|
||||
## Error Tracking
|
||||
|
||||
```typescript
|
||||
env.ANALYTICS.writeDataPoint({
|
||||
blobs: [endpoint, method, errorName, errorMessage.slice(0, 1000)],
|
||||
doubles: [1, timeToErrorMs],
|
||||
indexes: [customerId]
|
||||
});
|
||||
```
|
||||
|
||||
## Performance Monitoring
|
||||
|
||||
```typescript
|
||||
env.ANALYTICS.writeDataPoint({
|
||||
blobs: [pathname, method, cacheStatus, status],
|
||||
doubles: [latencyMs, 1],
|
||||
indexes: [userId]
|
||||
});
|
||||
|
||||
// Query: P95 latency by endpoint
|
||||
// SELECT blob1, quantile(0.95)(double1) AS p95_ms FROM perf GROUP BY blob1
|
||||
```
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
| ❌ Wrong | ✅ Correct |
|
||||
|----------|-----------|
|
||||
| `await writeDataPoint()` | `writeDataPoint()` (fire-and-forget) |
|
||||
| `indexes: [method]` (low cardinality) | `blobs: [method]`, `indexes: [userId]` |
|
||||
| `blobs: [JSON.stringify(obj)]` | Store ID in blob, full object in D1/KV |
|
||||
| Write every request at 10M/min | Pre-aggregate per second |
|
||||
| Query from Worker | Query from external service/API |
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Design schema upfront** - Document blob/double/index assignments
|
||||
2. **Always include count metric** - `doubles: [latency, 1]` for AVG calculations
|
||||
3. **Use enums for blobs** - Consistent values like `Status.SUCCESS`
|
||||
4. **Handle sampling** - Use ratios (avg_latency = SUM(latency)/SUM(count))
|
||||
5. **Test queries early** - Validate schema before heavy writes
|
||||
|
||||
## Schema Template
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Dataset: my_metrics
|
||||
*
|
||||
* Blobs:
|
||||
* blob1: endpoint, blob2: method, blob3: status
|
||||
*
|
||||
* Doubles:
|
||||
* double1: latency_ms, double2: count (always 1)
|
||||
*
|
||||
* Indexes:
|
||||
* index1: customer_id (high cardinality)
|
||||
*/
|
||||
```
|
||||
@@ -0,0 +1,44 @@
|
||||
# Cloudflare API Shield Reference
|
||||
|
||||
Expert guidance for API Shield - comprehensive API security suite for discovery, protection, and monitoring.
|
||||
|
||||
## Reading Order
|
||||
|
||||
| Task | Files to Read |
|
||||
|------|---------------|
|
||||
| Initial setup | README → configuration.md |
|
||||
| Implement JWT validation | configuration.md → api.md |
|
||||
| Add schema validation | configuration.md → patterns.md |
|
||||
| Detect API attacks | patterns.md → api.md |
|
||||
| Debug issues | gotchas.md |
|
||||
|
||||
## Feature Selection
|
||||
|
||||
What protection do you need?
|
||||
|
||||
```
|
||||
├─ Validate request/response structure → Schema Validation 2.0 (configuration.md)
|
||||
├─ Verify auth tokens → JWT Validation (configuration.md)
|
||||
├─ Client certificates → mTLS (configuration.md)
|
||||
├─ Detect BOLA attacks → BOLA Detection (patterns.md)
|
||||
├─ Track auth coverage → Auth Posture (patterns.md)
|
||||
├─ Stop volumetric abuse → Abuse Detection (patterns.md)
|
||||
└─ Discover shadow APIs → API Discovery (api.md)
|
||||
```
|
||||
|
||||
## In This Reference
|
||||
|
||||
- **[configuration.md](configuration.md)** - Setup, session identifiers, rules, token/mTLS configs
|
||||
- **[api.md](api.md)** - Endpoint management, discovery, validation APIs, GraphQL operations
|
||||
- **[patterns.md](patterns.md)** - Common patterns, progressive rollout, OWASP mappings, workflows
|
||||
- **[gotchas.md](gotchas.md)** - Troubleshooting, false positives, performance, best practices
|
||||
|
||||
## Quick Start
|
||||
|
||||
API Shield: Enterprise-grade API security (Discovery, Schema Validation 2.0, JWT, mTLS, BOLA Detection, Auth Posture). Available as Enterprise add-on with preview access.
|
||||
|
||||
## See Also
|
||||
|
||||
- [API Shield Docs](https://developers.cloudflare.com/api-shield/)
|
||||
- [API Reference](https://developers.cloudflare.com/api/resources/api_gateway/)
|
||||
- [OWASP API Security Top 10](https://owasp.org/www-project-api-security/)
|
||||
141
.agents/skills/cloudflare-deploy/references/api-shield/api.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# API Reference
|
||||
|
||||
Base: `/zones/{zone_id}/api_gateway`
|
||||
|
||||
## Endpoints
|
||||
|
||||
```bash
|
||||
GET /operations # List
|
||||
GET /operations/{op_id} # Get single
|
||||
POST /operations/item # Create: {endpoint,host,method}
|
||||
POST /operations # Bulk: {operations:[{endpoint,host,method}]}
|
||||
DELETE /operations/{op_id} # Delete
|
||||
DELETE /operations # Bulk delete: {operation_ids:[...]}
|
||||
```
|
||||
|
||||
## Discovery
|
||||
|
||||
```bash
|
||||
GET /discovery/operations # List discovered
|
||||
PATCH /discovery/operations/{op_id} # Update: {state:"saved"|"ignored"}
|
||||
PATCH /discovery/operations # Bulk: {operation_ids:{id:{state}}}
|
||||
GET /discovery # OpenAPI export
|
||||
```
|
||||
|
||||
## Config
|
||||
|
||||
```bash
|
||||
GET /configuration # Get session ID config
|
||||
PUT /configuration # Update: {auth_id_characteristics:[{name,type:"header"|"cookie"}]}
|
||||
```
|
||||
|
||||
## Token Validation
|
||||
|
||||
```bash
|
||||
GET /token_validation # List
|
||||
POST /token_validation # Create: {name,location:{header:"..."},jwks:"..."}
|
||||
POST /jwt_validation_rules # Rule: {name,hostname,token_validation_id,action:"block"}
|
||||
```
|
||||
|
||||
## Workers Integration
|
||||
|
||||
### Access JWT Claims
|
||||
```js
|
||||
export default {
|
||||
async fetch(req, env) {
|
||||
// Access validated JWT payload
|
||||
const jwt = req.cf?.jwt?.payload?.[env.JWT_CONFIG_ID]?.[0];
|
||||
if (jwt) {
|
||||
const userId = jwt.sub;
|
||||
const role = jwt.role;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Access mTLS Info
|
||||
```js
|
||||
export default {
|
||||
async fetch(req, env) {
|
||||
const tls = req.cf?.tlsClientAuth;
|
||||
if (tls?.certVerified === 'SUCCESS') {
|
||||
const fingerprint = tls.certFingerprintSHA256;
|
||||
// Authenticated client
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Dynamic JWKS Update
|
||||
```js
|
||||
export default {
|
||||
async scheduled(event, env) {
|
||||
const jwks = await (await fetch('https://auth.example.com/.well-known/jwks.json')).json();
|
||||
await fetch(`https://api.cloudflare.com/client/v4/zones/${env.ZONE_ID}/api_gateway/token_validation/${env.CONFIG_ID}`, {
|
||||
method: 'PATCH',
|
||||
headers: {'Authorization': `Bearer ${env.CF_API_TOKEN}`, 'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({jwks: JSON.stringify(jwks)})
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Firewall Fields
|
||||
|
||||
### Core Fields
|
||||
```js
|
||||
cf.api_gateway.auth_id_present // Session ID present
|
||||
cf.api_gateway.request_violates_schema // Schema violation
|
||||
cf.api_gateway.fallthrough_triggered // No endpoint match
|
||||
cf.tls_client_auth.cert_verified // mTLS cert valid
|
||||
cf.tls_client_auth.cert_fingerprint_sha256
|
||||
```
|
||||
|
||||
### JWT Validation (2026)
|
||||
```js
|
||||
// Modern validation syntax
|
||||
is_jwt_valid(http.request.jwt.payload["{config_id}"][0])
|
||||
|
||||
// Legacy (still supported)
|
||||
cf.api_gateway.jwt_claims_valid
|
||||
|
||||
// Extract claims
|
||||
lookup_json_string(http.request.jwt.payload["{config_id}"][0], "claim_name")
|
||||
```
|
||||
|
||||
### Risk Labels (2026)
|
||||
```js
|
||||
// BOLA detection
|
||||
cf.api_gateway.cf-risk-bola-enumeration // Sequential resource access detected
|
||||
cf.api_gateway.cf-risk-bola-pollution // Parameter pollution detected
|
||||
|
||||
// Authentication posture
|
||||
cf.api_gateway.cf-risk-missing-auth // Endpoint lacks authentication
|
||||
cf.api_gateway.cf-risk-mixed-auth // Inconsistent auth patterns
|
||||
```
|
||||
|
||||
## BOLA Detection
|
||||
|
||||
```bash
|
||||
GET /user_schemas/{schema_id}/bola # Get BOLA config
|
||||
PATCH /user_schemas/{schema_id}/bola # Update: {enabled:true}
|
||||
```
|
||||
|
||||
## Auth Posture
|
||||
|
||||
```bash
|
||||
GET /discovery/authentication_posture # List unprotected endpoints
|
||||
```
|
||||
|
||||
## GraphQL Protection
|
||||
|
||||
```bash
|
||||
GET /settings/graphql_protection # Get limits
|
||||
PUT /settings/graphql_protection # Set: {max_depth,max_size}
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [configuration.md](configuration.md) - Setup guides for all features
|
||||
- [patterns.md](patterns.md) - Firewall rules and common patterns
|
||||
- [API Gateway API Docs](https://developers.cloudflare.com/api/resources/api_gateway/)
|
||||
@@ -0,0 +1,192 @@
|
||||
# Configuration
|
||||
|
||||
## Schema Validation 2.0 Setup
|
||||
|
||||
> ⚠️ **Classic Schema Validation deprecated.** Use Schema Validation 2.0.
|
||||
|
||||
**Upload schema (Dashboard):**
|
||||
```
|
||||
Security > API Shield > Schema Validation > Add validation
|
||||
- Upload .yml/.yaml/.json (OpenAPI v3.0)
|
||||
- Endpoints auto-added to Endpoint Management
|
||||
- Action: Log | Block | None
|
||||
- Body inspection: JSON payloads
|
||||
```
|
||||
|
||||
**Change validation action:**
|
||||
```
|
||||
Security > API Shield > Settings > Schema Validation
|
||||
Per-endpoint: Filter → ellipses → Change action
|
||||
Default action: Set global mitigation action
|
||||
```
|
||||
|
||||
**Migration from Classic:**
|
||||
```
|
||||
1. Export existing schema (if available)
|
||||
2. Delete all Classic schema validation rules
|
||||
3. Wait 5 min for cache clear
|
||||
4. Re-upload via Schema Validation 2.0 interface
|
||||
5. Verify in Security > Events
|
||||
```
|
||||
|
||||
**Fallthrough rule** (catch-all unknown endpoints):
|
||||
```
|
||||
Security > API Shield > Settings > Fallthrough > Use Template
|
||||
- Select hostnames
|
||||
- Create rule with cf.api_gateway.fallthrough_triggered
|
||||
- Action: Log (discover) or Block (strict)
|
||||
```
|
||||
|
||||
**Body inspection:** Supports `application/json`, `*/*`, `application/*`. Disable origin MIME sniffing to prevent bypasses.
|
||||
|
||||
## JWT Validation
|
||||
|
||||
**Setup token config:**
|
||||
```
|
||||
Security > API Shield > Settings > JWT Settings > Add configuration
|
||||
- Name: "Auth0 JWT Config"
|
||||
- Location: Header/Cookie + name (e.g., "Authorization")
|
||||
- JWKS: Paste public keys from IdP
|
||||
```
|
||||
|
||||
**Create validation rule:**
|
||||
```
|
||||
Security > API Shield > API Rules > Add rule
|
||||
- Hostname: api.example.com
|
||||
- Deselect endpoints to ignore
|
||||
- Token config: Select config
|
||||
- Enforce presence: Ignore or Mark as non-compliant
|
||||
- Action: Log/Block/Challenge
|
||||
```
|
||||
|
||||
**Rate limit by JWT claim:**
|
||||
```wirefilter
|
||||
lookup_json_string(http.request.jwt.claims["{config_id}"][0], "sub")
|
||||
```
|
||||
|
||||
**Special cases:**
|
||||
- Two JWTs, different IdPs: Create 2 configs, select both, "Validate all"
|
||||
- IdP migration: 2 configs + 2 rules, adjust actions per state
|
||||
- Bearer prefix: API Shield handles with/without
|
||||
- Nested claims: Dot notation `user.email`
|
||||
|
||||
## Mutual TLS (mTLS)
|
||||
|
||||
**Setup:**
|
||||
```
|
||||
SSL/TLS > Client Certificates > Create Certificate
|
||||
- Generate CF-managed CA (all plans)
|
||||
- Upload custom CA (Enterprise, max 5)
|
||||
```
|
||||
|
||||
**Configure mTLS rule:**
|
||||
```
|
||||
Security > API Shield > mTLS
|
||||
- Select hostname(s)
|
||||
- Choose certificate(s)
|
||||
- Action: Block/Log/Challenge
|
||||
```
|
||||
|
||||
**Test:**
|
||||
```bash
|
||||
openssl req -x509 -newkey rsa:4096 -keyout client-key.pem -out client-cert.pem -days 365
|
||||
curl https://api.example.com/endpoint --cert client-cert.pem --key client-key.pem
|
||||
```
|
||||
|
||||
## Session Identifiers
|
||||
|
||||
Critical for BOLA Detection, Sequence Mitigation, and analytics. Configure header/cookie that uniquely IDs API users.
|
||||
|
||||
**Examples:** JWT sub claim, session token, API key, custom user ID header
|
||||
|
||||
**Configure:**
|
||||
```
|
||||
Security > API Shield > Settings > Session Identifiers
|
||||
- Type: Header/Cookie
|
||||
- Name: "X-User-ID" or "Authorization"
|
||||
```
|
||||
|
||||
## BOLA Detection
|
||||
|
||||
Detects Broken Object Level Authorization attacks (enumeration + parameter pollution).
|
||||
|
||||
**Enable:**
|
||||
```
|
||||
Security > API Shield > Schema Validation > [Select Schema] > BOLA Detection
|
||||
- Enable detection
|
||||
- Threshold: Sensitivity level (Low/Medium/High)
|
||||
- Action: Log or Block
|
||||
```
|
||||
|
||||
**Requirements:**
|
||||
- Schema Validation 2.0 enabled
|
||||
- Session identifiers configured
|
||||
- Minimum traffic: 1000+ requests/day per endpoint
|
||||
|
||||
## Authentication Posture
|
||||
|
||||
Identifies unprotected or inconsistently protected endpoints.
|
||||
|
||||
**View report:**
|
||||
```
|
||||
Security > API Shield > Authentication Posture
|
||||
- Shows endpoints lacking JWT/mTLS
|
||||
- Highlights mixed authentication patterns
|
||||
```
|
||||
|
||||
**Remediate:**
|
||||
1. Review flagged endpoints
|
||||
2. Add JWT validation rules
|
||||
3. Configure mTLS for sensitive endpoints
|
||||
4. Monitor posture score
|
||||
|
||||
## Volumetric Abuse + GraphQL
|
||||
|
||||
**Volumetric Abuse Detection:**
|
||||
`Security > API Shield > Settings > Volumetric Abuse Detection`
|
||||
- Enable per-endpoint monitoring, set thresholds, action: Log | Challenge | Block
|
||||
|
||||
**GraphQL Protection:**
|
||||
`Security > API Shield > Settings > GraphQL Protection`
|
||||
- Max query depth: 10, max size: 100KB, block introspection (production)
|
||||
|
||||
## Terraform
|
||||
|
||||
```hcl
|
||||
# Session identifier
|
||||
resource "cloudflare_api_shield" "main" {
|
||||
zone_id = var.zone_id
|
||||
auth_id_characteristics {
|
||||
type = "header"
|
||||
name = "Authorization"
|
||||
}
|
||||
}
|
||||
|
||||
# Add endpoint
|
||||
resource "cloudflare_api_shield_operation" "users_get" {
|
||||
zone_id = var.zone_id
|
||||
method = "GET"
|
||||
host = "api.example.com"
|
||||
endpoint = "/api/users/{id}"
|
||||
}
|
||||
|
||||
# JWT validation rule
|
||||
resource "cloudflare_ruleset" "jwt_validation" {
|
||||
zone_id = var.zone_id
|
||||
name = "API JWT Validation"
|
||||
kind = "zone"
|
||||
phase = "http_request_firewall_custom"
|
||||
|
||||
rules {
|
||||
action = "block"
|
||||
expression = "(http.host eq \"api.example.com\" and not is_jwt_valid(http.request.jwt.payload[\"{config_id}\"][0]))"
|
||||
description = "Block invalid JWTs"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [api.md](api.md) - API endpoints and Workers integration
|
||||
- [patterns.md](patterns.md) - Firewall rules and deployment patterns
|
||||
- [gotchas.md](gotchas.md) - Troubleshooting and limits
|
||||
@@ -0,0 +1,125 @@
|
||||
# Gotchas & Troubleshooting
|
||||
|
||||
## Common Errors
|
||||
|
||||
### "Schema Validation 2.0 not working after migration"
|
||||
|
||||
**Cause:** Classic rules still active, conflicting with new system
|
||||
**Solution:**
|
||||
1. Delete ALL Classic schema validation rules
|
||||
2. Clear Cloudflare cache (wait 5 min)
|
||||
3. Re-upload schema via new Schema Validation 2.0 interface
|
||||
4. Verify in Security > Events
|
||||
5. Check action is set (Log/Block)
|
||||
|
||||
### "Schema validation blocking valid requests"
|
||||
|
||||
**Cause:** Schema too restrictive, missing fields, or incorrect types
|
||||
**Solution:**
|
||||
1. Check Firewall Events for violation details
|
||||
2. Review schema in Settings
|
||||
3. Test schema in Swagger Editor
|
||||
4. Use Log mode to validate before blocking
|
||||
5. Update schema with correct specifications
|
||||
6. Ensure Schema Validation 2.0 (not Classic)
|
||||
|
||||
### "JWT validation failing"
|
||||
|
||||
**Cause:** JWKS mismatch with IdP, expired token, wrong header/cookie name, or clock skew
|
||||
**Solution:**
|
||||
1. Verify JWKS matches IdP configuration
|
||||
2. Check token `exp` claim is valid
|
||||
3. Confirm header/cookie name matches config
|
||||
4. Test token at jwt.io
|
||||
5. Account for clock skew (±5 min tolerance)
|
||||
6. Use modern syntax: `is_jwt_valid(http.request.jwt.payload["{config_id}"][0])`
|
||||
|
||||
### "BOLA detection false positives"
|
||||
|
||||
**Cause:** Legitimate sequential access patterns, bulk operations, or sensitivity too high
|
||||
**Solution:**
|
||||
1. Review BOLA events in Security > Events
|
||||
2. Lower sensitivity threshold (High → Medium → Low)
|
||||
3. Exclude legitimate bulk operations from detection
|
||||
4. Ensure session identifiers uniquely identify users
|
||||
5. Verify minimum traffic requirements met (1000+ req/day)
|
||||
|
||||
### "Risk labels not appearing in firewall rules"
|
||||
|
||||
**Cause:** Feature not enabled, insufficient traffic, or missing session identifiers
|
||||
**Solution:**
|
||||
1. Verify Schema Validation 2.0 enabled
|
||||
2. Enable BOLA Detection in schema settings
|
||||
3. Configure session identifiers (required for BOLA)
|
||||
4. Wait 24-48h for ML model training
|
||||
5. Check minimum traffic thresholds met
|
||||
|
||||
### "Endpoint discovery not finding APIs"
|
||||
|
||||
**Cause:** Insufficient traffic (<500 reqs/10d), non-2xx responses, Worker direct requests, or incorrect session ID config
|
||||
**Solution:** Ensure 500+ requests in 10 days, 2xx responses from edge (not Workers direct), configure session IDs correctly. ML updates daily.
|
||||
|
||||
### "Sequence detection false positives"
|
||||
|
||||
**Cause:** Lookback window issues, non-unique session IDs, or model sensitivity
|
||||
**Solution:**
|
||||
1. Review lookback settings (10 reqs to managed endpoints, 10min window)
|
||||
2. Ensure session ID uniqueness per user (not shared tokens)
|
||||
3. Adjust positive/negative model balance
|
||||
4. Exclude legitimate workflows from detection
|
||||
|
||||
### "GraphQL protection blocking valid queries"
|
||||
|
||||
**Cause:** Query depth/size limits too restrictive, complex but legitimate queries
|
||||
**Solution:**
|
||||
1. Review blocked query patterns in Security > Events
|
||||
2. Increase max_depth (default: 10) if needed
|
||||
3. Increase max_size (default: 100KB) for complex queries
|
||||
4. Whitelist specific query signatures
|
||||
5. Use Log mode to tune before blocking
|
||||
|
||||
### "Token invalid"
|
||||
|
||||
**Cause:** Configuration error, JWKS mismatch, or expired token
|
||||
**Solution:** Verify config matches IdP, update JWKS, check token expiration
|
||||
|
||||
### "Schema violation"
|
||||
|
||||
**Cause:** Missing required fields, wrong data types, or spec mismatch
|
||||
**Solution:** Review schema against actual requests, ensure all required fields present, validate types match spec
|
||||
|
||||
### "Fallthrough"
|
||||
|
||||
**Cause:** Unknown endpoint or pattern mismatch
|
||||
**Solution:** Update schema with all endpoints, check path pattern matching
|
||||
|
||||
### "mTLS failed"
|
||||
|
||||
**Cause:** Certificate untrusted/expired or wrong CA
|
||||
**Solution:** Verify cert chain, check expiration, confirm correct CA uploaded
|
||||
|
||||
## Limits (2026)
|
||||
|
||||
| Resource/Limit | Value | Notes |
|
||||
|----------------|-------|-------|
|
||||
| OpenAPI version | v3.0.x only | No external refs, must be valid |
|
||||
| Schema operations | 10K (Enterprise) | Contact for higher limits |
|
||||
| JWT validation sources | Headers/cookies only | No query params/body |
|
||||
| Endpoint discovery | 500+ reqs/10d | Minimum for ML model |
|
||||
| Path normalization | Automatic | `/profile/238` → `/profile/{var1}` |
|
||||
| Schema parameters | No `content` field | No object param validation |
|
||||
| BOLA detection | 1000+ reqs/day/endpoint | Per-endpoint minimum |
|
||||
| Session ID uniqueness | Required | BOLA/Sequence need unique IDs |
|
||||
| GraphQL max depth | 1-50 | Default: 10 |
|
||||
| GraphQL max size | 1KB-1MB | Default: 100KB |
|
||||
| JWT claim nesting | 10 levels max | Use dot notation |
|
||||
| mTLS CA certificates | 5 custom max | CF-managed unlimited |
|
||||
| Schema upload size | 5MB max | Compressed OpenAPI spec |
|
||||
| Volumetric abuse baseline | 7 days training | Initial ML period |
|
||||
| Auth Posture refresh | Daily | Updated nightly |
|
||||
|
||||
## See Also
|
||||
|
||||
- [configuration.md](configuration.md) - Setup guides to avoid common issues
|
||||
- [patterns.md](patterns.md) - Best practices and progressive rollout
|
||||
- [API Shield Docs](https://developers.cloudflare.com/api-shield/)
|
||||
@@ -0,0 +1,180 @@
|
||||
# Patterns & Use Cases
|
||||
|
||||
## Protect API with Schema + JWT
|
||||
|
||||
```bash
|
||||
# 1. Upload OpenAPI schema
|
||||
POST /zones/{zone_id}/api_gateway/user_schemas
|
||||
|
||||
# 2. Configure JWT validation
|
||||
POST /zones/{zone_id}/api_gateway/token_validation
|
||||
{
|
||||
"name": "Auth0",
|
||||
"location": {"header": "Authorization"},
|
||||
"jwks": "{...}"
|
||||
}
|
||||
|
||||
# 3. Create JWT rule
|
||||
POST /zones/{zone_id}/api_gateway/jwt_validation_rules
|
||||
|
||||
# 4. Set schema validation action
|
||||
PUT /zones/{zone_id}/api_gateway/settings/schema_validation
|
||||
{"validation_default_mitigation_action": "block"}
|
||||
```
|
||||
|
||||
## Progressive Rollout
|
||||
|
||||
```
|
||||
1. Log mode: Observe false positives
|
||||
- Schema: Action = Log
|
||||
- JWT: Action = Log
|
||||
|
||||
2. Block subset: Protect critical endpoints
|
||||
- Change specific endpoint actions to Block
|
||||
- Monitor firewall events
|
||||
|
||||
3. Full enforcement: Block all violations
|
||||
- Change default action to Block
|
||||
- Handle fallthrough with custom rule
|
||||
```
|
||||
|
||||
## BOLA Detection
|
||||
|
||||
### Enumeration Detection
|
||||
Detects sequential resource access (e.g., `/users/1`, `/users/2`, `/users/3`).
|
||||
|
||||
```javascript
|
||||
// Block BOLA enumeration attempts
|
||||
(cf.api_gateway.cf-risk-bola-enumeration and http.host eq "api.example.com")
|
||||
// Action: Block or Challenge
|
||||
```
|
||||
|
||||
### Parameter Pollution
|
||||
Detects duplicate/excessive parameters in requests.
|
||||
|
||||
```javascript
|
||||
// Block parameter pollution
|
||||
(cf.api_gateway.cf-risk-bola-pollution and http.host eq "api.example.com")
|
||||
// Action: Block
|
||||
```
|
||||
|
||||
### Combined BOLA Protection
|
||||
```javascript
|
||||
// Comprehensive BOLA rule
|
||||
(cf.api_gateway.cf-risk-bola-enumeration or cf.api_gateway.cf-risk-bola-pollution)
|
||||
and http.host eq "api.example.com"
|
||||
// Action: Block
|
||||
```
|
||||
|
||||
## Authentication Posture
|
||||
|
||||
### Detect Missing Auth
|
||||
```javascript
|
||||
// Log endpoints lacking authentication
|
||||
(cf.api_gateway.cf-risk-missing-auth and http.host eq "api.example.com")
|
||||
// Action: Log (for audit)
|
||||
```
|
||||
|
||||
### Detect Mixed Auth
|
||||
```javascript
|
||||
// Alert on inconsistent auth patterns
|
||||
(cf.api_gateway.cf-risk-mixed-auth and http.host eq "api.example.com")
|
||||
// Action: Log (review required)
|
||||
```
|
||||
|
||||
## Fallthrough Detection (Shadow APIs)
|
||||
|
||||
```javascript
|
||||
// WAF Custom Rule
|
||||
(cf.api_gateway.fallthrough_triggered and http.host eq "api.example.com")
|
||||
// Action: Log (discover unknown) or Block (strict)
|
||||
```
|
||||
|
||||
## Rate Limiting by User
|
||||
|
||||
```javascript
|
||||
// Rate Limiting Rule (modern syntax)
|
||||
(http.host eq "api.example.com" and
|
||||
is_jwt_valid(http.request.jwt.payload["{config_id}"][0]))
|
||||
|
||||
// Rate: 100 req/60s
|
||||
// Counting expression: lookup_json_string(http.request.jwt.payload["{config_id}"][0], "sub")
|
||||
```
|
||||
|
||||
## Volumetric Abuse Response
|
||||
|
||||
```javascript
|
||||
// Detect abnormal traffic spikes
|
||||
(cf.api_gateway.volumetric_abuse_detected and http.host eq "api.example.com")
|
||||
// Action: Challenge or Rate Limit
|
||||
|
||||
// Combined with rate limiting
|
||||
(cf.api_gateway.volumetric_abuse_detected or
|
||||
cf.threat_score gt 50) and http.host eq "api.example.com"
|
||||
// Action: JS Challenge
|
||||
```
|
||||
|
||||
## GraphQL Protection
|
||||
|
||||
```javascript
|
||||
// Block oversized queries
|
||||
(http.request.uri.path eq "/graphql" and
|
||||
cf.api_gateway.graphql_query_size gt 100000)
|
||||
// Action: Block
|
||||
|
||||
// Block deep nested queries
|
||||
(http.request.uri.path eq "/graphql" and
|
||||
cf.api_gateway.graphql_query_depth gt 10)
|
||||
// Action: Block
|
||||
```
|
||||
|
||||
## Architecture Patterns
|
||||
|
||||
**Public API:** Discovery + Schema Validation 2.0 + JWT + Rate Limiting + Bot Management
|
||||
**Partner API:** mTLS + Schema Validation + Sequence Mitigation
|
||||
**Internal API:** Discovery + Schema Learning + Auth Posture
|
||||
|
||||
## OWASP API Security Top 10 Mapping (2026)
|
||||
|
||||
| OWASP Issue | API Shield Solutions |
|
||||
|-------------|---------------------|
|
||||
| API1:2023 Broken Object Level Authorization | **BOLA Detection** (enumeration + pollution), Sequence mitigation, Schema, JWT, Rate Limiting |
|
||||
| API2:2023 Broken Authentication | **Auth Posture**, mTLS, JWT validation, Bot Management |
|
||||
| API3:2023 Broken Object Property Auth | Schema validation, JWT validation |
|
||||
| API4:2023 Unrestricted Resource Access | Rate Limiting, **Volumetric Abuse Detection**, **GraphQL Protection**, Bot Management |
|
||||
| API5:2023 Broken Function Level Auth | Schema validation, JWT validation, Auth Posture |
|
||||
| API6:2023 Unrestricted Business Flows | Sequence mitigation, Bot Management |
|
||||
| API7:2023 SSRF | Schema validation, WAF managed rules |
|
||||
| API8:2023 Security Misconfiguration | **Schema Validation 2.0**, Auth Posture, WAF rules |
|
||||
| API9:2023 Improper Inventory Management | **API Discovery**, Schema learning, Auth Posture |
|
||||
| API10:2023 Unsafe API Consumption | JWT validation, Schema validation, WAF managed |
|
||||
|
||||
## Monitoring
|
||||
|
||||
**Security Events:** `Security > Events` → Filter: Action = block, Service = API Shield
|
||||
**Firewall Analytics:** `Analytics > Security` → Filter by `cf.api_gateway.*` fields
|
||||
**Logpush fields:** APIGatewayAuthIDPresent, APIGatewayRequestViolatesSchema, APIGatewayFallthroughDetected, JWTValidationResult
|
||||
|
||||
## Availability (2026)
|
||||
|
||||
| Feature | Availability | Notes |
|
||||
|---------|-------------|-------|
|
||||
| mTLS (CF-managed CA) | All plans | Self-service |
|
||||
| Endpoint Management | All plans | Limited operations |
|
||||
| Schema Validation 2.0 | All plans | Limited operations |
|
||||
| API Discovery | Enterprise | 10K+ ops |
|
||||
| JWT Validation | Enterprise add-on | Full validation |
|
||||
| BOLA Detection | Enterprise add-on | Requires session IDs |
|
||||
| Auth Posture | Enterprise add-on | Security audit |
|
||||
| Volumetric Abuse Detection | Enterprise add-on | Traffic analysis |
|
||||
| GraphQL Protection | Enterprise add-on | Query limits |
|
||||
| Sequence Mitigation | Enterprise (beta) | Contact team |
|
||||
| Full Suite | Enterprise add-on | All features |
|
||||
|
||||
**Enterprise limits:** 10K operations (contact for higher). Preview access available for non-contract evaluation.
|
||||
|
||||
## See Also
|
||||
|
||||
- [configuration.md](configuration.md) - Setup all features before creating rules
|
||||
- [api.md](api.md) - Firewall field reference and API endpoints
|
||||
- [gotchas.md](gotchas.md) - Common issues and limits
|
||||
65
.agents/skills/cloudflare-deploy/references/api/README.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# Cloudflare API Integration
|
||||
|
||||
Guide for working with Cloudflare's REST API - authentication, SDK usage, common patterns, and troubleshooting.
|
||||
|
||||
## Quick Decision Tree
|
||||
|
||||
```
|
||||
How are you calling the Cloudflare API?
|
||||
├─ From Workers runtime → Use bindings, not REST API (see ../bindings/)
|
||||
├─ Server-side (Node/Python/Go) → Official SDK (see api.md)
|
||||
├─ CLI/scripts → Wrangler or curl (see configuration.md)
|
||||
├─ Infrastructure-as-code → See ../pulumi/ or ../terraform/
|
||||
└─ One-off requests → curl examples (see api.md)
|
||||
```
|
||||
|
||||
## SDK Selection
|
||||
|
||||
| Language | Package | Best For | Default Retries |
|
||||
|----------|---------|----------|-----------------|
|
||||
| TypeScript | `cloudflare` | Node.js, Bun, Next.js, Workers | 2 |
|
||||
| Python | `cloudflare` | FastAPI, Django, scripts | 2 |
|
||||
| Go | `cloudflare-go/v4` | CLI tools, microservices | 10 |
|
||||
|
||||
All SDKs are Stainless-generated from OpenAPI spec (consistent APIs).
|
||||
|
||||
## Authentication Methods
|
||||
|
||||
| Method | Security | Use Case | Scope |
|
||||
|--------|----------|----------|-------|
|
||||
| **API Token** ✓ | Scoped, rotatable | Production | Per-zone or account |
|
||||
| API Key + Email | Full account access | Legacy only | Everything |
|
||||
| User Service Key | Limited | Origin CA certs only | Origin CA |
|
||||
|
||||
**Always use API tokens** for new projects.
|
||||
|
||||
## Rate Limits
|
||||
|
||||
| Limit | Value |
|
||||
|-------|-------|
|
||||
| Per user/token | 1200 requests / 5 minutes |
|
||||
| Per IP | 200 requests / second |
|
||||
| GraphQL | 320 / 5 minutes (cost-based) |
|
||||
|
||||
## Reading Order
|
||||
|
||||
| Task | Files to Read |
|
||||
|------|---------------|
|
||||
| Initialize SDK client | api.md |
|
||||
| Configure auth/timeout/retry | configuration.md |
|
||||
| Find usage patterns | patterns.md |
|
||||
| Debug errors/rate limits | gotchas.md |
|
||||
| Product-specific APIs | ../workers/, ../r2/, ../kv/, etc. |
|
||||
|
||||
## In This Reference
|
||||
|
||||
- **[api.md](api.md)** - SDK client initialization, pagination, error handling, examples
|
||||
- **[configuration.md](configuration.md)** - Environment variables, SDK config, Wrangler setup
|
||||
- **[patterns.md](patterns.md)** - Real-world patterns, batch operations, workflows
|
||||
- **[gotchas.md](gotchas.md)** - Rate limits, SDK-specific issues, troubleshooting
|
||||
|
||||
## See Also
|
||||
|
||||
- [Cloudflare API Docs](https://developers.cloudflare.com/api/)
|
||||
- [Bindings Reference](../bindings/) - Workers runtime bindings (preferred over REST API)
|
||||
- [Wrangler Reference](../wrangler/) - CLI tool for Cloudflare development
|
||||
204
.agents/skills/cloudflare-deploy/references/api/api.md
Normal file
@@ -0,0 +1,204 @@
|
||||
# API Reference
|
||||
|
||||
## Client Initialization
|
||||
|
||||
### TypeScript
|
||||
|
||||
```typescript
|
||||
import Cloudflare from 'cloudflare';
|
||||
|
||||
const client = new Cloudflare({
|
||||
apiToken: process.env.CLOUDFLARE_API_TOKEN,
|
||||
});
|
||||
```
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
from cloudflare import Cloudflare
|
||||
|
||||
client = Cloudflare(api_token=os.environ.get("CLOUDFLARE_API_TOKEN"))
|
||||
|
||||
# For async:
|
||||
from cloudflare import AsyncCloudflare
|
||||
client = AsyncCloudflare(api_token=os.environ["CLOUDFLARE_API_TOKEN"])
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/cloudflare/cloudflare-go/v4"
|
||||
"github.com/cloudflare/cloudflare-go/v4/option"
|
||||
)
|
||||
|
||||
client := cloudflare.NewClient(
|
||||
option.WithAPIToken(os.Getenv("CLOUDFLARE_API_TOKEN")),
|
||||
)
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
### API Token (Recommended)
|
||||
|
||||
**Create token**: Dashboard → My Profile → API Tokens → Create Token
|
||||
|
||||
```bash
|
||||
export CLOUDFLARE_API_TOKEN='your-token-here'
|
||||
|
||||
curl "https://api.cloudflare.com/client/v4/zones" \
|
||||
--header "Authorization: Bearer $CLOUDFLARE_API_TOKEN"
|
||||
```
|
||||
|
||||
**Token scopes**: Always use minimal permissions (zone-specific, time-limited).
|
||||
|
||||
### API Key (Legacy)
|
||||
|
||||
```bash
|
||||
curl "https://api.cloudflare.com/client/v4/zones" \
|
||||
--header "X-Auth-Email: user@example.com" \
|
||||
--header "X-Auth-Key: $CLOUDFLARE_API_KEY"
|
||||
```
|
||||
|
||||
**Not recommended:** Full account access, cannot scope permissions.
|
||||
|
||||
## Auto-Pagination
|
||||
|
||||
All SDKs support automatic pagination for list operations.
|
||||
|
||||
```typescript
|
||||
// TypeScript: for await...of
|
||||
for await (const zone of client.zones.list()) {
|
||||
console.log(zone.id);
|
||||
}
|
||||
```
|
||||
|
||||
```python
|
||||
# Python: iterator protocol
|
||||
for zone in client.zones.list():
|
||||
print(zone.id)
|
||||
```
|
||||
|
||||
```go
|
||||
// Go: ListAutoPaging
|
||||
iter := client.Zones.ListAutoPaging(ctx, cloudflare.ZoneListParams{})
|
||||
for iter.Next() {
|
||||
zone := iter.Current()
|
||||
fmt.Println(zone.ID)
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```typescript
|
||||
try {
|
||||
const zone = await client.zones.get({ zone_id: 'xxx' });
|
||||
} catch (err) {
|
||||
if (err instanceof Cloudflare.NotFoundError) {
|
||||
// 404
|
||||
} else if (err instanceof Cloudflare.RateLimitError) {
|
||||
// 429 - SDK auto-retries with backoff
|
||||
} else if (err instanceof Cloudflare.APIError) {
|
||||
console.log(err.status, err.message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Common Error Types:**
|
||||
- `AuthenticationError` (401) - Invalid token
|
||||
- `PermissionDeniedError` (403) - Insufficient scope
|
||||
- `NotFoundError` (404) - Resource not found
|
||||
- `RateLimitError` (429) - Rate limit exceeded
|
||||
- `InternalServerError` (≥500) - Cloudflare error
|
||||
|
||||
## Zone Management
|
||||
|
||||
```typescript
|
||||
// List zones
|
||||
const zones = await client.zones.list({
|
||||
account: { id: 'account-id' },
|
||||
status: 'active',
|
||||
});
|
||||
|
||||
// Create zone
|
||||
const zone = await client.zones.create({
|
||||
account: { id: 'account-id' },
|
||||
name: 'example.com',
|
||||
type: 'full', // or 'partial'
|
||||
});
|
||||
|
||||
// Update zone
|
||||
await client.zones.edit('zone-id', {
|
||||
paused: false,
|
||||
});
|
||||
|
||||
// Delete zone
|
||||
await client.zones.delete('zone-id');
|
||||
```
|
||||
|
||||
```go
|
||||
// Go: requires cloudflare.F() wrapper
|
||||
zone, err := client.Zones.New(ctx, cloudflare.ZoneNewParams{
|
||||
Account: cloudflare.F(cloudflare.ZoneNewParamsAccount{
|
||||
ID: cloudflare.F("account-id"),
|
||||
}),
|
||||
Name: cloudflare.F("example.com"),
|
||||
Type: cloudflare.F(cloudflare.ZoneNewParamsTypeFull),
|
||||
})
|
||||
```
|
||||
|
||||
## DNS Management
|
||||
|
||||
```typescript
|
||||
// Create DNS record
|
||||
await client.dns.records.create({
|
||||
zone_id: 'zone-id',
|
||||
type: 'A',
|
||||
name: 'subdomain.example.com',
|
||||
content: '192.0.2.1',
|
||||
ttl: 1, // auto
|
||||
proxied: true, // Orange cloud
|
||||
});
|
||||
|
||||
// List DNS records (with auto-pagination)
|
||||
for await (const record of client.dns.records.list({
|
||||
zone_id: 'zone-id',
|
||||
type: 'A',
|
||||
})) {
|
||||
console.log(record.name, record.content);
|
||||
}
|
||||
|
||||
// Update DNS record
|
||||
await client.dns.records.update({
|
||||
zone_id: 'zone-id',
|
||||
dns_record_id: 'record-id',
|
||||
type: 'A',
|
||||
name: 'subdomain.example.com',
|
||||
content: '203.0.113.1',
|
||||
proxied: true,
|
||||
});
|
||||
|
||||
// Delete DNS record
|
||||
await client.dns.records.delete({
|
||||
zone_id: 'zone-id',
|
||||
dns_record_id: 'record-id',
|
||||
});
|
||||
```
|
||||
|
||||
```python
|
||||
# Python example
|
||||
client.dns.records.create(
|
||||
zone_id="zone-id",
|
||||
type="A",
|
||||
name="subdomain.example.com",
|
||||
content="192.0.2.1",
|
||||
ttl=1,
|
||||
proxied=True,
|
||||
)
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [configuration.md](./configuration.md) - SDK configuration, environment variables
|
||||
- [patterns.md](./patterns.md) - Real-world patterns and workflows
|
||||
- [gotchas.md](./gotchas.md) - Rate limits, troubleshooting
|
||||
160
.agents/skills/cloudflare-deploy/references/api/configuration.md
Normal file
@@ -0,0 +1,160 @@
|
||||
# Configuration
|
||||
|
||||
## Environment Variables
|
||||
|
||||
### Set Variables
|
||||
|
||||
| Platform | Command |
|
||||
|----------|---------|
|
||||
| Linux/macOS | `export CLOUDFLARE_API_TOKEN='token'` |
|
||||
| PowerShell | `$env:CLOUDFLARE_API_TOKEN = 'token'` |
|
||||
| Windows CMD | `set CLOUDFLARE_API_TOKEN=token` |
|
||||
|
||||
**Security:** Never commit tokens. Use `.env` files (gitignored) or secret managers.
|
||||
|
||||
### .env File Pattern
|
||||
|
||||
```bash
|
||||
# .env (add to .gitignore)
|
||||
CLOUDFLARE_API_TOKEN=your-token-here
|
||||
CLOUDFLARE_ACCOUNT_ID=your-account-id
|
||||
```
|
||||
|
||||
```typescript
|
||||
// TypeScript
|
||||
import 'dotenv/config';
|
||||
|
||||
const client = new Cloudflare({
|
||||
apiToken: process.env.CLOUDFLARE_API_TOKEN,
|
||||
});
|
||||
```
|
||||
|
||||
```python
|
||||
# Python
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
client = Cloudflare(api_token=os.environ["CLOUDFLARE_API_TOKEN"])
|
||||
```
|
||||
|
||||
## SDK Configuration
|
||||
|
||||
### TypeScript
|
||||
|
||||
```typescript
|
||||
const client = new Cloudflare({
|
||||
apiToken: process.env.CLOUDFLARE_API_TOKEN,
|
||||
timeout: 120000, // 2 min (default 60s), in milliseconds
|
||||
maxRetries: 5, // default 2
|
||||
baseURL: 'https://...', // proxy (rare)
|
||||
});
|
||||
|
||||
// Per-request overrides
|
||||
await client.zones.get(
|
||||
{ zone_id: 'zone-id' },
|
||||
{ timeout: 5000, maxRetries: 0 }
|
||||
);
|
||||
```
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
client = Cloudflare(
|
||||
api_token=os.environ["CLOUDFLARE_API_TOKEN"],
|
||||
timeout=120, # seconds (default 60)
|
||||
max_retries=5, # default 2
|
||||
base_url="https://...", # proxy (rare)
|
||||
)
|
||||
|
||||
# Per-request overrides
|
||||
client.with_options(timeout=5, max_retries=0).zones.get(zone_id="zone-id")
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```go
|
||||
client := cloudflare.NewClient(
|
||||
option.WithAPIToken(os.Getenv("CLOUDFLARE_API_TOKEN")),
|
||||
option.WithMaxRetries(5), // default 10 (higher than TS/Python)
|
||||
option.WithRequestTimeout(2 * time.Minute), // default 60s
|
||||
option.WithBaseURL("https://..."), // proxy (rare)
|
||||
)
|
||||
|
||||
// Per-request overrides
|
||||
client.Zones.Get(ctx, "zone-id", option.WithMaxRetries(0))
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
| Option | TypeScript | Python | Go | Default |
|
||||
|--------|-----------|--------|-----|---------|
|
||||
| Timeout | `timeout` (ms) | `timeout` (s) | `WithRequestTimeout` | 60s |
|
||||
| Retries | `maxRetries` | `max_retries` | `WithMaxRetries` | 2 (Go: 10) |
|
||||
| Base URL | `baseURL` | `base_url` | `WithBaseURL` | api.cloudflare.com |
|
||||
|
||||
**Note:** Go SDK has higher default retries (10) than TypeScript/Python (2).
|
||||
|
||||
## Timeout Configuration
|
||||
|
||||
**When to increase:**
|
||||
- Large zone transfers
|
||||
- Bulk DNS operations
|
||||
- Worker script uploads
|
||||
|
||||
```typescript
|
||||
const client = new Cloudflare({
|
||||
timeout: 300000, // 5 minutes
|
||||
});
|
||||
```
|
||||
|
||||
## Retry Configuration
|
||||
|
||||
**When to increase:** Rate-limit-heavy workflows, flaky network
|
||||
|
||||
**When to decrease:** Fast-fail requirements, user-facing requests
|
||||
|
||||
```typescript
|
||||
// Increase retries for batch operations
|
||||
const client = new Cloudflare({ maxRetries: 10 });
|
||||
|
||||
// Disable retries for fast-fail
|
||||
const fastClient = new Cloudflare({ maxRetries: 0 });
|
||||
```
|
||||
|
||||
## Wrangler CLI Integration
|
||||
|
||||
```bash
|
||||
# Configure authentication
|
||||
wrangler login
|
||||
# Or
|
||||
export CLOUDFLARE_API_TOKEN='token'
|
||||
|
||||
# Common commands that use API
|
||||
wrangler deploy # Uploads worker via API
|
||||
wrangler kv:key put # KV operations
|
||||
wrangler r2 bucket create # R2 operations
|
||||
wrangler d1 execute # D1 operations
|
||||
wrangler pages deploy # Pages operations
|
||||
|
||||
# Get API configuration
|
||||
wrangler whoami # Shows authenticated user
|
||||
```
|
||||
|
||||
### wrangler.toml
|
||||
|
||||
```toml
|
||||
name = "my-worker"
|
||||
main = "src/index.ts"
|
||||
compatibility_date = "2024-01-01"
|
||||
account_id = "your-account-id"
|
||||
|
||||
# Can also use env vars:
|
||||
# CLOUDFLARE_ACCOUNT_ID
|
||||
# CLOUDFLARE_API_TOKEN
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [api.md](./api.md) - Client initialization, authentication
|
||||
- [gotchas.md](./gotchas.md) - Rate limits, timeout errors
|
||||
- [Wrangler Reference](../wrangler/) - CLI tool details
|
||||
225
.agents/skills/cloudflare-deploy/references/api/gotchas.md
Normal file
@@ -0,0 +1,225 @@
|
||||
# Gotchas & Troubleshooting
|
||||
|
||||
## Rate Limits & 429 Errors
|
||||
|
||||
**Actual Limits:**
|
||||
- **1200 requests / 5 minutes** per user/token (global)
|
||||
- **200 requests / second** per IP address
|
||||
- **GraphQL: 320 / 5 minutes** (cost-based)
|
||||
|
||||
**SDK Behavior:**
|
||||
- Auto-retry with exponential backoff (default 2 retries, Go: 10)
|
||||
- Respects `Retry-After` header
|
||||
- Throws `RateLimitError` after exhausting retries
|
||||
|
||||
**Solution:**
|
||||
|
||||
```typescript
|
||||
// Increase retries for rate-limit-heavy workflows
|
||||
const client = new Cloudflare({ maxRetries: 5 });
|
||||
|
||||
// Add application-level throttling
|
||||
import pLimit from 'p-limit';
|
||||
const limit = pLimit(10); // Max 10 concurrent requests
|
||||
```
|
||||
|
||||
## SDK-Specific Issues
|
||||
|
||||
### Go: Required Field Wrapper
|
||||
|
||||
**Problem:** Go SDK requires `cloudflare.F()` wrapper for optional fields.
|
||||
|
||||
```go
|
||||
// ❌ WRONG - Won't compile or send field
|
||||
client.Zones.New(ctx, cloudflare.ZoneNewParams{
|
||||
Name: "example.com",
|
||||
})
|
||||
|
||||
// ✅ CORRECT
|
||||
client.Zones.New(ctx, cloudflare.ZoneNewParams{
|
||||
Name: cloudflare.F("example.com"),
|
||||
Account: cloudflare.F(cloudflare.ZoneNewParamsAccount{
|
||||
ID: cloudflare.F("account-id"),
|
||||
}),
|
||||
})
|
||||
```
|
||||
|
||||
**Why:** Distinguishes between zero value, null, and omitted fields.
|
||||
|
||||
### Python: Async vs Sync Clients
|
||||
|
||||
**Problem:** Using sync client in async context or vice versa.
|
||||
|
||||
```python
|
||||
# ❌ WRONG - Can't await sync client
|
||||
from cloudflare import Cloudflare
|
||||
client = Cloudflare()
|
||||
await client.zones.list() # TypeError
|
||||
|
||||
# ✅ CORRECT - Use AsyncCloudflare
|
||||
from cloudflare import AsyncCloudflare
|
||||
client = AsyncCloudflare()
|
||||
await client.zones.list()
|
||||
```
|
||||
|
||||
## Token Permission Errors (403)
|
||||
|
||||
**Problem:** API returns 403 Forbidden despite valid token.
|
||||
|
||||
**Cause:** Token lacks required permissions (scope).
|
||||
|
||||
**Scopes Required:**
|
||||
|
||||
| Operation | Required Scope |
|
||||
|-----------|----------------|
|
||||
| List zones | Zone:Read (zone-level or account-level) |
|
||||
| Create zone | Zone:Edit (account-level) |
|
||||
| Edit DNS | DNS:Edit (zone-level) |
|
||||
| Deploy Worker | Workers Script:Edit (account-level) |
|
||||
| Read KV | Workers KV Storage:Read |
|
||||
| Write KV | Workers KV Storage:Edit |
|
||||
|
||||
**Solution:** Re-create token with correct permissions in Dashboard → My Profile → API Tokens.
|
||||
|
||||
## Pagination Truncation
|
||||
|
||||
**Problem:** Only getting first 20 results (default page size).
|
||||
|
||||
**Solution:** Use auto-pagination iterators.
|
||||
|
||||
```typescript
|
||||
// ❌ WRONG - Only first page (20 items)
|
||||
const page = await client.zones.list();
|
||||
|
||||
// ✅ CORRECT - All results
|
||||
const zones = [];
|
||||
for await (const zone of client.zones.list()) {
|
||||
zones.push(zone);
|
||||
}
|
||||
```
|
||||
|
||||
## Workers Subrequests
|
||||
|
||||
**Problem:** Rate limit hit faster than expected in Workers.
|
||||
|
||||
**Cause:** Workers subrequests count as separate API calls.
|
||||
|
||||
**Solution:** Use bindings instead of REST API in Workers (see ../bindings/).
|
||||
|
||||
```typescript
|
||||
// ❌ WRONG - REST API in Workers (counts against rate limit)
|
||||
const client = new Cloudflare({ apiToken: env.CLOUDFLARE_API_TOKEN });
|
||||
const zones = await client.zones.list();
|
||||
|
||||
// ✅ CORRECT - Use bindings (no rate limit)
|
||||
// Access via env.MY_BINDING
|
||||
```
|
||||
|
||||
## Authentication Errors (401)
|
||||
|
||||
**Problem:** "Authentication failed" or "Invalid token"
|
||||
|
||||
**Causes:**
|
||||
- Token expired
|
||||
- Token deleted/revoked
|
||||
- Token not set in environment
|
||||
- Wrong token format
|
||||
|
||||
**Solution:**
|
||||
|
||||
```typescript
|
||||
// Verify token is set
|
||||
if (!process.env.CLOUDFLARE_API_TOKEN) {
|
||||
throw new Error('CLOUDFLARE_API_TOKEN not set');
|
||||
}
|
||||
|
||||
// Test token
|
||||
const user = await client.user.tokens.verify();
|
||||
console.log('Token valid:', user.status);
|
||||
```
|
||||
|
||||
## Timeout Errors
|
||||
|
||||
**Problem:** Request times out (default 60s).
|
||||
|
||||
**Cause:** Large operations (bulk DNS, zone transfers).
|
||||
|
||||
**Solution:** Increase timeout or split operations.
|
||||
|
||||
```typescript
|
||||
// Increase timeout
|
||||
const client = new Cloudflare({
|
||||
timeout: 300000, // 5 minutes
|
||||
});
|
||||
|
||||
// Or split operations
|
||||
const batchSize = 100;
|
||||
for (let i = 0; i < records.length; i += batchSize) {
|
||||
const batch = records.slice(i, i + batchSize);
|
||||
await processBatch(batch);
|
||||
}
|
||||
```
|
||||
|
||||
## Zone Not Found (404)
|
||||
|
||||
**Problem:** Zone ID valid but returns 404.
|
||||
|
||||
**Causes:**
|
||||
- Zone not in account associated with token
|
||||
- Zone deleted
|
||||
- Wrong zone ID format
|
||||
|
||||
**Solution:**
|
||||
|
||||
```typescript
|
||||
// List all zones to find correct ID
|
||||
for await (const zone of client.zones.list()) {
|
||||
console.log(zone.id, zone.name);
|
||||
}
|
||||
```
|
||||
|
||||
## Limits Reference
|
||||
|
||||
| Resource/Limit | Value | Notes |
|
||||
|----------------|-------|-------|
|
||||
| API rate limit | 1200/5min | Per user/token |
|
||||
| IP rate limit | 200/sec | Per IP |
|
||||
| GraphQL rate limit | 320/5min | Cost-based |
|
||||
| Parallel requests (recommended) | < 10 | Avoid overwhelming API |
|
||||
| Default page size | 20 | Use auto-pagination |
|
||||
| Max page size | 50 | Some endpoints |
|
||||
|
||||
## Best Practices
|
||||
|
||||
**Security:**
|
||||
- Never commit tokens
|
||||
- Use minimal permissions
|
||||
- Rotate tokens regularly
|
||||
- Set token expiration
|
||||
|
||||
**Performance:**
|
||||
- Batch operations
|
||||
- Use pagination wisely
|
||||
- Cache responses
|
||||
- Handle rate limits
|
||||
|
||||
**Code Organization:**
|
||||
|
||||
```typescript
|
||||
// Create reusable client instance
|
||||
export const cfClient = new Cloudflare({
|
||||
apiToken: process.env.CLOUDFLARE_API_TOKEN,
|
||||
maxRetries: 5,
|
||||
});
|
||||
|
||||
// Wrap common operations
|
||||
export async function getZoneDetails(zoneId: string) {
|
||||
return await cfClient.zones.get({ zone_id: zoneId });
|
||||
}
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [api.md](./api.md) - Error types, authentication
|
||||
- [configuration.md](./configuration.md) - Timeout/retry configuration
|
||||
- [patterns.md](./patterns.md) - Error handling patterns
|
||||
204
.agents/skills/cloudflare-deploy/references/api/patterns.md
Normal file
@@ -0,0 +1,204 @@
|
||||
# Common Patterns
|
||||
|
||||
## List All with Auto-Pagination
|
||||
|
||||
**Problem:** API returns paginated results. Default page size is 20.
|
||||
|
||||
**Solution:** Use SDK auto-pagination to iterate all results.
|
||||
|
||||
```typescript
|
||||
// TypeScript
|
||||
for await (const zone of client.zones.list()) {
|
||||
console.log(zone.name);
|
||||
}
|
||||
```
|
||||
|
||||
```python
|
||||
# Python
|
||||
for zone in client.zones.list():
|
||||
print(zone.name)
|
||||
```
|
||||
|
||||
```go
|
||||
// Go
|
||||
iter := client.Zones.ListAutoPaging(ctx, cloudflare.ZoneListParams{})
|
||||
for iter.Next() {
|
||||
fmt.Println(iter.Current().Name)
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling with Retry
|
||||
|
||||
**Problem:** Rate limits (429) and transient errors need retry.
|
||||
|
||||
**Solution:** SDKs auto-retry with exponential backoff. Customize as needed.
|
||||
|
||||
```typescript
|
||||
// Increase retries for rate-limit-heavy operations
|
||||
const client = new Cloudflare({ maxRetries: 5 });
|
||||
|
||||
try {
|
||||
const zone = await client.zones.create({ /* ... */ });
|
||||
} catch (err) {
|
||||
if (err instanceof Cloudflare.RateLimitError) {
|
||||
// Already retried 5 times with backoff
|
||||
const retryAfter = err.headers['retry-after'];
|
||||
console.log(`Rate limited. Retry after ${retryAfter}s`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Batch Parallel Operations
|
||||
|
||||
**Problem:** Need to create multiple resources quickly.
|
||||
|
||||
**Solution:** Use `Promise.all()` for parallel requests (respect rate limits).
|
||||
|
||||
```typescript
|
||||
// Create multiple DNS records in parallel
|
||||
const records = ['www', 'api', 'cdn'].map(subdomain =>
|
||||
client.dns.records.create({
|
||||
zone_id: 'zone-id',
|
||||
type: 'A',
|
||||
name: `${subdomain}.example.com`,
|
||||
content: '192.0.2.1',
|
||||
})
|
||||
);
|
||||
await Promise.all(records);
|
||||
```
|
||||
|
||||
**Controlled concurrency** (avoid rate limits):
|
||||
|
||||
```typescript
|
||||
import pLimit from 'p-limit';
|
||||
const limit = pLimit(10); // Max 10 concurrent
|
||||
|
||||
const subdomains = ['www', 'api', 'cdn', /* many more */];
|
||||
const records = subdomains.map(subdomain =>
|
||||
limit(() => client.dns.records.create({
|
||||
zone_id: 'zone-id',
|
||||
type: 'A',
|
||||
name: `${subdomain}.example.com`,
|
||||
content: '192.0.2.1',
|
||||
}))
|
||||
);
|
||||
await Promise.all(records);
|
||||
```
|
||||
|
||||
## Zone CRUD Workflow
|
||||
|
||||
```typescript
|
||||
// Create
|
||||
const zone = await client.zones.create({
|
||||
account: { id: 'account-id' },
|
||||
name: 'example.com',
|
||||
type: 'full',
|
||||
});
|
||||
|
||||
// Read
|
||||
const fetched = await client.zones.get({ zone_id: zone.id });
|
||||
|
||||
// Update
|
||||
await client.zones.edit(zone.id, { paused: false });
|
||||
|
||||
// Delete
|
||||
await client.zones.delete(zone.id);
|
||||
```
|
||||
|
||||
## DNS Bulk Update
|
||||
|
||||
```typescript
|
||||
// Fetch all A records
|
||||
const records = [];
|
||||
for await (const record of client.dns.records.list({
|
||||
zone_id: 'zone-id',
|
||||
type: 'A',
|
||||
})) {
|
||||
records.push(record);
|
||||
}
|
||||
|
||||
// Update all to new IP
|
||||
await Promise.all(records.map(record =>
|
||||
client.dns.records.update({
|
||||
zone_id: 'zone-id',
|
||||
dns_record_id: record.id,
|
||||
type: 'A',
|
||||
name: record.name,
|
||||
content: '203.0.113.1', // New IP
|
||||
proxied: record.proxied,
|
||||
ttl: record.ttl,
|
||||
})
|
||||
));
|
||||
```
|
||||
|
||||
## Filter and Collect Results
|
||||
|
||||
```typescript
|
||||
// Find all proxied A records
|
||||
const proxiedRecords = [];
|
||||
for await (const record of client.dns.records.list({
|
||||
zone_id: 'zone-id',
|
||||
type: 'A',
|
||||
})) {
|
||||
if (record.proxied) {
|
||||
proxiedRecords.push(record);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Error Recovery Pattern
|
||||
|
||||
```typescript
|
||||
async function createZoneWithRetry(name: string, maxAttempts = 3) {
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
try {
|
||||
return await client.zones.create({
|
||||
account: { id: 'account-id' },
|
||||
name,
|
||||
type: 'full',
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof Cloudflare.RateLimitError && attempt < maxAttempts) {
|
||||
const retryAfter = parseInt(err.headers['retry-after'] || '5');
|
||||
console.log(`Rate limited, waiting ${retryAfter}s (retry ${attempt}/${maxAttempts})`);
|
||||
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Conditional Update Pattern
|
||||
|
||||
```typescript
|
||||
// Only update if zone is active
|
||||
const zone = await client.zones.get({ zone_id: 'zone-id' });
|
||||
if (zone.status === 'active') {
|
||||
await client.zones.edit(zone.id, { paused: false });
|
||||
}
|
||||
```
|
||||
|
||||
## Batch with Error Handling
|
||||
|
||||
```typescript
|
||||
// Process multiple zones, continue on errors
|
||||
const results = await Promise.allSettled(
|
||||
zoneIds.map(id => client.zones.get({ zone_id: id }))
|
||||
);
|
||||
|
||||
results.forEach((result, i) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
console.log(`Zone ${i}: ${result.value.name}`);
|
||||
} else {
|
||||
console.error(`Zone ${i} failed:`, result.reason.message);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [api.md](./api.md) - SDK client initialization, basic operations
|
||||
- [gotchas.md](./gotchas.md) - Rate limits, common errors
|
||||
- [configuration.md](./configuration.md) - SDK configuration options
|
||||
@@ -0,0 +1,90 @@
|
||||
# Cloudflare Argo Smart Routing Skill Reference
|
||||
|
||||
## Overview
|
||||
|
||||
Cloudflare Argo Smart Routing is a performance optimization service that detects real-time network issues and routes web traffic across the most efficient network path. It continuously monitors network conditions and intelligently routes traffic through the fastest, most reliable routes in Cloudflare's network.
|
||||
|
||||
**Note on Smart Shield:** Argo Smart Routing is being integrated into Cloudflare's Smart Shield product for enhanced DDoS protection and performance. Existing Argo customers maintain full functionality with gradual migration to Smart Shield features.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Enable via cURL
|
||||
```bash
|
||||
curl -X PATCH "https://api.cloudflare.com/client/v4/zones/{zone_id}/argo/smart_routing" \
|
||||
-H "Authorization: Bearer YOUR_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"value": "on"}'
|
||||
```
|
||||
|
||||
### Enable via TypeScript SDK
|
||||
```typescript
|
||||
import Cloudflare from 'cloudflare';
|
||||
|
||||
const client = new Cloudflare({ apiToken: process.env.CLOUDFLARE_API_TOKEN });
|
||||
|
||||
const result = await client.argo.smartRouting.edit({
|
||||
zone_id: 'your-zone-id',
|
||||
value: 'on',
|
||||
});
|
||||
|
||||
console.log(`Argo enabled: ${result.value}`);
|
||||
```
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### What It Does
|
||||
- **Intelligent routing**: Detects congestion, outages, packet loss in real-time
|
||||
- **Global optimization**: Routes across 300+ Cloudflare data centers
|
||||
- **Automatic failover**: Switches paths when issues detected (typically <1s)
|
||||
- **Works with existing setup**: No origin changes required
|
||||
|
||||
### Billing Model
|
||||
- Usage-based: Charged per GB of traffic (excluding DDoS/WAF mitigated traffic)
|
||||
- Requires billing configuration before enabling
|
||||
- Available on Enterprise+ plans (check zone eligibility)
|
||||
|
||||
### When to Use
|
||||
- **High-traffic production sites** with global user base
|
||||
- **Latency-sensitive applications** (APIs, real-time services)
|
||||
- **Sites behind Cloudflare proxy** (orange-clouded DNS records)
|
||||
- **Combined with Tiered Cache** for maximum performance gains
|
||||
|
||||
### When NOT to Use
|
||||
- Development/staging environments (cost control)
|
||||
- Low-traffic sites (<1TB/month) where cost may exceed benefit
|
||||
- Sites with primarily single-region traffic
|
||||
|
||||
## Should I Enable Argo?
|
||||
|
||||
| Your Situation | Recommendation |
|
||||
|----------------|----------------|
|
||||
| Global production app, >1TB/month traffic | ✅ Enable - likely ROI positive |
|
||||
| Enterprise plan, latency-critical APIs | ✅ Enable - performance matters |
|
||||
| Regional site, <100GB/month traffic | ⚠️ Evaluate - cost may not justify |
|
||||
| Development/staging environment | ❌ Disable - use in production only |
|
||||
| Not yet configured billing | ❌ Configure billing first |
|
||||
|
||||
## Reading Order by Task
|
||||
|
||||
| Your Goal | Start With | Then Read |
|
||||
|-----------|------------|-----------|
|
||||
| Enable Argo for first time | Quick Start above → [configuration.md](configuration.md) | [gotchas.md](gotchas.md) |
|
||||
| Use TypeScript/Python SDK | [api.md](api.md) | [patterns.md](patterns.md) |
|
||||
| Terraform/IaC setup | [configuration.md](configuration.md) | - |
|
||||
| Enable for Spectrum TCP app | [patterns.md](patterns.md) → Spectrum section | [api.md](api.md) |
|
||||
| Troubleshoot enablement issue | [gotchas.md](gotchas.md) | [api.md](api.md) |
|
||||
| Manage billing/usage | [patterns.md](patterns.md) → Billing section | [gotchas.md](gotchas.md) |
|
||||
|
||||
## In This Reference
|
||||
|
||||
- **[api.md](api.md)** - API endpoints, SDK methods, error handling, Python/TypeScript examples
|
||||
- **[configuration.md](configuration.md)** - Terraform setup, environment config, billing configuration
|
||||
- **[patterns.md](patterns.md)** - Tiered Cache integration, Spectrum TCP apps, billing management, validation patterns
|
||||
- **[gotchas.md](gotchas.md)** - Common errors, permission issues, limits, best practices
|
||||
|
||||
## See Also
|
||||
|
||||
- [Cloudflare Argo Smart Routing Docs](https://developers.cloudflare.com/argo-smart-routing/)
|
||||
- [Cloudflare Smart Shield](https://developers.cloudflare.com/smart-shield/)
|
||||
- [Spectrum Documentation](https://developers.cloudflare.com/spectrum/)
|
||||
- [Tiered Cache](https://developers.cloudflare.com/cache/how-to/tiered-cache/)
|
||||
@@ -0,0 +1,240 @@
|
||||
## API Reference
|
||||
|
||||
**Note on Smart Shield:** Argo Smart Routing is being integrated into Cloudflare's Smart Shield product. API endpoints remain stable; existing integrations continue to work without changes.
|
||||
|
||||
### Base Endpoint
|
||||
```
|
||||
https://api.cloudflare.com/client/v4
|
||||
```
|
||||
|
||||
### Authentication
|
||||
Use API tokens with Zone:Argo Smart Routing:Edit permissions:
|
||||
|
||||
```bash
|
||||
# Headers required
|
||||
X-Auth-Email: user@example.com
|
||||
Authorization: Bearer YOUR_API_TOKEN
|
||||
```
|
||||
|
||||
### Get Argo Smart Routing Status
|
||||
|
||||
**Endpoint:** `GET /zones/{zone_id}/argo/smart_routing`
|
||||
|
||||
**Description:** Retrieves current Argo Smart Routing enablement status.
|
||||
|
||||
**cURL Example:**
|
||||
```bash
|
||||
curl -X GET "https://api.cloudflare.com/client/v4/zones/{zone_id}/argo/smart_routing" \
|
||||
-H "Authorization: Bearer YOUR_API_TOKEN" \
|
||||
-H "Content-Type: application/json"
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"result": {
|
||||
"id": "smart_routing",
|
||||
"value": "on",
|
||||
"editable": true,
|
||||
"modified_on": "2024-01-11T12:00:00Z"
|
||||
},
|
||||
"success": true,
|
||||
"errors": [],
|
||||
"messages": []
|
||||
}
|
||||
```
|
||||
|
||||
**TypeScript SDK Example:**
|
||||
```typescript
|
||||
import Cloudflare from 'cloudflare';
|
||||
|
||||
const client = new Cloudflare({
|
||||
apiToken: process.env.CLOUDFLARE_API_TOKEN
|
||||
});
|
||||
|
||||
const status = await client.argo.smartRouting.get({ zone_id: 'your-zone-id' });
|
||||
console.log(`Argo status: ${status.value}, editable: ${status.editable}`);
|
||||
```
|
||||
|
||||
**Python SDK Example:**
|
||||
```python
|
||||
from cloudflare import Cloudflare
|
||||
|
||||
client = Cloudflare(api_token=os.environ.get('CLOUDFLARE_API_TOKEN'))
|
||||
|
||||
status = client.argo.smart_routing.get(zone_id='your-zone-id')
|
||||
print(f"Argo status: {status.value}, editable: {status.editable}")
|
||||
```
|
||||
|
||||
### Update Argo Smart Routing Status
|
||||
|
||||
**Endpoint:** `PATCH /zones/{zone_id}/argo/smart_routing`
|
||||
|
||||
**Description:** Enable or disable Argo Smart Routing for a zone.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"value": "on" // or "off"
|
||||
}
|
||||
```
|
||||
|
||||
**cURL Example:**
|
||||
```bash
|
||||
curl -X PATCH "https://api.cloudflare.com/client/v4/zones/{zone_id}/argo/smart_routing" \
|
||||
-H "Authorization: Bearer YOUR_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"value": "on"}'
|
||||
```
|
||||
|
||||
**TypeScript SDK Example:**
|
||||
```typescript
|
||||
const result = await client.argo.smartRouting.edit({
|
||||
zone_id: 'your-zone-id',
|
||||
value: 'on',
|
||||
});
|
||||
console.log(`Updated: ${result.value} at ${result.modified_on}`);
|
||||
```
|
||||
|
||||
**Python SDK Example:**
|
||||
```python
|
||||
result = client.argo.smart_routing.edit(
|
||||
zone_id='your-zone-id',
|
||||
value='on'
|
||||
)
|
||||
print(f"Updated: {result.value} at {result.modified_on}")
|
||||
```
|
||||
|
||||
## Checking Editability Before Updates
|
||||
|
||||
**Critical:** Always check the `editable` field before attempting to enable/disable Argo. When `editable: false`, the zone has restrictions (billing not configured, insufficient permissions, or plan limitations).
|
||||
|
||||
**Pattern:**
|
||||
```typescript
|
||||
async function safelyEnableArgo(client: Cloudflare, zoneId: string): Promise<boolean> {
|
||||
const status = await client.argo.smartRouting.get({ zone_id: zoneId });
|
||||
|
||||
if (!status.editable) {
|
||||
console.error('Cannot modify Argo: editable=false (check billing/permissions)');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (status.value === 'on') {
|
||||
console.log('Argo already enabled');
|
||||
return true;
|
||||
}
|
||||
|
||||
await client.argo.smartRouting.edit({ zone_id: zoneId, value: 'on' });
|
||||
console.log('Argo enabled successfully');
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
**Python Pattern:**
|
||||
```python
|
||||
def safely_enable_argo(client: Cloudflare, zone_id: str) -> bool:
|
||||
status = client.argo.smart_routing.get(zone_id=zone_id)
|
||||
|
||||
if not status.editable:
|
||||
print('Cannot modify Argo: editable=false (check billing/permissions)')
|
||||
return False
|
||||
|
||||
if status.value == 'on':
|
||||
print('Argo already enabled')
|
||||
return True
|
||||
|
||||
client.argo.smart_routing.edit(zone_id=zone_id, value='on')
|
||||
print('Argo enabled successfully')
|
||||
return True
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
The TypeScript SDK provides typed error classes for robust error handling:
|
||||
|
||||
```typescript
|
||||
import Cloudflare from 'cloudflare';
|
||||
import { APIError, APIConnectionError, RateLimitError } from 'cloudflare';
|
||||
|
||||
async function enableArgoWithErrorHandling(client: Cloudflare, zoneId: string) {
|
||||
try {
|
||||
const result = await client.argo.smartRouting.edit({
|
||||
zone_id: zoneId,
|
||||
value: 'on',
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (error instanceof RateLimitError) {
|
||||
console.error('Rate limited. Retry after:', error.response?.headers.get('retry-after'));
|
||||
// Implement exponential backoff
|
||||
} else if (error instanceof APIError) {
|
||||
console.error('API error:', error.status, error.message);
|
||||
if (error.status === 403) {
|
||||
console.error('Permission denied - check API token scopes');
|
||||
} else if (error.status === 400) {
|
||||
console.error('Bad request - verify zone_id and payload');
|
||||
}
|
||||
} else if (error instanceof APIConnectionError) {
|
||||
console.error('Connection failed:', error.message);
|
||||
// Retry with exponential backoff
|
||||
} else {
|
||||
console.error('Unexpected error:', error);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Python Error Handling:**
|
||||
```python
|
||||
from cloudflare import Cloudflare, APIError, RateLimitError
|
||||
|
||||
def enable_argo_with_error_handling(client: Cloudflare, zone_id: str):
|
||||
try:
|
||||
result = client.argo.smart_routing.edit(zone_id=zone_id, value='on')
|
||||
return result
|
||||
except RateLimitError as e:
|
||||
print(f"Rate limited. Retry after: {e.response.headers.get('retry-after')}")
|
||||
raise
|
||||
except APIError as e:
|
||||
print(f"API error: {e.status} - {e.message}")
|
||||
if e.status == 403:
|
||||
print('Permission denied - check API token scopes')
|
||||
elif e.status == 400:
|
||||
print('Bad request - verify zone_id and payload')
|
||||
raise
|
||||
except Exception as e:
|
||||
print(f"Unexpected error: {e}")
|
||||
raise
|
||||
```
|
||||
|
||||
## Response Schema
|
||||
|
||||
All Argo Smart Routing API responses follow this structure:
|
||||
|
||||
```typescript
|
||||
interface ArgoSmartRoutingResponse {
|
||||
result: {
|
||||
id: 'smart_routing';
|
||||
value: 'on' | 'off';
|
||||
editable: boolean;
|
||||
modified_on: string; // ISO 8601 timestamp
|
||||
};
|
||||
success: boolean;
|
||||
errors: Array<{
|
||||
code: number;
|
||||
message: string;
|
||||
}>;
|
||||
messages: Array<string>;
|
||||
}
|
||||
```
|
||||
|
||||
## Key Response Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `value` | `"on" \| "off"` | Current enablement status |
|
||||
| `editable` | `boolean` | Whether changes are allowed (check before PATCH) |
|
||||
| `modified_on` | `string` | ISO timestamp of last modification |
|
||||
| `success` | `boolean` | Whether request succeeded |
|
||||
| `errors` | `Array` | Error details if `success: false`
|
||||
@@ -0,0 +1,197 @@
|
||||
## Configuration Management
|
||||
|
||||
**Note on Smart Shield Evolution:** Argo Smart Routing is being integrated into Smart Shield. Configuration methods below remain valid; Terraform and IaC patterns unchanged.
|
||||
|
||||
### Infrastructure as Code (Terraform)
|
||||
|
||||
```hcl
|
||||
# terraform/argo.tf
|
||||
# Note: Use Cloudflare Terraform provider
|
||||
|
||||
resource "cloudflare_argo" "example" {
|
||||
zone_id = var.zone_id
|
||||
smart_routing = "on"
|
||||
tiered_caching = "on"
|
||||
}
|
||||
|
||||
variable "zone_id" {
|
||||
description = "Cloudflare Zone ID"
|
||||
type = string
|
||||
}
|
||||
|
||||
output "argo_enabled" {
|
||||
value = cloudflare_argo.example.smart_routing
|
||||
description = "Argo Smart Routing status"
|
||||
}
|
||||
```
|
||||
|
||||
### Environment-Based Configuration
|
||||
|
||||
```typescript
|
||||
// config/argo.ts
|
||||
interface ArgoEnvironmentConfig {
|
||||
enabled: boolean;
|
||||
tieredCache: boolean;
|
||||
monitoring: {
|
||||
usageAlerts: boolean;
|
||||
threshold: number;
|
||||
};
|
||||
}
|
||||
|
||||
const configs: Record<string, ArgoEnvironmentConfig> = {
|
||||
production: {
|
||||
enabled: true,
|
||||
tieredCache: true,
|
||||
monitoring: {
|
||||
usageAlerts: true,
|
||||
threshold: 1000, // GB
|
||||
},
|
||||
},
|
||||
staging: {
|
||||
enabled: true,
|
||||
tieredCache: false,
|
||||
monitoring: {
|
||||
usageAlerts: false,
|
||||
threshold: 100, // GB
|
||||
},
|
||||
},
|
||||
development: {
|
||||
enabled: false,
|
||||
tieredCache: false,
|
||||
monitoring: {
|
||||
usageAlerts: false,
|
||||
threshold: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function getArgoConfig(env: string): ArgoEnvironmentConfig {
|
||||
return configs[env] || configs.development;
|
||||
}
|
||||
```
|
||||
|
||||
### Pulumi Configuration
|
||||
|
||||
```typescript
|
||||
// pulumi/argo.ts
|
||||
import * as cloudflare from '@pulumi/cloudflare';
|
||||
|
||||
const zone = new cloudflare.Zone('example-zone', {
|
||||
zone: 'example.com',
|
||||
plan: 'enterprise',
|
||||
});
|
||||
|
||||
const argoSettings = new cloudflare.Argo('argo-config', {
|
||||
zoneId: zone.id,
|
||||
smartRouting: 'on',
|
||||
tieredCaching: 'on',
|
||||
});
|
||||
|
||||
export const argoEnabled = argoSettings.smartRouting;
|
||||
export const zoneId = zone.id;
|
||||
```
|
||||
|
||||
## Billing Configuration
|
||||
|
||||
Before enabling Argo Smart Routing, ensure billing is configured for the account:
|
||||
|
||||
**Prerequisites:**
|
||||
1. Valid payment method on file
|
||||
2. Enterprise or higher plan
|
||||
3. Zone must have billing enabled
|
||||
|
||||
**Check Billing Status via Dashboard:**
|
||||
1. Navigate to Account → Billing
|
||||
2. Verify payment method configured
|
||||
3. Check zone subscription status
|
||||
|
||||
**Note:** Attempting to enable Argo without billing configured will result in `editable: false` in API responses.
|
||||
|
||||
## Environment Variable Setup
|
||||
|
||||
**Required Environment Variables:**
|
||||
```bash
|
||||
# .env
|
||||
CLOUDFLARE_API_TOKEN=your_api_token_here
|
||||
CLOUDFLARE_ZONE_ID=your_zone_id_here
|
||||
CLOUDFLARE_ACCOUNT_ID=your_account_id_here
|
||||
|
||||
# Optional
|
||||
ARGO_ENABLED=true
|
||||
ARGO_TIERED_CACHE=true
|
||||
```
|
||||
|
||||
**TypeScript Configuration Loader:**
|
||||
```typescript
|
||||
// config/env.ts
|
||||
import { z } from 'zod';
|
||||
|
||||
const envSchema = z.object({
|
||||
CLOUDFLARE_API_TOKEN: z.string().min(1),
|
||||
CLOUDFLARE_ZONE_ID: z.string().min(1),
|
||||
CLOUDFLARE_ACCOUNT_ID: z.string().min(1),
|
||||
ARGO_ENABLED: z.string().optional().default('false'),
|
||||
ARGO_TIERED_CACHE: z.string().optional().default('false'),
|
||||
});
|
||||
|
||||
export const env = envSchema.parse(process.env);
|
||||
|
||||
export const argoConfig = {
|
||||
enabled: env.ARGO_ENABLED === 'true',
|
||||
tieredCache: env.ARGO_TIERED_CACHE === 'true',
|
||||
};
|
||||
```
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
**GitHub Actions Example:**
|
||||
```yaml
|
||||
# .github/workflows/deploy-argo.yml
|
||||
name: Deploy Argo Configuration
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'terraform/argo.tf'
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Terraform
|
||||
uses: hashicorp/setup-terraform@v2
|
||||
|
||||
- name: Terraform Init
|
||||
run: terraform init
|
||||
working-directory: ./terraform
|
||||
|
||||
- name: Terraform Apply
|
||||
run: terraform apply -auto-approve
|
||||
working-directory: ./terraform
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
TF_VAR_zone_id: ${{ secrets.CLOUDFLARE_ZONE_ID }}
|
||||
```
|
||||
|
||||
## Enterprise Preview Program
|
||||
|
||||
For early access to Argo Smart Routing features and Smart Shield integration:
|
||||
|
||||
**Eligibility:**
|
||||
- Enterprise plan customers
|
||||
- Active Cloudflare support contract
|
||||
- Production traffic >100GB/month
|
||||
|
||||
**How to Join:**
|
||||
1. Contact Cloudflare account team or support
|
||||
2. Request Argo/Smart Shield preview access
|
||||
3. Receive preview zone configuration
|
||||
|
||||
**Preview Features:**
|
||||
- Enhanced analytics and reporting
|
||||
- Smart Shield DDoS integration
|
||||
- Advanced routing policies
|
||||
- Priority support for routing issues
|
||||
@@ -0,0 +1,111 @@
|
||||
## Best Practices Summary
|
||||
|
||||
**Smart Shield Note:** Argo Smart Routing evolving into Smart Shield. Best practices below remain applicable; monitor Cloudflare changelog for Smart Shield updates.
|
||||
|
||||
1. **Always check editability** before attempting to enable/disable Argo
|
||||
2. **Set up billing notifications** to avoid unexpected costs
|
||||
3. **Combine with Tiered Cache** for maximum performance benefit
|
||||
4. **Use in production only** - disable for dev/staging to control costs
|
||||
5. **Monitor analytics** - require 500+ requests in 48h for detailed metrics
|
||||
6. **Handle errors gracefully** - check for billing, permissions, zone compatibility
|
||||
7. **Test configuration changes** in staging before production
|
||||
8. **Use TypeScript SDK** for type safety and better developer experience
|
||||
9. **Implement retry logic** for API calls in production systems
|
||||
10. **Document zone-specific settings** for team visibility
|
||||
|
||||
## Common Errors
|
||||
|
||||
### "Argo unavailable"
|
||||
|
||||
**Problem:** API returns error "Argo Smart Routing is unavailable for this zone"
|
||||
|
||||
**Cause:** Zone not eligible or billing not set up
|
||||
|
||||
**Solution:**
|
||||
1. Verify zone has Enterprise or higher plan
|
||||
2. Check billing is configured in Account → Billing
|
||||
3. Ensure payment method is valid and current
|
||||
4. Contact Cloudflare support if eligibility unclear
|
||||
|
||||
### "Cannot enable/disable"
|
||||
|
||||
**Problem:** API call succeeds but status remains unchanged, or `editable: false` in GET response
|
||||
|
||||
**Cause:** Insufficient permissions or zone restrictions
|
||||
|
||||
**Solution:**
|
||||
1. Check API token has `Zone:Argo Smart Routing:Edit` permission
|
||||
2. Verify `editable: true` in GET response before attempting PATCH
|
||||
3. If `editable: false`, check:
|
||||
- Billing configured for account
|
||||
- Zone plan includes Argo (Enterprise+)
|
||||
- No active zone holds or suspensions
|
||||
- API token has correct scopes
|
||||
|
||||
### `editable: false` Error
|
||||
|
||||
**Problem:** GET request returns `"editable": false`, preventing enable/disable
|
||||
|
||||
**Cause:** Zone-level restrictions from billing, plan, or permissions
|
||||
|
||||
**Solution Pattern:**
|
||||
```typescript
|
||||
const status = await client.argo.smartRouting.get({ zone_id: zoneId });
|
||||
|
||||
if (!status.editable) {
|
||||
// Don't attempt to modify - will fail
|
||||
console.error('Cannot modify Argo settings:');
|
||||
console.error('- Check billing is configured');
|
||||
console.error('- Verify zone has Enterprise+ plan');
|
||||
console.error('- Confirm API token has Edit permission');
|
||||
throw new Error('Argo is not editable for this zone');
|
||||
}
|
||||
|
||||
// Safe to proceed with enable/disable
|
||||
await client.argo.smartRouting.edit({ zone_id: zoneId, value: 'on' });
|
||||
```
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
**Problem:** `429 Too Many Requests` error from API
|
||||
|
||||
**Cause:** Exceeded API rate limits (typically 1200 requests per 5 minutes)
|
||||
|
||||
**Solution:**
|
||||
```typescript
|
||||
import { RateLimitError } from 'cloudflare';
|
||||
|
||||
try {
|
||||
await client.argo.smartRouting.edit({ zone_id: zoneId, value: 'on' });
|
||||
} catch (error) {
|
||||
if (error instanceof RateLimitError) {
|
||||
const retryAfter = error.response?.headers.get('retry-after');
|
||||
console.log(`Rate limited. Retry after ${retryAfter} seconds`);
|
||||
|
||||
// Implement exponential backoff
|
||||
await new Promise(resolve => setTimeout(resolve, (retryAfter || 60) * 1000));
|
||||
// Retry request
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Limits
|
||||
|
||||
| Resource/Limit | Value | Notes |
|
||||
|----------------|-------|-------|
|
||||
| Min requests for analytics | 500 in 48h | For detailed metrics via GraphQL |
|
||||
| Zones supported | Enterprise+ | Check zone plan in dashboard |
|
||||
| Billing requirement | Must be configured | Before enabling; verify payment method |
|
||||
| API rate limit | 1200 req / 5 min | Per API token across all endpoints |
|
||||
| Spectrum apps | No hard limit | Each app can enable Argo independently |
|
||||
| Traffic counting | Proxied only | Only orange-clouded DNS records count |
|
||||
| DDoS/WAF exemption | Yes | Mitigated traffic excluded from billing |
|
||||
| Analytics latency | 1-5 minutes | Real-time metrics not available |
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Official Argo Smart Routing Docs](https://developers.cloudflare.com/argo-smart-routing/)
|
||||
- [Cloudflare Smart Shield](https://developers.cloudflare.com/smart-shield/)
|
||||
- [API Authentication](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/)
|
||||
- [Cloudflare TypeScript SDK](https://github.com/cloudflare/cloudflare-typescript)
|
||||
- [Cloudflare Python SDK](https://github.com/cloudflare/cloudflare-python)
|
||||
@@ -0,0 +1,104 @@
|
||||
# Integration Patterns
|
||||
|
||||
## Enable Argo + Tiered Cache
|
||||
|
||||
```typescript
|
||||
async function enableOptimalPerformance(client: Cloudflare, zoneId: string) {
|
||||
await Promise.all([
|
||||
client.argo.smartRouting.edit({ zone_id: zoneId, value: 'on' }),
|
||||
client.argo.tieredCaching.edit({ zone_id: zoneId, value: 'on' }),
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
**Flow:** Visitor → Edge (Lower-Tier) → [Cache Miss] → Upper-Tier → [Cache Miss + Argo] → Origin
|
||||
|
||||
**Impact:** Argo ~30% latency reduction + Tiered Cache 50-80% origin offload
|
||||
|
||||
## Usage Analytics (GraphQL)
|
||||
|
||||
```graphql
|
||||
query ArgoAnalytics($zoneTag: string!) {
|
||||
viewer {
|
||||
zones(filter: { zoneTag: $zoneTag }) {
|
||||
httpRequestsAdaptiveGroups(limit: 1000) {
|
||||
sum { argoBytes, bytes }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Billing:** ~$0.10/GB. DDoS-mitigated and WAF-blocked traffic NOT charged.
|
||||
|
||||
## Spectrum TCP Integration
|
||||
|
||||
Enable Argo for non-HTTP traffic (databases, game servers, IoT):
|
||||
|
||||
```typescript
|
||||
// Update existing app
|
||||
await client.spectrum.apps.update(appId, { zone_id: zoneId, argo_smart_routing: true });
|
||||
|
||||
// Create new app with Argo
|
||||
await client.spectrum.apps.create({
|
||||
zone_id: zoneId,
|
||||
dns: { type: 'CNAME', name: 'tcp.example.com' },
|
||||
origin_direct: ['tcp://origin.example.com:3306'],
|
||||
protocol: 'tcp/3306',
|
||||
argo_smart_routing: true,
|
||||
});
|
||||
```
|
||||
|
||||
**Use cases:** MySQL/PostgreSQL (3306/5432), game servers, MQTT (1883), SSH (22)
|
||||
|
||||
## Pre-Flight Validation
|
||||
|
||||
```typescript
|
||||
async function validateArgoEligibility(client: Cloudflare, zoneId: string) {
|
||||
const status = await client.argo.smartRouting.get({ zone_id: zoneId });
|
||||
const zone = await client.zones.get({ zone_id: zoneId });
|
||||
|
||||
const issues: string[] = [];
|
||||
if (!status.editable) issues.push('Zone not editable');
|
||||
if (['free', 'pro'].includes(zone.plan.legacy_id)) issues.push('Requires Business+ plan');
|
||||
if (zone.status !== 'active') issues.push('Zone not active');
|
||||
|
||||
return { canEnable: issues.length === 0, issues };
|
||||
}
|
||||
```
|
||||
|
||||
## Post-Enable Verification
|
||||
|
||||
```typescript
|
||||
async function verifyArgoEnabled(client: Cloudflare, zoneId: string): Promise<boolean> {
|
||||
await new Promise(r => setTimeout(r, 2000)); // Wait for propagation
|
||||
const status = await client.argo.smartRouting.get({ zone_id: zoneId });
|
||||
return status.value === 'on';
|
||||
}
|
||||
```
|
||||
|
||||
## Full Setup Pattern
|
||||
|
||||
```typescript
|
||||
async function setupArgo(client: Cloudflare, zoneId: string) {
|
||||
// 1. Validate
|
||||
const { canEnable, issues } = await validateArgoEligibility(client, zoneId);
|
||||
if (!canEnable) throw new Error(issues.join(', '));
|
||||
|
||||
// 2. Enable both features
|
||||
await Promise.all([
|
||||
client.argo.smartRouting.edit({ zone_id: zoneId, value: 'on' }),
|
||||
client.argo.tieredCaching.edit({ zone_id: zoneId, value: 'on' }),
|
||||
]);
|
||||
|
||||
// 3. Verify
|
||||
const [argo, cache] = await Promise.all([
|
||||
client.argo.smartRouting.get({ zone_id: zoneId }),
|
||||
client.argo.tieredCaching.get({ zone_id: zoneId }),
|
||||
]);
|
||||
|
||||
return { argo: argo.value === 'on', tieredCache: cache.value === 'on' };
|
||||
}
|
||||
```
|
||||
|
||||
**When to combine:** High-traffic sites (>1TB/mo), global users, cacheable content.
|
||||
122
.agents/skills/cloudflare-deploy/references/bindings/README.md
Normal file
@@ -0,0 +1,122 @@
|
||||
# Cloudflare Bindings Skill Reference
|
||||
|
||||
Expert guidance on Cloudflare Workers Bindings - the runtime APIs that connect Workers to Cloudflare platform resources.
|
||||
|
||||
## What Are Bindings?
|
||||
|
||||
Bindings are how Workers access Cloudflare resources (storage, compute, services) via the `env` object. They're configured in `wrangler.jsonc`, type-safe via TypeScript, and zero-overhead at runtime.
|
||||
|
||||
## Reading Order
|
||||
|
||||
1. **This file** - Binding catalog and selection guide
|
||||
2. **[api.md](api.md)** - TypeScript types and env access patterns
|
||||
3. **[configuration.md](configuration.md)** - Complete wrangler.jsonc examples
|
||||
4. **[patterns.md](patterns.md)** - Best practices and common patterns
|
||||
5. **[gotchas.md](gotchas.md)** - Critical pitfalls and troubleshooting
|
||||
|
||||
## Binding Catalog
|
||||
|
||||
### Storage Bindings
|
||||
|
||||
| Binding | Use Case | Access Pattern |
|
||||
|---------|----------|----------------|
|
||||
| **KV** | Key-value cache, CDN-backed reads | `env.MY_KV.get(key)` |
|
||||
| **R2** | Object storage (S3-compatible) | `env.MY_BUCKET.get(key)` |
|
||||
| **D1** | SQL database (SQLite) | `env.DB.prepare(sql).all()` |
|
||||
| **Durable Objects** | Coordination, real-time state | `env.MY_DO.get(id)` |
|
||||
| **Vectorize** | Vector embeddings search | `env.VECTORIZE.query(vector)` |
|
||||
| **Queues** | Async message processing | `env.MY_QUEUE.send(msg)` |
|
||||
|
||||
### Compute Bindings
|
||||
|
||||
| Binding | Use Case | Access Pattern |
|
||||
|---------|----------|----------------|
|
||||
| **Service** | Worker-to-Worker RPC | `env.MY_SERVICE.fetch(req)` |
|
||||
| **Workers AI** | LLM inference | `env.AI.run(model, input)` |
|
||||
| **Browser Rendering** | Headless Chrome | `env.BROWSER.fetch(url)` |
|
||||
|
||||
### Platform Bindings
|
||||
|
||||
| Binding | Use Case | Access Pattern |
|
||||
|---------|----------|----------------|
|
||||
| **Analytics Engine** | Custom metrics | `env.ANALYTICS.writeDataPoint(data)` |
|
||||
| **mTLS** | Client certificates | `env.MY_CERT` (string) |
|
||||
| **Hyperdrive** | Database pooling | `env.HYPERDRIVE.connectionString` |
|
||||
| **Rate Limiting** | Request throttling | `env.RATE_LIMITER.limit(id)` |
|
||||
| **Workflows** | Long-running workflows | `env.MY_WORKFLOW.create()` |
|
||||
|
||||
### Configuration Bindings
|
||||
|
||||
| Binding | Use Case | Access Pattern |
|
||||
|---------|----------|----------------|
|
||||
| **Environment Variables** | Non-sensitive config | `env.API_URL` (string) |
|
||||
| **Secrets** | Sensitive values | `env.API_KEY` (string) |
|
||||
| **Text/Data Blobs** | Static files | `env.MY_BLOB` (string) |
|
||||
| **WASM** | WebAssembly modules | `env.MY_WASM` (WebAssembly.Module) |
|
||||
|
||||
## Quick Selection Guide
|
||||
|
||||
**Need persistent storage?**
|
||||
- Key-value < 25MB → **KV**
|
||||
- Files/objects → **R2**
|
||||
- Relational data → **D1**
|
||||
- Real-time coordination → **Durable Objects**
|
||||
|
||||
**Need AI/compute?**
|
||||
- LLM inference → **Workers AI**
|
||||
- Scraping/PDFs → **Browser Rendering**
|
||||
- Call another Worker → **Service binding**
|
||||
|
||||
**Need async processing?**
|
||||
- Background jobs → **Queues**
|
||||
|
||||
**Need config?**
|
||||
- Public values → **Environment Variables**
|
||||
- Secrets → **Secrets** (never commit)
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. **Add binding to wrangler.jsonc:**
|
||||
```jsonc
|
||||
{
|
||||
"kv_namespaces": [
|
||||
{ "binding": "MY_KV", "id": "your-kv-id" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
2. **Generate types:**
|
||||
```bash
|
||||
npx wrangler types
|
||||
```
|
||||
|
||||
3. **Access in Worker:**
|
||||
```typescript
|
||||
export default {
|
||||
async fetch(request, env, ctx) {
|
||||
await env.MY_KV.put('key', 'value');
|
||||
return new Response('OK');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Type Safety
|
||||
|
||||
Bindings are fully typed via `wrangler types`. See [api.md](api.md) for details.
|
||||
|
||||
## Limits
|
||||
|
||||
- 64 bindings max per Worker (all types combined)
|
||||
- See [gotchas.md](gotchas.md) for per-binding limits
|
||||
|
||||
## Key Concepts
|
||||
|
||||
**Zero-overhead access:** Bindings compiled into Worker, no network calls to access
|
||||
**Type-safe:** Full TypeScript support via `wrangler types`
|
||||
**Per-environment:** Different IDs for dev/staging/production
|
||||
**Secrets vs Vars:** Secrets encrypted at rest, never in config files
|
||||
|
||||
## See Also
|
||||
|
||||
- [Cloudflare Docs: Bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/)
|
||||
- [Wrangler Configuration](https://developers.cloudflare.com/workers/wrangler/configuration/)
|
||||
203
.agents/skills/cloudflare-deploy/references/bindings/api.md
Normal file
@@ -0,0 +1,203 @@
|
||||
# Bindings API Reference
|
||||
|
||||
## TypeScript Types
|
||||
|
||||
Cloudflare generates binding types via `npx wrangler types`. This creates `.wrangler/types/runtime.d.ts` with your Env interface.
|
||||
|
||||
### Generated Env Interface
|
||||
|
||||
After running `wrangler types`, TypeScript knows your bindings:
|
||||
|
||||
```typescript
|
||||
interface Env {
|
||||
// From wrangler.jsonc bindings
|
||||
MY_KV: KVNamespace;
|
||||
MY_BUCKET: R2Bucket;
|
||||
DB: D1Database;
|
||||
MY_SERVICE: Fetcher;
|
||||
AI: Ai;
|
||||
|
||||
// From vars
|
||||
API_URL: string;
|
||||
|
||||
// From secrets (set via wrangler secret put)
|
||||
API_KEY: string;
|
||||
}
|
||||
```
|
||||
|
||||
### Binding Types
|
||||
|
||||
| Config | TypeScript Type | Package |
|
||||
|--------|-----------------|---------|
|
||||
| `kv_namespaces` | `KVNamespace` | `@cloudflare/workers-types` |
|
||||
| `r2_buckets` | `R2Bucket` | `@cloudflare/workers-types` |
|
||||
| `d1_databases` | `D1Database` | `@cloudflare/workers-types` |
|
||||
| `durable_objects.bindings` | `DurableObjectNamespace` | `@cloudflare/workers-types` |
|
||||
| `vectorize` | `VectorizeIndex` | `@cloudflare/workers-types` |
|
||||
| `queues.producers` | `Queue` | `@cloudflare/workers-types` |
|
||||
| `services` | `Fetcher` | `@cloudflare/workers-types` |
|
||||
| `ai` | `Ai` | `@cloudflare/workers-types` |
|
||||
| `browser` | `Fetcher` | `@cloudflare/workers-types` |
|
||||
| `analytics_engine_datasets` | `AnalyticsEngineDataset` | `@cloudflare/workers-types` |
|
||||
| `hyperdrive` | `Hyperdrive` | `@cloudflare/workers-types` |
|
||||
| `rate_limiting` | `RateLimit` | `@cloudflare/workers-types` |
|
||||
| `workflows` | `Workflow` | `@cloudflare/workers-types` |
|
||||
| `mtls_certificates` / `vars` / `text_blobs` / `data_blobs` | `string` | Built-in |
|
||||
| `wasm_modules` | `WebAssembly.Module` | Built-in |
|
||||
|
||||
## Accessing Bindings
|
||||
|
||||
### Method 1: fetch() Handler (Recommended)
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
|
||||
const value = await env.MY_KV.get('key');
|
||||
return new Response(value);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Why:** Type-safe, aligns with Workers API, supports ctx for waitUntil/passThroughOnException.
|
||||
|
||||
### Method 2: Hono Framework
|
||||
|
||||
```typescript
|
||||
import { Hono } from 'hono';
|
||||
|
||||
const app = new Hono<{ Bindings: Env }>();
|
||||
|
||||
app.get('/', async (c) => {
|
||||
const value = await c.env.MY_KV.get('key');
|
||||
return c.json({ value });
|
||||
});
|
||||
|
||||
export default app;
|
||||
```
|
||||
|
||||
**Why:** c.env auto-typed, ergonomic for routing-heavy apps.
|
||||
|
||||
### Method 3: Module Workers (Legacy)
|
||||
|
||||
```typescript
|
||||
export async function handleRequest(request: Request, env: Env): Promise<Response> {
|
||||
const value = await env.MY_KV.get('key');
|
||||
return new Response(value);
|
||||
}
|
||||
|
||||
addEventListener('fetch', (event) => {
|
||||
// env not directly available - requires workarounds
|
||||
});
|
||||
```
|
||||
|
||||
**Avoid:** Use fetch() handler instead (Method 1).
|
||||
|
||||
## Type Generation Workflow
|
||||
|
||||
### Initial Setup
|
||||
|
||||
```bash
|
||||
# Install wrangler
|
||||
npm install -D wrangler
|
||||
|
||||
# Generate types from wrangler.jsonc
|
||||
npx wrangler types
|
||||
```
|
||||
|
||||
### After Changing Bindings
|
||||
|
||||
```bash
|
||||
# Added/modified binding in wrangler.jsonc
|
||||
npx wrangler types
|
||||
|
||||
# TypeScript now sees updated Env interface
|
||||
```
|
||||
|
||||
**Note:** `wrangler types` outputs to `.wrangler/types/runtime.d.ts`. TypeScript picks this up automatically if `@cloudflare/workers-types` is in `tsconfig.json` `"types"` array.
|
||||
|
||||
## Key Binding Methods
|
||||
|
||||
**KV:**
|
||||
```typescript
|
||||
await env.MY_KV.get(key, { type: 'json' }); // text|json|arrayBuffer|stream
|
||||
await env.MY_KV.put(key, value, { expirationTtl: 3600 });
|
||||
await env.MY_KV.delete(key);
|
||||
await env.MY_KV.list({ prefix: 'user:' });
|
||||
```
|
||||
|
||||
**R2:**
|
||||
```typescript
|
||||
await env.BUCKET.get(key);
|
||||
await env.BUCKET.put(key, value);
|
||||
await env.BUCKET.delete(key);
|
||||
await env.BUCKET.list({ prefix: 'images/' });
|
||||
```
|
||||
|
||||
**D1:**
|
||||
```typescript
|
||||
await env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId).first();
|
||||
await env.DB.batch([stmt1, stmt2]);
|
||||
```
|
||||
|
||||
**Service:**
|
||||
```typescript
|
||||
await env.MY_SERVICE.fetch(new Request('https://fake/path'));
|
||||
```
|
||||
|
||||
**Workers AI:**
|
||||
```typescript
|
||||
await env.AI.run('@cf/meta/llama-3.1-8b-instruct', { prompt: 'Hello' });
|
||||
```
|
||||
|
||||
**Queues:**
|
||||
```typescript
|
||||
await env.MY_QUEUE.send({ userId: 123, action: 'process' });
|
||||
```
|
||||
|
||||
**Durable Objects:**
|
||||
```typescript
|
||||
const id = env.MY_DO.idFromName('user-123');
|
||||
const stub = env.MY_DO.get(id);
|
||||
await stub.fetch(new Request('https://fake/increment'));
|
||||
```
|
||||
|
||||
## Runtime vs Build-Time Types
|
||||
|
||||
| Type Source | When Generated | Use Case |
|
||||
|-------------|----------------|----------|
|
||||
| `@cloudflare/workers-types` | npm install | Base Workers APIs (Request, Response, etc.) |
|
||||
| `wrangler types` | After config change | Your specific bindings (Env interface) |
|
||||
|
||||
**Install both:**
|
||||
```bash
|
||||
npm install -D @cloudflare/workers-types
|
||||
npx wrangler types
|
||||
```
|
||||
|
||||
## Type Safety Best Practices
|
||||
|
||||
1. **Never use `any` for env:**
|
||||
```typescript
|
||||
// ❌ BAD
|
||||
async fetch(request: Request, env: any) { }
|
||||
|
||||
// ✅ GOOD
|
||||
async fetch(request: Request, env: Env) { }
|
||||
```
|
||||
|
||||
2. **Run wrangler types after config changes:**
|
||||
```bash
|
||||
# After editing wrangler.jsonc
|
||||
npx wrangler types
|
||||
```
|
||||
|
||||
3. **Check generated types match config:**
|
||||
```bash
|
||||
# View generated Env interface
|
||||
cat .wrangler/types/runtime.d.ts
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [Workers Types Package](https://www.npmjs.com/package/@cloudflare/workers-types)
|
||||
- [Wrangler Types Command](https://developers.cloudflare.com/workers/wrangler/commands/#types)
|
||||
@@ -0,0 +1,188 @@
|
||||
# Binding Configuration Reference
|
||||
|
||||
## Storage Bindings
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"kv_namespaces": [{ "binding": "MY_KV", "id": "..." }],
|
||||
"r2_buckets": [{ "binding": "MY_BUCKET", "bucket_name": "my-bucket" }],
|
||||
"d1_databases": [{ "binding": "DB", "database_name": "my-db", "database_id": "..." }],
|
||||
"durable_objects": { "bindings": [{ "name": "MY_DO", "class_name": "MyDO" }] },
|
||||
"vectorize": [{ "binding": "VECTORIZE", "index_name": "my-index" }],
|
||||
"queues": { "producers": [{ "binding": "MY_QUEUE", "queue": "my-queue" }] }
|
||||
}
|
||||
```
|
||||
|
||||
**Create commands:**
|
||||
```bash
|
||||
npx wrangler kv namespace create MY_KV
|
||||
npx wrangler r2 bucket create my-bucket
|
||||
npx wrangler d1 create my-db
|
||||
npx wrangler vectorize create my-index --dimensions=768 --metric=cosine
|
||||
npx wrangler queues create my-queue
|
||||
|
||||
# List existing resources
|
||||
npx wrangler kv namespace list
|
||||
npx wrangler r2 bucket list
|
||||
npx wrangler d1 list
|
||||
npx wrangler vectorize list
|
||||
npx wrangler queues list
|
||||
```
|
||||
|
||||
## Compute Bindings
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"services": [{
|
||||
"binding": "MY_SERVICE",
|
||||
"service": "other-worker",
|
||||
"environment": "production" // Optional: target specific env
|
||||
}],
|
||||
"ai": { "binding": "AI" },
|
||||
"browser": { "binding": "BROWSER" },
|
||||
"workflows": [{ "binding": "MY_WORKFLOW", "name": "my-workflow" }]
|
||||
}
|
||||
```
|
||||
|
||||
**Create workflows:**
|
||||
```bash
|
||||
npx wrangler workflows create my-workflow
|
||||
```
|
||||
|
||||
## Platform Bindings
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"analytics_engine_datasets": [{ "binding": "ANALYTICS" }],
|
||||
"mtls_certificates": [{ "binding": "MY_CERT", "certificate_id": "..." }],
|
||||
"hyperdrive": [{ "binding": "HYPERDRIVE", "id": "..." }],
|
||||
"unsafe": {
|
||||
"bindings": [{ "name": "RATE_LIMITER", "type": "ratelimit", "namespace_id": "..." }]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration Bindings
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"vars": {
|
||||
"API_URL": "https://api.example.com",
|
||||
"MAX_RETRIES": "3"
|
||||
},
|
||||
"text_blobs": { "MY_TEXT": "./data/template.html" },
|
||||
"data_blobs": { "MY_DATA": "./data/config.bin" },
|
||||
"wasm_modules": { "MY_WASM": "./build/module.wasm" }
|
||||
}
|
||||
```
|
||||
|
||||
**Secrets (never in config):**
|
||||
```bash
|
||||
npx wrangler secret put API_KEY
|
||||
```
|
||||
|
||||
## Environment-Specific Configuration
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"name": "my-worker",
|
||||
"vars": { "ENV": "production" },
|
||||
"kv_namespaces": [{ "binding": "CACHE", "id": "prod-kv-id" }],
|
||||
|
||||
"env": {
|
||||
"staging": {
|
||||
"vars": { "ENV": "staging" },
|
||||
"kv_namespaces": [{ "binding": "CACHE", "id": "staging-kv-id" }]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Deploy:**
|
||||
```bash
|
||||
npx wrangler deploy # Production
|
||||
npx wrangler deploy --env staging
|
||||
```
|
||||
|
||||
## Local Development
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"kv_namespaces": [{
|
||||
"binding": "MY_KV",
|
||||
"id": "prod-id",
|
||||
"preview_id": "dev-id" // Used in wrangler dev
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
**Or use remote:**
|
||||
```bash
|
||||
npx wrangler dev --remote # Uses production bindings
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"$schema": "./node_modules/wrangler/config-schema.json",
|
||||
"name": "my-app",
|
||||
"main": "src/index.ts",
|
||||
"compatibility_date": "2025-01-01",
|
||||
|
||||
"vars": { "API_URL": "https://api.example.com" },
|
||||
"kv_namespaces": [{ "binding": "CACHE", "id": "abc123" }],
|
||||
"r2_buckets": [{ "binding": "ASSETS", "bucket_name": "my-assets" }],
|
||||
"d1_databases": [{ "binding": "DB", "database_name": "my-db", "database_id": "xyz789" }],
|
||||
"services": [{ "binding": "AUTH", "service": "auth-worker" }],
|
||||
"ai": { "binding": "AI" }
|
||||
}
|
||||
```
|
||||
|
||||
## Binding-Specific Configuration
|
||||
|
||||
### Durable Objects with Class Export
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{ "name": "COUNTER", "class_name": "Counter", "script_name": "my-worker" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// In same Worker or script_name Worker
|
||||
export class Counter {
|
||||
constructor(private state: DurableObjectState, private env: Env) {}
|
||||
async fetch(request: Request) { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
### Queue Consumers
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"queues": {
|
||||
"producers": [{ "binding": "MY_QUEUE", "queue": "my-queue" }],
|
||||
"consumers": [{ "queue": "my-queue", "max_batch_size": 10 }]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Queue consumer handler: `export default { async queue(batch, env) { /* process batch.messages */ } }`
|
||||
|
||||
## Key Points
|
||||
|
||||
- **64 binding limit** (all types combined)
|
||||
- **Secrets**: Always use `wrangler secret put`, never commit
|
||||
- **Types**: Run `npx wrangler types` after config changes
|
||||
- **Environments**: Use `env` field for staging/production variants
|
||||
- **Development**: Use `preview_id` or `--remote` flag
|
||||
- **IDs vs Names**: Some bindings use `id` (KV, D1), others use `name` (R2, Queues)
|
||||
|
||||
## See Also
|
||||
|
||||
- [Wrangler Configuration](https://developers.cloudflare.com/workers/wrangler/configuration/)
|
||||
208
.agents/skills/cloudflare-deploy/references/bindings/gotchas.md
Normal file
@@ -0,0 +1,208 @@
|
||||
# Binding Gotchas and Troubleshooting
|
||||
|
||||
## Critical: Global Scope Mutation
|
||||
|
||||
### ❌ THE #1 GOTCHA: Caching env in Global Scope
|
||||
|
||||
```typescript
|
||||
// ❌ DANGEROUS - env cached at deploy time
|
||||
const apiKey = env.API_KEY; // ERROR: env not available in global scope
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env) {
|
||||
// Uses undefined or stale value!
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Why it breaks:**
|
||||
- `env` not available in global scope
|
||||
- If using workarounds, secrets may not update without redeployment
|
||||
- Leads to "Cannot read property 'X' of undefined" errors
|
||||
|
||||
**✅ Always access env per-request:**
|
||||
```typescript
|
||||
export default {
|
||||
async fetch(request: Request, env: Env) {
|
||||
const apiKey = env.API_KEY; // Fresh every request
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Common Errors
|
||||
|
||||
### "env.MY_KV is undefined"
|
||||
|
||||
**Cause:** Name mismatch or not configured
|
||||
**Solution:** Check wrangler.jsonc (case-sensitive), run `npx wrangler types`, verify `npx wrangler kv namespace list`
|
||||
|
||||
### "Property 'MY_KV' does not exist on type 'Env'"
|
||||
|
||||
**Cause:** Types not generated
|
||||
**Solution:** `npx wrangler types`
|
||||
|
||||
### "preview_id is required for --remote"
|
||||
|
||||
**Cause:** Missing preview binding
|
||||
**Solution:** Add `"preview_id": "dev-id"` or use `npx wrangler dev` (local mode)
|
||||
|
||||
### "Secret updated but Worker still uses old value"
|
||||
|
||||
**Cause:** Cached in global scope or not redeployed
|
||||
**Solution:** Avoid global caching, redeploy after secret change
|
||||
|
||||
### "KV get() returns null for existing key"
|
||||
|
||||
**Cause:** Eventual consistency (60s), wrong namespace, wrong environment
|
||||
**Solution:**
|
||||
```bash
|
||||
# Check key exists
|
||||
npx wrangler kv key get --binding=MY_KV "your-key"
|
||||
|
||||
# Verify namespace ID
|
||||
npx wrangler kv namespace list
|
||||
|
||||
# Check environment
|
||||
npx wrangler deployments list
|
||||
```
|
||||
|
||||
### "D1 database not found"
|
||||
|
||||
**Solution:** `npx wrangler d1 list`, verify ID in wrangler.jsonc
|
||||
|
||||
### "Service binding returns 'No such service'"
|
||||
|
||||
**Cause:** Target Worker not deployed, name mismatch, environment mismatch
|
||||
**Solution:**
|
||||
```bash
|
||||
# List deployed Workers
|
||||
npx wrangler deployments list --name=target-worker
|
||||
|
||||
# Check service binding config
|
||||
cat wrangler.jsonc | grep -A2 services
|
||||
|
||||
# Deploy target first
|
||||
cd ../target-worker && npx wrangler deploy
|
||||
```
|
||||
|
||||
### "Rate limit exceeded" on KV writes
|
||||
|
||||
**Cause:** >1 write/second per key
|
||||
**Solution:** Use different keys, Durable Objects, or Queues
|
||||
|
||||
## Type Safety Gotchas
|
||||
|
||||
### Missing @cloudflare/workers-types
|
||||
|
||||
**Error:** `Cannot find name 'Request'`
|
||||
**Solution:** `npm install -D @cloudflare/workers-types`, add to tsconfig.json `"types"`
|
||||
|
||||
### Binding Type Mismatches
|
||||
|
||||
```typescript
|
||||
// ❌ Wrong - KV returns string | null
|
||||
const value: string = await env.MY_KV.get('key');
|
||||
|
||||
// ✅ Handle null
|
||||
const value = await env.MY_KV.get('key');
|
||||
if (!value) return new Response('Not found', { status: 404 });
|
||||
```
|
||||
|
||||
## Environment Gotchas
|
||||
|
||||
### Wrong Environment Deployed
|
||||
|
||||
**Solution:** Check `npx wrangler deployments list`, use `--env` flag
|
||||
|
||||
### Secrets Not Per-Environment
|
||||
|
||||
**Solution:** Set per environment: `npx wrangler secret put API_KEY --env staging`
|
||||
|
||||
## Development Gotchas
|
||||
|
||||
**wrangler dev vs deploy:**
|
||||
- dev: Uses `preview_id` or local bindings, secrets not available
|
||||
- deploy: Uses production `id`, secrets available
|
||||
|
||||
**Access secrets in dev:** `npx wrangler dev --remote`
|
||||
**Persist local data:** `npx wrangler dev --persist`
|
||||
|
||||
## Performance Gotchas
|
||||
|
||||
### Sequential Binding Calls
|
||||
|
||||
```typescript
|
||||
// ❌ Slow
|
||||
const user = await env.DB.prepare('...').first();
|
||||
const config = await env.MY_KV.get('config');
|
||||
|
||||
// ✅ Parallel
|
||||
const [user, config] = await Promise.all([
|
||||
env.DB.prepare('...').first(),
|
||||
env.MY_KV.get('config')
|
||||
]);
|
||||
```
|
||||
|
||||
## Security Gotchas
|
||||
|
||||
**❌ Secrets in logs:** `console.log('Key:', env.API_KEY)` - visible in dashboard
|
||||
**✅** `console.log('Key:', env.API_KEY ? '***' : 'missing')`
|
||||
|
||||
**❌ Exposing env:** `return Response.json(env)` - exposes all bindings
|
||||
**✅** Never return env object in responses
|
||||
|
||||
## Limits Reference
|
||||
|
||||
| Resource | Limit | Impact | Plan |
|
||||
|----------|-------|--------|------|
|
||||
| **Bindings per Worker** | 64 total | All binding types combined | All |
|
||||
| **Environment variables** | 64 max, 5KB each | Per Worker | All |
|
||||
| **Secret size** | 1KB | Per secret | All |
|
||||
| **KV key size** | 512 bytes | UTF-8 encoded | All |
|
||||
| **KV value size** | 25 MB | Per value | All |
|
||||
| **KV writes per key** | 1/second | Per key; exceeding = 429 error | All |
|
||||
| **KV list() results** | 1000 keys | Per call; use cursor for more | All |
|
||||
| **KV operations** | 1000 reads/day | Free tier only | Free |
|
||||
| **R2 object size** | 5 TB | Per object | All |
|
||||
| **R2 operations** | 1M Class A/month free | Writes | All |
|
||||
| **D1 database size** | 10 GB | Per database | All |
|
||||
| **D1 rows per query** | 100,000 | Result set limit | All |
|
||||
| **D1 databases** | 10 | Free tier | Free |
|
||||
| **Queue batch size** | 100 messages | Per consumer batch | All |
|
||||
| **Queue message size** | 128 KB | Per message | All |
|
||||
| **Service binding calls** | Unlimited | Counts toward CPU time | All |
|
||||
| **Durable Objects** | 1M requests/month free | First 1M | Free |
|
||||
|
||||
## Debugging Tips
|
||||
|
||||
```bash
|
||||
# Check configuration
|
||||
npx wrangler deploy --dry-run # Validate config without deploying
|
||||
npx wrangler kv namespace list # List KV namespaces
|
||||
npx wrangler secret list # List secrets (not values)
|
||||
npx wrangler deployments list # Recent deployments
|
||||
|
||||
# Inspect bindings
|
||||
npx wrangler kv key list --binding=MY_KV
|
||||
npx wrangler kv key get --binding=MY_KV "key-name"
|
||||
npx wrangler r2 object get my-bucket/file.txt
|
||||
npx wrangler d1 execute my-db --command="SELECT * FROM sqlite_master"
|
||||
|
||||
# Test locally
|
||||
npx wrangler dev # Local mode
|
||||
npx wrangler dev --remote # Production bindings
|
||||
npx wrangler dev --persist # Persist data across restarts
|
||||
|
||||
# Verify types
|
||||
npx wrangler types
|
||||
cat .wrangler/types/runtime.d.ts | grep "interface Env"
|
||||
|
||||
# Debug specific binding issues
|
||||
npx wrangler tail # Stream logs in real-time
|
||||
npx wrangler tail --format=pretty # Formatted logs
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [Workers Limits](https://developers.cloudflare.com/workers/platform/limits/)
|
||||
- [Wrangler Commands](https://developers.cloudflare.com/workers/wrangler/commands/)
|
||||
200
.agents/skills/cloudflare-deploy/references/bindings/patterns.md
Normal file
@@ -0,0 +1,200 @@
|
||||
# Binding Patterns and Best Practices
|
||||
|
||||
## Service Binding Patterns
|
||||
|
||||
### RPC via Service Bindings
|
||||
|
||||
```typescript
|
||||
// auth-worker
|
||||
export default {
|
||||
async fetch(request: Request, env: Env) {
|
||||
const token = request.headers.get('Authorization');
|
||||
return new Response(JSON.stringify({ valid: await validateToken(token) }));
|
||||
}
|
||||
}
|
||||
|
||||
// api-worker
|
||||
const response = await env.AUTH_SERVICE.fetch(
|
||||
new Request('https://fake-host/validate', {
|
||||
headers: { 'Authorization': token }
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
**Why RPC?** Zero latency (same datacenter), no DNS, free, type-safe.
|
||||
|
||||
**HTTP vs Service:**
|
||||
```typescript
|
||||
// ❌ HTTP (slow, paid, cross-region latency)
|
||||
await fetch('https://auth-worker.example.com/validate');
|
||||
|
||||
// ✅ Service binding (fast, free, same isolate)
|
||||
await env.AUTH_SERVICE.fetch(new Request('https://fake-host/validate'));
|
||||
```
|
||||
|
||||
**URL doesn't matter:** Service bindings ignore hostname/protocol, routing happens via binding name.
|
||||
|
||||
### Typed Service RPC
|
||||
|
||||
```typescript
|
||||
// shared-types.ts
|
||||
export interface AuthRequest { token: string; }
|
||||
export interface AuthResponse { valid: boolean; userId?: string; }
|
||||
|
||||
// auth-worker
|
||||
export default {
|
||||
async fetch(request: Request): Promise<Response> {
|
||||
const body: AuthRequest = await request.json();
|
||||
const response: AuthResponse = { valid: true, userId: '123' };
|
||||
return Response.json(response);
|
||||
}
|
||||
}
|
||||
|
||||
// api-worker
|
||||
const response = await env.AUTH_SERVICE.fetch(
|
||||
new Request('https://fake/validate', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ token } satisfies AuthRequest)
|
||||
})
|
||||
);
|
||||
const data: AuthResponse = await response.json();
|
||||
```
|
||||
|
||||
## Secrets Management
|
||||
|
||||
```bash
|
||||
# Set secret
|
||||
npx wrangler secret put API_KEY
|
||||
cat api-key.txt | npx wrangler secret put API_KEY
|
||||
npx wrangler secret put API_KEY --env staging
|
||||
```
|
||||
|
||||
```typescript
|
||||
// Use secret
|
||||
const response = await fetch('https://api.example.com', {
|
||||
headers: { 'Authorization': `Bearer ${env.API_KEY}` }
|
||||
});
|
||||
```
|
||||
|
||||
**Never commit secrets:**
|
||||
```jsonc
|
||||
// ❌ NEVER
|
||||
{ "vars": { "API_KEY": "sk_live_abc123" } }
|
||||
```
|
||||
|
||||
## Testing with Mock Bindings
|
||||
|
||||
### Vitest Mock
|
||||
|
||||
```typescript
|
||||
import { vi } from 'vitest';
|
||||
|
||||
const mockKV: KVNamespace = {
|
||||
get: vi.fn(async (key) => key === 'test' ? 'value' : null),
|
||||
put: vi.fn(async () => {}),
|
||||
delete: vi.fn(async () => {}),
|
||||
list: vi.fn(async () => ({ keys: [], list_complete: true, cursor: '' })),
|
||||
getWithMetadata: vi.fn(),
|
||||
} as unknown as KVNamespace;
|
||||
|
||||
const mockEnv: Env = { MY_KV: mockKV };
|
||||
const mockCtx: ExecutionContext = {
|
||||
waitUntil: vi.fn(),
|
||||
passThroughOnException: vi.fn(),
|
||||
};
|
||||
|
||||
const response = await worker.fetch(
|
||||
new Request('http://localhost/test'),
|
||||
mockEnv,
|
||||
mockCtx
|
||||
);
|
||||
```
|
||||
|
||||
## Binding Access Patterns
|
||||
|
||||
### Lazy Access
|
||||
|
||||
```typescript
|
||||
// ✅ Access only when needed
|
||||
if (url.pathname === '/cached') {
|
||||
const cached = await env.MY_KV.get('data');
|
||||
if (cached) return new Response(cached);
|
||||
}
|
||||
```
|
||||
|
||||
### Parallel Access
|
||||
|
||||
```typescript
|
||||
// ✅ Parallelize independent calls
|
||||
const [user, config, cache] = await Promise.all([
|
||||
env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId).first(),
|
||||
env.MY_KV.get('config'),
|
||||
env.CACHE.get('data')
|
||||
]);
|
||||
```
|
||||
|
||||
## Storage Selection
|
||||
|
||||
### KV: CDN-Backed Reads
|
||||
|
||||
```typescript
|
||||
const config = await env.MY_KV.get('app-config', { type: 'json' });
|
||||
```
|
||||
|
||||
**Use when:** Read-heavy, <25MB, global distribution, eventual consistency OK
|
||||
**Latency:** <10ms reads (cached), writes eventually consistent (60s)
|
||||
|
||||
### D1: Relational Queries
|
||||
|
||||
```typescript
|
||||
const results = await env.DB.prepare(`
|
||||
SELECT u.name, COUNT(o.id) FROM users u
|
||||
LEFT JOIN orders o ON u.id = o.user_id GROUP BY u.id
|
||||
`).all();
|
||||
```
|
||||
|
||||
**Use when:** Relational data, JOINs, ACID transactions
|
||||
**Limits:** 10GB database size, 100k rows per query
|
||||
|
||||
### R2: Large Objects
|
||||
|
||||
```typescript
|
||||
const object = await env.MY_BUCKET.get('large-file.zip');
|
||||
return new Response(object.body);
|
||||
```
|
||||
|
||||
**Use when:** Files >25MB, S3-compatible API needed
|
||||
**Limits:** 5TB per object, unlimited storage
|
||||
|
||||
### Durable Objects: Coordination
|
||||
|
||||
```typescript
|
||||
const id = env.COUNTER.idFromName('global');
|
||||
const stub = env.COUNTER.get(id);
|
||||
await stub.fetch(new Request('https://fake/increment'));
|
||||
```
|
||||
|
||||
**Use when:** Strong consistency, real-time coordination, WebSocket state
|
||||
**Guarantees:** Single-threaded execution, transactional storage
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
**❌ Hardcoding credentials:** `const apiKey = 'sk_live_abc123'`
|
||||
**✅** `npx wrangler secret put API_KEY`
|
||||
|
||||
**❌ Using REST API:** `fetch('https://api.cloudflare.com/.../kv/...')`
|
||||
**✅** `env.MY_KV.get('key')`
|
||||
|
||||
**❌ Polling storage:** `setInterval(() => env.KV.get('config'), 1000)`
|
||||
**✅** Use Durable Objects for real-time state
|
||||
|
||||
**❌ Large data in vars:** `{ "vars": { "HUGE_CONFIG": "..." } }` (5KB max)
|
||||
**✅** `env.MY_KV.put('config', data)`
|
||||
|
||||
**❌ Caching env globally:** `const apiKey = env.API_KEY` outside fetch()
|
||||
**✅** Access `env.API_KEY` per-request inside fetch()
|
||||
|
||||
## See Also
|
||||
|
||||
- [Service Bindings Docs](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/)
|
||||
- [Miniflare Testing](https://miniflare.dev/)
|
||||
@@ -0,0 +1,94 @@
|
||||
# Cloudflare Bot Management
|
||||
|
||||
Enterprise-grade bot detection, protection, and mitigation using ML/heuristics, bot scores, JavaScript detections, and verified bot handling.
|
||||
|
||||
## Overview
|
||||
|
||||
Bot Management provides multi-tier protection:
|
||||
- **Free (Bot Fight Mode)**: Auto-blocks definite bots, no config
|
||||
- **Pro/Business (Super Bot Fight Mode)**: Configurable actions, static resource protection, analytics groupings
|
||||
- **Enterprise (Bot Management)**: Granular 1-99 scores, WAF integration, JA3/JA4 fingerprinting, Workers API, Advanced Analytics
|
||||
|
||||
## Quick Start
|
||||
|
||||
```txt
|
||||
# Dashboard: Security > Bots
|
||||
# Enterprise: Deploy rule template
|
||||
(cf.bot_management.score eq 1 and not cf.bot_management.verified_bot) → Block
|
||||
(cf.bot_management.score le 29 and not cf.bot_management.verified_bot) → Managed Challenge
|
||||
```
|
||||
|
||||
## What Do You Need?
|
||||
|
||||
```txt
|
||||
├─ Initial setup → configuration.md
|
||||
│ ├─ Free tier → "Bot Fight Mode"
|
||||
│ ├─ Pro/Business → "Super Bot Fight Mode"
|
||||
│ └─ Enterprise → "Bot Management for Enterprise"
|
||||
├─ Workers API integration → api.md
|
||||
├─ WAF rules → patterns.md
|
||||
├─ Debugging → gotchas.md
|
||||
└─ Analytics → api.md#bot-analytics
|
||||
```
|
||||
|
||||
## Reading Order
|
||||
|
||||
| Task | Files to Read |
|
||||
|------|---------------|
|
||||
| Enable bot protection | README → configuration.md |
|
||||
| Workers bot detection | README → api.md |
|
||||
| WAF rule templates | README → patterns.md |
|
||||
| Debug bot issues | gotchas.md |
|
||||
| Advanced analytics | api.md#bot-analytics |
|
||||
|
||||
## Core Concepts
|
||||
|
||||
**Bot Scores**: 1-99 (1 = definitely automated, 99 = definitely human). Threshold: <30 indicates bot traffic. Enterprise gets granular 1-99; Pro/Business get groupings only.
|
||||
|
||||
**Detection Engines**: Heuristics (known fingerprints, assigns score=1), ML (majority of detections, supervised learning on billions of requests), Anomaly Detection (optional, baseline traffic analysis), JavaScript Detections (headless browser detection).
|
||||
|
||||
**Verified Bots**: Allowlisted good bots (search engines, AI crawlers) verified via reverse DNS or Web Bot Auth. Access via `cf.bot_management.verified_bot` or `cf.verified_bot_category`.
|
||||
|
||||
## Platform Limits
|
||||
|
||||
| Plan | Bot Scores | JA3/JA4 | Custom Rules | Analytics Retention |
|
||||
|------|------------|---------|--------------|---------------------|
|
||||
| Free | No (auto-block only) | No | 5 | N/A (no analytics) |
|
||||
| Pro/Business | Groupings only | No | 20/100 | 30 days (72h at a time) |
|
||||
| Enterprise | 1-99 granular | Yes | 1,000+ | 30 days (1 week at a time) |
|
||||
|
||||
## Basic Patterns
|
||||
|
||||
```typescript
|
||||
// Workers: Check bot score
|
||||
export default {
|
||||
async fetch(request: Request): Promise<Response> {
|
||||
const botScore = request.cf?.botManagement?.score;
|
||||
if (botScore && botScore < 30 && !request.cf?.botManagement?.verifiedBot) {
|
||||
return new Response('Bot detected', { status: 403 });
|
||||
}
|
||||
return fetch(request);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
```txt
|
||||
# WAF: Block definite bots
|
||||
(cf.bot_management.score eq 1 and not cf.bot_management.verified_bot)
|
||||
|
||||
# WAF: Protect sensitive endpoints
|
||||
(cf.bot_management.score lt 50 and http.request.uri.path in {"/login" "/checkout"} and not cf.bot_management.verified_bot)
|
||||
```
|
||||
|
||||
## In This Reference
|
||||
|
||||
- [configuration.md](./configuration.md) - Product tiers, WAF rule setup, JavaScript Detections, ML auto-updates
|
||||
- [api.md](./api.md) - Workers BotManagement interface, WAF fields, JA4 Signals
|
||||
- [patterns.md](./patterns.md) - E-commerce, API protection, mobile app allowlisting, SEO-friendly handling
|
||||
- [gotchas.md](./gotchas.md) - False positives/negatives, score=0 issues, JSD limitations, CSP requirements
|
||||
|
||||
## See Also
|
||||
|
||||
- [waf](../waf/) - WAF custom rules for bot enforcement
|
||||
- [workers](../workers/) - Workers request.cf.botManagement API
|
||||
- [api-shield](../api-shield/) - API-specific bot protection
|
||||
@@ -0,0 +1,169 @@
|
||||
# Bot Management API
|
||||
|
||||
## Workers: BotManagement Interface
|
||||
|
||||
```typescript
|
||||
interface BotManagement {
|
||||
score: number; // 1-99 (Enterprise), 0 if not computed
|
||||
verifiedBot: boolean; // Is verified bot
|
||||
staticResource: boolean; // Serves static resource
|
||||
ja3Hash: string; // JA3 fingerprint (Enterprise, HTTPS only)
|
||||
ja4: string; // JA4 fingerprint (Enterprise, HTTPS only)
|
||||
jsDetection?: {
|
||||
passed: boolean; // Passed JS detection (if enabled)
|
||||
};
|
||||
detectionIds: number[]; // Heuristic detection IDs
|
||||
corporateProxy?: boolean; // From corporate proxy (Enterprise)
|
||||
}
|
||||
|
||||
// DEPRECATED: Use botManagement.score instead
|
||||
// request.cf.clientTrustScore (legacy, duplicate of botManagement.score)
|
||||
|
||||
// Access via request.cf
|
||||
import type { IncomingRequestCfProperties } from '@cloudflare/workers-types';
|
||||
|
||||
export default {
|
||||
async fetch(request: Request): Promise<Response> {
|
||||
const cf = request.cf as IncomingRequestCfProperties | undefined;
|
||||
const botMgmt = cf?.botManagement;
|
||||
|
||||
if (!botMgmt) return fetch(request);
|
||||
if (botMgmt.verifiedBot) return fetch(request); // Allow verified bots
|
||||
if (botMgmt.score === 1) return new Response('Blocked', { status: 403 });
|
||||
if (botMgmt.score < 30) return new Response('Challenge required', { status: 429 });
|
||||
|
||||
return fetch(request);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## WAF Fields Reference
|
||||
|
||||
```txt
|
||||
# Score fields
|
||||
cf.bot_management.score # 0-99 (0 = not computed)
|
||||
cf.bot_management.verified_bot # boolean
|
||||
cf.bot_management.static_resource # boolean
|
||||
cf.bot_management.ja3_hash # string (Enterprise)
|
||||
cf.bot_management.ja4 # string (Enterprise)
|
||||
cf.bot_management.detection_ids # array
|
||||
cf.bot_management.js_detection.passed # boolean
|
||||
cf.bot_management.corporate_proxy # boolean (Enterprise)
|
||||
cf.verified_bot_category # string
|
||||
|
||||
# Workers equivalent
|
||||
request.cf.botManagement.score
|
||||
request.cf.botManagement.verifiedBot
|
||||
request.cf.botManagement.ja3Hash
|
||||
request.cf.botManagement.ja4
|
||||
request.cf.botManagement.jsDetection.passed
|
||||
request.cf.verifiedBotCategory
|
||||
```
|
||||
|
||||
## JA4 Signals (Enterprise)
|
||||
|
||||
```typescript
|
||||
import type { IncomingRequestCfProperties } from '@cloudflare/workers-types';
|
||||
|
||||
interface JA4Signals {
|
||||
// Ratios (0.0-1.0)
|
||||
heuristic_ratio_1h?: number; // Fraction flagged by heuristics
|
||||
browser_ratio_1h?: number; // Fraction from real browsers
|
||||
cache_ratio_1h?: number; // Fraction hitting cache
|
||||
h2h3_ratio_1h?: number; // Fraction using HTTP/2 or HTTP/3
|
||||
// Ranks (relative position in distribution)
|
||||
uas_rank_1h?: number; // User-Agent diversity rank
|
||||
paths_rank_1h?: number; // Path diversity rank
|
||||
reqs_rank_1h?: number; // Request volume rank
|
||||
ips_rank_1h?: number; // IP diversity rank
|
||||
// Quantiles (0.0-1.0, percentile in distribution)
|
||||
reqs_quantile_1h?: number; // Request volume quantile
|
||||
ips_quantile_1h?: number; // IP count quantile
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request: Request): Promise<Response> {
|
||||
const cf = request.cf as IncomingRequestCfProperties | undefined;
|
||||
const ja4Signals = cf?.ja4Signals as JA4Signals | undefined;
|
||||
|
||||
if (!ja4Signals) return fetch(request); // Not available for HTTP or Worker routing
|
||||
|
||||
// Check for anomalous behavior
|
||||
// High heuristic_ratio or low browser_ratio = suspicious
|
||||
const heuristicRatio = ja4Signals.heuristic_ratio_1h ?? 0;
|
||||
const browserRatio = ja4Signals.browser_ratio_1h ?? 0;
|
||||
|
||||
if (heuristicRatio > 0.5 || browserRatio < 0.3) {
|
||||
return new Response('Suspicious traffic', { status: 403 });
|
||||
}
|
||||
|
||||
return fetch(request);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
See [patterns.md](./patterns.md) for Workers examples: mobile app allowlisting, corporate proxy exemption, datacenter detection, conditional delay, and more.
|
||||
|
||||
## Bot Analytics
|
||||
|
||||
### Access Locations
|
||||
- Dashboard: Security > Bots (old) or Security > Analytics > Bot analysis (new)
|
||||
- GraphQL API for programmatic access
|
||||
- Security Events & Security Analytics
|
||||
- Logpush/Logpull
|
||||
|
||||
### Available Data
|
||||
- **Enterprise BM**: Bot scores (1-99), bot score source, distribution
|
||||
- **Pro/Business**: Bot groupings (automated, likely automated, likely human)
|
||||
- Top attributes: IPs, paths, user agents, countries
|
||||
- Detection sources: Heuristics, ML, AD, JSD
|
||||
- Verified bot categories
|
||||
|
||||
### Time Ranges
|
||||
- **Enterprise BM**: Up to 1 week at a time, 30 days history
|
||||
- **Pro/Business**: Up to 72 hours at a time, 30 days history
|
||||
- Real-time in most cases, adaptive sampling (1-10% depending on volume)
|
||||
|
||||
## Logpush Fields
|
||||
|
||||
```txt
|
||||
BotScore # 1-99 or 0 if not computed
|
||||
BotScoreSrc # Detection engine (ML, Heuristics, etc.)
|
||||
BotTags # Classification tags
|
||||
BotDetectionIDs # Heuristic detection IDs
|
||||
```
|
||||
|
||||
**BotScoreSrc values:**
|
||||
- `"Heuristics"` - Known fingerprint
|
||||
- `"Machine Learning"` - ML model
|
||||
- `"Anomaly Detection"` - Baseline anomaly
|
||||
- `"JS Detection"` - JavaScript check
|
||||
- `"Cloudflare Service"` - Zero Trust
|
||||
- `"Not Computed"` - Score = 0
|
||||
|
||||
Access via Logpush (stream to cloud storage/SIEM), Logpull (API to fetch logs), or GraphQL API (query analytics data).
|
||||
|
||||
## Testing with Miniflare
|
||||
|
||||
Miniflare provides mock botManagement data for local development:
|
||||
|
||||
**Default values:**
|
||||
- `score: 99` (human)
|
||||
- `verifiedBot: false`
|
||||
- `corporateProxy: false`
|
||||
- `ja3Hash: "25b4882c2bcb50cd6b469ff28c596742"`
|
||||
- `staticResource: false`
|
||||
- `detectionIds: []`
|
||||
|
||||
**Override in tests:**
|
||||
```typescript
|
||||
import { getPlatformProxy } from 'wrangler';
|
||||
|
||||
const { cf, dispose } = await getPlatformProxy();
|
||||
// cf.botManagement is frozen mock object
|
||||
expect(cf.botManagement.score).toBe(99);
|
||||
```
|
||||
|
||||
For custom test data, mock request.cf in your test setup.
|
||||
@@ -0,0 +1,163 @@
|
||||
# Bot Management Configuration
|
||||
|
||||
## Product Tiers
|
||||
|
||||
**Note:** Dashboard paths differ between old and new UI:
|
||||
- **New:** Security > Settings > Filter "Bot traffic"
|
||||
- **Old:** Security > Bots
|
||||
|
||||
Both UIs access same settings.
|
||||
|
||||
### Bot Score Groupings (Pro/Business)
|
||||
|
||||
Pro/Business users see bot score groupings instead of granular 1-99 scores:
|
||||
|
||||
| Score | Grouping | Meaning |
|
||||
|-------|----------|---------|
|
||||
| 0 | Not computed | Bot Management didn't run |
|
||||
| 1 | Automated | Definite bot (heuristic match) |
|
||||
| 2-29 | Likely automated | Probably bot (ML detection) |
|
||||
| 30-99 | Likely human | Probably human |
|
||||
| N/A | Verified bot | Allowlisted good bot |
|
||||
|
||||
Enterprise plans get granular 1-99 scores for custom thresholds.
|
||||
|
||||
### Bot Fight Mode (Free)
|
||||
- Auto-blocks definite bots (score=1), excludes verified bots by default
|
||||
- JavaScript Detections always enabled, no configuration options
|
||||
|
||||
### Super Bot Fight Mode (Pro/Business)
|
||||
```txt
|
||||
Dashboard: Security > Bots > Configure
|
||||
- Definitely automated: Block/Challenge
|
||||
- Likely automated: Challenge/Allow
|
||||
- Verified bots: Allow (recommended)
|
||||
- Static resource protection: ON (may block mail clients)
|
||||
- JavaScript Detections: Optional
|
||||
```
|
||||
|
||||
### Bot Management for Enterprise
|
||||
```txt
|
||||
Dashboard: Security > Bots > Configure > Auto-updates: ON (recommended)
|
||||
|
||||
# Template 1: Block definite bots
|
||||
(cf.bot_management.score eq 1 and not cf.bot_management.verified_bot and not cf.bot_management.static_resource)
|
||||
Action: Block
|
||||
|
||||
# Template 2: Challenge likely bots
|
||||
(cf.bot_management.score ge 2 and cf.bot_management.score le 29 and not cf.bot_management.verified_bot and not cf.bot_management.static_resource)
|
||||
Action: Managed Challenge
|
||||
```
|
||||
|
||||
## JavaScript Detections Setup
|
||||
|
||||
### Enable via Dashboard
|
||||
```txt
|
||||
Security > Bots > Configure Bot Management > JS Detections: ON
|
||||
|
||||
Update CSP: script-src 'self' /cdn-cgi/challenge-platform/;
|
||||
```
|
||||
|
||||
### Manual JS Injection (API)
|
||||
```html
|
||||
<script>
|
||||
function jsdOnload() {
|
||||
window.cloudflare.jsd.executeOnce({ callback: function(result) { console.log('JSD:', result); } });
|
||||
}
|
||||
</script>
|
||||
<script src="/cdn-cgi/challenge-platform/scripts/jsd/api.js?onload=jsdOnload" async></script>
|
||||
```
|
||||
|
||||
**Use API for**: Selective deployment on specific pages
|
||||
**Don't combine**: Zone-wide toggle + manual injection
|
||||
|
||||
### WAF Rules for JSD
|
||||
```txt
|
||||
# NEVER use on first page visit (needs HTML page first)
|
||||
(not cf.bot_management.js_detection.passed and http.request.uri.path eq "/api/user/create" and http.request.method eq "POST" and not cf.bot_management.verified_bot)
|
||||
Action: Managed Challenge (always use Managed Challenge, not Block)
|
||||
```
|
||||
|
||||
### Limitations
|
||||
- First request won't have JSD data (needs HTML page first)
|
||||
- Strips ETags from HTML responses
|
||||
- Not supported with CSP via `<meta>` tags
|
||||
- Websocket endpoints not supported
|
||||
- Native mobile apps won't pass
|
||||
- cf_clearance cookie: 15-minute lifespan, max 4096 bytes
|
||||
|
||||
## __cf_bm Cookie
|
||||
|
||||
Cloudflare sets `__cf_bm` cookie to smooth bot scores across user sessions:
|
||||
|
||||
- **Purpose:** Reduces false positives from score volatility
|
||||
- **Scope:** Per-domain, HTTP-only
|
||||
- **Lifespan:** Session duration
|
||||
- **Privacy:** No PII—only session classification
|
||||
- **Automatic:** No configuration required
|
||||
|
||||
Bot scores for repeat visitors consider session history via this cookie.
|
||||
|
||||
## Static Resource Protection
|
||||
|
||||
**File Extensions**: ico, jpg, png, jpeg, gif, css, js, tif, tiff, bmp, pict, webp, svg, svgz, class, jar, txt, csv, doc, docx, xls, xlsx, pdf, ps, pls, ppt, pptx, ttf, otf, woff, woff2, eot, eps, ejs, swf, torrent, midi, mid, m3u8, m4a, mp3, ogg, ts
|
||||
**Plus**: `/.well-known/` path (all files)
|
||||
|
||||
```txt
|
||||
# Exclude static resources from bot rules
|
||||
(cf.bot_management.score lt 30 and not cf.bot_management.static_resource)
|
||||
```
|
||||
|
||||
**WARNING**: May block mail clients fetching static images
|
||||
|
||||
## JA3/JA4 Fingerprinting (Enterprise)
|
||||
|
||||
```txt
|
||||
# Block specific attack fingerprint
|
||||
(cf.bot_management.ja3_hash eq "8b8e3d5e3e8b3d5e")
|
||||
|
||||
# Allow mobile app by fingerprint
|
||||
(cf.bot_management.ja4 eq "your_mobile_app_fingerprint")
|
||||
```
|
||||
|
||||
Only available for HTTPS/TLS traffic. Missing for Worker-routed traffic or HTTP requests.
|
||||
|
||||
## Verified Bot Categories
|
||||
|
||||
```txt
|
||||
# Allow search engines only
|
||||
(cf.verified_bot_category eq "Search Engine Crawler")
|
||||
|
||||
# Block AI crawlers
|
||||
(cf.verified_bot_category eq "AI Crawler")
|
||||
Action: Block
|
||||
|
||||
# Or use dashboard: Security > Settings > Bot Management > Block AI Bots
|
||||
```
|
||||
|
||||
| Category | String Value | Example |
|
||||
|----------|--------------|---------|
|
||||
| AI Crawler | `AI Crawler` | GPTBot, Claude-Web |
|
||||
| AI Assistant | `AI Assistant` | Perplexity-User, DuckAssistBot |
|
||||
| AI Search | `AI Search` | OAI-SearchBot |
|
||||
| Accessibility | `Accessibility` | Accessible Web Bot |
|
||||
| Academic Research | `Academic Research` | Library of Congress |
|
||||
| Advertising & Marketing | `Advertising & Marketing` | Google Adsbot |
|
||||
| Aggregator | `Aggregator` | Pinterest, Indeed |
|
||||
| Archiver | `Archiver` | Internet Archive, CommonCrawl |
|
||||
| Feed Fetcher | `Feed Fetcher` | RSS/Podcast updaters |
|
||||
| Monitoring & Analytics | `Monitoring & Analytics` | Uptime monitors |
|
||||
| Page Preview | `Page Preview` | Facebook/Slack link preview |
|
||||
| SEO | `Search Engine Optimization` | Google Lighthouse |
|
||||
| Security | `Security` | Vulnerability scanners |
|
||||
| Social Media Marketing | `Social Media Marketing` | Brandwatch |
|
||||
| Webhooks | `Webhooks` | Payment processors |
|
||||
| Other | `Other` | Uncategorized bots |
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **ML Auto-Updates**: Enable on Enterprise for latest models
|
||||
- **Start with Managed Challenge**: Test before blocking
|
||||
- **Always exclude verified bots**: Use `not cf.bot_management.verified_bot`
|
||||
- **Exempt corporate proxies**: For B2B traffic via `cf.bot_management.corporate_proxy`
|
||||
- **Use static resource exception**: Improves performance, reduces overhead
|
||||
@@ -0,0 +1,114 @@
|
||||
# Bot Management Gotchas
|
||||
|
||||
## Common Errors
|
||||
|
||||
### "Bot Score = 0"
|
||||
|
||||
**Cause:** Bot Management didn't run (internal Cloudflare request, Worker routing to zone (Orange-to-Orange), or request handled before BM (Redirect Rules, etc.))
|
||||
**Solution:** Check request flow and ensure Bot Management runs in request lifecycle
|
||||
|
||||
### "JavaScript Detections Not Working"
|
||||
|
||||
**Cause:** `js_detection.passed` always false or undefined due to: CSP headers don't allow `/cdn-cgi/challenge-platform/`, using on first page visit (needs HTML page first), ad blockers or disabled JS, JSD not enabled in dashboard, or using Block action (must use Managed Challenge)
|
||||
**Solution:** Add CSP header `Content-Security-Policy: script-src 'self' /cdn-cgi/challenge-platform/;` and ensure JSD is enabled with Managed Challenge action
|
||||
|
||||
### "False Positives (Legitimate Users Blocked)"
|
||||
|
||||
**Cause:** Bot detection incorrectly flagging legitimate users
|
||||
**Solution:** Check Bot Analytics for affected IPs/paths, identify detection source (ML, Heuristics, etc.), create exception rule like `(cf.bot_management.score lt 30 and http.request.uri.path eq "/problematic-path")` with Action: Skip (Bot Management), or allowlist by IP/ASN/country
|
||||
|
||||
### "False Negatives (Bots Not Caught)"
|
||||
|
||||
**Cause:** Bots bypassing detection
|
||||
**Solution:** Lower score threshold (30 → 50), enable JavaScript Detections, add JA3/JA4 fingerprinting rules, or use rate limiting as fallback
|
||||
|
||||
### "Verified Bot Blocked"
|
||||
|
||||
**Cause:** Search engine bot blocked by WAF Managed Rules (not just Bot Management)
|
||||
**Solution:** Create WAF exception for specific rule ID and verify bot via reverse DNS
|
||||
|
||||
### "Yandex Bot Blocked During IP Update"
|
||||
|
||||
**Cause:** Yandex updates bot IPs; new IPs unrecognized for 48h during propagation
|
||||
**Solution:**
|
||||
1. Check Security Events for specific WAF rule ID blocking Yandex
|
||||
2. Create WAF exception:
|
||||
```txt
|
||||
(http.user_agent contains "YandexBot" and ip.src in {<yandex-ip-range>})
|
||||
Action: Skip (WAF Managed Ruleset)
|
||||
```
|
||||
3. Monitor Bot Analytics for 48h
|
||||
4. Remove exception after propagation completes
|
||||
|
||||
Issue resolves automatically after 48h. Contact Cloudflare Support if persists.
|
||||
|
||||
### "JA3/JA4 Missing"
|
||||
|
||||
**Cause:** Non-HTTPS traffic, Worker routing traffic, Orange-to-Orange traffic via Worker, or Bot Management skipped
|
||||
**Solution:** JA3/JA4 only available for HTTPS/TLS traffic; check request routing
|
||||
|
||||
**JA3/JA4 Not User-Unique:** Same browser/library version = same fingerprint
|
||||
- Don't use for user identification
|
||||
- Use for client profiling only
|
||||
- Fingerprints change with browser updates
|
||||
|
||||
## Bot Verification Methods
|
||||
|
||||
Cloudflare verifies bots via:
|
||||
|
||||
1. **Reverse DNS (IP validation):** Traditional method—bot IP resolves to expected domain
|
||||
2. **Web Bot Auth:** Modern cryptographic verification—faster propagation
|
||||
|
||||
When `verifiedBot=true`, bot passed at least one method.
|
||||
|
||||
**Inactive verified bots:** IPs removed after 24h of no traffic.
|
||||
|
||||
## Detection Engine Behavior
|
||||
|
||||
| Engine | Score | Timing | Plan | Notes |
|
||||
|--------|-------|--------|------|-------|
|
||||
| Heuristics | Always 1 | Immediate | All | Known fingerprints—overrides ML |
|
||||
| ML | 1-99 | Immediate | All | Majority of detections |
|
||||
| Anomaly Detection | Influences | After baseline | Enterprise | Optional, baseline analysis |
|
||||
| JavaScript Detections | Pass/fail | After JS | Pro+ | Headless browser detection |
|
||||
| Cloudflare Service | N/A | N/A | Enterprise | Zero Trust internal source |
|
||||
|
||||
**Priority:** Heuristics > ML—if heuristic matches, score=1 regardless of ML.
|
||||
|
||||
## Limits
|
||||
|
||||
| Limit | Value | Notes |
|
||||
|-------|-------|-------|
|
||||
| Bot Score = 0 | Means not computed | Not score = 100 |
|
||||
| First request JSD data | May not be available | JSD data appears on subsequent requests |
|
||||
| Score accuracy | Not 100% guaranteed | False positives/negatives possible |
|
||||
| JSD on first HTML page visit | Not supported | Requires subsequent page load |
|
||||
| JSD requirements | JavaScript-enabled browser | Won't work with JS disabled or ad blockers |
|
||||
| JSD ETag stripping | Strips ETags from HTML responses | May affect caching behavior |
|
||||
| JSD CSP compatibility | Requires specific CSP | Not compatible with some CSP configurations |
|
||||
| JSD meta CSP tags | Not supported | Must use HTTP headers |
|
||||
| JSD WebSocket support | Not supported | WebSocket endpoints won't work with JSD |
|
||||
| JSD mobile app support | Native apps won't pass | Only works in browsers |
|
||||
| JA3/JA4 traffic type | HTTPS/TLS only | Not available for non-HTTPS traffic |
|
||||
| JA3/JA4 Worker routing | Missing for Worker-routed traffic | Check request routing |
|
||||
| JA3/JA4 uniqueness | Not unique per user | Shared by clients with same browser/library |
|
||||
| JA3/JA4 stability | Can change with updates | Browser/library updates affect fingerprints |
|
||||
| WAF custom rules (Free) | 5 | Varies by plan |
|
||||
| WAF custom rules (Pro) | 20 | Varies by plan |
|
||||
| WAF custom rules (Business) | 100 | Varies by plan |
|
||||
| WAF custom rules (Enterprise) | 1,000+ | Varies by plan |
|
||||
| Workers CPU time | Varies by plan | Applies to bot logic |
|
||||
| Bot Analytics sampling | 1-10% adaptive | High-volume zones sampled more aggressively |
|
||||
| Bot Analytics history | 30 days max | Historical data retention limit |
|
||||
| CSP requirements for JSD | Must allow `/cdn-cgi/challenge-platform/` | Required for JSD to function |
|
||||
|
||||
### Plan Restrictions
|
||||
|
||||
| Feature | Free | Pro/Business | Enterprise |
|
||||
|---------|------|--------------|------------|
|
||||
| Granular scores (1-99) | No | No | Yes |
|
||||
| JA3/JA4 | No | No | Yes |
|
||||
| Anomaly Detection | No | No | Yes |
|
||||
| Corporate Proxy detection | No | No | Yes |
|
||||
| Verified bot categories | Limited | Limited | Full |
|
||||
| Custom WAF rules | 5 | 20/100 | 1,000+ |
|
||||
@@ -0,0 +1,182 @@
|
||||
# Bot Management Patterns
|
||||
|
||||
## E-commerce Protection
|
||||
|
||||
```txt
|
||||
# High security for checkout
|
||||
(cf.bot_management.score lt 50 and http.request.uri.path in {"/checkout" "/cart/add"} and not cf.bot_management.verified_bot and not cf.bot_management.corporate_proxy)
|
||||
Action: Managed Challenge
|
||||
```
|
||||
|
||||
## API Protection
|
||||
|
||||
```txt
|
||||
# Protect API with JS detection + score
|
||||
(http.request.uri.path matches "^/api/" and (cf.bot_management.score lt 30 or not cf.bot_management.js_detection.passed) and not cf.bot_management.verified_bot)
|
||||
Action: Block
|
||||
```
|
||||
|
||||
## SEO-Friendly Bot Handling
|
||||
|
||||
```txt
|
||||
# Allow search engine crawlers
|
||||
(cf.bot_management.score lt 30 and not cf.verified_bot_category in {"Search Engine Crawler"})
|
||||
Action: Managed Challenge
|
||||
```
|
||||
|
||||
## Block AI Scrapers
|
||||
|
||||
```txt
|
||||
# Block training crawlers only (allow AI assistants/search)
|
||||
(cf.verified_bot_category eq "AI Crawler")
|
||||
Action: Block
|
||||
|
||||
# Block all AI-related bots (training + assistants + search)
|
||||
(cf.verified_bot_category in {"AI Crawler" "AI Assistant" "AI Search"})
|
||||
Action: Block
|
||||
|
||||
# Allow AI Search, block AI Crawler and AI Assistant
|
||||
(cf.verified_bot_category in {"AI Crawler" "AI Assistant"})
|
||||
Action: Block
|
||||
|
||||
# Or use dashboard: Security > Settings > Bot Management > Block AI Bots
|
||||
```
|
||||
|
||||
## Rate Limiting by Bot Score
|
||||
|
||||
```txt
|
||||
# Stricter limits for suspicious traffic
|
||||
(cf.bot_management.score lt 50)
|
||||
Rate: 10 requests per 10 seconds
|
||||
|
||||
(cf.bot_management.score ge 50)
|
||||
Rate: 100 requests per 10 seconds
|
||||
```
|
||||
|
||||
## Mobile App Allowlisting
|
||||
|
||||
```txt
|
||||
# Identify mobile app by JA3/JA4
|
||||
(cf.bot_management.ja4 in {"fingerprint1" "fingerprint2"})
|
||||
Action: Skip (all remaining rules)
|
||||
```
|
||||
|
||||
## Datacenter Detection
|
||||
|
||||
```typescript
|
||||
import type { IncomingRequestCfProperties } from '@cloudflare/workers-types';
|
||||
|
||||
// Low score + not corporate proxy = likely datacenter bot
|
||||
export default {
|
||||
async fetch(request: Request): Promise<Response> {
|
||||
const cf = request.cf as IncomingRequestCfProperties | undefined;
|
||||
const botMgmt = cf?.botManagement;
|
||||
|
||||
if (botMgmt?.score && botMgmt.score < 30 &&
|
||||
!botMgmt.corporateProxy && !botMgmt.verifiedBot) {
|
||||
return new Response('Datacenter traffic blocked', { status: 403 });
|
||||
}
|
||||
|
||||
return fetch(request);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Conditional Delay (Tarpit)
|
||||
|
||||
```typescript
|
||||
import type { IncomingRequestCfProperties } from '@cloudflare/workers-types';
|
||||
|
||||
// Add delay proportional to bot suspicion
|
||||
export default {
|
||||
async fetch(request: Request): Promise<Response> {
|
||||
const cf = request.cf as IncomingRequestCfProperties | undefined;
|
||||
const botMgmt = cf?.botManagement;
|
||||
|
||||
if (botMgmt?.score && botMgmt.score < 50 && !botMgmt.verifiedBot) {
|
||||
// Delay: 0-2 seconds for scores 50-0
|
||||
const delayMs = Math.max(0, (50 - botMgmt.score) * 40);
|
||||
await new Promise(r => setTimeout(r, delayMs));
|
||||
}
|
||||
|
||||
return fetch(request);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Layered Defense
|
||||
|
||||
```txt
|
||||
1. Bot Management (score-based)
|
||||
2. JavaScript Detections (for JS-capable clients)
|
||||
3. Rate Limiting (fallback protection)
|
||||
4. WAF Managed Rules (OWASP, etc.)
|
||||
```
|
||||
|
||||
## Progressive Enhancement
|
||||
|
||||
```txt
|
||||
Public content: High threshold (score < 10)
|
||||
Authenticated: Medium threshold (score < 30)
|
||||
Sensitive: Low threshold (score < 50) + JSD
|
||||
```
|
||||
|
||||
## Zero Trust for Bots
|
||||
|
||||
```txt
|
||||
1. Default deny (all scores < 30)
|
||||
2. Allowlist verified bots
|
||||
3. Allowlist mobile apps (JA3/JA4)
|
||||
4. Allowlist corporate proxies
|
||||
5. Allowlist static resources
|
||||
```
|
||||
|
||||
## Workers: Score + JS Detection
|
||||
|
||||
```typescript
|
||||
import type { IncomingRequestCfProperties } from '@cloudflare/workers-types';
|
||||
|
||||
export default {
|
||||
async fetch(request: Request): Promise<Response> {
|
||||
const cf = request.cf as IncomingRequestCfProperties | undefined;
|
||||
const botMgmt = cf?.botManagement;
|
||||
const url = new URL(request.url);
|
||||
|
||||
if (botMgmt?.staticResource) return fetch(request); // Skip static
|
||||
|
||||
// API endpoints: require JS detection + good score
|
||||
if (url.pathname.startsWith('/api/')) {
|
||||
const jsDetectionPassed = botMgmt?.jsDetection?.passed ?? false;
|
||||
const score = botMgmt?.score ?? 100;
|
||||
|
||||
if (!jsDetectionPassed || score < 30) {
|
||||
return new Response('Unauthorized', { status: 401 });
|
||||
}
|
||||
}
|
||||
|
||||
return fetch(request);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Rate Limiting by JWT Claim + Bot Score
|
||||
|
||||
```txt
|
||||
# Enterprise: Combine bot score with JWT validation
|
||||
Rate limiting > Custom rules
|
||||
- Field: lookup_json_string(http.request.jwt.claims["{config_id}"][0], "sub")
|
||||
- Matches: user ID claim
|
||||
- Additional condition: cf.bot_management.score lt 50
|
||||
```
|
||||
|
||||
## WAF Integration Points
|
||||
|
||||
- **WAF Custom Rules**: Primary enforcement mechanism
|
||||
- **Rate Limiting Rules**: Bot score as dimension, stricter limits for low scores
|
||||
- **Transform Rules**: Pass score to origin via custom header
|
||||
- **Workers**: Programmatic bot logic, custom scoring algorithms
|
||||
- **Page Rules / Configuration Rules**: Zone-level overrides, path-specific settings
|
||||
|
||||
## See Also
|
||||
|
||||
- [gotchas.md](./gotchas.md) - Common errors, false positives/negatives, limitations
|
||||
@@ -0,0 +1,78 @@
|
||||
# Cloudflare Browser Rendering Skill Reference
|
||||
|
||||
**Description**: Expert knowledge for Cloudflare Browser Rendering - control headless Chrome on Cloudflare's global network for browser automation, screenshots, PDFs, web scraping, testing, and content generation.
|
||||
|
||||
**When to use**: Any task involving Cloudflare Browser Rendering including: taking screenshots, generating PDFs, web scraping, browser automation, testing web applications, extracting structured data, capturing page metrics, or automating browser interactions.
|
||||
|
||||
## Decision Tree
|
||||
|
||||
### REST API vs Workers Bindings
|
||||
|
||||
**Use REST API when:**
|
||||
- One-off, stateless tasks (screenshot, PDF, content fetch)
|
||||
- No Workers infrastructure yet
|
||||
- Simple integrations from external services
|
||||
- Need quick prototyping without deployment
|
||||
|
||||
**Use Workers Bindings when:**
|
||||
- Complex browser automation workflows
|
||||
- Need session reuse for performance
|
||||
- Multiple page interactions per request
|
||||
- Custom scripting and logic required
|
||||
- Building production applications
|
||||
|
||||
### Puppeteer vs Playwright
|
||||
|
||||
| Feature | Puppeteer | Playwright |
|
||||
|---------|-----------|------------|
|
||||
| API Style | Chrome DevTools Protocol | High-level abstractions |
|
||||
| Selectors | CSS, XPath | CSS, text, role, test-id |
|
||||
| Best for | Advanced control, CDP access | Quick automation, testing |
|
||||
| Learning curve | Steeper | Gentler |
|
||||
|
||||
**Use Puppeteer:** Need CDP protocol access, Chrome-specific features, migration from existing Puppeteer code
|
||||
**Use Playwright:** Modern selector APIs, cross-browser patterns, faster development
|
||||
|
||||
## Tier Limits Summary
|
||||
|
||||
| Limit | Free Tier | Paid Tier |
|
||||
|-------|-----------|-----------|
|
||||
| Daily browser time | 10 minutes | Unlimited* |
|
||||
| Concurrent sessions | 3 | 30 |
|
||||
| Requests per minute | 6 | 180 |
|
||||
|
||||
*Subject to fair-use policy. See [gotchas.md](gotchas.md) for details.
|
||||
|
||||
## Reading Order
|
||||
|
||||
**New to Browser Rendering:**
|
||||
1. [configuration.md](configuration.md) - Setup and deployment
|
||||
2. [patterns.md](patterns.md) - Common use cases with examples
|
||||
3. [api.md](api.md) - API reference
|
||||
4. [gotchas.md](gotchas.md) - Avoid common pitfalls
|
||||
|
||||
**Specific task:**
|
||||
- **Setup/deployment** → [configuration.md](configuration.md)
|
||||
- **API reference/endpoints** → [api.md](api.md)
|
||||
- **Example code/patterns** → [patterns.md](patterns.md)
|
||||
- **Debugging/troubleshooting** → [gotchas.md](gotchas.md)
|
||||
|
||||
**REST API users:**
|
||||
- Start with [api.md](api.md) REST API section
|
||||
- Check [gotchas.md](gotchas.md) for rate limits
|
||||
|
||||
**Workers users:**
|
||||
- Start with [configuration.md](configuration.md)
|
||||
- Review [patterns.md](patterns.md) for session management
|
||||
- Reference [api.md](api.md) for Workers Bindings
|
||||
|
||||
## In This Reference
|
||||
|
||||
- **[configuration.md](configuration.md)** - Setup, deployment, wrangler config, compatibility
|
||||
- **[api.md](api.md)** - REST API endpoints + Workers Bindings (Puppeteer/Playwright)
|
||||
- **[patterns.md](patterns.md)** - Common patterns, use cases, real examples
|
||||
- **[gotchas.md](gotchas.md)** - Troubleshooting, best practices, tier limits, common errors
|
||||
|
||||
## See Also
|
||||
|
||||
- [Cloudflare Docs](https://developers.cloudflare.com/browser-rendering/)
|
||||
@@ -0,0 +1,108 @@
|
||||
# Browser Rendering API
|
||||
|
||||
## REST API
|
||||
|
||||
**Base:** `https://api.cloudflare.com/client/v4/accounts/{accountId}/browser-rendering`
|
||||
**Auth:** `Authorization: Bearer <token>` (Browser Rendering - Edit permission)
|
||||
|
||||
### Endpoints
|
||||
|
||||
| Endpoint | Description | Key Options |
|
||||
|----------|-------------|-------------|
|
||||
| `/content` | Get rendered HTML | `url`, `waitUntil` |
|
||||
| `/screenshot` | Capture image | `screenshotOptions: {type, fullPage, clip}` |
|
||||
| `/pdf` | Generate PDF | `pdfOptions: {format, landscape, margin}` |
|
||||
| `/snapshot` | HTML + inlined resources | `url` |
|
||||
| `/scrape` | Extract by selectors | `selectors: ["h1", ".price"]` |
|
||||
| `/json` | AI-structured extraction | `schema: {name: "string", price: "number"}` |
|
||||
| `/links` | Get all links | `url` |
|
||||
| `/markdown` | Convert to markdown | `url` |
|
||||
|
||||
```bash
|
||||
curl -X POST '.../browser-rendering/screenshot' \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{"url":"https://example.com","screenshotOptions":{"fullPage":true}}'
|
||||
```
|
||||
|
||||
## Workers Binding
|
||||
|
||||
```jsonc
|
||||
// wrangler.jsonc
|
||||
{ "browser": { "binding": "MYBROWSER" } }
|
||||
```
|
||||
|
||||
## Puppeteer
|
||||
|
||||
```typescript
|
||||
import puppeteer from "@cloudflare/puppeteer";
|
||||
|
||||
const browser = await puppeteer.launch(env.MYBROWSER, { keep_alive: 600000 });
|
||||
const page = await browser.newPage();
|
||||
await page.goto('https://example.com', { waitUntil: 'networkidle0' });
|
||||
|
||||
// Content
|
||||
const html = await page.content();
|
||||
const title = await page.title();
|
||||
|
||||
// Screenshot/PDF
|
||||
await page.screenshot({ fullPage: true, type: 'png' });
|
||||
await page.pdf({ format: 'A4', printBackground: true });
|
||||
|
||||
// Interaction
|
||||
await page.click('#button');
|
||||
await page.type('#input', 'text');
|
||||
await page.evaluate(() => document.querySelector('h1')?.textContent);
|
||||
|
||||
// Session management
|
||||
const sessions = await puppeteer.sessions(env.MYBROWSER);
|
||||
const limits = await puppeteer.limits(env.MYBROWSER);
|
||||
|
||||
await browser.close();
|
||||
```
|
||||
|
||||
## Playwright
|
||||
|
||||
```typescript
|
||||
import { launch, connect } from "@cloudflare/playwright";
|
||||
|
||||
const browser = await launch(env.MYBROWSER, { keep_alive: 600000 });
|
||||
const page = await browser.newPage();
|
||||
|
||||
await page.goto('https://example.com', { waitUntil: 'networkidle' });
|
||||
|
||||
// Modern selectors
|
||||
await page.locator('.button').click();
|
||||
await page.getByText('Submit').click();
|
||||
await page.getByTestId('search').fill('query');
|
||||
|
||||
// Context for isolation
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 1920, height: 1080 },
|
||||
userAgent: 'custom'
|
||||
});
|
||||
|
||||
await browser.close();
|
||||
```
|
||||
|
||||
## Session Management
|
||||
|
||||
```typescript
|
||||
// List sessions
|
||||
await puppeteer.sessions(env.MYBROWSER);
|
||||
|
||||
// Connect to existing
|
||||
await puppeteer.connect(env.MYBROWSER, sessionId);
|
||||
|
||||
// Check limits
|
||||
await puppeteer.limits(env.MYBROWSER);
|
||||
// { remaining: ms, total: ms, concurrent: n }
|
||||
```
|
||||
|
||||
## Key Options
|
||||
|
||||
| Option | Values |
|
||||
|--------|--------|
|
||||
| `waitUntil` | `load`, `domcontentloaded`, `networkidle0`, `networkidle2` |
|
||||
| `keep_alive` | Max 600000ms (10 min) |
|
||||
| `screenshot.type` | `png`, `jpeg` |
|
||||
| `pdf.format` | `A4`, `Letter`, `Legal` |
|
||||
@@ -0,0 +1,78 @@
|
||||
# Configuration & Setup
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @cloudflare/puppeteer # or @cloudflare/playwright
|
||||
```
|
||||
|
||||
**Use Cloudflare packages** - standard `puppeteer`/`playwright` won't work in Workers.
|
||||
|
||||
## wrangler.json
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "browser-worker",
|
||||
"main": "src/index.ts",
|
||||
"compatibility_date": "2025-01-01",
|
||||
"compatibility_flags": ["nodejs_compat"],
|
||||
"browser": {
|
||||
"binding": "MYBROWSER"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Required:** `nodejs_compat` flag and `browser.binding`.
|
||||
|
||||
## TypeScript
|
||||
|
||||
```typescript
|
||||
interface Env {
|
||||
MYBROWSER: Fetcher;
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
// ...
|
||||
}
|
||||
} satisfies ExportedHandler<Env>;
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
wrangler dev --remote # --remote required for browser binding
|
||||
```
|
||||
|
||||
**Local mode does NOT support Browser Rendering** - must use `--remote`.
|
||||
|
||||
## REST API
|
||||
|
||||
No wrangler config needed. Get API token with "Browser Rendering - Edit" permission.
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
'https://api.cloudflare.com/client/v4/accounts/{accountId}/browser-rendering/screenshot' \
|
||||
-H 'Authorization: Bearer TOKEN' \
|
||||
-d '{"url": "https://example.com"}' --output screenshot.png
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
| Requirement | Value |
|
||||
|-------------|-------|
|
||||
| Node.js compatibility | `nodejs_compat` flag |
|
||||
| Compatibility date | 2023-03-01+ |
|
||||
| Module format | ES modules only |
|
||||
| Browser | Chromium 119+ (no Firefox/Safari) |
|
||||
|
||||
**Not supported:** WebGL, WebRTC, extensions, `file://` protocol, Service Worker syntax.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Error | Solution |
|
||||
|-------|----------|
|
||||
| `MYBROWSER is undefined` | Use `wrangler dev --remote` |
|
||||
| `nodejs_compat not enabled` | Add to `compatibility_flags` |
|
||||
| `Module not found` | `npm install @cloudflare/puppeteer` |
|
||||
| `Browser Rendering not available` | Enable in dashboard |
|
||||
@@ -0,0 +1,88 @@
|
||||
# Browser Rendering Gotchas
|
||||
|
||||
## Tier Limits
|
||||
|
||||
| Limit | Free | Paid |
|
||||
|-------|------|------|
|
||||
| Daily browser time | 10 min | Unlimited* |
|
||||
| Concurrent sessions | 3 | 30 |
|
||||
| Requests/minute | 6 | 180 |
|
||||
| Session keep-alive | 10 min max | 10 min max |
|
||||
|
||||
*Subject to fair-use policy.
|
||||
|
||||
**Check quota:**
|
||||
```typescript
|
||||
const limits = await puppeteer.limits(env.MYBROWSER);
|
||||
// { remaining: 540000, total: 600000, concurrent: 2 }
|
||||
```
|
||||
|
||||
## Always Close Browsers
|
||||
|
||||
```typescript
|
||||
const browser = await puppeteer.launch(env.MYBROWSER);
|
||||
try {
|
||||
const page = await browser.newPage();
|
||||
await page.goto("https://example.com");
|
||||
return new Response(await page.content());
|
||||
} finally {
|
||||
await browser.close(); // ALWAYS in finally
|
||||
}
|
||||
```
|
||||
|
||||
**Workers vs REST:** REST auto-closes after timeout. Workers must call `close()` or session stays open until `keep_alive` expires.
|
||||
|
||||
## Optimize Concurrency
|
||||
|
||||
```typescript
|
||||
// ❌ 3 sessions (hits free tier limit)
|
||||
const browser1 = await puppeteer.launch(env.MYBROWSER);
|
||||
const browser2 = await puppeteer.launch(env.MYBROWSER);
|
||||
|
||||
// ✅ 1 session, multiple pages
|
||||
const browser = await puppeteer.launch(env.MYBROWSER);
|
||||
const page1 = await browser.newPage();
|
||||
const page2 = await browser.newPage();
|
||||
```
|
||||
|
||||
## Common Errors
|
||||
|
||||
| Error | Cause | Fix |
|
||||
|-------|-------|-----|
|
||||
| Session limit exceeded | Too many concurrent | Close unused browsers, use pages not browsers |
|
||||
| Page navigation timeout | Slow page or `networkidle` on busy page | Increase timeout, use `waitUntil: "load"` |
|
||||
| Session not found | Expired session | Catch error, launch new session |
|
||||
| Evaluation failed | DOM element missing | Use `?.` optional chaining |
|
||||
| Protocol error: Target closed | Page closed during operation | Await all ops before closing |
|
||||
|
||||
## page.evaluate() Gotchas
|
||||
|
||||
```typescript
|
||||
// ❌ Outer scope not available
|
||||
const selector = "h1";
|
||||
await page.evaluate(() => document.querySelector(selector));
|
||||
|
||||
// ✅ Pass as argument
|
||||
await page.evaluate((sel) => document.querySelector(sel)?.textContent, selector);
|
||||
```
|
||||
|
||||
## Performance
|
||||
|
||||
**waitUntil options (fastest to slowest):**
|
||||
1. `domcontentloaded` - DOM ready
|
||||
2. `load` - load event (default)
|
||||
3. `networkidle0` - no network for 500ms
|
||||
|
||||
**Block unnecessary resources:**
|
||||
```typescript
|
||||
await page.setRequestInterception(true);
|
||||
page.on("request", (req) => {
|
||||
if (["image", "stylesheet", "font"].includes(req.resourceType())) {
|
||||
req.abort();
|
||||
} else {
|
||||
req.continue();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**Session reuse:** Cold start ~1-2s, warm connect ~100-200ms. Store sessionId in KV for reuse.
|
||||
@@ -0,0 +1,91 @@
|
||||
# Browser Rendering Patterns
|
||||
|
||||
## Basic Worker
|
||||
|
||||
```typescript
|
||||
import puppeteer from "@cloudflare/puppeteer";
|
||||
|
||||
export default {
|
||||
async fetch(request, env) {
|
||||
const browser = await puppeteer.launch(env.MYBROWSER);
|
||||
try {
|
||||
const page = await browser.newPage();
|
||||
await page.goto("https://example.com");
|
||||
return new Response(await page.content());
|
||||
} finally {
|
||||
await browser.close(); // ALWAYS in finally
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Session Reuse
|
||||
|
||||
Keep sessions alive for performance:
|
||||
```typescript
|
||||
let sessionId = await env.SESSION_KV.get("browser-session");
|
||||
if (sessionId) {
|
||||
browser = await puppeteer.connect(env.MYBROWSER, sessionId);
|
||||
} else {
|
||||
browser = await puppeteer.launch(env.MYBROWSER, { keep_alive: 600000 });
|
||||
await env.SESSION_KV.put("browser-session", browser.sessionId(), { expirationTtl: 600 });
|
||||
}
|
||||
// Don't close browser to keep session alive
|
||||
```
|
||||
|
||||
## Common Operations
|
||||
|
||||
| Task | Code |
|
||||
|------|------|
|
||||
| Screenshot | `await page.screenshot({ type: "png", fullPage: true })` |
|
||||
| PDF | `await page.pdf({ format: "A4", printBackground: true })` |
|
||||
| Extract data | `await page.evaluate(() => document.querySelector('h1').textContent)` |
|
||||
| Fill form | `await page.type('#input', 'value'); await page.click('button')` |
|
||||
| Wait nav | `await Promise.all([page.waitForNavigation(), page.click('a')])` |
|
||||
|
||||
## Parallel Scraping
|
||||
|
||||
```typescript
|
||||
const pages = await Promise.all(urls.map(() => browser.newPage()));
|
||||
await Promise.all(pages.map((p, i) => p.goto(urls[i])));
|
||||
const titles = await Promise.all(pages.map(p => p.title()));
|
||||
```
|
||||
|
||||
## Playwright Selectors
|
||||
|
||||
```typescript
|
||||
import { launch } from "@cloudflare/playwright";
|
||||
const browser = await launch(env.MYBROWSER);
|
||||
await page.getByRole("button", { name: "Sign in" }).click();
|
||||
await page.getByLabel("Email").fill("user@example.com");
|
||||
await page.getByTestId("submit-button").click();
|
||||
```
|
||||
|
||||
## Incognito Contexts
|
||||
|
||||
Isolated sessions without multiple browsers:
|
||||
```typescript
|
||||
const ctx1 = await browser.createIncognitoBrowserContext();
|
||||
const ctx2 = await browser.createIncognitoBrowserContext();
|
||||
// Each has isolated cookies/storage
|
||||
```
|
||||
|
||||
## Quota Check
|
||||
|
||||
```typescript
|
||||
const limits = await puppeteer.limits(env.MYBROWSER);
|
||||
if (limits.remaining < 60000) return new Response("Quota low", { status: 429 });
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```typescript
|
||||
try {
|
||||
await page.goto(url, { timeout: 30000, waitUntil: "networkidle0" });
|
||||
} catch (e) {
|
||||
if (e.message.includes("timeout")) return new Response("Timeout", { status: 504 });
|
||||
if (e.message.includes("Session limit")) return new Response("Too many sessions", { status: 429 });
|
||||
} finally {
|
||||
if (browser) await browser.close();
|
||||
}
|
||||
```
|
||||
111
.agents/skills/cloudflare-deploy/references/c3/README.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# C3 (create-cloudflare)
|
||||
|
||||
Official CLI for scaffolding Cloudflare Workers and Pages projects with templates, TypeScript, and instant deployment.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Interactive (recommended for first-time)
|
||||
npm create cloudflare@latest my-app
|
||||
|
||||
# Worker (API/WebSocket/Cron)
|
||||
npm create cloudflare@latest my-api -- --type=hello-world --ts
|
||||
|
||||
# Pages (static/SSG/full-stack)
|
||||
npm create cloudflare@latest my-site -- --type=web-app --framework=astro --platform=pages
|
||||
```
|
||||
|
||||
## Platform Decision Tree
|
||||
|
||||
```
|
||||
What are you building?
|
||||
|
||||
├─ API / WebSocket / Cron / Email handler
|
||||
│ └─ Workers (default) - no --platform flag needed
|
||||
│ npm create cloudflare@latest my-api -- --type=hello-world
|
||||
|
||||
├─ Static site / SSG / Documentation
|
||||
│ └─ Pages - requires --platform=pages
|
||||
│ npm create cloudflare@latest my-site -- --type=web-app --framework=astro --platform=pages
|
||||
|
||||
├─ Full-stack app (Next.js/Remix/SvelteKit)
|
||||
│ ├─ Need Durable Objects, Queues, or Workers-only features?
|
||||
│ │ └─ Workers (default)
|
||||
│ └─ Otherwise use Pages for git integration and branch previews
|
||||
│ └─ Add --platform=pages
|
||||
|
||||
└─ Convert existing project
|
||||
└─ npm create cloudflare@latest . -- --type=pre-existing --existing-script=./src/worker.ts
|
||||
```
|
||||
|
||||
**Critical:** Pages projects require `--platform=pages` flag. Without it, C3 defaults to Workers.
|
||||
|
||||
## Interactive Flow
|
||||
|
||||
When run without flags, C3 prompts in this order:
|
||||
|
||||
1. **Project name** - Directory to create (defaults to current dir with `.`)
|
||||
2. **Application type** - `hello-world`, `web-app`, `demo`, `pre-existing`, `remote-template`
|
||||
3. **Platform** - `workers` (default) or `pages` (for web apps only)
|
||||
4. **Framework** - If web-app: `next`, `remix`, `astro`, `react-router`, `solid`, `svelte`, etc.
|
||||
5. **TypeScript** - `yes` (recommended) or `no`
|
||||
6. **Git** - Initialize repository? `yes` or `no`
|
||||
7. **Deploy** - Deploy now? `yes` or `no` (requires `wrangler login`)
|
||||
|
||||
## Installation Methods
|
||||
|
||||
```bash
|
||||
# NPM
|
||||
npm create cloudflare@latest
|
||||
|
||||
# Yarn
|
||||
yarn create cloudflare
|
||||
|
||||
# PNPM
|
||||
pnpm create cloudflare@latest
|
||||
```
|
||||
|
||||
## In This Reference
|
||||
|
||||
| File | Purpose | Use When |
|
||||
|------|---------|----------|
|
||||
| **api.md** | Complete CLI flag reference | Scripting, CI/CD, advanced usage |
|
||||
| **configuration.md** | Generated files, bindings, types | Understanding output, customization |
|
||||
| **patterns.md** | Workflows, CI/CD, monorepos | Real-world integration |
|
||||
| **gotchas.md** | Troubleshooting failures | Deployment blocked, errors |
|
||||
|
||||
## Reading Order
|
||||
|
||||
| Task | Read |
|
||||
|------|------|
|
||||
| Create first project | README only |
|
||||
| Set up CI/CD | README → api → patterns |
|
||||
| Debug failed deploy | gotchas |
|
||||
| Understand generated files | configuration |
|
||||
| Full CLI reference | api |
|
||||
| Create custom template | patterns → configuration |
|
||||
| Convert existing project | README → patterns |
|
||||
|
||||
## Post-Creation
|
||||
|
||||
```bash
|
||||
cd my-app
|
||||
|
||||
# Local dev with hot reload
|
||||
npm run dev
|
||||
|
||||
# Generate TypeScript types for bindings
|
||||
npm run cf-typegen
|
||||
|
||||
# Deploy to Cloudflare
|
||||
npm run deploy
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- **workers/README.md** - Workers runtime, bindings, APIs
|
||||
- **workers-ai/README.md** - AI/ML models
|
||||
- **pages/README.md** - Pages-specific features
|
||||
- **wrangler/README.md** - Wrangler CLI beyond initial setup
|
||||
- **d1/README.md** - SQLite database
|
||||
- **r2/README.md** - Object storage
|
||||
71
.agents/skills/cloudflare-deploy/references/c3/api.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# C3 CLI Reference
|
||||
|
||||
## Invocation
|
||||
|
||||
```bash
|
||||
npm create cloudflare@latest [name] [-- flags] # NPM requires --
|
||||
yarn create cloudflare [name] [flags]
|
||||
pnpm create cloudflare@latest [name] [-- flags]
|
||||
```
|
||||
|
||||
## Core Flags
|
||||
|
||||
| Flag | Values | Description |
|
||||
|------|--------|-------------|
|
||||
| `--type` | `hello-world`, `web-app`, `demo`, `pre-existing`, `remote-template` | Application type |
|
||||
| `--platform` | `workers` (default), `pages` | Target platform |
|
||||
| `--framework` | `next`, `remix`, `astro`, `react-router`, `solid`, `svelte`, `qwik`, `vue`, `angular`, `hono` | Web framework (requires `--type=web-app`) |
|
||||
| `--lang` | `ts`, `js`, `python` | Language (for `--type=hello-world`) |
|
||||
| `--ts` / `--no-ts` | - | TypeScript for web apps |
|
||||
|
||||
## Deployment Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--deploy` / `--no-deploy` | Deploy immediately (prompts interactive, skips in CI) |
|
||||
| `--git` / `--no-git` | Initialize git (default: yes) |
|
||||
| `--open` | Open browser after deploy |
|
||||
|
||||
## Advanced Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--template=user/repo` | GitHub template or local path |
|
||||
| `--existing-script=./src/worker.ts` | Existing script (requires `--type=pre-existing`) |
|
||||
| `--category=ai\|database\|realtime` | Demo filter (requires `--type=demo`) |
|
||||
| `--experimental` | Enable experimental features |
|
||||
| `--wrangler-defaults` | Skip wrangler prompts |
|
||||
|
||||
## Environment Variables
|
||||
|
||||
```bash
|
||||
CLOUDFLARE_API_TOKEN=xxx # For deployment
|
||||
CLOUDFLARE_ACCOUNT_ID=xxx # Account ID
|
||||
CF_TELEMETRY_DISABLED=1 # Disable telemetry
|
||||
```
|
||||
|
||||
## Exit Codes
|
||||
|
||||
`0` success, `1` user abort, `2` error
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# TypeScript Worker
|
||||
npm create cloudflare@latest my-api -- --type=hello-world --lang=ts --no-deploy
|
||||
|
||||
# Next.js on Pages
|
||||
npm create cloudflare@latest my-app -- --type=web-app --framework=next --platform=pages --ts
|
||||
|
||||
# Astro blog
|
||||
npm create cloudflare@latest my-blog -- --type=web-app --framework=astro --ts --deploy
|
||||
|
||||
# CI: non-interactive
|
||||
npm create cloudflare@latest my-app -- --type=web-app --framework=next --ts --no-git --no-deploy
|
||||
|
||||
# GitHub template
|
||||
npm create cloudflare@latest -- --template=cloudflare/templates/worker-openapi
|
||||
|
||||
# Convert existing project
|
||||
npm create cloudflare@latest . -- --type=pre-existing --existing-script=./build/worker.js
|
||||
```
|
||||
@@ -0,0 +1,81 @@
|
||||
# C3 Generated Configuration
|
||||
|
||||
## Output Structure
|
||||
|
||||
```
|
||||
my-app/
|
||||
├── src/index.ts # Worker entry point
|
||||
├── wrangler.jsonc # Cloudflare config
|
||||
├── package.json # Scripts
|
||||
├── tsconfig.json
|
||||
└── .gitignore
|
||||
```
|
||||
|
||||
## wrangler.jsonc
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/cloudflare/workers-sdk/main/packages/wrangler/config-schema.json",
|
||||
"name": "my-app",
|
||||
"main": "src/index.ts",
|
||||
"compatibility_date": "2026-01-27"
|
||||
}
|
||||
```
|
||||
|
||||
## Binding Placeholders
|
||||
|
||||
C3 generates **placeholder IDs** that must be replaced before deploy:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"kv_namespaces": [{ "binding": "MY_KV", "id": "placeholder_kv_id" }],
|
||||
"d1_databases": [{ "binding": "DB", "database_id": "00000000-..." }]
|
||||
}
|
||||
```
|
||||
|
||||
**Replace with real IDs:**
|
||||
```bash
|
||||
npx wrangler kv namespace create MY_KV # Returns real ID
|
||||
npx wrangler d1 create my-database # Returns real database_id
|
||||
```
|
||||
|
||||
**Deployment error if not replaced:**
|
||||
```
|
||||
Error: Invalid KV namespace ID "placeholder_kv_id"
|
||||
```
|
||||
|
||||
## Scripts
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"dev": "wrangler dev",
|
||||
"deploy": "wrangler deploy",
|
||||
"cf-typegen": "wrangler types"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Type Generation
|
||||
|
||||
Run after adding bindings:
|
||||
```bash
|
||||
npm run cf-typegen
|
||||
```
|
||||
|
||||
Generates `.wrangler/types/runtime.d.ts`:
|
||||
```typescript
|
||||
interface Env {
|
||||
MY_KV: KVNamespace;
|
||||
DB: D1Database;
|
||||
}
|
||||
```
|
||||
|
||||
## Post-Creation Checklist
|
||||
|
||||
1. Review `wrangler.jsonc` - check name, compatibility_date
|
||||
2. Replace placeholder binding IDs with real resource IDs
|
||||
3. Run `npm run cf-typegen`
|
||||
4. Test: `npm run dev`
|
||||
5. Deploy: `npm run deploy`
|
||||
6. Add secrets: `npx wrangler secret put SECRET_NAME`
|
||||
92
.agents/skills/cloudflare-deploy/references/c3/gotchas.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# C3 Troubleshooting
|
||||
|
||||
## Deployment Issues
|
||||
|
||||
### Placeholder IDs
|
||||
|
||||
**Error:** "Invalid namespace ID"
|
||||
**Fix:** Replace placeholders in wrangler.jsonc with real IDs:
|
||||
```bash
|
||||
npx wrangler kv namespace create MY_KV # Get real ID
|
||||
```
|
||||
|
||||
### Authentication
|
||||
|
||||
**Error:** "Not authenticated"
|
||||
**Fix:** `npx wrangler login` or set `CLOUDFLARE_API_TOKEN`
|
||||
|
||||
### Name Conflict
|
||||
|
||||
**Error:** "Worker already exists"
|
||||
**Fix:** Change `name` in wrangler.jsonc
|
||||
|
||||
## Platform Selection
|
||||
|
||||
| Need | Platform |
|
||||
|------|----------|
|
||||
| Git integration, branch previews | `--platform=pages` |
|
||||
| Durable Objects, D1, Queues | Workers (default) |
|
||||
|
||||
Wrong platform? Recreate with correct `--platform` flag.
|
||||
|
||||
## TypeScript Issues
|
||||
|
||||
**"Cannot find name 'KVNamespace'"**
|
||||
```bash
|
||||
npm run cf-typegen # Regenerate types
|
||||
# Restart TS server in editor
|
||||
```
|
||||
|
||||
**Missing types after config change:** Re-run `npm run cf-typegen`
|
||||
|
||||
## Package Manager
|
||||
|
||||
**Multiple lockfiles causing issues:**
|
||||
```bash
|
||||
rm pnpm-lock.yaml # If using npm
|
||||
rm package-lock.json # If using pnpm
|
||||
```
|
||||
|
||||
## CI/CD
|
||||
|
||||
**CI hangs on prompts:**
|
||||
```bash
|
||||
npm create cloudflare@latest my-app -- \
|
||||
--type=hello-world --lang=ts --no-git --no-deploy
|
||||
```
|
||||
|
||||
**Auth in CI:**
|
||||
```yaml
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
```
|
||||
|
||||
## Framework-Specific
|
||||
|
||||
| Framework | Issue | Fix |
|
||||
|-----------|-------|-----|
|
||||
| Next.js | create-next-app failed | `npm cache clean --force`, retry |
|
||||
| Astro | Adapter missing | Install `@astrojs/cloudflare` |
|
||||
| Remix | Module errors | Update `@remix-run/cloudflare*` |
|
||||
|
||||
## Compatibility Date
|
||||
|
||||
**"Feature X requires compatibility_date >= ..."**
|
||||
**Fix:** Update `compatibility_date` in wrangler.jsonc to today's date
|
||||
|
||||
## Node.js Version
|
||||
|
||||
**"Node.js version not supported"**
|
||||
**Fix:** Install Node.js 18+ (`nvm install 20`)
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Error | Cause | Fix |
|
||||
|-------|-------|-----|
|
||||
| Invalid namespace ID | Placeholder binding | Create resource, update config |
|
||||
| Not authenticated | No login | `npx wrangler login` |
|
||||
| Cannot find KVNamespace | Missing types | `npm run cf-typegen` |
|
||||
| Worker already exists | Name conflict | Change `name` |
|
||||
| CI hangs | Missing flags | Add --type, --lang, --no-deploy |
|
||||
| Template not found | Bad name | Check cloudflare/templates |
|
||||
82
.agents/skills/cloudflare-deploy/references/c3/patterns.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# C3 Usage Patterns
|
||||
|
||||
## Quick Workflows
|
||||
|
||||
```bash
|
||||
# TypeScript API Worker
|
||||
npm create cloudflare@latest my-api -- --type=hello-world --lang=ts --deploy
|
||||
|
||||
# Next.js on Pages
|
||||
npm create cloudflare@latest my-app -- --type=web-app --framework=next --platform=pages --ts --deploy
|
||||
|
||||
# Astro static site
|
||||
npm create cloudflare@latest my-blog -- --type=web-app --framework=astro --platform=pages --ts
|
||||
```
|
||||
|
||||
## CI/CD (GitHub Actions)
|
||||
|
||||
```yaml
|
||||
- name: Deploy
|
||||
run: npm run deploy
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
```
|
||||
|
||||
**Non-interactive requires:**
|
||||
```bash
|
||||
--type=<value> # Required
|
||||
--no-git # Recommended (CI already in git)
|
||||
--no-deploy # Deploy separately with secrets
|
||||
--framework=<value> # For web-app
|
||||
--ts / --no-ts # Required
|
||||
```
|
||||
|
||||
## Monorepo
|
||||
|
||||
C3 detects workspace config (`package.json` workspaces or `pnpm-workspace.yaml`).
|
||||
|
||||
```bash
|
||||
cd packages/
|
||||
npm create cloudflare@latest my-worker -- --type=hello-world --lang=ts --no-deploy
|
||||
```
|
||||
|
||||
## Custom Templates
|
||||
|
||||
```bash
|
||||
# GitHub repo
|
||||
npm create cloudflare@latest -- --template=username/repo
|
||||
npm create cloudflare@latest -- --template=cloudflare/templates/worker-openapi
|
||||
|
||||
# Local path
|
||||
npm create cloudflare@latest my-app -- --template=../my-template
|
||||
```
|
||||
|
||||
**Template requires `c3.config.json`:**
|
||||
```json
|
||||
{
|
||||
"name": "my-template",
|
||||
"category": "hello-world",
|
||||
"copies": [{ "path": "src/" }, { "path": "wrangler.jsonc" }],
|
||||
"transforms": [{ "path": "package.json", "jsonc": { "name": "{{projectName}}" }}]
|
||||
}
|
||||
```
|
||||
|
||||
## Existing Projects
|
||||
|
||||
```bash
|
||||
# Add Cloudflare to existing Worker
|
||||
npm create cloudflare@latest . -- --type=pre-existing --existing-script=./dist/index.js
|
||||
|
||||
# Add to existing framework app
|
||||
npm create cloudflare@latest . -- --type=web-app --framework=next --platform=pages --ts
|
||||
```
|
||||
|
||||
## Post-Creation Checklist
|
||||
|
||||
1. Review `wrangler.jsonc` - set `compatibility_date`, verify `name`
|
||||
2. Create bindings: `wrangler kv namespace create`, `wrangler d1 create`, `wrangler r2 bucket create`
|
||||
3. Generate types: `npm run cf-typegen`
|
||||
4. Test: `npm run dev`
|
||||
5. Deploy: `npm run deploy`
|
||||
6. Set secrets: `wrangler secret put SECRET_NAME`
|
||||
@@ -0,0 +1,147 @@
|
||||
# Cloudflare Cache Reserve
|
||||
|
||||
**Persistent cache storage built on R2 for long-term content retention**
|
||||
|
||||
## Smart Shield Integration
|
||||
|
||||
Cache Reserve is part of **Smart Shield**, Cloudflare's comprehensive security and performance suite:
|
||||
|
||||
- **Smart Shield Advanced tier**: Includes 2TB Cache Reserve storage
|
||||
- **Standalone purchase**: Available separately if not using Smart Shield
|
||||
- **Migration**: Existing standalone customers can migrate to Smart Shield bundles
|
||||
|
||||
**Decision**: Already on Smart Shield Advanced? Cache Reserve is included. Otherwise evaluate standalone purchase vs Smart Shield upgrade.
|
||||
|
||||
## Overview
|
||||
|
||||
Cache Reserve is Cloudflare's persistent, large-scale cache storage layer built on R2. It acts as the ultimate upper-tier cache, storing cacheable content for extended periods (30+ days) to maximize cache hits, reduce origin egress fees, and shield origins from repeated requests for long-tail content.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### What is Cache Reserve?
|
||||
|
||||
- **Persistent storage layer**: Built on R2, sits above tiered cache hierarchy
|
||||
- **Long-term retention**: 30-day default retention, extended on each access
|
||||
- **Automatic operation**: Works seamlessly with existing CDN, no code changes required
|
||||
- **Origin shielding**: Dramatically reduces origin egress by serving cached content longer
|
||||
- **Usage-based pricing**: Pay only for storage + read/write operations
|
||||
|
||||
### Cache Hierarchy
|
||||
|
||||
```
|
||||
Visitor Request
|
||||
↓
|
||||
Lower-Tier Cache (closest to visitor)
|
||||
↓ (on miss)
|
||||
Upper-Tier Cache (closest to origin)
|
||||
↓ (on miss)
|
||||
Cache Reserve (R2 persistent storage)
|
||||
↓ (on miss)
|
||||
Origin Server
|
||||
```
|
||||
|
||||
### How It Works
|
||||
|
||||
1. **On cache miss**: Content fetched from origin <20><> written to Cache Reserve + edge caches simultaneously
|
||||
2. **On edge eviction**: Content may be evicted from edge cache but remains in Cache Reserve
|
||||
3. **On subsequent request**: If edge cache misses but Cache Reserve hits → content restored to edge caches
|
||||
4. **Retention**: Assets remain in Cache Reserve for 30 days since last access (configurable via TTL)
|
||||
|
||||
## When to Use Cache Reserve
|
||||
|
||||
```
|
||||
Need persistent caching?
|
||||
├─ High origin egress costs → Cache Reserve ✓
|
||||
├─ Long-tail content (archives, media libraries) → Cache Reserve ✓
|
||||
├─ Already using Smart Shield Advanced → Included! ✓
|
||||
├─ Video streaming with seeking (range requests) → ✗ Not supported
|
||||
├─ Dynamic/personalized content → ✗ Use edge cache only
|
||||
├─ Need per-request cache control from Workers → ✗ Use R2 directly
|
||||
└─ Frequently updated content (< 10hr lifetime) → ✗ Not eligible
|
||||
```
|
||||
|
||||
## Asset Eligibility
|
||||
|
||||
Cache Reserve only stores assets meeting **ALL** criteria:
|
||||
|
||||
- Cacheable per Cloudflare's standard rules
|
||||
- Minimum 10-hour TTL (36000 seconds)
|
||||
- `Content-Length` header present
|
||||
- Original files only (not transformed images)
|
||||
|
||||
### Eligibility Checklist
|
||||
|
||||
Use this checklist to verify if an asset is eligible:
|
||||
|
||||
- [ ] Zone has Cache Reserve enabled
|
||||
- [ ] Zone has Tiered Cache enabled (required)
|
||||
- [ ] Asset TTL ≥ 10 hours (36,000 seconds)
|
||||
- [ ] `Content-Length` header present on origin response
|
||||
- [ ] No `Set-Cookie` header (or uses private directive)
|
||||
- [ ] `Vary` header is NOT `*` (can be `Accept-Encoding`)
|
||||
- [ ] Not an image transformation variant (original images OK)
|
||||
- [ ] Not a range request (no HTTP 206 support)
|
||||
- [ ] Not O2O (Orange-to-Orange) proxied request
|
||||
|
||||
**All boxes must be checked for Cache Reserve eligibility.**
|
||||
|
||||
### Not Eligible
|
||||
|
||||
- Assets with TTL < 10 hours
|
||||
- Responses without `Content-Length` header
|
||||
- Image transformation variants (original images are eligible)
|
||||
- Responses with `Set-Cookie` headers
|
||||
- Responses with `Vary: *` header
|
||||
- Assets from R2 public buckets on same zone
|
||||
- O2O (Orange-to-Orange) setup requests
|
||||
- **Range requests** (video seeking, partial content downloads)
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Enable via Dashboard
|
||||
https://dash.cloudflare.com/caching/cache-reserve
|
||||
# Click "Enable Storage Sync" or "Purchase" button
|
||||
```
|
||||
|
||||
**Prerequisites:**
|
||||
- Paid Cache Reserve plan or Smart Shield Advanced required
|
||||
- Tiered Cache required for optimal performance
|
||||
|
||||
## Essential Commands
|
||||
|
||||
```bash
|
||||
# Check Cache Reserve status
|
||||
curl -X GET "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/cache/cache_reserve" \
|
||||
-H "Authorization: Bearer $API_TOKEN"
|
||||
|
||||
# Enable Cache Reserve
|
||||
curl -X PATCH "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/cache/cache_reserve" \
|
||||
-H "Authorization: Bearer $API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"value": "on"}'
|
||||
|
||||
# Check asset cache status
|
||||
curl -I https://example.com/asset.jpg | grep -i cache
|
||||
```
|
||||
|
||||
## In This Reference
|
||||
|
||||
| Task | Files |
|
||||
|------|-------|
|
||||
| Evaluate if Cache Reserve fits your use case | README.md (this file) |
|
||||
| Enable Cache Reserve for your zone | README.md + [configuration.md](./configuration.md) |
|
||||
| Use with Workers (understand limitations) | [api.md](./api.md) |
|
||||
| Setup via SDKs or IaC (TypeScript, Python, Terraform) | [configuration.md](./configuration.md) |
|
||||
| Optimize costs and debug issues | [patterns.md](./patterns.md) + [gotchas.md](./gotchas.md) |
|
||||
| Understand eligibility and troubleshoot | [gotchas.md](./gotchas.md) → [patterns.md](./patterns.md) |
|
||||
|
||||
**Files:**
|
||||
- [configuration.md](./configuration.md) - Setup, API, SDKs, and Cache Rules
|
||||
- [api.md](./api.md) - Purging, monitoring, Workers integration
|
||||
- [patterns.md](./patterns.md) - Best practices, cost optimization, debugging
|
||||
- [gotchas.md](./gotchas.md) - Common issues, limitations, troubleshooting
|
||||
|
||||
## See Also
|
||||
- [r2](../r2/) - Cache Reserve built on R2 storage
|
||||
- [workers](../workers/) - Workers integration with Cache API
|
||||
194
.agents/skills/cloudflare-deploy/references/cache-reserve/api.md
Normal file
@@ -0,0 +1,194 @@
|
||||
# Cache Reserve API
|
||||
|
||||
## Workers Integration
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────┐
|
||||
│ CRITICAL: Workers Cache API ≠ Cache Reserve │
|
||||
│ │
|
||||
│ • Workers caches.default / cache.put() → edge cache ONLY │
|
||||
│ • Cache Reserve → zone-level setting, automatic, no per-req │
|
||||
│ • You CANNOT selectively write to Cache Reserve from Workers │
|
||||
│ • Cache Reserve works with standard fetch(), not cache.put() │
|
||||
└────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Cache Reserve is a **zone-level configuration**, not a per-request API. It works automatically when enabled for the zone:
|
||||
|
||||
### Standard Fetch (Recommended)
|
||||
|
||||
```typescript
|
||||
// Cache Reserve works automatically via standard fetch
|
||||
export default {
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
// Standard fetch uses Cache Reserve automatically
|
||||
return await fetch(request);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Cache API Limitations
|
||||
|
||||
**IMPORTANT**: `cache.put()` is **NOT compatible** with Cache Reserve or Tiered Cache.
|
||||
|
||||
```typescript
|
||||
// ❌ WRONG: cache.put() bypasses Cache Reserve
|
||||
const cache = caches.default;
|
||||
let response = await cache.match(request);
|
||||
if (!response) {
|
||||
response = await fetch(request);
|
||||
await cache.put(request, response.clone()); // Bypasses Cache Reserve!
|
||||
}
|
||||
|
||||
// ✅ CORRECT: Use standard fetch for Cache Reserve compatibility
|
||||
return await fetch(request);
|
||||
|
||||
// ✅ CORRECT: Use Cache API only for custom cache namespaces
|
||||
const customCache = await caches.open('my-custom-cache');
|
||||
let response = await customCache.match(request);
|
||||
if (!response) {
|
||||
response = await fetch(request);
|
||||
await customCache.put(request, response.clone()); // Custom cache OK
|
||||
}
|
||||
```
|
||||
|
||||
## Purging and Cache Management
|
||||
|
||||
### Purge by URL (Instant)
|
||||
|
||||
```typescript
|
||||
// Purge specific URL from Cache Reserve immediately
|
||||
const purgeCacheReserveByURL = async (
|
||||
zoneId: string,
|
||||
apiToken: string,
|
||||
urls: string[]
|
||||
) => {
|
||||
const response = await fetch(
|
||||
`https://api.cloudflare.com/client/v4/zones/${zoneId}/purge_cache`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${apiToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ files: urls })
|
||||
}
|
||||
);
|
||||
return await response.json();
|
||||
};
|
||||
|
||||
// Example usage
|
||||
await purgeCacheReserveByURL('zone123', 'token456', [
|
||||
'https://example.com/image.jpg',
|
||||
'https://example.com/video.mp4'
|
||||
]);
|
||||
```
|
||||
|
||||
### Purge by Tag/Host/Prefix (Revalidation)
|
||||
|
||||
```typescript
|
||||
// Purge by cache tag - forces revalidation, not immediate removal
|
||||
await fetch(
|
||||
`https://api.cloudflare.com/client/v4/zones/${zoneId}/purge_cache`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': `Bearer ${apiToken}`, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ tags: ['tag1', 'tag2'] })
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
**Purge behavior:**
|
||||
- **By URL**: Immediate removal from Cache Reserve + edge cache
|
||||
- **By tag/host/prefix**: Revalidation only, assets remain in storage (costs continue)
|
||||
|
||||
### Clear All Cache Reserve Data
|
||||
|
||||
```typescript
|
||||
// Requires Cache Reserve OFF first
|
||||
await fetch(
|
||||
`https://api.cloudflare.com/client/v4/zones/${zoneId}/cache/cache_reserve_clear`,
|
||||
{ method: 'POST', headers: { 'Authorization': `Bearer ${apiToken}` } }
|
||||
);
|
||||
|
||||
// Check status: GET same endpoint returns { state: "In-progress" | "Completed" }
|
||||
```
|
||||
|
||||
**Process**: Disable Cache Reserve → Call clear endpoint → Wait up to 24hr → Re-enable
|
||||
|
||||
## Monitoring and Analytics
|
||||
|
||||
### Dashboard Analytics
|
||||
|
||||
Navigate to **Caching > Cache Reserve** to view:
|
||||
|
||||
- **Egress Savings**: Total bytes served from Cache Reserve vs origin egress cost saved
|
||||
- **Requests Served**: Cache Reserve hits vs misses breakdown
|
||||
- **Storage Used**: Current GB stored in Cache Reserve (billed monthly)
|
||||
- **Operations**: Class A (writes) and Class B (reads) operation counts
|
||||
- **Cost Tracking**: Estimated monthly costs based on current usage
|
||||
|
||||
### Logpush Integration
|
||||
|
||||
```typescript
|
||||
// Logpush field: CacheReserveUsed (boolean) - filter for Cache Reserve hits
|
||||
// Query Cache Reserve hits in analytics
|
||||
const logpushQuery = `
|
||||
SELECT
|
||||
ClientRequestHost,
|
||||
COUNT(*) as requests,
|
||||
SUM(EdgeResponseBytes) as bytes_served,
|
||||
COUNT(CASE WHEN CacheReserveUsed = true THEN 1 END) as cache_reserve_hits,
|
||||
COUNT(CASE WHEN CacheReserveUsed = false THEN 1 END) as cache_reserve_misses
|
||||
FROM http_requests
|
||||
WHERE Timestamp >= NOW() - INTERVAL '24 hours'
|
||||
GROUP BY ClientRequestHost
|
||||
ORDER BY requests DESC
|
||||
`;
|
||||
|
||||
// Filter only Cache Reserve hits
|
||||
const crHitsQuery = `
|
||||
SELECT ClientRequestHost, COUNT(*) as requests, SUM(EdgeResponseBytes) as bytes
|
||||
FROM http_requests
|
||||
WHERE CacheReserveUsed = true AND Timestamp >= NOW() - INTERVAL '7 days'
|
||||
GROUP BY ClientRequestHost
|
||||
ORDER BY bytes DESC
|
||||
`;
|
||||
```
|
||||
|
||||
### GraphQL Analytics
|
||||
|
||||
```graphql
|
||||
query CacheReserveAnalytics($zoneTag: string, $since: string, $until: string) {
|
||||
viewer {
|
||||
zones(filter: { zoneTag: $zoneTag }) {
|
||||
httpRequests1dGroups(
|
||||
filter: { datetime_geq: $since, datetime_leq: $until }
|
||||
limit: 1000
|
||||
) {
|
||||
dimensions { date }
|
||||
sum {
|
||||
cachedBytes
|
||||
cachedRequests
|
||||
bytes
|
||||
requests
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Pricing
|
||||
|
||||
```typescript
|
||||
// Storage: $0.015/GB-month | Class A (writes): $4.50/M | Class B (reads): $0.36/M
|
||||
// Cache miss: 1A + 1B | Cache hit: 1B | Assets >1GB: proportionally more ops
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [README](./README.md) - Overview and core concepts
|
||||
- [Configuration](./configuration.md) - Setup and Cache Rules
|
||||
- [Patterns](./patterns.md) - Best practices and optimization
|
||||
- [Gotchas](./gotchas.md) - Common issues and troubleshooting
|
||||
@@ -0,0 +1,169 @@
|
||||
# Cache Reserve Configuration
|
||||
|
||||
## Dashboard Setup
|
||||
|
||||
**Minimum steps to enable:**
|
||||
|
||||
```bash
|
||||
# Navigate to dashboard
|
||||
https://dash.cloudflare.com/caching/cache-reserve
|
||||
|
||||
# Click "Enable Storage Sync" or "Purchase" button
|
||||
```
|
||||
|
||||
**Prerequisites:**
|
||||
- Paid Cache Reserve plan or Smart Shield Advanced required
|
||||
- Tiered Cache **required** for Cache Reserve to function optimally
|
||||
|
||||
## API Configuration
|
||||
|
||||
### REST API
|
||||
|
||||
```bash
|
||||
# Enable
|
||||
curl -X PATCH "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/cache/cache_reserve" \
|
||||
-H "Authorization: Bearer $API_TOKEN" -H "Content-Type: application/json" \
|
||||
-d '{"value": "on"}'
|
||||
|
||||
# Check status
|
||||
curl -X GET "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/cache/cache_reserve" \
|
||||
-H "Authorization: Bearer $API_TOKEN"
|
||||
```
|
||||
|
||||
### TypeScript SDK
|
||||
|
||||
```bash
|
||||
npm install cloudflare
|
||||
```
|
||||
|
||||
```typescript
|
||||
import Cloudflare from 'cloudflare';
|
||||
|
||||
const client = new Cloudflare({
|
||||
apiToken: process.env.CLOUDFLARE_API_TOKEN,
|
||||
});
|
||||
|
||||
// Enable Cache Reserve
|
||||
await client.cache.cacheReserve.edit({
|
||||
zone_id: 'abc123',
|
||||
value: 'on',
|
||||
});
|
||||
|
||||
// Get Cache Reserve status
|
||||
const status = await client.cache.cacheReserve.get({
|
||||
zone_id: 'abc123',
|
||||
});
|
||||
console.log(status.value); // 'on' or 'off'
|
||||
```
|
||||
|
||||
### Python SDK
|
||||
|
||||
```bash
|
||||
pip install cloudflare
|
||||
```
|
||||
|
||||
```python
|
||||
from cloudflare import Cloudflare
|
||||
|
||||
client = Cloudflare(api_token=os.environ.get("CLOUDFLARE_API_TOKEN"))
|
||||
|
||||
# Enable Cache Reserve
|
||||
client.cache.cache_reserve.edit(
|
||||
zone_id="abc123",
|
||||
value="on"
|
||||
)
|
||||
|
||||
# Get Cache Reserve status
|
||||
status = client.cache.cache_reserve.get(zone_id="abc123")
|
||||
print(status.value) # 'on' or 'off'
|
||||
```
|
||||
|
||||
### Terraform
|
||||
|
||||
```hcl
|
||||
terraform {
|
||||
required_providers {
|
||||
cloudflare = {
|
||||
source = "cloudflare/cloudflare"
|
||||
version = "~> 4.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "cloudflare" {
|
||||
api_token = var.cloudflare_api_token
|
||||
}
|
||||
|
||||
resource "cloudflare_zone_cache_reserve" "example" {
|
||||
zone_id = var.zone_id
|
||||
enabled = true
|
||||
}
|
||||
|
||||
# Tiered Cache is required for Cache Reserve
|
||||
resource "cloudflare_tiered_cache" "example" {
|
||||
zone_id = var.zone_id
|
||||
cache_type = "smart"
|
||||
}
|
||||
```
|
||||
|
||||
### Pulumi
|
||||
|
||||
```typescript
|
||||
import * as cloudflare from "@pulumi/cloudflare";
|
||||
|
||||
// Enable Cache Reserve
|
||||
const cacheReserve = new cloudflare.ZoneCacheReserve("example", {
|
||||
zoneId: zoneId,
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
// Enable Tiered Cache (required)
|
||||
const tieredCache = new cloudflare.TieredCache("example", {
|
||||
zoneId: zoneId,
|
||||
cacheType: "smart",
|
||||
});
|
||||
```
|
||||
|
||||
### Required API Token Permissions
|
||||
|
||||
- `Zone Settings Read`
|
||||
- `Zone Settings Write`
|
||||
- `Zone Read`
|
||||
- `Zone Write`
|
||||
|
||||
## Cache Rules Integration
|
||||
|
||||
Control Cache Reserve eligibility via Cache Rules:
|
||||
|
||||
```typescript
|
||||
// Enable for static assets
|
||||
{
|
||||
action: 'set_cache_settings',
|
||||
action_parameters: {
|
||||
cache_reserve: { eligible: true, minimum_file_ttl: 86400 },
|
||||
edge_ttl: { mode: 'override_origin', default: 86400 },
|
||||
cache: true
|
||||
},
|
||||
expression: '(http.request.uri.path matches "\\.(jpg|png|webp|pdf|zip)$")'
|
||||
}
|
||||
|
||||
// Disable for APIs
|
||||
{
|
||||
action: 'set_cache_settings',
|
||||
action_parameters: { cache_reserve: { eligible: false } },
|
||||
expression: '(http.request.uri.path matches "^/api/")'
|
||||
}
|
||||
|
||||
// Create via API: PUT to zones/{zone_id}/rulesets/phases/http_request_cache_settings/entrypoint
|
||||
```
|
||||
|
||||
## Wrangler Integration
|
||||
|
||||
Cache Reserve works automatically with Workers deployed via Wrangler. No special wrangler.jsonc configuration needed - enable Cache Reserve via Dashboard or API for the zone.
|
||||
|
||||
## See Also
|
||||
|
||||
- [README](./README.md) - Overview and core concepts
|
||||
- [API Reference](./api.md) - Purging and monitoring APIs
|
||||
- [Patterns](./patterns.md) - Best practices and optimization
|
||||
- [Gotchas](./gotchas.md) - Common issues and troubleshooting
|
||||
@@ -0,0 +1,132 @@
|
||||
# Cache Reserve Gotchas
|
||||
|
||||
## Common Errors
|
||||
|
||||
### "Assets Not Being Cached in Cache Reserve"
|
||||
|
||||
**Cause:** Asset is not cacheable, TTL < 10 hours, Content-Length header missing, or blocking headers present (Set-Cookie, Vary: *)
|
||||
**Solution:** Ensure minimum TTL of 10+ hours (`Cache-Control: public, max-age=36000`), add Content-Length header, remove Set-Cookie header, and set `Vary: Accept-Encoding` (not *)
|
||||
|
||||
### "Range Requests Not Working" (Video Seeking Fails)
|
||||
|
||||
**Cause:** Cache Reserve does **NOT** support range requests (HTTP 206 Partial Content)
|
||||
**Solution:** Range requests bypass Cache Reserve entirely. For video streaming with seeking:
|
||||
- Use edge cache only (shorter TTLs)
|
||||
- Consider R2 with direct access for range-heavy workloads
|
||||
- Accept that seekable content won't benefit from Cache Reserve persistence
|
||||
|
||||
### "Origin Bandwidth Higher Than Expected"
|
||||
|
||||
**Cause:** Cache Reserve fetches **uncompressed** content from origin, even though it serves compressed to visitors
|
||||
**Solution:**
|
||||
- If origin charges by bandwidth, factor in uncompressed transfer costs
|
||||
- Cache Reserve compresses for visitors automatically (saves visitor bandwidth)
|
||||
- Compare: origin egress savings vs higher uncompressed fetch costs
|
||||
|
||||
### "Cloudflare Images Not Caching with Cache Reserve"
|
||||
|
||||
**Cause:** Cloudflare Images with `Vary: Accept` header (format negotiation) is incompatible with Cache Reserve
|
||||
**Solution:**
|
||||
- Cache Reserve silently skips images with Vary for format negotiation
|
||||
- Original images (non-transformed) may still be eligible
|
||||
- Use Cloudflare Images variants or edge cache for transformed images
|
||||
|
||||
### "High Class A Operations Costs"
|
||||
|
||||
**Cause:** Frequent cache misses, short TTLs, or frequent revalidation
|
||||
**Solution:** Increase TTL for stable content (24+ hours), enable Tiered Cache to reduce direct Cache Reserve misses, or use stale-while-revalidate
|
||||
|
||||
### "Purge Not Working as Expected"
|
||||
|
||||
**Cause:** Purge by tag only triggers revalidation but doesn't remove from Cache Reserve storage
|
||||
**Solution:** Use purge by URL for immediate removal, or disable Cache Reserve then clear all data for complete removal
|
||||
|
||||
### "O2O (Orange-to-Orange) Assets Not Caching"
|
||||
|
||||
**Cause:** Orange-to-Orange (proxied zone requesting another proxied zone on Cloudflare) bypasses Cache Reserve
|
||||
**Solution:**
|
||||
- **What is O2O**: Zone A (proxied) → Zone B (proxied), both on Cloudflare
|
||||
- **Detection**: Check `cf-cache-status` for `BYPASS` and review request path
|
||||
- **Workaround**: Use R2 or direct origin access instead of O2O proxy chains
|
||||
|
||||
### "Cache Reserve must be OFF before clearing data"
|
||||
|
||||
**Cause:** Attempting to clear Cache Reserve data while it's still enabled
|
||||
**Solution:** Disable Cache Reserve first, wait briefly for propagation (5s), then clear data (can take up to 24 hours)
|
||||
|
||||
## Limits
|
||||
|
||||
| Limit | Value | Notes |
|
||||
|-------|-------|-------|
|
||||
| Minimum TTL | 10 hours (36000 seconds) | Assets with shorter TTL not eligible |
|
||||
| Default retention | 30 days (2592000 seconds) | Configurable |
|
||||
| Maximum file size | Same as R2 limits | No practical limit |
|
||||
| Purge/clear time | Up to 24 hours | Complete propagation time |
|
||||
| Plan requirement | Paid Cache Reserve or Smart Shield | Not available on free plans |
|
||||
| Content-Length header | Required | Must be present for eligibility |
|
||||
| Set-Cookie header | Blocks caching | Must not be present (or use private directive) |
|
||||
| Vary header | Cannot be * | Can use Vary: Accept-Encoding |
|
||||
| Image transformations | Variants not eligible | Original images only |
|
||||
| Range requests | NOT supported | HTTP 206 bypasses Cache Reserve |
|
||||
| Compression | Fetches uncompressed | Serves compressed to visitors |
|
||||
| Worker control | Zone-level only | Cannot control per-request |
|
||||
| O2O requests | Bypassed | Orange-to-Orange not eligible |
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- **Official Docs**: https://developers.cloudflare.com/cache/advanced-configuration/cache-reserve/
|
||||
- **API Reference**: https://developers.cloudflare.com/api/resources/cache/subresources/cache_reserve/
|
||||
- **Cache Rules**: https://developers.cloudflare.com/cache/how-to/cache-rules/
|
||||
- **Workers Cache API**: https://developers.cloudflare.com/workers/runtime-apis/cache/
|
||||
- **R2 Documentation**: https://developers.cloudflare.com/r2/
|
||||
- **Smart Shield**: https://developers.cloudflare.com/smart-shield/
|
||||
- **Tiered Cache**: https://developers.cloudflare.com/cache/how-to/tiered-cache/
|
||||
|
||||
## Troubleshooting Flowchart
|
||||
|
||||
Asset not caching in Cache Reserve?
|
||||
|
||||
```
|
||||
1. Is Cache Reserve enabled for zone?
|
||||
→ No: Enable via Dashboard or API
|
||||
→ Yes: Continue to step 2
|
||||
|
||||
2. Is Tiered Cache enabled?
|
||||
→ No: Enable Tiered Cache (required!)
|
||||
→ Yes: Continue to step 3
|
||||
|
||||
3. Does asset have TTL ≥ 10 hours?
|
||||
→ No: Increase via Cache Rules (edge_ttl override)
|
||||
→ Yes: Continue to step 4
|
||||
|
||||
4. Is Content-Length header present?
|
||||
→ No: Fix origin to include Content-Length
|
||||
→ Yes: Continue to step 5
|
||||
|
||||
5. Is Set-Cookie header present?
|
||||
→ Yes: Remove Set-Cookie or scope appropriately
|
||||
→ No: Continue to step 6
|
||||
|
||||
6. Is Vary header set to *?
|
||||
→ Yes: Change to specific value (e.g., Accept-Encoding)
|
||||
→ No: Continue to step 7
|
||||
|
||||
7. Is this a range request?
|
||||
→ Yes: Range requests bypass Cache Reserve (not supported)
|
||||
→ No: Continue to step 8
|
||||
|
||||
8. Is this an O2O (Orange-to-Orange) request?
|
||||
→ Yes: O2O bypasses Cache Reserve
|
||||
→ No: Continue to step 9
|
||||
|
||||
9. Check Logpush CacheReserveUsed field
|
||||
→ Filter logs to see if assets ever hit Cache Reserve
|
||||
→ Verify cf-cache-status header (should be HIT after first request)
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [README](./README.md) - Overview and core concepts
|
||||
- [Configuration](./configuration.md) - Setup and Cache Rules
|
||||
- [API Reference](./api.md) - Purging and monitoring
|
||||
- [Patterns](./patterns.md) - Best practices and optimization
|
||||
@@ -0,0 +1,197 @@
|
||||
# Cache Reserve Patterns
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Always Enable Tiered Cache
|
||||
|
||||
```typescript
|
||||
// Cache Reserve is designed for use WITH Tiered Cache
|
||||
const configuration = {
|
||||
tieredCache: 'enabled', // Required for optimal performance
|
||||
cacheReserve: 'enabled', // Works best with Tiered Cache
|
||||
|
||||
hierarchy: [
|
||||
'Lower-Tier Cache (visitor)',
|
||||
'Upper-Tier Cache (origin region)',
|
||||
'Cache Reserve (persistent)',
|
||||
'Origin'
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
### 2. Set Appropriate Cache-Control Headers
|
||||
|
||||
```typescript
|
||||
// Origin response headers for Cache Reserve eligibility
|
||||
const originHeaders = {
|
||||
'Cache-Control': 'public, max-age=86400', // 24hr (minimum 10hr)
|
||||
'Content-Length': '1024000', // Required
|
||||
'Cache-Tag': 'images,product-123', // Optional: purging
|
||||
'ETag': '"abc123"', // Optional: revalidation
|
||||
// Avoid: 'Set-Cookie' and 'Vary: *' prevent caching
|
||||
};
|
||||
```
|
||||
|
||||
### 3. Use Cache Rules for Fine-Grained Control
|
||||
|
||||
```typescript
|
||||
// Different TTLs for different content types
|
||||
const cacheRules = [
|
||||
{
|
||||
description: 'Long-term cache for immutable assets',
|
||||
expression: '(http.request.uri.path matches "^/static/.*\\.[a-f0-9]{8}\\.")',
|
||||
action_parameters: {
|
||||
cache_reserve: { eligible: true },
|
||||
edge_ttl: { mode: 'override_origin', default: 2592000 }, // 30 days
|
||||
cache: true
|
||||
}
|
||||
},
|
||||
{
|
||||
description: 'Moderate cache for regular images',
|
||||
expression: '(http.request.uri.path matches "\\.(jpg|png|webp)$")',
|
||||
action_parameters: {
|
||||
cache_reserve: { eligible: true },
|
||||
edge_ttl: { mode: 'override_origin', default: 86400 }, // 24 hours
|
||||
cache: true
|
||||
}
|
||||
},
|
||||
{
|
||||
description: 'Exclude API from Cache Reserve',
|
||||
expression: '(http.request.uri.path matches "^/api/")',
|
||||
action_parameters: { cache_reserve: { eligible: false }, cache: false }
|
||||
}
|
||||
];
|
||||
```
|
||||
|
||||
### 4. Making Assets Cache Reserve Eligible from Workers
|
||||
|
||||
**Note**: This modifies response headers to meet eligibility criteria but does NOT directly control Cache Reserve storage (which is zone-level automatic).
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
const response = await fetch(request);
|
||||
if (!response.ok) return response;
|
||||
|
||||
const headers = new Headers(response.headers);
|
||||
headers.set('Cache-Control', 'public, max-age=36000'); // 10hr minimum
|
||||
headers.delete('Set-Cookie'); // Blocks caching
|
||||
|
||||
// Ensure Content-Length present
|
||||
if (!headers.has('Content-Length')) {
|
||||
const blob = await response.blob();
|
||||
headers.set('Content-Length', blob.size.toString());
|
||||
return new Response(blob, { status: response.status, headers });
|
||||
}
|
||||
|
||||
return new Response(response.body, { status: response.status, headers });
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 5. Hostname Best Practices
|
||||
|
||||
Use Worker's hostname for efficient caching - avoid overriding hostname unnecessarily.
|
||||
|
||||
## Architecture Patterns
|
||||
|
||||
### Multi-Tier Caching + Immutable Assets
|
||||
|
||||
```typescript
|
||||
// Optimal: L1 (visitor) → L2 (region) → L3 (Cache Reserve) → Origin
|
||||
export default {
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
const url = new URL(request.url);
|
||||
const isImmutable = /\.[a-f0-9]{8,}\.(js|css|jpg|png|woff2)$/.test(url.pathname);
|
||||
const response = await fetch(request);
|
||||
|
||||
if (isImmutable) {
|
||||
const headers = new Headers(response.headers);
|
||||
headers.set('Cache-Control', 'public, max-age=31536000, immutable');
|
||||
return new Response(response.body, { status: response.status, headers });
|
||||
}
|
||||
return response;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Cost Optimization
|
||||
|
||||
### Cost Calculator
|
||||
|
||||
```typescript
|
||||
interface CacheReserveEstimate {
|
||||
avgAssetSizeGB: number;
|
||||
uniqueAssets: number;
|
||||
monthlyReads: number;
|
||||
monthlyWrites: number;
|
||||
originEgressCostPerGB: number; // e.g., AWS: $0.09/GB
|
||||
}
|
||||
|
||||
function estimateMonthlyCost(input: CacheReserveEstimate) {
|
||||
// Cache Reserve pricing
|
||||
const storageCostPerGBMonth = 0.015;
|
||||
const classAPerMillion = 4.50; // writes
|
||||
const classBPerMillion = 0.36; // reads
|
||||
|
||||
// Calculate Cache Reserve costs
|
||||
const totalStorageGB = input.avgAssetSizeGB * input.uniqueAssets;
|
||||
const storageCost = totalStorageGB * storageCostPerGBMonth;
|
||||
const writeCost = (input.monthlyWrites / 1_000_000) * classAPerMillion;
|
||||
const readCost = (input.monthlyReads / 1_000_000) * classBPerMillion;
|
||||
|
||||
const cacheReserveCost = storageCost + writeCost + readCost;
|
||||
|
||||
// Calculate origin egress cost (what you'd pay without Cache Reserve)
|
||||
const totalTrafficGB = (input.monthlyReads * input.avgAssetSizeGB);
|
||||
const originEgressCost = totalTrafficGB * input.originEgressCostPerGB;
|
||||
|
||||
// Savings calculation
|
||||
const savings = originEgressCost - cacheReserveCost;
|
||||
const savingsPercent = ((savings / originEgressCost) * 100).toFixed(1);
|
||||
|
||||
return {
|
||||
cacheReserveCost: `$${cacheReserveCost.toFixed(2)}`,
|
||||
originEgressCost: `$${originEgressCost.toFixed(2)}`,
|
||||
monthlySavings: `$${savings.toFixed(2)}`,
|
||||
savingsPercent: `${savingsPercent}%`,
|
||||
breakdown: {
|
||||
storage: `$${storageCost.toFixed(2)}`,
|
||||
writes: `$${writeCost.toFixed(2)}`,
|
||||
reads: `$${readCost.toFixed(2)}`,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Example: Media library
|
||||
const mediaLibrary = estimateMonthlyCost({
|
||||
avgAssetSizeGB: 0.005, // 5MB images
|
||||
uniqueAssets: 10_000,
|
||||
monthlyReads: 5_000_000,
|
||||
monthlyWrites: 50_000,
|
||||
originEgressCostPerGB: 0.09, // AWS S3
|
||||
});
|
||||
|
||||
console.log(mediaLibrary);
|
||||
// {
|
||||
// cacheReserveCost: "$9.98",
|
||||
// originEgressCost: "$25.00",
|
||||
// monthlySavings: "$15.02",
|
||||
// savingsPercent: "60.1%",
|
||||
// breakdown: { storage: "$0.75", writes: "$0.23", reads: "$9.00" }
|
||||
// }
|
||||
```
|
||||
|
||||
### Optimization Guidelines
|
||||
|
||||
- **Set appropriate TTLs**: 10hr minimum, 24hr+ optimal for stable content, 30d max cautiously
|
||||
- **Cache high-value stable assets**: Images, media, fonts, archives, documentation
|
||||
- **Exclude frequently changing**: APIs, user-specific content, real-time data
|
||||
- **Compression note**: Cache Reserve fetches uncompressed from origin, serves compressed to visitors - factor in origin egress costs
|
||||
|
||||
## See Also
|
||||
|
||||
- [README](./README.md) - Overview and core concepts
|
||||
- [Configuration](./configuration.md) - Setup and Cache Rules
|
||||
- [API Reference](./api.md) - Purging and monitoring
|
||||
- [Gotchas](./gotchas.md) - Common issues and troubleshooting
|
||||
@@ -0,0 +1,85 @@
|
||||
# Cloudflare Containers Skill Reference
|
||||
|
||||
**APPLIES TO: Cloudflare Containers ONLY - NOT general Cloudflare Workers**
|
||||
|
||||
Use when working with Cloudflare Containers: deploying containerized apps on Workers platform, configuring container-enabled Durable Objects, managing container lifecycle, or implementing stateful/stateless container patterns.
|
||||
|
||||
## Beta Status
|
||||
|
||||
⚠️ Containers is currently in **beta**. API may change without notice. No SLA guarantees. Custom instance types added Jan 2026.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
**Container as Durable Object:** Each container is a Durable Object with persistent identity. Accessed via `getByName(id)` or `getRandom()`.
|
||||
|
||||
**Image deployment:** Images pre-fetched globally. Deployments use rolling strategy (not instant like Workers).
|
||||
|
||||
**Lifecycle:** cold start (2-3s) → running → `sleepAfter` timeout → stopped. No autoscaling - manual load balancing via `getRandom()`.
|
||||
|
||||
**Persistent identity, ephemeral disk:** Container ID persists, but disk resets on stop. Use Durable Object storage for persistence.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```typescript
|
||||
import { Container } from "@cloudflare/containers";
|
||||
|
||||
export class MyContainer extends Container {
|
||||
defaultPort = 8080;
|
||||
sleepAfter = "30m";
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env) {
|
||||
const container = env.MY_CONTAINER.getByName("instance-1");
|
||||
await container.startAndWaitForPorts();
|
||||
return container.fetch(request);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Reading Order
|
||||
|
||||
| Task | Files |
|
||||
|------|-------|
|
||||
| Setup new container project | README → configuration.md |
|
||||
| Implement container logic | README → api.md → patterns.md |
|
||||
| Choose routing pattern | patterns.md (routing section) |
|
||||
| Debug issues | gotchas.md |
|
||||
| Production hardening | gotchas.md → patterns.md (lifecycle) |
|
||||
|
||||
## Routing Decision Tree
|
||||
|
||||
**How should requests reach containers?**
|
||||
|
||||
- **Same user/session → same container:** Use `getByName(sessionId)` for session affinity
|
||||
- **Stateless, spread load:** Use `getRandom()` for load balancing
|
||||
- **Job per container:** Use `getByName(jobId)` + explicit lifecycle management
|
||||
- **Single global instance:** Use `getByName("singleton")`
|
||||
|
||||
## When to Use Containers vs Workers
|
||||
|
||||
**Use Containers when:**
|
||||
- Need stateful, long-lived processes (sessions, WebSockets, games)
|
||||
- Running existing containerized apps (Node.js, Python, custom binaries)
|
||||
- Need filesystem access or specific system dependencies
|
||||
- Per-user/session isolation with dedicated compute
|
||||
|
||||
**Use Workers when:**
|
||||
- Stateless HTTP handlers
|
||||
- Sub-millisecond cold starts required
|
||||
- Auto-scaling to zero critical
|
||||
- Simple request/response patterns
|
||||
|
||||
## In This Reference
|
||||
|
||||
- **[configuration.md](configuration.md)** - Wrangler config, instance types, Container class properties, environment variables, account limits
|
||||
- **[api.md](api.md)** - Container class API, startup methods, communication (HTTP/TCP/WebSocket), routing helpers, lifecycle hooks, scheduling, state inspection
|
||||
- **[patterns.md](patterns.md)** - Routing patterns (session affinity, load balancing, singleton), WebSocket forwarding, graceful shutdown, Workflow/Queue integration
|
||||
- **[gotchas.md](gotchas.md)** - Critical gotchas (WebSocket, startup methods), common errors with solutions, specific limits, beta caveats
|
||||
|
||||
## See Also
|
||||
|
||||
- [Durable Objects](../durable-objects/) - Containers extend Durable Objects
|
||||
- [Workflows](../workflows/) - Orchestrate container operations
|
||||
- [Queues](../queues/) - Trigger containers from queue messages
|
||||
- [Cloudflare Docs](https://developers.cloudflare.com/containers/)
|
||||
187
.agents/skills/cloudflare-deploy/references/containers/api.md
Normal file
@@ -0,0 +1,187 @@
|
||||
## Container Class API
|
||||
|
||||
```typescript
|
||||
import { Container } from "@cloudflare/containers";
|
||||
|
||||
export class MyContainer extends Container {
|
||||
defaultPort = 8080;
|
||||
requiredPorts = [8080];
|
||||
sleepAfter = "30m";
|
||||
enableInternet = true;
|
||||
pingEndpoint = "/health";
|
||||
envVars = {};
|
||||
entrypoint = [];
|
||||
|
||||
onStart() { /* container started */ }
|
||||
onStop() { /* container stopping */ }
|
||||
onError(error: Error) { /* container error */ }
|
||||
onActivityExpired(): boolean { /* timeout, return true to stay alive */ }
|
||||
async alarm() { /* scheduled task */ }
|
||||
}
|
||||
```
|
||||
|
||||
## Routing
|
||||
|
||||
**getByName(id)** - Named instance for session affinity, per-user state
|
||||
**getRandom()** - Random instance for load balancing stateless services
|
||||
|
||||
```typescript
|
||||
const container = env.MY_CONTAINER.getByName("user-123");
|
||||
const container = env.MY_CONTAINER.getRandom();
|
||||
```
|
||||
|
||||
## Startup Methods
|
||||
|
||||
### start() - Basic start (8s timeout)
|
||||
|
||||
```typescript
|
||||
await container.start();
|
||||
await container.start({ envVars: { KEY: "value" } });
|
||||
```
|
||||
|
||||
Returns when **process starts**, NOT when ports ready. Use for fire-and-forget.
|
||||
|
||||
### startAndWaitForPorts() - Recommended (20s timeout)
|
||||
|
||||
```typescript
|
||||
await container.startAndWaitForPorts(); // Uses requiredPorts
|
||||
await container.startAndWaitForPorts({ ports: [8080, 9090] });
|
||||
await container.startAndWaitForPorts({
|
||||
ports: [8080],
|
||||
startOptions: { envVars: { KEY: "value" } }
|
||||
});
|
||||
```
|
||||
|
||||
Returns when **ports listening**. Use before HTTP/TCP requests.
|
||||
|
||||
**Port resolution:** explicit ports → requiredPorts → defaultPort → port 33
|
||||
|
||||
### waitForPort() - Wait for specific port
|
||||
|
||||
```typescript
|
||||
await container.waitForPort(8080);
|
||||
await container.waitForPort(8080, { timeout: 30000 });
|
||||
```
|
||||
|
||||
## Communication
|
||||
|
||||
### fetch() - HTTP with WebSocket support
|
||||
|
||||
```typescript
|
||||
// ✅ Supports WebSocket upgrades
|
||||
const response = await container.fetch(request);
|
||||
const response = await container.fetch("http://container/api", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ data: "value" })
|
||||
});
|
||||
```
|
||||
|
||||
**Use for:** All HTTP, especially WebSocket.
|
||||
|
||||
### containerFetch() - HTTP only (no WebSocket)
|
||||
|
||||
```typescript
|
||||
// ❌ No WebSocket support
|
||||
const response = await container.containerFetch(request);
|
||||
```
|
||||
|
||||
**⚠️ Critical:** Use `fetch()` for WebSocket, not `containerFetch()`.
|
||||
|
||||
### TCP Connections
|
||||
|
||||
```typescript
|
||||
const port = this.ctx.container.getTcpPort(8080);
|
||||
const conn = port.connect();
|
||||
await conn.opened;
|
||||
|
||||
if (request.body) await request.body.pipeTo(conn.writable);
|
||||
return new Response(conn.readable);
|
||||
```
|
||||
|
||||
### switchPort() - Change default port
|
||||
|
||||
```typescript
|
||||
this.switchPort(8081); // Subsequent fetch() uses this port
|
||||
```
|
||||
|
||||
## Lifecycle Hooks
|
||||
|
||||
### onStart()
|
||||
|
||||
Called when container process starts (ports may not be ready). Runs in `blockConcurrencyWhile` - no concurrent requests.
|
||||
|
||||
```typescript
|
||||
onStart() {
|
||||
console.log("Container starting");
|
||||
}
|
||||
```
|
||||
|
||||
### onStop()
|
||||
|
||||
Called when SIGTERM received. 15 minutes until SIGKILL. Use for graceful shutdown.
|
||||
|
||||
```typescript
|
||||
onStop() {
|
||||
// Save state, close connections, flush logs
|
||||
}
|
||||
```
|
||||
|
||||
### onError()
|
||||
|
||||
Called when container crashes or fails to start.
|
||||
|
||||
```typescript
|
||||
onError(error: Error) {
|
||||
console.error("Container error:", error);
|
||||
}
|
||||
```
|
||||
|
||||
### onActivityExpired()
|
||||
|
||||
Called when `sleepAfter` timeout reached. Return `true` to stay alive, `false` to stop.
|
||||
|
||||
```typescript
|
||||
onActivityExpired(): boolean {
|
||||
if (this.hasActiveConnections()) return true; // Keep alive
|
||||
return false; // OK to stop
|
||||
}
|
||||
```
|
||||
|
||||
## Scheduling
|
||||
|
||||
```typescript
|
||||
export class ScheduledContainer extends Container {
|
||||
async fetch(request: Request) {
|
||||
await this.schedule(Date.now() + 60000); // 1 minute
|
||||
await this.schedule("2026-01-28T00:00:00Z"); // ISO string
|
||||
return new Response("Scheduled");
|
||||
}
|
||||
|
||||
async alarm() {
|
||||
// Called when schedule fires (SQLite-backed, survives restarts)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**⚠️ Don't override `alarm()` directly when using `schedule()` helper.**
|
||||
|
||||
## State Inspection
|
||||
|
||||
### External state check
|
||||
|
||||
```typescript
|
||||
const state = await container.getState();
|
||||
// state.status: "starting" | "running" | "stopping" | "stopped"
|
||||
```
|
||||
|
||||
### Internal state check
|
||||
|
||||
```typescript
|
||||
export class MyContainer extends Container {
|
||||
async fetch(request: Request) {
|
||||
if (this.ctx.container.running) { ... }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**⚠️ Use `getState()` for external checks, `ctx.container.running` for internal.**
|
||||
@@ -0,0 +1,188 @@
|
||||
## Wrangler Configuration
|
||||
|
||||
### Basic Container Config
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"name": "my-worker",
|
||||
"main": "src/index.ts",
|
||||
"compatibility_date": "2026-01-10",
|
||||
"containers": [
|
||||
{
|
||||
"class_name": "MyContainer",
|
||||
"image": "./Dockerfile", // Path to Dockerfile or directory with Dockerfile
|
||||
"instance_type": "standard-1", // Predefined or custom (see below)
|
||||
"max_instances": 10
|
||||
}
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{
|
||||
"name": "MY_CONTAINER",
|
||||
"class_name": "MyContainer"
|
||||
}
|
||||
]
|
||||
},
|
||||
"migrations": [
|
||||
{
|
||||
"tag": "v1",
|
||||
"new_sqlite_classes": ["MyContainer"] // Must use new_sqlite_classes
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Key config requirements:
|
||||
- `image` - Path to Dockerfile or directory containing Dockerfile
|
||||
- `class_name` - Must match Container class export name
|
||||
- `max_instances` - Max concurrent container instances
|
||||
- Must configure Durable Objects binding AND migrations
|
||||
|
||||
### Instance Types
|
||||
|
||||
#### Predefined Types
|
||||
|
||||
| Type | vCPU | Memory | Disk |
|
||||
|------|------|--------|------|
|
||||
| lite | 1/16 | 256 MiB | 2 GB |
|
||||
| basic | 1/4 | 1 GiB | 4 GB |
|
||||
| standard-1 | 1/2 | 4 GiB | 8 GB |
|
||||
| standard-2 | 1 | 6 GiB | 12 GB |
|
||||
| standard-3 | 2 | 8 GiB | 16 GB |
|
||||
| standard-4 | 4 | 12 GiB | 20 GB |
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"containers": [
|
||||
{
|
||||
"class_name": "MyContainer",
|
||||
"image": "./Dockerfile",
|
||||
"instance_type": "standard-2" // Use predefined type
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Custom Types (Jan 2026 Feature)
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"containers": [
|
||||
{
|
||||
"class_name": "MyContainer",
|
||||
"image": "./Dockerfile",
|
||||
"instance_type_custom": {
|
||||
"vcpu": 2, // 1-4 vCPU
|
||||
"memory_mib": 8192, // 512-12288 MiB (up to 12 GiB)
|
||||
"disk_mib": 16384 // 2048-20480 MiB (up to 20 GB)
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Custom type constraints:**
|
||||
- Minimum 3 GiB memory per vCPU
|
||||
- Maximum 2 GB disk per 1 GiB memory
|
||||
- Max 4 vCPU, 12 GiB memory, 20 GB disk per container
|
||||
|
||||
### Account Limits
|
||||
|
||||
| Resource | Limit | Notes |
|
||||
|----------|-------|-------|
|
||||
| Total memory (all containers) | 400 GiB | Across all running containers |
|
||||
| Total vCPU (all containers) | 100 | Across all running containers |
|
||||
| Total disk (all containers) | 2 TB | Across all running containers |
|
||||
| Image storage per account | 50 GB | Stored container images |
|
||||
|
||||
### Container Class Properties
|
||||
|
||||
```typescript
|
||||
import { Container } from "@cloudflare/containers";
|
||||
|
||||
export class MyContainer extends Container {
|
||||
// Port Configuration
|
||||
defaultPort = 8080; // Default port for fetch() calls
|
||||
requiredPorts = [8080, 9090]; // Ports to wait for in startAndWaitForPorts()
|
||||
|
||||
// Lifecycle
|
||||
sleepAfter = "30m"; // Inactivity timeout (5m, 30m, 2h, etc.)
|
||||
|
||||
// Network
|
||||
enableInternet = true; // Allow outbound internet access
|
||||
|
||||
// Health Check
|
||||
pingEndpoint = "/health"; // Health check endpoint path
|
||||
|
||||
// Environment
|
||||
envVars = { // Environment variables passed to container
|
||||
NODE_ENV: "production",
|
||||
LOG_LEVEL: "info"
|
||||
};
|
||||
|
||||
// Startup
|
||||
entrypoint = ["/bin/start.sh"]; // Override image entrypoint (optional)
|
||||
}
|
||||
```
|
||||
|
||||
**Property details:**
|
||||
|
||||
- **`defaultPort`**: Port used when calling `container.fetch()` without explicit port. Falls back to port 33 if not set.
|
||||
|
||||
- **`requiredPorts`**: Array of ports that must be listening before `startAndWaitForPorts()` returns. First port becomes default if `defaultPort` not set.
|
||||
|
||||
- **`sleepAfter`**: Duration string (e.g., "5m", "30m", "2h"). Container stops after this period of inactivity. Timer resets on each request.
|
||||
|
||||
- **`enableInternet`**: Boolean. If `true`, container can make outbound HTTP/TCP requests.
|
||||
|
||||
- **`pingEndpoint`**: Path used for health checks. Should respond with 2xx status.
|
||||
|
||||
- **`envVars`**: Object of environment variables. Merged with runtime-provided vars (see below).
|
||||
|
||||
- **`entrypoint`**: Array of strings. Overrides container image's CMD/ENTRYPOINT.
|
||||
|
||||
### Runtime Environment Variables
|
||||
|
||||
Cloudflare automatically provides these environment variables to containers:
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `CLOUDFLARE_APPLICATION_ID` | Worker application ID |
|
||||
| `CLOUDFLARE_COUNTRY_A2` | Two-letter country code of request origin |
|
||||
| `CLOUDFLARE_LOCATION` | Cloudflare data center location |
|
||||
| `CLOUDFLARE_REGION` | Region identifier |
|
||||
| `CLOUDFLARE_DURABLE_OBJECT_ID` | Container's Durable Object ID |
|
||||
|
||||
Custom `envVars` from Container class are merged with these. Custom vars override runtime vars if names conflict.
|
||||
|
||||
### Image Management
|
||||
|
||||
**Distribution model:** Images pre-fetched to all global locations before deployment. Ensures fast cold starts (2-3s typical).
|
||||
|
||||
**Rolling deploys:** Unlike Workers (instant), container deployments roll out gradually. Old versions continue running during rollout.
|
||||
|
||||
**Ephemeral disk:** Container disk is ephemeral and resets on each stop. Use Durable Object storage (`this.ctx.storage`) for persistence.
|
||||
|
||||
## wrangler.toml Format
|
||||
|
||||
```toml
|
||||
name = "my-worker"
|
||||
main = "src/index.ts"
|
||||
compatibility_date = "2026-01-10"
|
||||
|
||||
[[containers]]
|
||||
class_name = "MyContainer"
|
||||
image = "./Dockerfile"
|
||||
instance_type = "standard-2"
|
||||
max_instances = 10
|
||||
|
||||
[[durable_objects.bindings]]
|
||||
name = "MY_CONTAINER"
|
||||
class_name = "MyContainer"
|
||||
|
||||
[[migrations]]
|
||||
tag = "v1"
|
||||
new_sqlite_classes = ["MyContainer"]
|
||||
```
|
||||
|
||||
Both `wrangler.jsonc` and `wrangler.toml` are supported. Use `wrangler.jsonc` for comments and better IDE support.
|
||||
@@ -0,0 +1,178 @@
|
||||
## Critical Gotchas
|
||||
|
||||
### ⚠️ WebSocket: fetch() vs containerFetch()
|
||||
|
||||
**Problem:** WebSocket connections fail silently
|
||||
|
||||
**Cause:** `containerFetch()` doesn't support WebSocket upgrades
|
||||
|
||||
**Fix:** Always use `fetch()` for WebSocket
|
||||
|
||||
```typescript
|
||||
// ❌ WRONG
|
||||
return container.containerFetch(request);
|
||||
|
||||
// ✅ CORRECT
|
||||
return container.fetch(request);
|
||||
```
|
||||
|
||||
### ⚠️ startAndWaitForPorts() vs start()
|
||||
|
||||
**Problem:** "connection refused" after `start()`
|
||||
|
||||
**Cause:** `start()` returns when process starts, NOT when ports ready
|
||||
|
||||
**Fix:** Use `startAndWaitForPorts()` before requests
|
||||
|
||||
```typescript
|
||||
// ❌ WRONG
|
||||
await container.start();
|
||||
return container.fetch(request);
|
||||
|
||||
// ✅ CORRECT
|
||||
await container.startAndWaitForPorts();
|
||||
return container.fetch(request);
|
||||
```
|
||||
|
||||
### ⚠️ Activity Timeout on Long Operations
|
||||
|
||||
**Problem:** Container stops during long work
|
||||
|
||||
**Cause:** `sleepAfter` based on request activity, not internal work
|
||||
|
||||
**Fix:** Renew timeout by touching storage
|
||||
|
||||
```typescript
|
||||
const interval = setInterval(() => {
|
||||
this.ctx.storage.put("keepalive", Date.now());
|
||||
}, 60000);
|
||||
|
||||
try {
|
||||
await this.doLongWork(data);
|
||||
} finally {
|
||||
clearInterval(interval);
|
||||
}
|
||||
```
|
||||
|
||||
### ⚠️ blockConcurrencyWhile for Startup
|
||||
|
||||
**Problem:** Race conditions during initialization
|
||||
|
||||
**Fix:** Use `blockConcurrencyWhile` for atomic initialization
|
||||
|
||||
```typescript
|
||||
await this.ctx.blockConcurrencyWhile(async () => {
|
||||
if (!this.initialized) {
|
||||
await this.startAndWaitForPorts();
|
||||
this.initialized = true;
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### ⚠️ Lifecycle Hooks Block Requests
|
||||
|
||||
**Problem:** Container unresponsive during `onStart()`
|
||||
|
||||
**Cause:** Hooks run in `blockConcurrencyWhile` - no concurrent requests
|
||||
|
||||
**Fix:** Keep hooks fast, avoid long operations
|
||||
|
||||
### ⚠️ Don't Override alarm() When Using schedule()
|
||||
|
||||
**Problem:** Scheduled tasks don't execute
|
||||
|
||||
**Cause:** `schedule()` uses `alarm()` internally
|
||||
|
||||
**Fix:** Implement `alarm()` to handle scheduled tasks
|
||||
|
||||
## Common Errors
|
||||
|
||||
### "Container start timeout"
|
||||
|
||||
**Cause:** Container took >8s (`start()`) or >20s (`startAndWaitForPorts()`)
|
||||
|
||||
**Solutions:**
|
||||
- Optimize image (smaller base, fewer layers)
|
||||
- Check `entrypoint` correct
|
||||
- Verify app listens on correct ports
|
||||
- Increase timeout if needed
|
||||
|
||||
### "Port not available"
|
||||
|
||||
**Cause:** Calling `fetch()` before port ready
|
||||
|
||||
**Solution:** Use `startAndWaitForPorts()`
|
||||
|
||||
### "Container memory exceeded"
|
||||
|
||||
**Cause:** Using more memory than instance type allows
|
||||
|
||||
**Solutions:**
|
||||
- Use larger instance type (standard-2, standard-3, standard-4)
|
||||
- Optimize app memory usage
|
||||
- Use custom instance type
|
||||
|
||||
```jsonc
|
||||
"instance_type_custom": {
|
||||
"vcpu": 2,
|
||||
"memory_mib": 8192
|
||||
}
|
||||
```
|
||||
|
||||
### "Max instances reached"
|
||||
|
||||
**Cause:** All `max_instances` slots in use
|
||||
|
||||
**Solutions:**
|
||||
- Increase `max_instances`
|
||||
- Implement proper `sleepAfter`
|
||||
- Use `getRandom()` for distribution
|
||||
- Check for instance leaks
|
||||
|
||||
### "No container instance available"
|
||||
|
||||
**Cause:** Account capacity limits reached
|
||||
|
||||
**Solutions:**
|
||||
- Check account limits
|
||||
- Review instance types across containers
|
||||
- Contact Cloudflare support
|
||||
|
||||
## Limits
|
||||
|
||||
| Resource | Limit | Notes |
|
||||
|----------|-------|-------|
|
||||
| Cold start | 2-3s | Image pre-fetched globally |
|
||||
| Graceful shutdown | 15 min | SIGTERM → SIGKILL |
|
||||
| `start()` timeout | 8s | Process start |
|
||||
| `startAndWaitForPorts()` timeout | 20s | Port ready |
|
||||
| Max vCPU per container | 4 | standard-4 or custom |
|
||||
| Max memory per container | 12 GiB | standard-4 or custom |
|
||||
| Max disk per container | 20 GB | Ephemeral, resets |
|
||||
| Account total memory | 400 GiB | All containers |
|
||||
| Account total vCPU | 100 | All containers |
|
||||
| Account total disk | 2 TB | All containers |
|
||||
| Image storage | 50 GB | Per account |
|
||||
| Disk persistence | None | Use DO storage |
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use `startAndWaitForPorts()` by default** - Prevents port errors
|
||||
2. **Set appropriate `sleepAfter`** - Balance resources vs cold starts
|
||||
3. **Use `fetch()` for WebSocket** - Not `containerFetch()`
|
||||
4. **Design for restarts** - Ephemeral disk, implement graceful shutdown
|
||||
5. **Monitor resources** - Stay within account limits
|
||||
6. **Keep hooks fast** - Run in `blockConcurrencyWhile`
|
||||
7. **Renew activity for long ops** - Touch storage to prevent timeout
|
||||
|
||||
## Beta Caveats
|
||||
|
||||
⚠️ Containers in **beta**:
|
||||
|
||||
- **API may change** without notice
|
||||
- **No SLA** guarantees
|
||||
- **Limited regions** initially
|
||||
- **No autoscaling** - manual via `getRandom()`
|
||||
- **Rolling deploys** only (not instant like Workers)
|
||||
|
||||
Plan for API changes, test thoroughly before production.
|
||||
@@ -0,0 +1,202 @@
|
||||
## Routing Patterns
|
||||
|
||||
### Session Affinity (Stateful)
|
||||
|
||||
```typescript
|
||||
export class SessionBackend extends Container {
|
||||
defaultPort = 3000;
|
||||
sleepAfter = "30m";
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env) {
|
||||
const sessionId = request.headers.get("X-Session-ID") || crypto.randomUUID();
|
||||
const container = env.SESSION_BACKEND.getByName(sessionId);
|
||||
await container.startAndWaitForPorts();
|
||||
return container.fetch(request);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Use:** User sessions, WebSocket, stateful games, per-user caching.
|
||||
|
||||
### Load Balancing (Stateless)
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
async fetch(request: Request, env: Env) {
|
||||
const container = env.STATELESS_API.getRandom();
|
||||
await container.startAndWaitForPorts();
|
||||
return container.fetch(request);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Use:** Stateless HTTP APIs, CPU-intensive work, read-only queries.
|
||||
|
||||
### Singleton Pattern
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
async fetch(request: Request, env: Env) {
|
||||
const container = env.GLOBAL_SERVICE.getByName("singleton");
|
||||
await container.startAndWaitForPorts();
|
||||
return container.fetch(request);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Use:** Global cache, centralized coordinator, single source of truth.
|
||||
|
||||
## WebSocket Forwarding
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
async fetch(request: Request, env: Env) {
|
||||
if (request.headers.get("Upgrade") === "websocket") {
|
||||
const sessionId = request.headers.get("X-Session-ID") || crypto.randomUUID();
|
||||
const container = env.WS_BACKEND.getByName(sessionId);
|
||||
await container.startAndWaitForPorts();
|
||||
|
||||
// ⚠️ MUST use fetch(), not containerFetch()
|
||||
return container.fetch(request);
|
||||
}
|
||||
return new Response("Not a WebSocket request", { status: 400 });
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**⚠️ Critical:** Always use `fetch()` for WebSocket.
|
||||
|
||||
## Graceful Shutdown
|
||||
|
||||
```typescript
|
||||
export class GracefulContainer extends Container {
|
||||
private connections = new Set<WebSocket>();
|
||||
|
||||
onStop() {
|
||||
// SIGTERM received, 15 minutes until SIGKILL
|
||||
for (const ws of this.connections) {
|
||||
ws.close(1001, "Server shutting down");
|
||||
}
|
||||
this.ctx.storage.put("shutdown-time", Date.now());
|
||||
}
|
||||
|
||||
onActivityExpired(): boolean {
|
||||
return this.connections.size > 0; // Keep alive if connections
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Concurrent Request Handling
|
||||
|
||||
```typescript
|
||||
export class SafeContainer extends Container {
|
||||
private initialized = false;
|
||||
|
||||
async fetch(request: Request) {
|
||||
await this.ctx.blockConcurrencyWhile(async () => {
|
||||
if (!this.initialized) {
|
||||
await this.startAndWaitForPorts();
|
||||
this.initialized = true;
|
||||
}
|
||||
});
|
||||
return super.fetch(request);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Use:** One-time initialization, preventing concurrent startup.
|
||||
|
||||
## Activity Timeout Renewal
|
||||
|
||||
```typescript
|
||||
export class LongRunningContainer extends Container {
|
||||
sleepAfter = "5m";
|
||||
|
||||
async processLongJob(data: unknown) {
|
||||
const interval = setInterval(() => {
|
||||
this.ctx.storage.put("keepalive", Date.now());
|
||||
}, 60000);
|
||||
|
||||
try {
|
||||
await this.doLongWork(data);
|
||||
} finally {
|
||||
clearInterval(interval);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Use:** Long operations exceeding `sleepAfter`.
|
||||
|
||||
## Multiple Port Routing
|
||||
|
||||
```typescript
|
||||
export class MultiPortContainer extends Container {
|
||||
requiredPorts = [8080, 8081, 9090];
|
||||
|
||||
async fetch(request: Request) {
|
||||
const path = new URL(request.url).pathname;
|
||||
if (path.startsWith("/grpc")) this.switchPort(8081);
|
||||
else if (path.startsWith("/metrics")) this.switchPort(9090);
|
||||
return super.fetch(request);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Use:** Multi-protocol services (HTTP + gRPC), separate metrics endpoints.
|
||||
|
||||
## Workflow Integration
|
||||
|
||||
```typescript
|
||||
import { WorkflowEntrypoint } from "cloudflare:workers";
|
||||
|
||||
export class ProcessingWorkflow extends WorkflowEntrypoint {
|
||||
async run(event, step) {
|
||||
const container = this.env.PROCESSOR.getByName(event.payload.jobId);
|
||||
|
||||
await step.do("start", async () => {
|
||||
await container.startAndWaitForPorts();
|
||||
});
|
||||
|
||||
const result = await step.do("process", async () => {
|
||||
return container.fetch("/process", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(event.payload.data)
|
||||
}).then(r => r.json());
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Use:** Orchestrating multi-step container operations, durable execution.
|
||||
|
||||
## Queue Consumer Integration
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
async queue(batch, env) {
|
||||
for (const msg of batch.messages) {
|
||||
try {
|
||||
const container = env.PROCESSOR.getByName(msg.body.jobId);
|
||||
await container.startAndWaitForPorts();
|
||||
|
||||
const response = await container.fetch("/process", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(msg.body)
|
||||
});
|
||||
|
||||
response.ok ? msg.ack() : msg.retry();
|
||||
} catch (err) {
|
||||
console.error("Queue processing error:", err);
|
||||
msg.retry();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Use:** Asynchronous job processing, batch operations, event-driven execution.
|
||||
@@ -0,0 +1,99 @@
|
||||
# Cloudflare Cron Triggers
|
||||
|
||||
Schedule Workers execution using cron expressions. Runs on Cloudflare's global network during underutilized periods.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **UTC-only execution** - All schedules run on UTC time
|
||||
- **5-field cron syntax** - Quartz scheduler extensions (L, W, #)
|
||||
- **Global propagation** - 15min deployment delay
|
||||
- **At-least-once delivery** - Rare duplicate executions possible
|
||||
- **Workflow integration** - Trigger long-running multi-step tasks
|
||||
- **Green Compute** - Optional carbon-aware scheduling during low-carbon periods
|
||||
|
||||
## Cron Syntax
|
||||
|
||||
```
|
||||
┌─────────── minute (0-59)
|
||||
│ ┌───────── hour (0-23)
|
||||
│ │ ┌─────── day of month (1-31)
|
||||
│ │ │ ┌───── month (1-12, JAN-DEC)
|
||||
│ │ │ │ ┌─── day of week (1-7, SUN-SAT, 1=Sunday)
|
||||
* * * * *
|
||||
```
|
||||
|
||||
**Special chars:** `*` (any), `,` (list), `-` (range), `/` (step), `L` (last), `W` (weekday), `#` (nth)
|
||||
|
||||
## Common Schedules
|
||||
|
||||
```bash
|
||||
*/5 * * * * # Every 5 minutes
|
||||
0 * * * * # Hourly
|
||||
0 2 * * * # Daily 2am UTC (off-peak)
|
||||
0 9 * * MON-FRI # Weekdays 9am UTC
|
||||
0 0 1 * * # Monthly 1st midnight UTC
|
||||
0 9 L * * # Last day of month 9am UTC
|
||||
0 10 * * MON#2 # 2nd Monday 10am UTC
|
||||
*/10 9-17 * * MON-FRI # Every 10min, 9am-5pm weekdays
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
**wrangler.jsonc:**
|
||||
```jsonc
|
||||
{
|
||||
"name": "my-cron-worker",
|
||||
"triggers": {
|
||||
"crons": ["*/5 * * * *", "0 2 * * *"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Handler:**
|
||||
```typescript
|
||||
export default {
|
||||
async scheduled(
|
||||
controller: ScheduledController,
|
||||
env: Env,
|
||||
ctx: ExecutionContext,
|
||||
): Promise<void> {
|
||||
console.log("Cron:", controller.cron);
|
||||
console.log("Time:", new Date(controller.scheduledTime));
|
||||
|
||||
ctx.waitUntil(asyncTask(env)); // Non-blocking
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
**Test locally:**
|
||||
```bash
|
||||
npx wrangler dev
|
||||
curl "http://localhost:8787/__scheduled?cron=*/5+*+*+*+*"
|
||||
```
|
||||
|
||||
## Limits
|
||||
|
||||
- **Free:** 3 triggers/worker, 10ms CPU
|
||||
- **Paid:** Unlimited triggers, 50ms CPU
|
||||
- **Propagation:** 15min global deployment
|
||||
- **Timezone:** UTC only
|
||||
|
||||
## Reading Order
|
||||
|
||||
**New to cron triggers?** Start here:
|
||||
1. This README - Overview and quick start
|
||||
2. [configuration.md](./configuration.md) - Set up your first cron trigger
|
||||
3. [api.md](./api.md) - Understand the handler API
|
||||
4. [patterns.md](./patterns.md) - Common use cases and examples
|
||||
|
||||
**Troubleshooting?** Jump to [gotchas.md](./gotchas.md)
|
||||
|
||||
## In This Reference
|
||||
- [configuration.md](./configuration.md) - wrangler config, env-specific schedules, Green Compute
|
||||
- [api.md](./api.md) - ScheduledController, noRetry(), waitUntil, testing patterns
|
||||
- [patterns.md](./patterns.md) - Use cases, monitoring, queue integration, Durable Objects
|
||||
- [gotchas.md](./gotchas.md) - Timezone issues, idempotency, security, testing
|
||||
|
||||
## See Also
|
||||
- [workflows](../workflows/) - Alternative for long-running scheduled tasks
|
||||
- [workers](../workers/) - Worker runtime documentation
|
||||
196
.agents/skills/cloudflare-deploy/references/cron-triggers/api.md
Normal file
@@ -0,0 +1,196 @@
|
||||
# Cron Triggers API
|
||||
|
||||
## Basic Handler
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
async scheduled(controller: ScheduledController, env: Env, ctx: ExecutionContext): Promise<void> {
|
||||
console.log("Cron executed:", new Date(controller.scheduledTime));
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
**JavaScript:** Same signature without types
|
||||
**Python:** `class Default(WorkerEntrypoint): async def scheduled(self, controller, env, ctx)`
|
||||
|
||||
## ScheduledController
|
||||
|
||||
```typescript
|
||||
interface ScheduledController {
|
||||
scheduledTime: number; // Unix ms when scheduled to run
|
||||
cron: string; // Expression that triggered (e.g., "*/5 * * * *")
|
||||
type: string; // Always "scheduled"
|
||||
noRetry(): void; // Prevent automatic retry on failure
|
||||
}
|
||||
```
|
||||
|
||||
**Prevent retry on failure:**
|
||||
```typescript
|
||||
export default {
|
||||
async scheduled(controller, env, ctx) {
|
||||
try {
|
||||
await riskyOperation(env);
|
||||
} catch (error) {
|
||||
// Don't retry - failure is expected/acceptable
|
||||
controller.noRetry();
|
||||
console.error("Operation failed, not retrying:", error);
|
||||
}
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
**When to use noRetry():**
|
||||
- External API failures outside your control (avoid hammering failed services)
|
||||
- Rate limit errors (retry would fail again immediately)
|
||||
- Duplicate execution detected (idempotency check failed)
|
||||
- Non-critical operations where skip is acceptable (analytics, caching)
|
||||
- Validation errors that won't resolve on retry
|
||||
|
||||
## Handler Parameters
|
||||
|
||||
**`controller: ScheduledController`**
|
||||
- Access cron expression and scheduled time
|
||||
|
||||
**`env: Env`**
|
||||
- All bindings: KV, R2, D1, secrets, service bindings
|
||||
|
||||
**`ctx: ExecutionContext`**
|
||||
- `ctx.waitUntil(promise)` - Extend execution for async tasks (logging, cleanup, external APIs)
|
||||
- First `waitUntil` failure recorded in Cron Events
|
||||
|
||||
## Multiple Schedules
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
async scheduled(controller, env, ctx) {
|
||||
switch (controller.cron) {
|
||||
case "*/3 * * * *": ctx.waitUntil(updateRecentData(env)); break;
|
||||
case "0 * * * *": ctx.waitUntil(processHourlyAggregation(env)); break;
|
||||
case "0 2 * * *": ctx.waitUntil(performDailyMaintenance(env)); break;
|
||||
default: console.warn(`Unhandled: ${controller.cron}`);
|
||||
}
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## ctx.waitUntil Usage
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
async scheduled(controller, env, ctx) {
|
||||
const data = await fetchCriticalData(); // Critical path
|
||||
|
||||
// Non-blocking background tasks
|
||||
ctx.waitUntil(Promise.all([
|
||||
logToAnalytics(data),
|
||||
cleanupOldRecords(env.DB),
|
||||
notifyWebhook(env.WEBHOOK_URL, data),
|
||||
]));
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Workflow Integration
|
||||
|
||||
```typescript
|
||||
import { WorkflowEntrypoint } from "cloudflare:workers";
|
||||
|
||||
export class DataProcessingWorkflow extends WorkflowEntrypoint {
|
||||
async run(event, step) {
|
||||
const data = await step.do("fetch-data", () => fetchLargeDataset());
|
||||
const processed = await step.do("process-data", () => processDataset(data));
|
||||
await step.do("store-results", () => storeResults(processed));
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
async scheduled(controller, env, ctx) {
|
||||
const instance = await env.MY_WORKFLOW.create({
|
||||
params: { scheduledTime: controller.scheduledTime, cron: controller.cron },
|
||||
});
|
||||
console.log(`Started workflow: ${instance.id}`);
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Testing Handler
|
||||
|
||||
**Local development (/__scheduled endpoint):**
|
||||
```bash
|
||||
# Start dev server
|
||||
npx wrangler dev
|
||||
|
||||
# Trigger any cron
|
||||
curl "http://localhost:8787/__scheduled?cron=*/5+*+*+*+*"
|
||||
|
||||
# Trigger specific cron with custom time
|
||||
curl "http://localhost:8787/__scheduled?cron=0+2+*+*+*&scheduledTime=1704067200000"
|
||||
```
|
||||
|
||||
**Query parameters:**
|
||||
- `cron` - Required. URL-encoded cron expression (use `+` for spaces)
|
||||
- `scheduledTime` - Optional. Unix timestamp in milliseconds (defaults to current time)
|
||||
|
||||
**Production security:** The `/__scheduled` endpoint is available in production and can be triggered by anyone. Block it or implement authentication - see [gotchas.md](./gotchas.md#security-concerns)
|
||||
|
||||
**Unit testing (Vitest):**
|
||||
```typescript
|
||||
// test/scheduled.test.ts
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { env } from "cloudflare:test";
|
||||
import worker from "../src/index";
|
||||
|
||||
describe("Scheduled Handler", () => {
|
||||
it("processes scheduled event", async () => {
|
||||
const controller = { scheduledTime: Date.now(), cron: "*/5 * * * *", type: "scheduled" as const, noRetry: () => {} };
|
||||
const ctx = { waitUntil: (p: Promise<any>) => p, passThroughOnException: () => {} };
|
||||
await worker.scheduled(controller, env, ctx);
|
||||
expect(await env.MY_KV.get("last_run")).toBeDefined();
|
||||
});
|
||||
|
||||
it("handles multiple crons", async () => {
|
||||
const ctx = { waitUntil: () => {}, passThroughOnException: () => {} };
|
||||
await worker.scheduled({ scheduledTime: Date.now(), cron: "*/5 * * * *", type: "scheduled", noRetry: () => {} }, env, ctx);
|
||||
expect(await env.MY_KV.get("last_type")).toBe("frequent");
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
**Automatic retries:**
|
||||
- Failed cron executions are retried automatically unless `noRetry()` is called
|
||||
- Retry happens after a delay (typically minutes)
|
||||
- Only first `waitUntil()` failure is recorded in Cron Events
|
||||
|
||||
**Best practices:**
|
||||
```typescript
|
||||
export default {
|
||||
async scheduled(controller, env, ctx) {
|
||||
try {
|
||||
await criticalOperation(env);
|
||||
} catch (error) {
|
||||
// Log error details
|
||||
console.error("Cron failed:", {
|
||||
cron: controller.cron,
|
||||
scheduledTime: controller.scheduledTime,
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
});
|
||||
|
||||
// Decide: retry or skip
|
||||
if (error.message.includes("rate limit")) {
|
||||
controller.noRetry(); // Skip retry for rate limits
|
||||
}
|
||||
// Otherwise allow automatic retry
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [README.md](./README.md) - Overview
|
||||
- [patterns.md](./patterns.md) - Use cases, examples
|
||||
- [gotchas.md](./gotchas.md) - Common errors, testing issues
|
||||
@@ -0,0 +1,180 @@
|
||||
# Cron Triggers Configuration
|
||||
|
||||
## wrangler.jsonc
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"$schema": "./node_modules/wrangler/config-schema.json",
|
||||
"name": "my-cron-worker",
|
||||
"main": "src/index.ts",
|
||||
"compatibility_date": "2025-01-01", // Use current date for new projects
|
||||
|
||||
"triggers": {
|
||||
"crons": [
|
||||
"*/5 * * * *", // Every 5 minutes
|
||||
"0 */2 * * *", // Every 2 hours
|
||||
"0 9 * * MON-FRI", // Weekdays at 9am UTC
|
||||
"0 2 1 * *" // Monthly on 1st at 2am UTC
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Green Compute (Beta)
|
||||
|
||||
Schedule crons during low-carbon periods for carbon-aware execution:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"name": "eco-cron-worker",
|
||||
"triggers": {
|
||||
"crons": ["0 2 * * *"]
|
||||
},
|
||||
"placement": {
|
||||
"mode": "smart" // Runs during low-carbon periods
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Modes:**
|
||||
- `"smart"` - Carbon-aware scheduling (may delay up to 24h for optimal window)
|
||||
- Default (no placement config) - Standard scheduling (no delay)
|
||||
|
||||
**How it works:**
|
||||
- Cloudflare delays execution until grid carbon intensity is lower
|
||||
- Maximum delay: 24 hours from scheduled time
|
||||
- Ideal for batch jobs with flexible timing requirements
|
||||
|
||||
**Use cases:**
|
||||
- Nightly data processing and ETL pipelines
|
||||
- Weekly/monthly report generation
|
||||
- Database backups and maintenance
|
||||
- Analytics aggregation
|
||||
- ML model training
|
||||
|
||||
**Not suitable for:**
|
||||
- Time-sensitive operations (SLA requirements)
|
||||
- User-facing features requiring immediate execution
|
||||
- Real-time monitoring and alerting
|
||||
- Compliance tasks with strict time windows
|
||||
|
||||
## Environment-Specific Schedules
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"name": "my-cron-worker",
|
||||
"triggers": {
|
||||
"crons": ["0 */6 * * *"] // Prod: every 6 hours
|
||||
},
|
||||
"env": {
|
||||
"staging": {
|
||||
"triggers": {
|
||||
"crons": ["*/15 * * * *"] // Staging: every 15min
|
||||
}
|
||||
},
|
||||
"dev": {
|
||||
"triggers": {
|
||||
"crons": ["*/5 * * * *"] // Dev: every 5min
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Schedule Format
|
||||
|
||||
**Structure:** `minute hour day-of-month month day-of-week`
|
||||
|
||||
**Special chars:** `*` (any), `,` (list), `-` (range), `/` (step), `L` (last), `W` (weekday), `#` (nth)
|
||||
|
||||
## Managing Triggers
|
||||
|
||||
**Remove all:** `"triggers": { "crons": [] }`
|
||||
**Preserve existing:** Omit `"triggers"` field entirely
|
||||
|
||||
## Deployment
|
||||
|
||||
```bash
|
||||
# Deploy with config crons
|
||||
npx wrangler deploy
|
||||
|
||||
# Deploy specific environment
|
||||
npx wrangler deploy --env production
|
||||
|
||||
# View deployments
|
||||
npx wrangler deployments list
|
||||
```
|
||||
|
||||
**⚠️ Changes take up to 15 minutes to propagate globally**
|
||||
|
||||
## API Management
|
||||
|
||||
**Get triggers:**
|
||||
```bash
|
||||
curl "https://api.cloudflare.com/client/v4/accounts/{account_id}/workers/scripts/{script_name}/schedules" \
|
||||
-H "Authorization: Bearer {api_token}"
|
||||
```
|
||||
|
||||
**Update triggers:**
|
||||
```bash
|
||||
curl -X PUT "https://api.cloudflare.com/client/v4/accounts/{account_id}/workers/scripts/{script_name}/schedules" \
|
||||
-H "Authorization: Bearer {api_token}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"crons": ["*/5 * * * *", "0 2 * * *"]}'
|
||||
```
|
||||
|
||||
**Delete all:**
|
||||
```bash
|
||||
curl -X PUT "https://api.cloudflare.com/client/v4/accounts/{account_id}/workers/scripts/{script_name}/schedules" \
|
||||
-H "Authorization: Bearer {api_token}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"crons": []}'
|
||||
```
|
||||
|
||||
## Combining Multiple Workers
|
||||
|
||||
For complex schedules, use multiple workers:
|
||||
|
||||
```jsonc
|
||||
// worker-frequent.jsonc
|
||||
{
|
||||
"name": "data-sync-frequent",
|
||||
"triggers": { "crons": ["*/5 * * * *"] }
|
||||
}
|
||||
|
||||
// worker-daily.jsonc
|
||||
{
|
||||
"name": "reports-daily",
|
||||
"triggers": { "crons": ["0 2 * * *"] },
|
||||
"placement": { "mode": "smart" }
|
||||
}
|
||||
|
||||
// worker-weekly.jsonc
|
||||
{
|
||||
"name": "cleanup-weekly",
|
||||
"triggers": { "crons": ["0 3 * * SUN"] }
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Separate CPU limits per worker
|
||||
- Independent error isolation
|
||||
- Different Green Compute policies
|
||||
- Easier to maintain and debug
|
||||
|
||||
## Validation
|
||||
|
||||
**Test cron syntax:**
|
||||
- [crontab.guru](https://crontab.guru/) - Interactive validator
|
||||
- Wrangler validates on deploy but won't catch logic errors
|
||||
|
||||
**Common mistakes:**
|
||||
- `0 0 * * *` runs daily at midnight UTC, not your local timezone
|
||||
- `*/60 * * * *` is invalid (use `0 * * * *` for hourly)
|
||||
- `0 2 31 * *` only runs on months with 31 days
|
||||
|
||||
## See Also
|
||||
|
||||
- [README.md](./README.md) - Overview, quick start
|
||||
- [api.md](./api.md) - Handler implementation
|
||||
- [patterns.md](./patterns.md) - Multi-cron routing examples
|
||||