开源桌面应用 OpenWork 的角色与权限模型:谁能发能力、谁能指派、谁只能用

2026-08-04

本文基于 openwork 仓库 commit 3b41381(2026-08-03)梳理,该项目仍在高频迭代,具体行为以仓库 https://github.com/different-ai/openwork 最新代码与文档为准。

读这套权限设计,最容易踩空的一步是把它当成一张角色表来读。它不是一张表,是两套彼此独立的坐标系叠在一起:组织角色决定你能改动组织本身的哪些东西,能力授权决定你能看见并执行哪一个具体的技能或连接。 这两套东西在代码里分属不同模块、不同的判定函数、不同的错误类型,一个人可以是组织里的普通成员,同时是某个能力包的 manager;也可以是组织管理员,却在某个私有集市里什么都搜不到(这一条有例外,后面会讲)。想清楚这一点,后面所有看起来别扭的行为都会变得合理。

这里说的 OpenWork,是 different-ai 放在 GitHub 上的那个开源桌面应用项目,跟同名的职场点评网站、跟中文里泛指的”开放工作”没有任何关系,下文出现这个词都指这个项目。

一、它想解决什么:能力是被分发的,不是被安装的

仓库 README 这样定位自己:一个免费开源的桌面应用,用来共享 AI 工作流,是 macOS、Windows、Linux 上 Claude Cowork 与 Codex 的开源替代品——这是项目自己的说法,不是本文替它下的判断。它的实际做法是:把技能、MCP 连接、连上的第三方服务打包成”能力”,然后通过一个 MCP 端点吐给你已经在用的 Agent。README 里写明这个 MCP 只暴露两个工具,search_capabilities 负责找你能用的东西,execute_capability 负责运行它。

这个形态直接决定了权限模型长什么样。传统做法是”装了就有、没装就没有”,权限问题被安装动作掩盖了。OpenWork 把安装这一步抽掉之后,search_capabilities 返回什么,就成了唯一的可见性边界。docs/marketplace-capabilities-architecture.md 把这层意思写得很直白:安装、也就是把文件复制进 .opencode/,退化成离线使用和版本固定的一种优化,不再是使用组织内容的前提。

所以这套权限的核心不是”谁是管理员”,而是”一次搜索该返回哪些行”。

站内已有的三篇相邻文章跟本篇是分工关系:AI 工具的权限模型怎么设计讲的是通用的权限抽象与思路,Agent 最小权限设计讲的是收敛授权范围的方法论,AI 工具支出审计讲的是花钱这条线怎么查账;本篇不重复这些,只做一件事——把 OpenWork 这一个具体仓库里的角色层级、能力授权、套餐门禁三者的接缝拆开给你看。

二、第一层坐标:组织角色,以及两份文档的口径差

先说一个你自己读仓库时一定会撞上的矛盾。packages/docs/cloud/members-and-rbac.mdx 面向用户,说 OpenWork Cloud 自带三个默认角色:Owner 拥有完整组织控制权,Admin 可以邀请人、管理团队和大部分共享资源,Member 只能用别人分享给他的资源。而架构文档 docs/cloud-organization-role-access.md 写的是四级:ownersuper-adminadminmember

以代码为准。ee/apps/den-api/src/organization-role-hierarchy.ts 里四个常量都在,并且给了明确的等级:owner 是 3,super-admin 是 2,admin 是 1,member 是 0,高的自动满足低的门槛。用户文档没提 super-admin,很可能是因为它在产品界面上不作为常规分配项出现;但你只要读 API 层的判定,就绕不开它。

四级层级里有几条硬规则值得单独记住:

组织有且只有一个 owner,这个角色不能通过邀请分配,也不能通过改成员角色赋予,owner 成员本身不能被移除。想换人,只能走所有权转移,且目标必须是一个活跃的 super-admin,转移完成后原 owner 降为 super-admin。这在架构文档和 organization-access.ts 的邀请校验里是对得上的——后者直接返回”Owner can only be assigned by the Den ownership transfer API”。

admin 这一级比多数人想象的窄。架构文档的角色矩阵写得很清楚:admin 能邀请人,但只能邀请 member;能移除非 owner 成员,但不能升降别人的角色;Settings 对它是只读,写不了。真正能改成员角色、能写 Settings 的是 super-admin 及以上。

