---
url: /docs/agentbuff-stack/execution-security.md
---
# 执行限制与验证

**当前状态：I3 与 I4 保护已接入，本地验收进行中。** 文件与个人任务接口已接入服务端原子限制，上传 / 任务提交 / 手动重试支持可选 Turnstile。收费与用户 API 密钥已复用业务限额；密钥自己的插件限额及版本接口见[教程](./api-keys)。真实验证码与云端负载仍待独立验收。

## 限制在哪里执行

文件写入和读取分别使用 `storage.uploadsPerMinute`、`storage.readsPerMinute`。服务端按当前登录账号和操作，在 PostgreSQL 用同一条件更新原子计数；通过账号与入口校验后，受限步骤的失败请求也计入。每分钟窗口根据数据库时间开始，拒绝后返回稳定的 `RATE_LIMITED`。HTTP 文件入口带 `Retry-After: 60`，tRPC 错误含 `executionError` 和 `retryAfterSeconds`。

限制在服务端执行，修改前端按钮或直接请求不会绕过。tRPC 批量请求的每个过程分别计数。当前策略是固定窗口，并不代表滚动窗口、全站并发上限或模型成本预算。独立站使用各自数据库，不能共享计数表。文件配额另用原子预留，不能用请求限流代替字节 / 数量占用。

当前个人文件与任务拒绝匿名会话；公开 JSON / 文本工具在浏览器本地处理，不调用收费接口。认证继续使用 Better Auth 自身限制。任务使用 `tasks.writesPerMinute` / `readsPerMinute` 和独立操作键，提交 / 重试 / 取消与读取分别计数；来源必须匹配 `APP_ORIGIN`，组织切换不扩大所有权。取消不要求验证码，便于用户停止等待。访客服务端入口及外部 API 限制随实际功能落实。

## 开启 Turnstile

1. 在 Cloudflare 创建属于该站和环境的 widget，配置实际允许的域名。
2. 在 API 环境填入 `PUBLIC_TURNSTILE_SITE_KEY` 和 `TURNSTILE_SECRET_KEY`。前者可公开，后者只保留在服务端，不提交到 Git。
3. 设置 `websiteConfig.security.turnstile.enabled = true`，执行 `bun config:check`，再一起构建前后端。

开启但缺少配置会使配置检查 / API 初始化失败，不静默变成未保护模式。两个值需成对配置；Cloudflare 官方测试键仅允许 development，staging / production 拒绝这些已知测试键。生成站不复制源站凭据，需要单独配置。

文件列表 API 只返回公开 site key 和动作 `file_upload`。工作台按此配置显式加载官方 SDK，关闭时不插入验证脚本。组件跟随浅深主题，使用紧凑尺寸，避免窄屏表单被 iframe 的最小宽度撑开；加载失败、验证失败或过期会清空 token 并提示重试。路由退出 / 重新验证时移除旧 widget，迟到回调不能重新激活旧 token。

上传把 token 放入 `X-Turnstile-Token` 请求头，原始文件仍为请求体。服务端先确认会话、本站来源与操作频率，再调用固定 Siteverify 地址，最后才读取文件并预留存储。只有成功且 `action === file_upload`、`hostname === new URL(APP_ORIGIN).hostname` 的主机名匹配时继续。客户端传来的域名或动作不决定服务端期望值。

任务提交和手动重试将 token 放在独立的 `verificationToken` 字段，分别要求 `task_submit` / `task_retry` 动作。服务端验证后才受理或开启新的尝试，token 不保存在请求指纹与数据库。页面复用同一 SDK 生命周期和错误恢复，公开接口只返回 site key 与期望动作。验证失败、错误域名 / 动作不会创建任务；批量请求的每项均验证并计数。

验证请求最长等待 10 秒；网络错误、服务异常、非成功 HTTP 响应或非法响应结构都拒绝操作。token 使用后不可再用，所以每次提交结束都会清空并重新生成；不自动重传上传文件，也不自动重试结果不确定的验证。失去上传响应时先刷新文件列表确认是否已保存。日志不记录 token、secret、provider 响应或文件内容。

