- 人工智能
- AI 应用
- AI Agent
- Agent 沙箱
- AI 安全治理
【免费下载链接】cloudflare-os
Agent workspace built on Cloudflare Workers for creating documents, building apps, and running agents with your company’s context and systems.
本篇技术指南以 packages/gatekeeper-slack/README.md 为骨架,深入讲解 Cloudflare OS 中Slack gatekeeper的设计与实战:它如何以独立的 Cloudflare Worker 运行、通过 OAuth 2.0 用户令牌(xoxp-…)为 Agent 提供对 Slack 工作区(频道、私信、线程、成员、搜索)的只读访问,以及如何按工作区 / 会话 / 线程三种粒度授权与路由。读完本文,你将掌握 Slack 应用的 OAuth 配置要点、三种资源粒度的作用域映射、会话 API 的使用方法,以及底层令牌轮换、越权防护与观察者校验的实现原理。
一、定位:一个只读的 Slack 访问中介
Slack gatekeeper 是 Cloudflare OS 中众多 gatekeeper 之一,职责是"中介"(mediate)一个 Gadget 对用户 Slack 工作区的访问。它的三个核心设计约束是:
- 只读:绝不发送或修改 Slack 数据,源码中以
unreachableAction()强制实现(见 slack.ts),三个 gatekeeper 的applyAction/rejectAction/revertAction均不可达; - 独立 Worker:作为自己的 Cloudflare Worker 运行,由后端通过
GATEKEEPER_SLACK绑定自动发现; - 用户视角:使用用户令牌(user token,
xoxp-…)而非机器人令牌(bot token),因此 Agent 看到的正是连接用户所能看到的内容——包括私有频道、私信与搜索。
从源码结构看,该包由slack.ts(Worker 入口、OAuth 流程、Durable Object、gatekeeper 实现)、slack-api.ts(Slack Web API 客户端)、types.d.ts(Agent 面向的会话 API 类型)与configurator/(三种授权配置 UI)组成,依赖@gadgets/gatekeeper-kit(连接握手、凭据暂存)、@gadgets/workshop-shared/gatekeeper(gatekeeper 框架契约)与@gadgets/configurator-ui(配置器 UI 组件)。
二、认证:OAuth 2.0 用户令牌与 Slack 应用配置
2.1 为什么用 user token
README 明确指出,认证采用 OAuth 2.0,通过user_scope请求用户令牌而非机器人令牌。这样 Agent 的访问边界与连接用户完全一致——包括私有频道、直接消息与全局搜索。这一选择也贯穿到令牌交换的实现:exchangeAuthCode 在换取授权码时从响应嵌套的authed_user中提取用户令牌、刷新令牌、授权 scope、用户 ID 与团队 ID,并在缺少authed_user.access_token时抛出"请确保应用请求 user scope(user_scope)"的明确错误。
2.2 创建 Slack 应用并注入凭据
在 Slack 应用管理后台(https://api.slack.com/apps)创建应用后,将客户端凭据注入 Worker 环境变量:
| 变量 | 用途 |
|---|---|
CLIENT_ID | Slack OAuth 应用客户端 ID(生产环境通过 wrangler secrets /.dev.vars注入,不写入 wrangler.jsonc) |
CLIENT_SECRET | Slack OAuth 应用客户端密钥 |
BASE_URL | 公共 Worker URL(不含尾部斜杠);本地开发默认http://localhost:8787/gatekeeper/slack |
本地开发时无需手动设置CLIENT_ID/CLIENT_SECRET:run-dev-server.ts 将根目录.dev.vars中的SLACK_CLIENT_ID/SLACK_CLIENT_SECRET映射为这两个变量,这也是包内dev脚本提示"在根目录运行pnpm dev-server"的原因(见 package.json)。
BASE_URL的解析逻辑见 slack.ts:getBaseUrl会去掉尾部斜杠,getBasePath提取其路径段,Worker 的fetch处理器会校验请求路径必须匹配该路径,否则抛错拒绝。
2.3 应用配置要点
按 README 与源码,Slack 应用必须正确配置三项:
- Redirect URL:必须与
<BASE_URL>/oauth完全一致。本地开发默认即http://localhost:8787/gatekeeper/slack/oauth——注意 Worker 的 OAuth 回调处理器正是监听相对路径/oauth(见 slack.ts),并用getBaseUrl(env) + "/oauth"作为redirect_uri传给 Slack。 - 启用令牌轮换(OAuth & Permissions → Token Rotation):启用后令牌短期有效(约 12 小时),通过
oauth.v2.access?grant_type=refresh_token自动刷新;未启用的传统长效令牌作为兜底按原样使用。轮换令牌是单次使用的,这正是 UserAccount DO 用#credentialUpdate串行化刷新、重连与吊销操作的原因。 - 申请所需 User Token Scopes:按授权资源粒度申请(见下节)。
users:read始终被请求,用于已连接账户展示与用户名解析(源码中即IDENTITY_SCOPES = ["users:read"],见 slack.ts)。
2.4 令牌生命周期管理(源码级)
令牌的存取与刷新全部收敛在UserAccountDurable Object(SQLite 存储,见 wrangler.jsonc)中:
- 换取与暂存:
acceptAuthCode完成授权码交换后写入accessToken/refreshToken/grantedScopes/userId/teamId;重连(reconnect)时新凭据先经stageCredentials暂存,待 Workshop 确认浏览器持有者后才由commitReconnect提交生效,期间绑定中的 Gadget 继续使用旧令牌(见 slack.ts); - 定时刷新:
getAccessToken在令牌过期前 5 分钟(ACCESS_TOKEN_EXPIRY_SAFETY_MS)触发刷新;刷新失败(invalid_refresh_token/token_expired/invalid_grant/token_revoked)时回调credentialsExpired通知后端并要求用户重新认证,见 slack-api.ts; - 自动清理:连接流程若从未完成,
setCallback会设置 1 小时闹钟(alarm)自毁;revoke会同时吊销 access 与 refresh 令牌并清空存储(见 slack.ts); - Nonce 防重放:32 字节加密随机 nonce 经历"initiation → oauth"两阶段,每阶段 10 分钟过期,用
crypto.subtle.timingSafeEqual常量时间比较校验,OAuth state 在换取前即被消费,防止回调重放(见 slack.ts)。
三、资源粒度与授权:三种 URL 模式、三类会话
访问按三种粒度授予,每种可授权资源映射到一个 URL 模式,同时驱动同意环节(申请哪些 OAuth scope)与路由环节(把资源 URL 路由到哪个 gatekeeper 类):
| 粒度 | URL 模式 | 会话类型 |
|---|---|---|
| 整个工作区 | https://*(全实例兜底) | SlackWorkspaceSession |
| 单个会话(频道 / 私信 / 群组私信) | https://app.slack.com/client/:teamId/:conversationId | SlackConversation |
| 单个线程 | https://*.slack.com/archives/:conversationId/:messageId | SlackThread |
工作区授权复用框架的账户级https://*模式;更具体的会话与线程 URL 优先。频道与私信共用同一种"Conversation"授权。源码中三种SupportedResource定义与SUPPORTED_RESOURCES列表见 slack.ts。
3.1 各资源的用户令牌作用域
| 资源 | 申请的用户令牌作用域 |
|---|---|
| 工作区 | team:read、会话读取类 scope、search:read |
| 会话 | 会话读取类 scope、search:read |
| 线程 | channels:history、groups:history、im:history、mpim:history |
| 始终申请 | users:read |
其中"会话读取类 scope"指channels/groups/im/mpim各自的:read与:history,即channels:read、channels:history、groups:read、groups:history、im:read、im:history、mpim:read、mpim:history八项(源码常量CONVERSATION_READ_SCOPES,见 slack.ts)。
作用域与资源的映射有完整的双向换算逻辑(见 slack.ts):
resourceUrlPatternsToScopes:把要授权的资源 URL 模式换算成 OAuth 请求的作用域集合(users:read恒在);grantedResourcesFromScopes:授权回调返回的grantedScopes反向换算成已授予的资源——只有全部必需 scope 都授予时该资源才暴露,避免"部分授权"造成的越权缝隙;- 未知的资源 URL 模式会直接抛错拒绝(
validateResourceUrlPatterns)。
3.2 配置器 UI:三种授权交互
授权环节由三套配置器 UI 支撑(源码在 configurator/):
- 工作区(workspace-configurator-ui.tsx):无需任何输入即就绪,资源 URL 自动解析为
https://app.slack.com/client/<teamId>; - 会话(conversation-configurator-ui.tsx):提供自动补全下拉框,可搜索频道 / 私信 / 群组私信;后端 ConversationConfiguratorUI 分页拉取用户会话并本地过滤,最多扫描 5 页、每页 200 条、返回上限 100 个选项;
- 线程(thread-configurator-ui.tsx):要求粘贴 Slack 消息链接(
https://<workspace>.slack.com/archives/C…/p…),并校验主机名必须以.slack.com结尾、路径为/archives/:conversationId/:messageId且 ID 符合[CDG][A-Z0-9]+与p[0-9]+格式。
四、会话 API:Agent 的只读能力面
完整类型定义见 types.d.ts,这是 Agent 面向的公开契约。三类会话的能力如下:
4.1SlackWorkspaceSession(整个工作区)
| 方法 | 说明 |
|---|---|
getInfo() | 获取工作区元数据(team ID、名称、域名) |
listChannels() | 列出连接用户所在的公开与私有频道(分页Cursor) |
listDirectMessages() | 列出连接用户参与的私信与群组私信(分页Cursor) |
listUsers() | 列出工作区成员(分页Cursor) |
getUser(userId) | 按 Slack 用户 ID 查询单个用户 |
getConversation(conversationId) | 获取指定会话(频道或私信)的能力对象;无权限则抛错 |
search(query) | 按 Slack 搜索语法跨工作区搜索消息(如from:@bob in:#engineering budget) |
4.2SlackConversation(单个会话)
| 方法 | 说明 |
|---|---|
getInfo() | 会话元数据(类型、名称、话题、成员数等) |
members() | 列出会话成员(分页Cursor);1:1 私信可能只返回连接用户本人,识别对方请用getInfo().peer |
listMessages() | 列出会话消息(最新在前,分页Cursor);线程回复不混排,需用getThread() |
getThread(threadTs) | 获取包含指定消息ts的线程能力(根消息或任一回应的ts均可) |
search(query) | 会话内搜索;无论查询怎么写,结果都硬性限制在本会话内(任何in:限定词都无法扩大范围) |
会话内搜索的安全边界值得强调:实现上先把查询改写为in:#<频道名> <query>作为搜索提示,但真正的权威边界是服务端按restrictChannelId过滤结果——searchMessages会丢弃所有channel.id !== restrictChannelId的匹配(见 slack-api.ts)。README 所称"硬限制(hard-restricted)"即源于此。
4.3SlackThread(单个线程)
| 方法 | 说明 |
|---|---|
getRoot() | 获取线程根(父)消息 |
listReplies() | 列出最多 1,000 条线程消息(含根消息,旧在前) |
listReplies的实现限制为最多 20 页、每页 50 条(MAX_REPLY_PAGES/HISTORY_PAGE_SIZE),超出部分截断(见 slack.ts)。
4.4 数据模型与分页约定
- 列表 / 搜索方法返回前向分页的
Cursor对象:反复调用next()直到返回null,使用完毕(含提前结束)需 dispose(见 types.d.ts)。SlackCursor内部串行化并发的next()调用,且每一页在返回给调用方之前都会经过授权检查(见 slack.ts); - 条目类型做了能力捆绑:
SlackConversationEntry把会话元数据与可读该会话的能力对象打包,SlackMessageEntry把消息与其线程能力打包,取到即可直接深入,无需二次查找(见 types.d.ts); - 已知提及(mention)会渲染为可读名称:
resolveText把@user、#channel、<!here>、<!subteam^…>、链接与 HTML 实体统一解析为可读文本(见 slack-api.ts),作者与提及用户会批量预取并缓存; - 消息时间戳(
ts)是线程的 ID;Slack permalink 中的p<秒><微秒>编码可还原为ts(messageIdToTs,见 slack.ts)。
五、底层原理:路由、授权与协作防护
5.1 从资源 URL 到 gatekeeper 类
SlackUserImpl.getGatekeeperClassFor解析 URL 并路由(见 slack.ts):
- 主机名以
.slack.com结尾且路径为/archives/<conversationId>/<messageId>→SlackThreadGatekeeperImpl(thread_ts参数或消息 ID 解码确定线程根); - 主机名为
app.slack.com且路径为/client/<teamId>/<conversationId>→SlackConversationGatekeeperImpl; - 其余
app.slack.com/client/<teamId>→SlackWorkspaceGatekeeperImpl; - 无法识别的 URL 直接抛"Unsupported Slack resource URL"。
会话对象(SlackWorkspaceSessionImpl等)由各 gatekeeper 的startSession(approvalQueue)创建,并持有 API 客户端、批准队列与(工作区场景下)观察者追踪器(见 slack.ts)。
5.2 观察者(Observer)校验:协作时如何防止越权
Cloudflare OS 支持把已读取的数据分享给协作观察者。Slack gatekeeper 的观察者校验完全基于观察者自己的令牌,由SlackVerifier回答两个问题(见 slack.ts):
getTeamId():观察者令牌所属工作区,用于确认其是同一工作区成员;hasConversationAccess(id):以观察者令牌调用conversations.info——公开频道对任何工作区成员可解析,私有频道 / 私信 / 群组私信仅对成员 / 参与者可解析,从而忠实执行 Slack 的 ACL;会话 ID 全局唯一,也顺带拒绝了跨工作区会话。
校验策略因绑定粒度而异(见 slack.ts):
- 会话 / 线程绑定:采用"ACL 检查(单单元)"策略。绑定即单个会话(线程继承其会话的 ACL),只需在
addObserver时用观察者自己的令牌确认其可读该会话;之后读取的内容不可能超出该会话,因此不追踪观察者,removeObserver为空操作; - 工作区绑定:采用"按会话的数据集追踪"策略。一个工作区绑定跨越大量 ACL 各异的会话,因此 gatekeeper 记录 Gadget 实际观察过哪些会话(
trackedConversation:…,状态pending→observed),addObserver要求工作区成员身份 + 对所有已观察会话的访问权;当首次观察新会话时(#prepareConversationObservation),会排除所有无权访问它的现有观察者,且观察被批准队列拦截时该会话保持pending、不会记为已泄露。校验的最终布尔结果只回传给 Slack gatekeeper 自身,因此可以信任。
5.3 每次读取都经过批准队列
所有暴露会话身份或内容的读取都经由authorizeConversationObservation走批准队列:工作区绑定下它会先做数据集追踪再授权,单单元绑定下则是普通授权(见 slack.ts)。例如listMessages每页都会提交"Read a page of N messages from conversation "的描述供批准;工作区级别的元数据读取(成员目录等)任何成员可见,会话 ID 列表为空时直接授权。
5.4 稳健的 API 客户端
slack-api.ts 中的客户端包含工程细节:
- 限流重试:对 429 响应最多重试 2 次,按
Retry-After头等待,上限 30 秒(RATE_LIMIT_MAX_RETRIES/RATE_LIMIT_MAX_WAIT_MS); - 并发上限:所有子请求扇出(批量预取用户、批量校验观察者)控制在 5 路并发以内,低于 Workers 6 路并发请求上限(
MAX_CONCURRENT_REQUESTS,见 slack.ts),并采用"逐批Promise.all、首个失败即整体失败"的fail-closed语义; - 错误映射:
SlackApiError把 Slack 错误码映射为可读消息,并区分isAccessError(令牌用户无权看到该资源,如channel_not_found、not_in_channel、no_permission)与isAuthError(令牌过期或吊销,如invalid_auth、token_expired)——观察者校验正是靠这一区分把"无法证明有权限"保守判定为"无权限"(见 slack-api.ts); - permalinks:通过无需额外 scope 的
auth.test获取工作区主机,为消息补齐https://<host>/archives/…链接(线程消息附带thread_ts与cid查询参数,恰好匹配线程资源 URL 模式)。
5.5 搜索的页式游标适配
Slack 的search.messages是基于页码而非游标的接口,客户端把页码编码进游标字符串(String(page + 1)),并根据paging.pages判断是否还有下一页(见 slack-api.ts)。
六、构建与部署
包内构建命令(见 package.json):
# 构建(含 capnweb RPC 校验代码生成) pnpm exec vp run -F @gadgets/slack-gatekeeper build相关脚本说明:
dev:包内不可直接启动,须在仓库根目录运行pnpm dev-server(由 run-dev-server.ts 统一拉起各 gatekeeper 并完成环境变量映射);deploy:先执行build:configurator再wrangler deploy;- 部署前的类型校验由 wrangler.jsonc 的构建钩子完成:
pnpm exec capnweb-validate build --out .wrangler/validate(见 wrangler.jsonc)。
运行时形态(见 wrangler.jsonc):Worker 需要 SQLite 持久化,迁移声明了UserAccount、SlackWorkspaceGatekeeperImpl、SlackConversationGatekeeperImpl、SlackThreadGatekeeperImpl四个 Durable Object 类。部署时把CLIENT_ID/CLIENT_SECRET作为 wrangler secrets 注入(它们刻意不写入 wrangler.jsonc)。
七、实践要点速查
- 最小配置路径:创建 Slack 应用 → 配置 Redirect URL 为
<BASE_URL>/oauth→ 开启 Token Rotation → 按需勾选 User Token Scopes → 注入CLIENT_ID/CLIENT_SECRET→pnpm dev-server本地联调; - 选择授权粒度:需要频道 / 私信 / 搜索全览选工作区(
SlackWorkspaceSession);只需单一频道或私信选会话(SlackConversation);只读一条讨论串选线程(SlackThread),scope 最小、暴露面最小; - 记住只读边界:三类会话都无写入 / 操作能力,
getAutoApprovableActions恒为空,任何 action 调用都会抛错; - 分页与释放:所有
Cursor都要next()至null并 dispose;线程回复上限 1,000 条;搜索查询非空且 ≤ 1,000 UTF-8 字节; - 会话内搜索不可逃逸:
SlackConversation.search无论查询写什么,结果都由服务端按会话 ID 硬过滤。
延伸阅读
- 连接流程与页面:@gadgets/gatekeeper-kit 的 connect-pages / credential-stage
- 观察者与授权框架契约:workshop-shared 的 gatekeeper 模块(
GatekeeperUser、ApprovalQueue、Cursor等) - 其他 gatekeeper 对照:gatekeeper-google、gatekeeper-github、gatekeeper-confluence
- 人工智能
- AI 应用
- AI Agent
- Agent 沙箱
- AI 安全治理
【免费下载链接】cloudflare-os
Agent workspace built on Cloudflare Workers for creating documents, building apps, and running agents with your company’s context and systems.
相关推荐
Cloudflare OS Notion 门卫(Notion Gatekeeper)实战指南:构建基于 Cloudflare Workers 的 Notion 页面与数据库访问代理
Cloudflare OS Notion 门卫(Notion Gatekeeper)实战指南:构建基于 Cloudflare Workers 的 Notion
人工智能AI 应用AI AgentAgent 沙箱AI 安全治理基于 Cloudflare OS Gatekeeper 的 Supabase 集成指南:OAuth2 连接、只读查询与人审 SQL 执行
基于 Cloudflare OS Gatekeeper 的 Supabase 集成指南:OAuth2 连接、只读查询与人审 SQL 执行 导读 本文围绕 Clo
人工智能AI 应用AI AgentAgent 沙箱AI 安全治理WeKan 在 Ubuntu Touch 上的安装与更新机制解析:OpenStore click 包与系统镜像 OTA
WeKan 在 Ubuntu Touch 上的安装与更新机制解析:OpenStore click 包与系统镜像 OTA Ubuntu Touch(UBports
人工智能AI 应用AI AgentAgent 沙箱AI 安全治理
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考