自定义角色是加法,不是替换。架构文档明确说自定义角色可以叠加委派权限,但不取代内置层级,内置角色名受保护,成员角色串里最高的那个内置角色决定 owner/super-admin/admin/member 这几道门。

委派权限目前的典型用法只有一个:security_configuration.manageee/apps/den-api/src/organization-access.ts 里它是被显式加进 better-auth 语句集合的资源与动作,用来把 SSO、SCIM、组织 API Key 的管理权从日常成员运维里拆出来。安全文档 packages/docs/cloud/security-and-operations.mdx 补了一句关键的:owner 默认拥有该权限并可通过自定义角色委派出去,默认的 admin 不会自动获得。

还有一条防提权的设计我觉得写得挺克制:邀请时能授出的角色,受邀请人自己已有权限的约束。validateAssignableOrganizationPermissionRecord 会逐条比对目标角色的权限记录是否是当前成员权限的子集,不是就返回”You can only invite members into roles with permissions you already have.”。这意味着一个默认 admin 没法通过新建角色的方式把自己没有的安全配置权发出去。

最后是时间维度的一道闸。ee/apps/den-api/src/routes/org/shared.tsPRIVILEGED_SESSION_MAX_AGE_MS 是 15 分钟,hasFreshPrivilegedSession 用会话创建时间做判定;安全文档同样写明,改 SSO、SCIM、API Key、组织设置、自定义角色、成员角色、账单、推理设置这些路由,都要求会话是最近 15 分钟内创建的。你用一个挂了三天的浏览器标签页去改角色,会被要求重新登录,这不是 bug。

三、第二层坐标:能力授权,谁能发、谁能指派、谁只能用

这一层跟组织角色几乎正交,代码在 ee/apps/den-api/src/routes/org/plugin-system/access.ts。它自己定义了一套三档角色,比大小的方式是纯数值:

const rolePriority: Record<PluginArchRole, number> = {
  viewer: 1,
  editor: 2,
  manager: 3,
}

授权对象有三种:orgWide(全组织)、orgMembershipId(具体某个成员)、teamId(某个团队)。授权可以挂在集市、插件、配置对象、连接器实例四类资源上,并且会向下级联——架构文档写明,成员能看到的集合等于直接的配置对象授权、加上插件授权级联到的配置对象、加上集市授权级联到的插件与配置对象;viewer 就足以搜索和执行。

现在回答标题里的三个问题。

谁能发?答案出乎意料地宽。 看这个函数:

export function hasPluginArchCapability(context: PluginArchActorContext, capability: PluginArchCapability) {
  if (capability === "plugin.create" || capability === "config_object.create") {
    return true
  }
  return isPluginArchOrgAdmin(context)
}

创建插件和创建配置对象,对组织里任何人都返回 true。要管理员身份的是另外三件:建集市、建连接器实例、建连接器账户。这个取舍是有内在逻辑的——写一个技能、攒一个插件属于个人生产行为,不牵涉共享凭据;而集市是分发渠道,连接器意味着要动第三方授权和密钥,那才是需要管理员把关的地方。

谁能指派?卡在资源的 manager 上,再加一道组织级的封顶。 授权写入的实现在 ee/apps/den-api/src/routes/org/plugin-system/store.ts

await requirePluginArchResourceRole({ ..., role: "manager" })
if (input.value.orgWide === true && !isPluginArchOrgAdmin(input.context)) {
  throw new PluginArchAuthorizationError(403, "forbidden", "Only organization owners and admins can grant org-wide access.")
}

也就是说,你在自己建的插件上天然是 manager,可以把它指派给某个人或某个团队;但想勾”全组织可见”,必须是 owner 或 admin。撤销授权是软撤销——路由描述用的词是 soft-revokes,数据行留着 removedAt,这对事后审计有用,但也意味着”删掉了”和”标记为已移除”是两回事。