Siteverify 请求使用 `redirect: "manual"`，仅接受成功 HTTP 响应，3xx 不跟随、不继续转发服务端 secret 或用户 token。2026-10-11 的实际 workerd 验收确认，原 `error` 模式在当前运行器中会于请求发送前失败；修复后正常请求、外站 Location 拒绝和十秒期限均通过。Cloudflare 文档仍列出三种重定向值，因此该结论限定于本项目已安装运行器的实际行为，不推断所有云端版本均不支持 `error`。

| 错误代码 | 行为 / 排查 |
| --- | --- |
| `RATE_LIMITED` | 等待窗口结束，再提交；不要连续自动调用 |
| `VERIFICATION_REQUIRED` | token 缺失或超长，先重新验证 |
| `VERIFICATION_FAILED` | 验证失败、过期 / 重放，或动作 / 域名不匹配 |
| `VERIFICATION_UNAVAILABLE` | 验证服务故障，保留输入并稍后重试；不会继续写存储 |
| `VERIFICATION_NOT_CONFIGURED` | 检查开关、公开 site key 与服务端 secret |

目前工作台 HTML 尚未设置 CSP。若后续增加，需按 Cloudflare 官方说明允许 widget 所需的脚本与 iframe 来源；文件下载的独立限制响应头保持原有策略。

## 验证口径与尚未完成项

服务模块测试控制 Siteverify 响应，覆盖成功、缺失配置、token 过长、错误动作 / 域名、重复 / 过期、网络失败、异常状态与响应结构。组件测试使用受控 SDK 回调，检查脚本失败重试、过期清空、卸载及旧回调失效，不下载第三方脚本或完成真实验证码。

实际 Hono 文件路由、Better Auth 会话、真实迁移的 PGlite 和本地 R2 共同验证：未通过验证时文件与配额表为空；成功后存储一份文件；公开列表不泄露 secret。真实 tRPC 三项批量读取、上限为二时，只有两项成功，第三项有 429 业务错误和重试提示，数据库计数为二。这些是本地集成证据，不代表真实 Cloudflare widget 验收或线上负载表现。

任务路由测试另验证缺 token、上传动作误用于任务、提交动作通过后只创建一个任务，以及真正 tRPC HTTP 批量读取的逐过程限制与重试字段。主预览默认保持验证关闭，实际任务操作可以检查且未加载第三方验证码脚本。真实 widget 的前端 / 服务端匹配、上线域名、网络错误下体验、真实验证码手机尺寸及负载成本仍需独立验收；收费与 API 密钥路径保留原计划要求。

```bash
bun files:validate
bun files:validate --turnstile
```

两个命令均使用一次性 loopback PostgreSQL、生产 workerd 和本地 R2，不给主库迁移或写入。第二条在自己的配置文件开启校验，使用受控 Siteverify 接口，不连接 Cloudflare、不下载 widget 或完成真实验证码。它核对缺失 / 超长 token、错误域名 / 动作、重放、非法响应、503、带外站 Location 的 302 以及实际十秒超时；拒绝后文件 / 配额表和 R2 都为空，正确响应只写一份文件，随后仍执行原上传 / 私有读取 / 删除 / 清理验收。额外探测消耗真实业务限额，程序只调整一次性账号的窗口以继续验证，没有提高产品限额。最终清理自己资源并核对正常网站配置字节未变。

文件入口的原生证据与共享校验函数五种动作的单元测试分别记录，不将这次检查扩大为真实任务 / 收费 / 营销 widget 或云端验收。

参考官方文档（服务端 / 请求重定向于 2026-10-11 复核）：[服务端校验](https://developers.cloudflare.com/turnstile/get-started/server-side-validation/)、[Worker Request 与重定向](https://developers.cloudflare.com/workers/runtime-apis/request/)、[显式组件渲染](https://developers.cloudflare.com/turnstile/get-started/client-side-rendering/)、[测试键与真实验证的区别](https://developers.cloudflare.com/turnstile/troubleshooting/testing/)。
