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 引擎。读完本文你将掌握该能力的设计动机与端到端用户流程、加密持久化与密钥管理的实现细节、回环网关与运行时注册机制、出站网络边界的安全约束以及项目源码与测试中的可验证依据。设计动机为什么需要托管式 OAuthOpenWork 的 Server 侧使用 enterprise-mcp-client 作为远程 MCP 消费的统一运行时。该包以依赖注入方式持有网络、持久化、租户/授权、诊断与时钟等契约可完整处理 MCP 协议协商与 OAuth 生命周期而 OpenCode 引擎的直接 MCP 客户端对部分提供商的 OAuth 流程支持不完整。Local Managed MCP OAuth 提供了一条兼容路径对通过 OpenWork 企业 MCP 客户端可以正常工作、但无法通过 OpenCode 直接 MCP 客户端完成授权的提供商OpenWork 在本地桌面工作区中为连接代为持有 OAuthOpenCode 侧只看到一个指向本地回环网关的远程 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可还原为添加远程 MCP 并展开 OpenWork-managed OAuth在本地桌面工作区的 MCP 连接界面添加一个远程 MCP Server展开OpenWork-managed OAuth选项。可选填写预注册信息如果提供商支持预注册客户端可输入已注册的clientId、clientSecret与requestedScopes若提供商支持动态客户端注册DCR这些字段可以留空由 OpenWork 代为注册。OpenWork 完成 OAuth 握手自动执行 OAuth discovery含authorizationServerIssuer绑定、必要时 DCR、PKCE 授权、回环回调、令牌交换以及一次带认证的tools/list校验确认令牌确实可用。OpenCode 接入工具OpenCode 收到一个指向 OpenWork 回环网关的远程 MCP 条目其oauth: false。OpenCode 能看到提供商工具但拿不到任何令牌与密钥。在服务端创建连接与启动授权的入口是POST /workspace/:id/mcp/managedserver.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/connectserver.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包括serverUrl、enabled、oauthapplicationType/requestedScopes/authorizationServerIssuer/clientId/clientSecret、statusneeds_auth/connecting/connected/reconnect_required、lastError、clientRegistrationDCR 结果、credential令牌、authorizations进行中的 PKCE 事务、discoveryOAuth 发现状态。通过企业 MCP 客户端完成握手startLocalManagedMcpAuthorization的执行顺序local-managed-mcp.ts将连接置为connecting并写入运行时 MCP 条目创建带 HMAC 签名的授权 statecreateAuthorizationState绑定 workspaceId/name/connectionId/redirectUri/过期时间与 nonce调用enterpriseClient().connect(...)由 enterprise-mcp-client 完成 OAuth discovery、DCR、PKCE 授权与令牌交换若结果为connected调用verifyTools执行一次tools/list校验并将状态置为connected若需要浏览器跳转则返回authorizeUrl并将状态置为needs_auth。回调端点GET /mcp/oauth/callbackserver.ts校验state与code后调用completeLocalManagedMcpAuthorizationlocal-managed-mcp.ts验证签名 state →completeAuthorization完成令牌提交 →tools/list校验 → 重写运行时条目。state 验证使用timingSafeEqual比较 HMAC-SHA256 签名并校验expiresAt10 分钟窗口与connectionId是否匹配。持久化与生命周期加密 Vault 与状态机加密存储格式Provider 凭据、OAuth 注册、发现状态与 PKCE 事务统一存放在运行时存储目录下的local-managed-mcp-vault.json中vaultPath由 local-managed-mcp.ts 计算并采用AES-256-GCM加密VaultEnvelopelocal-managed-mcp.ts{ schemaVersion: 2, index: { /* 明文、非机密字段投影不含 clientSecret 与令牌 */ }, vault: { schemaVersion: 1, algorithm: aes-256-gcm, iv: base64, tag: base64, data: base64 密文 } }写入时writeVaultlocal-managed-mcp.ts使用 12 字节随机 IV、以openwork-local-managed-mcp-v1作为 AAD关联数据并将文件以0o600权限写入临时文件后原子rename避免半写状态。密钥从哪里来resolveVaultKeylocal-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。状态机与生命周期操作操作实现效果创建createLocalManagedMcpConnectionL766校验 URL 后写入 Vault状态needs_auth并立即写入启用状态的运行时条目授权start/completeLocalManagedMcpAuthorizationconnecting→ 授权成功经tools/list校验后 →connected启停setLocalManagedMcpEnabledL1198同步 Vault 与运行时条目断开disconnectLocalManagedMcpL1215删除已存凭据、清空 PKCE 事务、置enabled: false、状态回needs_auth并停用网关条目删除deleteLocalManagedMcpL1231从 Vault 移除连接并清理运行时条目启动协调reconcileLocalManagedMcpRuntimeEntriesL758Server 每次启动时用当前回环端口与新的 Bearer重写托管运行时条目同时保留加密的 Provider 凭据安全存储变化后的恢复当 OS 安全存储发生变化如换机、重装、Keychain 重置导致密钥无法再解密 Vault 时recoverVaultLockedlocal-managed-mcp.ts会将无法解密的文件改名为local-managed-mcp-vault.json.openwork-backup-时间戳隔离归档从明文index非机密投影重建连接骨架状态统一置为reconnect_required并写入用户可读错误Secure storage on this device changed, so saved sign-ins were cleared. Reconnect to restore this connection.若为 v1 旧格式无 index同时清理失去 Vault 连接的孤儿网关运行时条目pruneOrphanedManagedRuntimeEntries。此外listLocalManagedMcpConnectionsSafeL844在密钥不可用时仍可基于明文 index 提供只读连接列表available: false保证 UI 可渲染而非报错inspectLocalManagedMcpVaultL881为诊断提供不触密的状态检查absent/ok/recovered/secure-storage-unavailable/unreadable。所有 Vault 读写都经由 per-path 队列withVaultQueue串行化防止并发写坏文件。回环网关与运行时注册OpenCode 只看到本地远程 MCP授权完成后OpenWork 会向 OpenCode 的运行时配置写入一条托管网关条目runtimeConfiglocal-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\0name)的 base64url 输出因此作用域被绑定到工作区 连接L717-L727。运行时条目不含秘密e2e 测试明确断言运行时配置JSON.stringify结果既不包含 Provider 地址、也不包含访问令牌local-managed-mcp.e2e.test.ts且oauth为false。网关端点POST/GET/DELETE /mcp/managed/:workspaceId/:nameserver.ts在handleLocalManagedMcpGatewayL1255中先以timingSafeEqual校验 Bearer未授权返回 401连接被禁用返回 503再使用WebStandardStreamableHTTPServerTransport启动一个仅暴露tools能力的 MCP Server并开启enableDnsRebindingProtection、allowedHosts限定为127.0.0.1:port与localhost:port。工具调用转发ListToolsRequestSchema与CallToolRequestSchema处理器内部调用 enterprise-mcp-client 的listTools/callTool由后者携带已保存的令牌向真实 Provider 发起请求凭据从未出现在回环网关的响应中。凭据失效即提示重连工具发现/执行失败时若 Vault 中已无凭据markReconnectWhenCredentialIsGoneL1122网关向 OpenCode 返回需要重连的明确错误已有的reconnect_required原因不会被后续失败覆盖。网络边界防止回环网关变成内网代理原文档强调托管 Provider 在显式本地开发之外必须使用 HTTPS 且只能解析到公网地址。该约束由 apps/server/src/local-managed-mcp-url-guard.ts 实现从四个层面落地1. URL 与协议校验assertLocalManagedMcpUrlL169仅允许http:/https:且非开发模式强制 HTTPSmanaged MCP egress requires HTTPS禁止 URL 内嵌用户名/密码创建连接时即校验非法地址返回400 managed_mcp_url_not_allowedlocal-managed-mcp.ts。2. 私有/保留地址阻断isLocalManagedMcpPrivateAddress覆盖完整的 IPv4 保留段10/8、127/8、100.64/10、169.254/16、172.16/12、192.0.0.0/24、192.0.2.0/24、192.88.99/24、192.168/16、198.18/15、198.51.100/24、203.0.113/24、组播与0.0.0.0/8等以及 IPv6 保留段::、::1、64:ff9b::、fc00::/7、fe80::、fec0::/10、ff00::/8、2001:db8::/32、Teredo/6to4 映射等见 L24-L98。3. DNS 解析与防重新绑定createLocalManagedMcpPublicLookupL145把验证后的解析结果直接交给 socket 连接器所有解析出的地址先经私有地址校验再进入net.connect后续 DNS 应答无法在验证与连接之间偷换地址防止 DNS rebinding。allowPrivateUrls仅在OPENWORK_DEV_MODE1或OPENWORK_ALLOW_PRIVATE_MCP_URLS1时放行L117-L119。4. 重定向再验证createLocalManagedMcpGuardedFetchL235以手动模式跟随重定向上限 5 跳每一跳都重新走 URL/协议/地址校验阻止 HTTPS → 非 HTTPS 降级跨源时禁止携带请求体重定向GET/HEAD之外的方法或带 body 的请求直接拒绝跨源跳转时删除authorization、cookie、proxy-authorization、mcp-session-id、last-event-id、x-api-key、x-auth-token等敏感头redirectedRequestInitL205-L226。这四层共同保证了一个被配置的 MCP URL 或恶意重定向无法把桌面网关变成内网请求代理。持久化契约enterprise-mcp-client 的三类端口托管实现通过createPersistencelocal-managed-mcp.ts向 openwork/enterprise-mcp-client 注入三类窄端口全部落盘到加密 Vault端口职责托管实现要点clientRegistrations预注册或 DCR 客户端的存取有clientId时直接以pre-registered:1版本返回否则保存 DCR 结果first-writer-wins已有注册不覆盖支持显式expiresAt与invalidateauthorizations有状态、可过期、可被消耗的 PKCE 事务用HMAC-SHA256(vaultKey, authorizationId)派生存储键保存codeVerifier与expiresAtload不消耗事务仅在令牌提交时原子消费credentials令牌的加载/保存/失效authorization-code提交会校验并消耗对应授权事务与客户端注册版本防并发乱序覆盖refresh提交通过expectedCredentialRevision做 compare-and-swap过期令牌无刷新令牌时置为reconnect_required每次写入都携带绝对commitExpiresAt与 abort signalensurePersistenceContextL522-L526生命周期超时或中止时适配器必须拒绝/回滚事务避免先报告超时、之后又静默写回凭据的竞态——这正是 enterprise-mcp-client 的安全不变量之一见其 README 的 Security invariants 章节。错误处理与诊断安全错误收敛当握手失败且诊断事件表明存在具体外部原因HTTP 4xx/5xx、OAuthError、RegistrationRejectedError、SdkHttpError、私有地址错误、明确的网络错误码如ECONNREFUSED/ECONNRESET/ETIMEDOUT等见NETWORK_FAILURE_CODESL1011-L1021时统一转换为502 managed_mcp_connection_failed与用户可读文案externalHandshakeApiErrorL1072-L1089并将连接置为reconnect_required。诊断脱敏updateConnectionStatus保存的lastError经sanitizeDiagnosticString处理且会用[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_MODE1/OPENWORK_ALLOW_PRIVATE_MCP_URLS1外托管 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),仅供参考