谁只能用? 拿到 viewer 的人。他在 Claude Code 或 Codex 里调 search_capabilities,命中的就是级联出来的那个可见集合。这里有个前面提到的例外:架构文档写明,通过成员表角色与 isOwner 解析出来的组织管理员,能看到全部活跃对象。所以”组织管理员在私有集市里什么都搜不到”这句话,在检索这条路径上并不成立——设计上给管理员开了后门,这是权限模型里必须知道的一条,不是实现疏漏。

团队这一层还有个容易忽略的属性。用户文档说,启用了 SCIM 的组织可以打开从 SCIM 组自动建团队,这些团队会标记为受 SCIM 管理,成员变更只能回身份提供商去改,手工建的团队不受影响。你如果把关键能力挂在一个 SCIM 托管团队上,那这条授权链的真正控制点已经不在 OpenWork 里了。

同样值得记的是 SSO 首次登录的默认值:即时开通只会给 Member 这个基线角色,身份提供商传来的 rolegroupsadmin 这类属性不会被用来授予组织角色。提权必须在 OpenWork 内部显式做一次。

组成部分它负责什么对应仓库位置你什么时候会碰到它
内置角色层级定义 owner / super-admin / admin / member 的等级与满足关系ee/apps/den-api/src/organization-role-hierarchy.ts判断某人能不能进某个后台页面、能不能改别人角色时
自定义角色与权限记录校验委派权限、限制”只能授出自己已有的权限”ee/apps/den-api/src/organization-access.ts想把 SSO/SCIM/API Key 管理拆成独立岗位时
能力资源授权三档角色决定谁能搜、能执行、能编辑、能再授权某个能力ee/apps/den-api/src/routes/org/plugin-system/access.ts把一个技能发给某个团队时
授权写入与软撤销建/撤授权,全组织授权额外要求组织管理员ee/apps/den-api/src/routes/org/plugin-system/store.ts勾”全组织可见”被 403 挡住时
可见集合的级联解析把集市、插件、配置对象三层授权合成检索结果ee/apps/den-api/src/mcp/marketplace-capabilities.ts成员反馈”搜不到你说的那个技能”时
特权会话新鲜度敏感写操作要求会话足够新ee/apps/den-api/src/routes/org/shared.ts改角色时被要求重新登录时
套餐权益判定按套餐档位算出企业能力开关,不足则 402ee/apps/den-api/src/entitlements.ts改 SSO 或桌面策略被拒时
桌面策略目录定义可被组织关掉的桌面功能项及给用户看的提示语packages/types/src/den/desktop-policies.ts员工端某个功能变灰时

四、套餐分级这条线,是怎么和权限缠在一起的

这是本篇最需要说清楚的接缝。套餐不是第三套角色,它是叠在管理动作上的一层开关。

docs/enterprise-plan-gating.md 把原则写成一句话:只对管理动作(写)设门槛,永远不对交付(读)和移除(删)设门槛。文档随后列了具体清单——注册或替换 SSO 连接、域名验证、创建或编辑桌面策略、以及当补丁涉及强制 SSO 或允许的桌面版本时的组织更新,这些要企业权益,否则返回 HTTP 402。而终端用户的 SSO 登录、已定义策略的下发接口、所有 GET、所有 DELETE、以及已开通连接的 SCIM 令牌使用,都不设门。文档给的理由是:今天在跑的东西不会因为套餐变化而停掉,新增的摩擦只在”改配置”这一步。

权益判定的实现是 ee/apps/den-api/src/entitlements.ts。这里有个细节值得你亲自去对一下:设计文档里列的权益键和代码里落地的并不一致。代码里是

export const ENTITLEMENT_KEYS = ["sso", "desktopPolicies", "orgControls", "analytics"] as const

而设计文档写的是另一组键名。这种设计稿与实现的漂移在活跃项目里很常见,但它提醒你:判定行为要看 entitlements.ts,文档只能当意图说明看。

档位本身在代码里是 freeteamenterprise 三档,来源可以是默认、Stripe、手工指定或历史沿用。判定逻辑很简单粗暴:门禁开关关着,或者档位是 enterprise,四个权益全开;否则全关。具体的套餐内容与商务规则会调整,以官方最新说明为准,本文不展开。

