news 2026/9/13 16:39:49

OpenWork Local Managed MCP OAuth 深度解析:让 OpenCode 引擎安全消费需要 OAuth 的远程 MCP

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
OpenWork Local Managed MCP OAuth 深度解析:让 OpenCode 引擎安全消费需要 OAuth 的远程 MCP

OpenWork Local Managed MCP OAuth 深度解析:让 OpenCode 引擎安全消费需要 OAuth 的远程 MCP

【免费下载链接】openworkThe open-source alternative to Claude Cowork (powered by opencode)项目地址: https://gitcode.com/GitHub_Trending/ope/openwork

导读

本文围绕 OpenWork 的 Local Managed MCP OAuth 能力展开:当自定义远程 MCP Server 依赖 OAuth 2.0 授权、而其 OAuth 流程无法被 OpenCode 引擎的直连 MCP 客户端正确处理时,OpenWork Desktop 可以在本地接管完整的 OAuth 生命周期(发现、动态客户端注册、PKCE 授权、令牌交换与刷新),并通过一个绑定到工作区与连接的回环 MCP 网关把工具暴露给内置 OpenCode 引擎。读完本文你将掌握:该能力的设计动机与端到端用户流程、加密持久化与密钥管理的实现细节、回环网关与运行时注册机制、出站网络边界的安全约束,以及项目源码与测试中的可验证依据。

设计动机:为什么需要"托管式 OAuth"

OpenWork 的 Server 侧使用 enterprise-mcp-client 作为远程 MCP 消费的统一运行时。该包以"依赖注入"方式持有网络、持久化、租户/授权、诊断与时钟等契约,可完整处理 MCP 协议协商与 OAuth 生命周期;而 OpenCode 引擎的直接 MCP 客户端对部分提供商的 OAuth 流程支持不完整。

Local Managed MCP OAuth 提供了一条兼容路径

  • 对"通过 OpenWork 企业 MCP 客户端可以正常工作、但无法通过 OpenCode 直接 MCP 客户端完成授权"的提供商,OpenWork 在本地桌面工作区中为连接代为持有 OAuth
  • OpenCode 侧只看到一个指向本地回环网关的远程 MCP 条目(oauth: false),它能看到提供商工具,却永远接触不到提供商访问令牌、刷新令牌或 OAuth 客户端密钥。

该路径是按连接显式开启(opt-in)的,且目前仅限桌面端(desktop-only)。既有的直接远程 MCP 与本地命令 MCP 保持原有路径不变(见 docs/features/local-managed-mcp-oauth/README.md)。

用户流程:四个步骤完成托管授权

原文档给出了标准用户流程,结合前端实现(add-mcp-modal.tsx 中managedOAuth: state.oauthExpanded、store.ts 中的managedOAuthAvailable)可还原为:

  1. 添加远程 MCP 并展开 OpenWork-managed OAuth:在本地桌面工作区的 MCP 连接界面添加一个远程 MCP Server,展开OpenWork-managed OAuth选项。
  2. 可选填写预注册信息:如果提供商支持预注册客户端,可输入已注册的clientIdclientSecretrequestedScopes;若提供商支持动态客户端注册(DCR),这些字段可以留空,由 OpenWork 代为注册。
  3. OpenWork 完成 OAuth 握手:自动执行 OAuth discovery(含authorizationServerIssuer绑定)、必要时 DCR、PKCE 授权、回环回调、令牌交换,以及一次带认证的tools/list校验(确认令牌确实可用)。
  4. OpenCode 接入工具:OpenCode 收到一个指向 OpenWork 回环网关的远程 MCP 条目,其oauth: false。OpenCode 能看到提供商工具,但拿不到任何令牌与密钥。

在服务端,创建连接与启动授权的入口是POST /workspace/:id/mcp/managed(server.ts),其请求体结构为:

{ name: string; // 连接名,如 "mock-oauth" url: string; // 远程 MCP Server 的 URL,需为公网 HTTPS oauth: { applicationType?: "native" | "web"; // 默认 "native" requestedScopes?: string[]; // 去重后保存 authorizationServerIssuer?: string; // 精确绑定授权服务器 issuer clientId?: string; // 预注册客户端(可选) clientSecret?: string; // 预注册密钥(可选) }; }

对应的连接状态查询为GET /workspace/:id/mcp/:name/managed,重新发起授权为POST /workspace/:id/mcp/:name/managed/connect(server.ts)。

核心实现:local-managed-mcp.ts 的组成

托管 MCP 的全部逻辑集中在 apps/server/src/local-managed-mcp.ts(约 1300 行),可划分为四个职责域:

职责域关键导出/常量说明
加密 VaultwriteVault/loadVaultLocked/recoverVaultLockedAES-256-GCM 加密的本地持久化文件
状态与持久化createPersistence实现 enterprise-mcp-client 的三类持久化端口
OAuth 编排startLocalManagedMcpAuthorization/completeLocalManagedMcpAuthorization发起与完成 PKCE 授权
回环网关handleLocalManagedMcpGateway/authorizeLocalManagedMcpGateway供 OpenCode 引擎调用的认证 MCP 网关

每条连接在 Vault 中保存的结构(StoredLocalManagedMcpConnection)包括:serverUrlenabledoauthapplicationType/requestedScopes/authorizationServerIssuer/clientId/clientSecret)、statusneeds_auth/connecting/connected/reconnect_required)、lastErrorclientRegistration(DCR 结果)、credential(令牌)、authorizations(进行中的 PKCE 事务)、discovery(OAuth 发现状态)。

通过企业 MCP 客户端完成握手

startLocalManagedMcpAuthorization的执行顺序(local-managed-mcp.ts):

  1. 将连接置为connecting,并写入运行时 MCP 条目;
  2. 创建带 HMAC 签名的授权 state(createAuthorizationState,绑定 workspaceId/name/connectionId/redirectUri/过期时间与 nonce);
  3. 调用enterpriseClient().connect(...),由 enterprise-mcp-client 完成 OAuth discovery、DCR、PKCE 授权与令牌交换;
  4. 若结果为connected,调用verifyTools执行一次tools/list校验并将状态置为connected;若需要浏览器跳转,则返回authorizeUrl并将状态置为needs_auth

回调端点GET /mcp/oauth/callback(server.ts)校验statecode后调用completeLocalManagedMcpAuthorization(local-managed-mcp.ts):验证签名 state →completeAuthorization完成令牌提交 →tools/list校验 → 重写运行时条目。state 验证使用timingSafeEqual比较 HMAC-SHA256 签名,并校验expiresAt(10 分钟窗口)与connectionId是否匹配。

持久化与生命周期:加密 Vault 与状态机

加密存储格式

Provider 凭据、OAuth 注册、发现状态与 PKCE 事务统一存放在运行时存储目录下的local-managed-mcp-vault.json中(vaultPath由 local-managed-mcp.ts 计算),并采用AES-256-GCM加密(VaultEnvelope,local-managed-mcp.ts):

{ "schemaVersion": 2, "index": { /* 明文、非机密字段投影(不含 clientSecret 与令牌) */ }, "vault": { "schemaVersion": 1, "algorithm": "aes-256-gcm", "iv": "<base64>", "tag": "<base64>", "data": "<base64 密文>" } }

写入时(writeVault,local-managed-mcp.ts)使用 12 字节随机 IV、以openwork-local-managed-mcp-v1作为 AAD(关联数据),并将文件以0o600权限写入临时文件后原子rename,避免半写状态。

密钥从哪里来

