给开源桌面应用 OpenWork 接外部服务:MCP 服务器、办公套件与搜索的授权边界
本文基于 openwork 仓库 commit 3b41381(2026-08-03)梳理,该项目仍在高频迭代,具体行为以仓库 https://github.com/different-ai/openwork 最新代码与文档为准。
给 OpenWork 接外部服务时,真正决定风险的不是你点了几次「连接」,而是这条连接的账号归属是谁、凭据落在哪台机器上。 同样是接 Slack,走组织托管的连接和自己在桌面端加一台自定义 MCP 服务器,凭据位置、可撤销性、以及出事之后谁能查审计日志,完全是三套答案。文档里把这三条路径写在了相邻的几篇里,但没有把「代价」并排列出来——这篇就干这件事。
先做个边界说明。OpenWork 是 Different AI 的开源桌面应用项目(仓库 github.com/different-ai/openwork),把技能、MCP 连接与外部服务打包成可共享的「能力」,和同名的职场点评网站、以及中文里泛指的「开放工作」没有关系,下文出现的 OpenWork 一律指这个项目。另外它的许可证是分层的:仓库根目录 LICENSE 写明 /ee 目录下的内容按 ee/LICENSE 定义的许可证发布(该文件头部标注为 Functional Source License, Version 1.1, MIT Future License,缩写 FSL-1.1-MIT,Copyright 2026 Different AI Inc),其余部分才是 MIT(Copyright 2026 Different AI)。而本文谈到的组织控制面——发布连接、分配访问、下发桌面策略——代码正好落在 ee/apps/ 与 ee/packages/ 里。所以不能笼统说一句「MIT 开源」就完事;能不能商用、能不能改,以许可证原文为准,本文不提供法律意见。
一、三条接入路径,先分清你走的是哪条
从桌面应用的视角看,外部能力进入 Agent 有三个入口,文档把它们放在 packages/docs/start-here/connect-your-stack/ 下的不同文件里:
第一条是 OpenWork Connect。成员在桌面应用的 Settings > OpenWork Connect 里,看到组织已经开放的服务并逐个登录。文档点名的服务包括 Gmail、Google Calendar、Google Drive、Slack、Notion、Linear。这条路要求你先登录一个 OpenWork 账号并加入一个启用了 Connect 的组织,而且文档特意强调,这和登录某个模型服务商的账号是两件事。
第二条是 自定义 MCP 服务器。Settings > Extensions 里的 Add Custom App,用于组织没有通过 Connect 提供的本地或自建服务器。文档开头就写明这是「advanced path」,只在 Connect 覆盖不到时才用。
第三条是 内置搜索开关。Exa 搜索在文档里被描述为一个全局设置,位置是 Settings -> Advanced,界面文案在 apps/app/src/i18n/locales/en.ts 里对应的键是 settings.enable_exa(“Enable Exa web search”)。它不是一台 MCP 服务器,走的是内置工具通路。
这三条的差别不是「难易程度」,是凭据归属。第一条的登录态按文档说法存在 OpenWork Cloud 而不是你这台机器上;第二条的 OAuth 客户端凭据是你自己填进桌面端配置的;第三条你甚至没有一个可见的第三方账号,只有一个开关。搞混了,后面所有的撤销动作都会做错地方。
如果你想先补 MCP 授权本身的通用加固手法,站内那篇MCP 授权链路加固讲的是协议层的通用做法,MCP Server 能力卡讲的是怎么给一堆服务器建立可检索的登记,而AI 办公自动化谈的是办公场景的落地价值;本篇不重复这三块,只盯 OpenWork 这一个项目里这三条接入路径各自的授权边界与账号归属。
二、加一台自定义 MCP 服务器:动态注册是分水岭
Add Custom App 的对话框要你填名字、URL,并指明服务器是否需要 OAuth。文档明确写了它面向的场景:支持动态 OAuth 客户端注册(dynamic OAuth client registration)的服务器。你点完之后,浏览器里跑完提供方的 OAuth 审批流程,再跳回 OpenWork。
问题出在不支持动态注册的服务器上。文档里给的处理方式是:如果服务器给了你一对预注册的 OAuth client ID 与 client secret,就走 Add Custom App > Advanced OAuth 填进去。报错信息长这样——Incompatible auth server: does not support dynamic client registration。看到这句就别在 URL 上折腾了,它说的是注册方式不匹配,不是地址写错。
这里还有个容易被忽略的选择:添加时可以选择把这个 app 加到当前工作区,还是加到全局配置(对所有工作区生效)。这一项决定了这台服务器的可见范围。如果你把一台带写权限的服务器加进了全局配置,那么以后每开一个工作区,Agent 默认就带着这套权限——工作区隔离在这一步被你自己抹掉了。关于隔离本身的收益,可以参考站内Agent 工作区隔离。
三、接办公套件:授权到什么程度,由 scope 那一行字决定
Slack 是文档里唯一被单独写了一篇的办公套件案例,因为它的官方 MCP 服务器不支持自动 OAuth 客户端注册,必须走预注册路径。这一篇(connect-slack-mcp.mdx)把授权边界写得最实。
第一件事:Slack 管理员需要在 Slack 应用的 OAuth & Permissions 里加一条重定向地址,文档给的值是 http://127.0.0.1:19876/mcp/oauth/callback。这是本地回环地址,说明这条流程是桌面端本机接住回调的。第二件事:文档说明这条流程不需要旧的 Slack bot token(xoxb-... 或 xapp-...),走的是用户令牌作用域(user token scopes)。
用户令牌作用域这几个字是关键——它意味着 Agent 在 Slack 里是以你的身份行动的,它能看到的就是你能看到的。文档给了一组「先只读」的起步作用域:
search:read.public search:read.private search:read.mpim search:read.im search:read.files search:read.users channels:history groups:history mpim:history im:history users:read users:read.email channels:read groups:read mpim:read
注意这一串里已经包含 search:read.private、im:history、users:read.email——私聊历史和成员邮箱都在里面。所谓「只读」并不等于「无害」,它只是不写而已。要让 Agent 真的在 Slack 里动手,才需要另一组:
chat:write reactions:write channels:write groups:write im:write mpim:write canvases:read canvases:write
文档自己也写了一句:Slack MCP uses user token scopes. Choose the smallest set that matches what your team wants agents to do. 这句话值得当成硬规则执行——把上面那一大串照抄进去是最省事的做法,也是暴露面最大的做法。按能力挑作用域的对照表在同一篇文档里,逐行对应「搜索公开与私有消息」「读文件」「发消息」「建频道」等能力。最小权限的做法可参考站内最小权限设计。
填完之后,OpenWork 会把这台 MCP 写进配置,文档给出的形状是:
{
"mcp": {
"slack": {
"type": "remote",
"enabled": true,
"url": "https://mcp.slack.com/mcp",
"oauth": {
"clientId": "your-slack-client-id",
"clientSecret": "your-slack-client-secret"
}
}
}
}
如果你填了作用域,文档说它还会被存成 oauth.scope,这就是 Slack 在同意页上展示给你的那一份清单。这段配置里躺着一个 client secret——文档也提醒了要把它排除在聊天记录和源码仓库之外。这条路径的代价在这里最直白:你把一份长期有效的第三方凭据,明文形态地交给了本机的配置文件。
四、接搜索:一个开关背后的数据流向
Exa 那一篇文档短到只有一段:它是一个全局设置,位置在 Settings -> Advanced。但短不等于无害。
从 apps/app/src/lib/build-in-tools.ts 里能看到内置搜索工具的类型定义,其中一行是:
export type WebSearchProvider = "exa" | "parallel";
同一个文件里,WebSearchInput 的字段包括 query、numResults、livecrawl、type、contextMaxCharacters,其中 livecrawl 的取值是 "fallback" | "preferred",type 的取值是 "auto" | "fast" | "deep"。WebSearchMetadata 会带上 provider 字段,说明每次搜索结果里记录了实际由哪家服务商执行。
对你意味着什么:这个开关一旦打开,Agent 在推理过程中构造的查询串会离开你的机器,去到一个外部搜索服务。查询串本身经常比你以为的敏感——排查线上问题时,Agent 很可能把内部服务名、异常堆栈里的路径、甚至客户标识拼进查询里。livecrawl 设成 preferred 还意味着它更倾向于实时抓取而不是吃缓存索引,这会让请求特征更明显。这不是「要不要用搜索」的问题,是「哪些工作区允许开这个开关」的问题。
五、团队共享连接,和个人连接是两码事
这是本篇最需要说清的一块。文档 packages/docs/cloud/share-with-your-team/shared-mcp-connections.mdx 里,管理员在 OpenWork Cloud 的 MCP Connections 面板 Add Connection,可以选预设(文档列举了 Notion、Linear、Stripe、Sentry、Exa、Context7)或直接填任意 MCP 服务器地址。真正的分岔在这一步:账号模式。
Individual accounts:每个人用自己的身份登录,文档说这是 OAuth 服务器的默认模式。Agent 以你的身份行动,提供方自己的权限规则照常生效,审计留在提供方那一侧、记在你名下。One org account:管理员登录一次(机器人账号、服务账号或 API key),所有被授权的成员,其 Agent 都以同一个身份行动。
这两种模式的差异不是配置细节,是责任归属的差异。One org account 下,Slack 或 Notion 那边的审计日志会显示所有动作来自同一个账号——你没法从提供方侧回答「是谁让 Agent 干的」,只能回到 OpenWork 这一侧去追。而且这个共享身份的权限通常是所有人权限的并集,等于给每个被授权成员发了一张越权通行证。真要用它,得先想清楚谁能被授权,以及出事之后怎么归因。关于这种权限叠加的失控模式,站内Agent 权限膨胀有更一般的讨论。
成员那一侧看到的是三组状态:Needs your sign-in(等你自己登录)、Needs admin setup(等管理员配)、Ready to use(可用,含组织托管的账号)。文档还写了一句必须放在心上的话:你的登录态存在 OpenWork Cloud,而不是这台机器上——好处是换设备免重连,代价是这份凭据的保管方从你变成了控制面。
管理员侧的几条行为也值得记:成员只看得到被显式授权的连接(工作区级、团队级或按人),文档强调这个限制由服务端在每次请求上执行,而不是靠客户端界面藏起来;撤销连接或撤销某人的访问,对所有设备立即生效;而已经在桌面端自己配过某个工具的成员,发布组织连接不会覆盖或删除他们的本地配置——这条意味着「组织统一管理」并不会自动收编存量的个人连接,你得单独去清。
控制面还能反过来限制桌面端。desktop-policies.mdx 列出的策略键包括 Custom providers、Enable OpenCode Zen Models、Multiple workspaces、Control Settings、Manage Extensions、Built-in Extensions、Welcome Page。也就是说,管理员可以直接关掉成员本地加扩展的能力。被策略禁用的内置扩展会从常规目录里隐藏,并在隐藏视图里显示 Disabled by organization。策略由桌面端缓存,重载时生效,并在切换活跃组织、切换 Cloud 账号或每小时的桌面配置刷新时更新。
六、这几个模块分别管什么
| 组成部分 | 它负责什么 | 对应仓库位置 | 你什么时候会碰到它 |
|---|---|---|---|
| OpenWork Connect(成员页) | 桌面端登录组织已开放的服务、查看连接就绪状态 | packages/docs/start-here/connect-your-stack/connect-services.mdx | 组织已经帮你配好 Gmail/Slack/Notion,你只需要登录自己那份 |
| Add Custom App(Extensions) | 加自建或本地 MCP 服务器,含 Advanced OAuth 预注册凭据 | packages/docs/start-here/connect-your-stack/add-an-mcp-server.mdx | 组织没提供、或者你在调自己写的 MCP 服务器 |
| Slack 自定义 MCP 流程 | 预注册 OAuth 应用 + 用户令牌作用域 + 本机回调 | packages/docs/start-here/connect-your-stack/connect-slack-mcp.mdx | 不走 Cloud、只想在本机接 Slack |
| 内置网页搜索 | 内置搜索工具的服务商与参数类型 | apps/app/src/lib/build-in-tools.ts、packages/docs/start-here/connect-your-stack/enable-exa-search.mdx | 你在 Settings -> Advanced 里打开搜索开关时 |
| MCP Connections(管理面) | 组织级发布连接、选账号模式、按人/团队授权 | packages/docs/cloud/share-with-your-team/shared-mcp-connections.mdx | 你是管理员,要统一管理团队的第三方连接 |
| Desktop policies | 下发桌面端能力开关,限制本地扩展与设置 | packages/docs/cloud/share-with-your-team/desktop-policies.mdx | 你发现本地某个功能变灰并提示由组织禁用 |
| OpenWork Connect MCP 端点 | 让外部 MCP 客户端复用组织能力 | packages/docs/cloud/run-in-the-cloud/cloud-mcp.mdx | 你想在 Claude Code、Codex、Cursor 里用同一套能力 |
| 企业侧代码 | 控制面与企业能力的实现 | ee/apps/(10 个)、ee/packages/(3 个),按 ee/LICENSE | 你要评估自托管、二次开发或商用边界时 |
顺带说清一个结构事实:这个仓库里 apps/ 有 4 个、packages/ 有 12 个,packages/docs/ 有 57 份 mdx(其中 model-context-protocol/ 目录下 10 份是各家客户端的接入指南),架构文档 docs/ 有 20 份 md,evals/ 有 26 份流程 md,apps/server/src/ 下有 138 个顶层 .ts,packaging/ 提供三种分发方式,全仓受版本控制的文件 3490 个。仓库 README 把自己定位成「Claude Cowork 和 Codex 的开源替代」——这是项目自己的说法,本文只如实转述,不替它背书这个对标关系。
七、边界与代价:它明确不管的事
它不替你决定授权范围。 Slack 那一篇给了作用域对照表,也给了「选最小集合」的建议,但填哪些是你的事。填多了没有任何机制会拦你,OAuth 同意页会照单展示,你点了就是点了。
它管不了提供方的策略。 文档写得很直:如果 Slack 工作区禁止安装应用,得先找 Slack 管理员批,OpenWork 绕不过去;如果某个作用域被工作区策略或套餐限制,只能从两边同时删掉再重试。这类规则会调整,以各服务商官方最新说明为准。
本地路径把长期凭据放在了你机器上。 走 Advanced OAuth 意味着一份 client secret 进入本机 MCP 配置。桌面应用被入侵、配置文件被同步进备份、误提交进仓库,都是现实存在的路径。这是「不依赖 Cloud」的直接代价。
Cloud 路径把凭据集中到了控制面。 好处是能一键撤销、跨设备生效、按人授权;代价是控制面成了新的高价值目标,而且管理员理论上拥有更多可见性。文档在 security-and-operations.mdx 里给了不少缓解措施——组织 API key 与 SCIM 令牌以哈希存储、SSO 配置加密存储、部分敏感数据库列用应用层 AES-256-GCM 加密(密钥来自 DEN_DB_ENCRYPTION_KEY)、特权路由要求较新的会话——但这些是部署方的责任清单,不是开箱即得的保证。文档自己也写了,私有或自托管部署下 TLS 证书、库存储加密、备份加密、私网策略都由运维方负责。
搜索开关是全局的。 文档把 Exa 描述为全局设置,不是按工作区的细粒度开关。你没法只在某一个「安全」的工作区里开它。
它不承诺任何路线图。 仓库里有 packages/docs/roadmap.mdx,但那是文档里的写法,不是本文的判断,也不构成对未来行为的承诺。
八、上手与避坑清单
别一上来就 Add Custom App。 会踩是因为工程师习惯性走「高级」路径,觉得自己配更可控。结果是你把一份 client secret 放进了本机,还绕开了组织已经配好的连接。避法:先打开 Settings > OpenWork Connect 看一眼,能在里面找到的服务一律走那条路;文档也把自定义路径明确限定为「组织没通过 Connect 提供」的场景。
别把两个界面名字搞混。 会踩是因为文档里两种写法都出现过——connect-services.mdx 写的是 Settings > OpenWork Connect,shared-mcp-connections.mdx 写的是 Settings > Connect。避法:认功能而不是认字面,找的是那个按「需要你登录 / 需要管理员配置 / 可用」三组分类的页面,界面文案会随版本变。
别拿只读作用域当安全底线。 会踩是因为「read」这个词让人放松。而文档给的读优先起步集合里包含私聊历史与成员邮箱。避法:按能力对照表逐条勾,写权限单独一轮评估再加;先跑一周只读,看 Agent 到底用到了哪几个工具,再补。
别忽略「工作区 / 全局」那个选择。 会踩是因为它在添加对话框里只是一个不起眼的选项。选了全局,这台服务器就跟着你所有工作区跑。避法:默认选当前工作区;只有确认过这台服务器无写权限、且确实每个项目都要用,才提升到全局。
别把重定向地址写错端口。 会踩是因为 Slack 那条回调是本机回环地址 http://127.0.0.1:19876/mcp/oauth/callback,跟大多数人熟悉的公网回调长得不一样,容易凭印象改。避法:从文档原样复制粘贴进 Slack 应用的 OAuth & Permissions;Slack 报重定向无效,先回去核这一行。
别把动态注册报错当成网络问题排查。 会踩是因为 Incompatible auth server: does not support dynamic client registration 这句话里有 auth server,很容易往连通性上想。避法:看到这句就直接去补 Advanced OAuth 里的 client ID 与 secret,重载引擎再试。
别以为发布了组织连接就统一了。 会踩是因为管理面上看着整齐了。但文档明说,成员已有的本地配置不会被删除或修改。避法:推组织连接的同时,单独发一轮通知让成员清掉本地重复的自定义 app,否则同一个服务会有两条权限不同的通路。
别在没定账号模式之前先建连接。 会踩是因为 Individual accounts 和 One org account 在界面上只是一个选项,但它决定了提供方那侧的审计能不能追到人。避法:建连接前先回答一个问题——出事之后,你要从哪里查是谁触发的?答不上来就别选 One org account。
收尾自检
接完一轮之后,拿这五个问题过一遍:这条连接的凭据现在躺在哪(本机配置、Cloud、还是根本没有第三方凭据);Agent 以谁的身份行动(我自己、还是一个共享账号);撤销要去哪里点,多久生效;作用域清单里有没有我当初没细看就勾上的写权限;这台服务器是挂在当前工作区还是全局。五个都答得上来,这条连接才算是配好了。
想继续往下读,按你的角色选:只用桌面端就从 packages/docs/start-here/connect-your-stack/ 这一整个目录读起;要管团队就读 packages/docs/cloud/share-with-your-team/shared-mcp-connections.mdx 和 desktop-policies.mdx;要评估自托管与商用边界,先读根目录 LICENSE 和 ee/LICENSE 原文,再看 packages/docs/cloud/security-and-operations.mdx 里那份运维责任清单。
本篇属于一个把开源AI 工作流桌面应用 OpenWork逐层拆开讲的系列,整体地图见 OpenWork 是什么:把技能与 MCP 打包成能力的开源桌面应用;沿着这条线往下,还可以看 OpenWork 开源桌面应用:技能、插件与 MCP 各在哪一层起作用 和 OpenWork 开源桌面应用怎么接模型:三条路径的机制与代价。