门禁总开关是 DEN_PLAN_GATING_ENABLED,默认关闭。这一点对自建部署很重要:文档明确说自托管安装默认保持关闭,除非运维方主动打开,开源与可迁出的叙事不受影响。换句话说,你自己拉起来的 Den,默认是全权益状态。

然后是许可证。这个仓库的许可证是分层的,不是笼统的”MIT 开源”。根目录 LICENSE 写明:/ee 目录下的全部内容按 ee/LICENSE 定义的 Fair Source 许可证(FSL-1.1-MIT,Notice 写的是 Copyright 2026 Different AI Inc),此外的部分才是 MIT(Copyright 2026 Different AI)。而本文讨论的组织角色判定、能力授权、套餐门禁,代码全都在 ee/apps/den-api/ee/packages/ 下——也就是说,整个团队控制面落在 Fair Source 那一侧。套餐门禁文档自己也在括号里点了这件事:这些功能的代码已经在 /ee 下,那是它们的授权边界。能不能商用、能不能改,一律以许可证原文为准,本文不提供法律意见。

另有一条独立于套餐的席位门禁:邀请成员时若超出免费席位,接口返回 402 并带 seat_subscription_required。它跟企业权益是两套判定,只是复用了同一个 HTTP 状态码。

顺带说仓库的形状,方便你估计要读多少东西:全仓 3490 个受版本控制文件,apps/ 4 个、packages/ 12 个,ee/apps/ 10 个、ee/packages/ 3 个;packages/docs/ 有 57 份 mdx,其中 model-context-protocol/ 下 10 份是各家客户端接入指南;docs/ 20 份架构 md,evals/ 26 份流程 md,apps/server/src/ 顶层 138 个 .ts;packaging/ 提供三种分发方式。权限这条线主要集中在 ee/apps/den-api/src/ 的那几个文件里。

五、边界与代价:它明确不管什么

授权粒度到”能不能执行”为止,不管”执行时能做什么”。 拿到 viewer 就能搜到并执行一个能力,执行返回的是指令性内容——架构文档把技能、上下文、自定义、代理这几类的执行语义描述为返回最新版本的原始文本,命令类会做参数替换后返回渲染结果,服务端不跑任何命令。它没有对”这段指令跑起来会碰哪些文件、花多少 token”的约束能力,那是你的 Agent 侧要管的事,可以对照Agent 权限给太大会怎样那条线去补。

它承认自己是提示注入面。 架构文档的安全一节第一条就写着:集市内容虽然经组织策划,但仍是第三方文本,指令性载荷是提示注入面。给出的缓解手段是每个载荷都带来源框注、桌面端不自动打开工具输出里的链接、严格的授权、以及由组织管理员控制发布内容。这几条是缓解,不是消除。

凭据集中带来集中的暴露面。 docs/external-mcp-oauth.md 说明,外部 MCP 连接的令牌、刷新令牌、客户端密钥、PKCE 验证串和待授权事务都加密存放在 Den 里,不进 Agent 引擎。这个设计避免了密钥散落到每台机器,代价是这个集中点本身成了要害:安全文档要求组织 API Key 和 SCIM 令牌以哈希存储、SSO 配置加密存储、敏感列用 DEN_DB_ENCRYPTION_KEY 做应用层 AES-256-GCM 加密,并且自托管方要自己负责传输层与存储层加密。你把团队的第三方授权交给它,就要连带接手这套密钥管理责任。

桌面端的控制权是真的会被收走的。 桌面策略目录在 packages/types/src/den/desktop-policies.ts,每一项都自带给用户看的提示语;里面有像”允许添加自定义供应商""允许创建多个工作区""允许访问和修改桌面应用设置”这样的开关,布尔值为 false 就是禁用。管理员在云端一改,员工本机对应功能就变灰并弹出提示。这个能力对企业管理是必要的,但它意味着装上这个桌面应用之后,本机的一部分配置自主权归了组织。

它明确不做的还有几样。 界面上的隐藏与禁用不算安全边界,架构文档说得很直接:路由守卫和处理器检查才是安全边界。组织角色也不等于平台级管理权——平台侧的 Den 管理员是白名单运维人员,组织的 owner 或 super-admin 不会因此获得平台访问。停用与不存在被设计成不可区分:被组织关掉总开关的情况下,检索返回空,执行返回 unknown_capability,跟”根本没有这个东西”字节级一致。