resolveVaultKey(local-managed-mcp.ts)决定密钥来源:

  • OpenWork Desktop:加密密钥由操作系统安全存储服务(如 macOS Keychain / Windows Credential Manager)持有,磁盘上只持久化受保护的密钥 Blob,与加密 Vault 分离存放;
  • 独立 Server:必须设置环境变量OPENWORK_ENCRYPTION_KEY,其值经 SHA-256 派生为 32 字节密钥;没有明文密钥文件的兜底。密钥缺失或长度非法时,接口返回503 managed_mcp_secure_storage_unavailable

状态机与生命周期操作

操作实现效果
创建createLocalManagedMcpConnection(L766)校验 URL 后写入 Vault(状态needs_auth),并立即写入启用状态的运行时条目
授权start/completeLocalManagedMcpAuthorizationconnecting→ 授权成功经tools/list校验后 →connected
启停setLocalManagedMcpEnabled(L1198)同步 Vault 与运行时条目
断开disconnectLocalManagedMcp(L1215)删除已存凭据、清空 PKCE 事务、置enabled: false、状态回needs_auth,并停用网关条目
删除deleteLocalManagedMcp(L1231)从 Vault 移除连接并清理运行时条目
启动协调reconcileLocalManagedMcpRuntimeEntries(L758)Server 每次启动时用当前回环端口与新的 Bearer重写托管运行时条目,同时保留加密的 Provider 凭据

安全存储变化后的恢复

当 OS 安全存储发生变化(如换机、重装、Keychain 重置)导致密钥无法再解密 Vault 时,recoverVaultLocked(local-managed-mcp.ts)会:

  1. 将无法解密的文件改名为local-managed-mcp-vault.json.openwork-backup-<时间戳>隔离归档
  2. 从明文index(非机密投影)重建连接骨架,状态统一置为reconnect_required,并写入用户可读错误:"Secure storage on this device changed, so saved sign-ins were cleared. Reconnect to restore this connection.";
  3. 若为 v1 旧格式(无 index),同时清理失去 Vault 连接的孤儿网关运行时条目(pruneOrphanedManagedRuntimeEntries)。

此外,listLocalManagedMcpConnectionsSafe(L844)在密钥不可用时仍可基于明文 index 提供只读连接列表(available: false),保证 UI 可渲染而非报错;inspectLocalManagedMcpVault(L881)为诊断提供不触密的状态检查(absent/ok/recovered/secure-storage-unavailable/unreadable)。所有 Vault 读写都经由 per-path 队列withVaultQueue串行化,防止并发写坏文件。

回环网关与运行时注册:OpenCode 只看到"本地远程 MCP"

授权完成后,OpenWork 会向 OpenCode 的运行时配置写入一条托管网关条目runtimeConfig,local-managed-mcp.ts):

{ "type": "remote", "url": "http://127.0.0.1:<port>/mcp/managed/<workspaceId>/<name>", "enabled": true, "headers": { "Authorization": "Bearer <gateway-token>" }, "oauth": false }

关键设计点:

  • Bearer 的生成:网关密钥gatewaySecret进程内randomBytes(32),仅存于内存(gatewaySecretByConfigWeakMap),每次 Server 重启都会轮换;令牌本身是HMAC-SHA256(secret, "<workspaceId>\0<name>")的 base64url 输出,因此作用域被绑定到工作区 + 连接(L717-L727)。
  • 运行时条目不含秘密:e2e 测试明确断言运行时配置JSON.stringify结果既不包含 Provider 地址、也不包含访问令牌(local-managed-mcp.e2e.test.ts),且oauthfalse
  • 网关端点POST/GET/DELETE /mcp/managed/:workspaceId/:name(server.ts)在handleLocalManagedMcpGateway(L1255)中先以timingSafeEqual校验 Bearer(未授权返回 401;连接被禁用返回 503),再使用WebStandardStreamableHTTPServerTransport启动一个仅暴露tools能力的 MCP Server,并开启enableDnsRebindingProtectionallowedHosts限定为127.0.0.1:<port>localhost:<port>
  • 工具调用转发ListToolsRequestSchemaCallToolRequestSchema处理器内部调用 enterprise-mcp-client 的listTools/callTool,由后者携带已保存的令牌向真实 Provider 发起请求;凭据从未出现在回环网关的响应中。
  • 凭据失效即提示重连:工具发现/执行失败时,若 Vault 中已无凭据(markReconnectWhenCredentialIsGone,L1122),网关向 OpenCode 返回"需要重连"的明确错误;已有的reconnect_required原因不会被后续失败覆盖。