六、上手与避坑清单

别照着用户文档的三角色去设计岗位。 会踩是因为 members-and-rbac.mdx 只讲 Owner/Admin/Member,你据此以为 admin 能改角色、能写设置,实际都不行。避法是先读 docs/cloud-organization-role-access.md 的角色矩阵,再用 organization-role-hierarchy.ts 核一遍等级,按四级设计岗位。

别把安全配置权和日常运维权绑在一起。 会踩是因为默认觉得”管理员就该什么都能管”,结果 SSO、SCIM、API Key 三样落到一堆人手里。避法是按安全文档的建议,用自定义角色单独承载 security_configuration.manage,跟日常的成员管理分开,并且记住默认 admin 拿不到这个权限——它得由已有该权限的人授出。

别指望 IdP 里的组名自动变成 OpenWork 的管理员。 会踩是因为习惯了别的系统靠 IdP 属性映射角色。这里明确不映射,SSO 首登只给 Member。避法是把提权当成一次显式的、需要复核和留痕的操作,从成员列表和审计记录里去查,而不是指望身份提供商推过来。

别以为撤销授权等于数据消失。 会踩是因为路由描述里是软撤销,行还在,只是标了移除时间。避法是把”撤销”理解成状态变更,检索与执行确实立刻不通了,但如果你的合规要求是彻底删除,得另找路径确认。

别在过期的标签页里改敏感设置。 会踩是因为特权路由要求 15 分钟内创建的会话,界面上不会提前提醒你。避法是改角色、改 SSO、轮换密钥之前先重新登录一次。

别把关键能力挂在 SCIM 托管的团队上而不留记录。 会踩是因为这类团队在 OpenWork 里改不动,成员进出全由身份提供商决定,出问题时你在 OpenWork 界面上无从下手。避法是明确区分手工团队和托管团队的用途,托管团队的授权变更要回 IdP 的变更流程里管。

自建部署别默认自己”没有企业功能”。 会踩是因为看了套餐文档就以为 SSO 和桌面策略被锁。实际上门禁总开关默认关闭,权益判定会全开。避法是清楚自己那套环境里 DEN_PLAN_GATING_ENABLED 是什么状态,再决定要不要主动开门禁。

别用”这是 MIT 开源项目”一句话给合规交差。 会踩是因为根目录确实有 MIT 文本,但它前面还有一段分层声明。避法是把 /ee 与非 /ee 分开评估,本文讨论的权限与控制面代码全在 /ee 下,判断以许可证原文为准。

收尾:三个问题自检

要验证你是不是真的读懂了这套模型,问自己三个问题就够:一个普通成员能不能造出一个新插件并把它发给同事?(能造、能发给人和团队,发给全组织不行。)一个默认 admin 能不能把某人提成 admin?(不能,那是 super-admin 及以上的事。)关掉套餐门禁总开关之后,SSO 还能不能配?(能,权益判定会全开。)

三个都答对,接下来按这个顺序读源码收益最高:先 organization-role-hierarchy.ts 建立等级直觉,再 organization-access.ts 看委派与防提权,然后 routes/org/plugin-system/access.ts 和同目录的 store.ts 看授权怎么落库,最后 entitlements.ts 看套餐这层怎么叠上去。想看它在 Agent 侧长什么样,ee/apps/den-api/src/mcp/marketplace-capabilities.ts 是可见集合真正被算出来的地方,也是排查”为什么他搜不到”的第一现场。

至于这套东西该不该引入你的团队,除了权限模型本身,还要一起考虑 MCP 授权面的加固方式,可以参考MCP 授权加固那篇的检查项一并评估。

本篇属于一个把开源AI 工作流桌面应用 OpenWork逐层拆开讲的系列,整体地图见 OpenWork 是什么:把技能与 MCP 打包成能力的开源桌面应用;沿着这条线往下,还可以看 OpenWork 开源桌面应用的团队控制面到底管什么、边界在哪开源桌面应用 OpenWork 接入企业身份:SSO 与 SCIM 分工

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。