网络边界:防止回环网关变成内网代理

原文档强调:托管 Provider 在显式本地开发之外必须使用 HTTPS 且只能解析到公网地址。该约束由 apps/server/src/local-managed-mcp-url-guard.ts 实现,从四个层面落地:

1. URL 与协议校验(assertLocalManagedMcpUrl,L169)

  • 仅允许http:/https:,且非开发模式强制 HTTPSmanaged MCP egress requires HTTPS);
  • 禁止 URL 内嵌用户名/密码;
  • 创建连接时即校验,非法地址返回400 managed_mcp_url_not_allowed(local-managed-mcp.ts)。

2. 私有/保留地址阻断

isLocalManagedMcpPrivateAddress覆盖完整的 IPv4 保留段(10/8127/8100.64/10169.254/16172.16/12192.0.0.0/24192.0.2.0/24192.88.99/24192.168/16198.18/15198.51.100/24203.0.113/24、组播与0.0.0.0/8等)以及 IPv6 保留段(::::164:ff9b::fc00::/7fe80::fec0::/10ff00::/82001:db8::/32、Teredo/6to4 映射等,见 L24-L98)。

3. DNS 解析与防重新绑定

createLocalManagedMcpPublicLookup(L145)把"验证后的解析结果"直接交给 socket 连接器:所有解析出的地址先经私有地址校验,再进入net.connect,后续 DNS 应答无法在验证与连接之间偷换地址(防止 DNS rebinding)。allowPrivateUrls仅在OPENWORK_DEV_MODE=1OPENWORK_ALLOW_PRIVATE_MCP_URLS=1时放行(L117-L119)。

4. 重定向再验证

createLocalManagedMcpGuardedFetch(L235)以手动模式跟随重定向(上限 5 跳),每一跳都重新走 URL/协议/地址校验:

  • 阻止 HTTPS → 非 HTTPS 降级
  • 跨源时禁止携带请求体重定向GET/HEAD之外的方法或带 body 的请求直接拒绝);
  • 跨源跳转时删除authorizationcookieproxy-authorizationmcp-session-idlast-event-idx-api-keyx-auth-token等敏感头(redirectedRequestInit,L205-L226)。

这四层共同保证了:一个被配置的 MCP URL 或恶意重定向无法把桌面网关变成内网请求代理。

持久化契约:enterprise-mcp-client 的三类端口

托管实现通过createPersistence(local-managed-mcp.ts)向 @openwork/enterprise-mcp-client 注入三类窄端口,全部落盘到加密 Vault:

端口职责托管实现要点
clientRegistrations预注册或 DCR 客户端的存取clientId时直接以pre-registered:1版本返回;否则保存 DCR 结果,first-writer-wins(已有注册不覆盖);支持显式expiresAtinvalidate
authorizations有状态、可过期、可被消耗的 PKCE 事务HMAC-SHA256(vaultKey, authorizationId)派生存储键;保存codeVerifierexpiresAtload不消耗事务,仅在令牌提交时原子消费
credentials令牌的加载/保存/失效authorization-code提交会校验并消耗对应授权事务与客户端注册版本(防并发乱序覆盖);refresh提交通过expectedCredentialRevision做 compare-and-swap,过期令牌无刷新令牌时置为reconnect_required

每次写入都携带绝对commitExpiresAt与 abort signal(ensurePersistenceContext,L522-L526):生命周期超时或中止时,适配器必须拒绝/回滚事务,避免"先报告超时、之后又静默写回凭据"的竞态——这正是 enterprise-mcp-client 的安全不变量之一(见其 README 的 "Security invariants" 章节)。

错误处理与诊断

  • 安全错误收敛:当握手失败且诊断事件表明存在具体外部原因(HTTP 4xx/5xx、OAuthErrorRegistrationRejectedErrorSdkHttpError、私有地址错误、明确的网络错误码如ECONNREFUSED/ECONNRESET/ETIMEDOUT等,见NETWORK_FAILURE_CODES,L1011-L1021)时,统一转换为502 managed_mcp_connection_failed与用户可读文案(externalHandshakeApiError,L1072-L1089),并将连接置为reconnect_required
  • 诊断脱敏updateConnectionStatus保存的lastErrorsanitizeDiagnosticString处理,且会用[REDACTED]替换 URL 查询串与 JSON 中的access_token/refresh_token/client_secret/code/state,并截断到 500 字符(L984-L1003)。
  • 输入校验不落盘:URL 非法、名称冲突(409 managed_mcp_exists)等输入错误不会写入 Vault。

验证与测试覆盖

针对该功能的聚焦测试位于 apps/server/src/local-managed-mcp.e2e.test.ts,与文档声明的覆盖范围一一对应:

测试(行号)覆盖点
L264无托管 Vault 时,普通 MCP fallback 路径不受影响
L284初始 OAuth 握手失败时回滚新建连接
L334DCR 与协议协商失败时的安全连接错误
L499可操作的输入错误不持久化连接
L563完整生命周期:拥有 OAuth、向 OpenCode 暴露工具、刷新令牌、重启存活、断开连接;并断言运行时条目不含 Provider 地址与令牌
L713安全存储密钥变化后隔离归档、重建 Vault、重新连接
L873安全存储不可用时明文 index 只读服务
L932无法解密的 legacy v1 Vault 隔离归档并清理孤儿网关条目

除聚焦 e2e 外,桌面端与 Server 端 TypeScript 项目以及编译后的内嵌 Server 构建也会纳入检查,确保桌面场景(依赖 OS 安全存储)与独立 Server 场景(依赖OPENWORK_ENCRYPTION_KEY)行为一致。

适用范围与边界

  • 桌面优先:托管路径目前仅对本地桌面工作区开放(用户界面入口位于桌面端 MCP 连接界面);独立 Server 部署需要自行提供OPENWORK_ENCRYPTION_KEY
  • 按连接开启:既有直接远程 MCP、本地命令 MCP 的连接方式不变,托管路径不会默认启用。
  • 仅公网 HTTPS:除显式开发模式(OPENWORK_DEV_MODE=1/OPENWORK_ALLOW_PRIVATE_MCP_URLS=1)外,托管 Provider 必须是公网 HTTPS 地址,且解析结果不得命中私有/保留网段。
  • 重启语义:OpenWork Server 每次重启都会轮换网关 Bearer 并通过启动协调重写运行时条目,凭据在 Vault 中保持不变,因此重启后连接无需重新授权即可继续使用。

【免费下载链接】openworkThe open-source alternative to Claude Cowork (powered by opencode)项目地址: https://gitcode.com/GitHub_Trending/ope/openwork

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 16:38:03

CentOS 7.8 部署 Oracle RAC 21c:从环境准备到图形/静默安装全攻略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 16:35:47

Lithe-IDEA:专为Spring Boot打造的轻量级Java IDE内核

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 16:34:50

示波器实操八大核心问题:从探头接地到FFT频谱精准测量

1. 这不是教科书&#xff0c;是我在电子实验室熬了17个通宵后写给新手的示波器通关指南“八个灵魂问题&#xff0c;带你入门示波器”——这标题乍看像鸡汤文&#xff0c;但如果你刚拆开一台二手DS1054Z、手边还摆着被烧黑的探头、示波器屏幕上跳着乱码波形&#xff0c;那这八个…

作者头像 李华