[#733][CODE] 云 MCP 改走标准 MCP OAuth:删静态 bearer、对齐回调、贯通 needs_auth - #813
Conversation
交付后自查发现两件与本票内容无关、但必须留痕的事一、分支上多出三个测试夹具产生的空提交(不是我的改动,PR 内容不受影响)
来源是 对本 PR 无影响,已逐条核实:
我没有去 rebase/force-push 清掉它们 —— 改写共享仓历史不是实现方的裁量范围;而且本仓是 shallow clone,本地 二、base 已落后
|
从「塞一个静态 bearer」改成「走标准 MCP OAuth」不是翻一个开关,三件事必须同时成立: - **`oauth` 由 `false` 改成对象。** 引擎 `mcp/index.ts:241` 的判别就是 `mcp.oauth === false`; 为 false 时不构造 `McpOAuthProvider`,SDK 的 401 分支(`streamableHttp.js:96` 要求 `this._authProvider` 在场)整条不走 ⇒ 该 server **结构上永远进不了 `needs_auth`**, 只能以 `failed` 收场,用户没有任何补救入口。 - **删掉 `headers.Authorization`。** 凭证改由引擎自己的 OAuth 凭证库按 server URL 绑定持久化 并自动刷新。`ALPHA_CLOUD_TOKEN` **没有**被删 —— 它还是 ADR-009「平台代付」的判据 (`platformPays`),只是不再当 MCP 的 Authorization 来源;一起删掉会顺手关掉 web search 主权闸。 - **`redirectUri` 显式写死 `http://127.0.0.1:19876/callback`。** 引擎默认是 `.../mcp/oauth/callback`,而 alpha-web 回环白名单只放 `.../callback` —— 默认值走这条路是断的。 owner 裁决走 3-b:alpha-code 侧显式配成白名单已放行的形态,不改 alpha-web。 `clientId` 用 CIMD URL `https://auth.tidelabs.click/oauth/clients/alpha-code-mcp.json` (不走 RFC 7591 动态注册,也不复用预注册的 `alpha-code` 客户端)。两个跨仓常量在 alpha-code 侧 是具名常量并被测试逐字钉死。 **配置里不输出 `scope`** —— 它在真实取值顺序里排最后,这条链上恒被压过: `@modelcontextprotocol/sdk@1.29.0` 的 `client/auth.js:167-176` `requestedScope || PRM.scopes_supported || clientMetadata.scope`,首次连接落 RS 的 PRM、 吃了 401 之后落 challenge 的 `scope=`,两条都在 config 之前。判据写进注释,不留一个看着像开关的死字段。 renderer 侧补 `needs_auth` 的用户出口:`InstalledState` 多一位 `needsAuth`(五臂联合从前被折成 两个布尔,`needs_auth` 在下游结构上不存在),已安装行与云连接器卡共用**同一个** `McpNeedsAuthAction` 定义,点下去调引擎现成的 `POST /mcp/:name/auth/authenticate`(开系统浏览器 + 等本地回调), 授权后状态从引擎重读而不是本地乐观改。`needs_client_registration` 刻意不并入 —— 那是缺 clientId, 点一百次也没用。 零改 `packages/opencode`(north-star `UPSTREAM_PATHS`):引擎侧的 OAuth 客户端能力本就现成,本票只接线。 Fixes #733 Refs jinjunnn/alpha-work#51
2a4323a to
e6d64da
Compare
更正上一条 + 分支已修复并 rebase上一条评论对成因的判断是错的,我收回。 我把它当成了 shallow clone 的计数假象;实际是测试套件真的往当前分支提交了。编排方用 我上一条只查了 已做的修复
GitHub 现在的账: 为什么这次
|
**B1(Blocker)—— 行为闸没走到最终那一跳。** kill-switch 下真定义不进
`OPENCODE_CONFIG_CONTENT`,只经 ARM/DEF 托管给 ext,由 `installCloudMcp()` **写回**;
用户拿到的是写回之后那一份。原有闸只断 main 的 config 与 DEF = 断中间产物:把写回行改成
「DEF 没有 headers 就补一个 Authorization」,11 条门**全绿** ⇒ 静态 bearer 可在最终咽喉复活。
- 新增生产链闸:真 `injectAlphaConfig` → **真 `installCloudMcp`**(从 ui-mac 测试直接 import
`packages/ext` 生产件,不是替身)→ 对最终 `cfg.mcp.cloud` 逐字面量断言 + 键名单排他。
- ext 夹具从 `headers.Authorization + oauth:false` 迁到 OAuth 形态 —— **夹具接受的形状就是这道闸
真正的边界**,旧夹具自带 headers 让那条变异分支根本不执行。`{file:}` 解析能力拆成独立的
**通用** JSON 用例(第三方 `X-Api-Key`),不再挂在云 server 上给静态 bearer 放行;
「读不到就整个不装」的 fail-closed 随之改用带引用的通用夹具守。
- 复跑同一变异:此前 0 处红,现在 **3 处红**(ui-mac 生产链 1 + ext 单元判据 1 + ext 真实装载基线 1)。
**M1(Major)—— 授权按钮会对用户说谎。** 端点 200 时 body 合法地可以是 `{status:"failed"}`
(`MCP.Status` 是五臂联合),而判据写成「不再 `needsAuth`」⇒ 界面宣布「已重新登录」而 MCP 仍不可用。
改成**正向** `store.mcp[name]?.connected === true` 才算成功;补第 8 条 DOM 负向用例
(200 + failed:断失败提示出现、成功提示不得出现、按钮不留在原地假装还能救)。
复跑错误实现确认变红。
**N1(Minor)—— 活动合同还在说旧话。** `docs/contracts/platform-integration.md` 的
MCP facade 与 Secret transport 两段、以及 `alpha-config-injection.ts` 同处生产注释:
说明静态 bearer 已退出、凭证归引擎 OAuth store,并点明**轮换 `ALPHA_CLOUD_TOKEN` 修不好
未授权的云 MCP**(维护者按旧文排障正会去轮换它),同时保留它仍是 platform-pays 判据这一事实。
历史验证证据一个字未改。
Refs #733
owner 批准就地修订(不新开 ADR、不改编号);触发者 `alpha-code#733`(REQ-130)。
**决策实质不变。** web search 的默认行为、B1 的登录态门控、B2 的 kill-switch 语义一个字没变;
变的只是本 ADR 下文反复引用的那条**凭证传递手段**已被取代:云 MCP 的定义里不再有
`headers.Authorization` / `{file:}` 引用,改为声明标准 MCP OAuth 客户端,凭证由引擎自己的
OAuth store 按 server URL 绑定持久化并自动刷新。
`ALPHA_CLOUD_TOKEN` **仍是** B1 的判据(与 `ALPHA_CLOUD_MCP_URL` 一起判平台代付并驱动 web search
主权),B1 那一行不必改 —— 它只是不再是云 MCP 的 Authorization 来源。排障含义写进 ADR:
**云 MCP 未授权时轮换 `ALPHA_CLOUD_TOKEN` 没有用**,正确动作是走 `needs_auth` → 浏览器授权。
另补一句这条 ADR 当初没写下来、事后咬了人的事实:**`oauth: false` 从来不是中性设置** ——
它让 `needs_auth` 对该 server 结构上不可达(引擎 `mcp/index.ts` 的 `oauth === false` 决定
要不要造 provider,没有 provider 则 SDK 的 401 分支整条不走),凭证缺失只能表现为 `failed`,
产品面上没有任何补救入口。同时点明 R6 的中和条目 `WITHHELD_CLOUD_MCP` **故意**保留
`oauth: false`(那里要的正是「连都别想连」),不要一并改掉。
R5/R6 的收口日志按史料保留(它记录的是当时相信什么,重写会毁掉这份记录);只在两处描述
**当前机制**的句子上就地标注修订:`installCloudMcp` 的 `{file:}` 括号、以及 ext 测试断言那句。
Refs #733
Fixes #733
Refs jinjunnn/alpha-work#51
这改了什么(大白话)
云 MCP 连接器从前是「app 塞一个静态令牌给引擎当 Authorization 头」。这次改成「引擎按标准 MCP OAuth 自己去授权、自己存令牌、自己刷新」。
顺带修好一个用户看得见的死角:令牌不在或过期时,扩展中心以前只会显示一行「未连接」,没有任何能点的东西。现在它说「需要重新登录」,旁边有一颗「去授权」——按下去开系统浏览器走授权,回来当场变成「已连接」。
三处必须同时成立,少一处这条路就是断的
oauth由false改成对象。 引擎packages/opencode/src/mcp/index.ts:241的判别就是mcp.oauth === false;为false时不构造McpOAuthProvider,而@modelcontextprotocol/sdk@1.29.0的dist/esm/client/streamableHttp.js:96要求this._authProvider在场才把 401 当认证错误 ⇒ 该 server 结构上永远进不了needs_auth,只能以failed收场。(oauth也不接受true:packages/core/src/v1/config/mcp.ts是Schema.Union([OAuth, Schema.Literal(false)])。)headers.Authorization。 凭证改由引擎自己的 OAuth 凭证库按 server URL 绑定持久化 + 自动刷新。redirectUri显式写死http://127.0.0.1:19876/callback。 引擎默认是.../mcp/oauth/callback,而 alpha-web 回环白名单只放.../callback。owner 裁决 3-b:alpha-code 侧显式配成白名单已放行的形态,不改 alpha-web。跨仓常量在 alpha-code 侧是具名常量(
packages/ui-mac/src/main/cloud-sidecar-config.ts),并被测试逐字钉死:clientId=https://auth.tidelabs.click/oauth/clients/alpha-code-mcp.json(CIMD URL,不走 RFC 7591 动态注册)redirectUri=http://127.0.0.1:19876/callback(无尾斜杠)配置里不输出
scope(这条会让人误诊成 flaky)实测取值顺序 ——
@modelcontextprotocol/sdk@1.29.0dist/esm/client/auth.js:167-176的determineScope:requestedScope来自 transport 的this._scope(streamableHttp.js:28初始undefined,:355-396在 401/403 后从WWW-Authenticate的scope=里取);clientMetadata.scope才是 config 的oauth.scope(packages/opencode/src/mcp/oauth-provider.ts:51)。⇒ 首次连接落 RS 的 PRM,吃了 401 之后落 challenge,两条都排在 config 之前,写进去不报错也不生效。判据写进了源码注释并点名到行,不留一个看着像开关的死字段。① base fail-set 实测值
base =
ffb7e9c6(分支基于alpha)。base fail-set = 空集 —— 本 worktree 的依赖已用scripts/worktree-link-deps.sh补齐,typecheck 与 DOM 测试没有出现软链假红。ffb7e9c6)src✅ all local gates green✅ all local gates green+10 = 新增
cloud-mcp-oauth.test.ts9 条 +mcp-needs-auth-wiring.test.tswrapper 1 条(其子进程另跑 7 条,数字钉在 wrapper 里)。另外单独跑了两个不在本地门内、但与本次改动同域的 alpha 自有引擎测试(它们在
packages/opencode这个 UPSTREAM_PATH 下,我一个字都不能改,所以必须确认没被我弄红):test/mcp/alpha-cloud-mcp-revival.test.ts+test/mcp/alpha-cloud-mcp-multisource.test.ts→ 15 pass / 0 fail(它们各自构造夹具,不 importmaterializeCloudMcpConfig,故不受签名变更影响 —— 这是跑出来的,不是推断的)。② 五个绕过实验逐条真实结果(全部本人实施 + 实测)
实验前
git status干净(已 commit),每条做完git checkout -- <file>还原并复核「HEAD 未动 + 工作树干净」。headers: { Authorization: \Bearer ${secretRef}` }`)定义里一个凭证通道都没有·引擎自己的 Remote schema 能解这份定义·生产注入面…不含任何凭证通道· 外加既有alpha-config-injection.test.ts三条redirectUri改回引擎默认/mcp/oauth/callback两个跨仓常量逐字钉死·引擎真的会用这份 oauth 造出我们要的 provider(parseRedirectUri解出的 path 变了)clientId换成别的串两个跨仓常量逐字钉死·生产注入面…(见下方「实验量出的缺陷」)oauth改回falseoauth 是对象而不是 false·引擎真的会用这份 oauth 造出 provider·clientInformation() 直接返回 CIMD URL·生产注入面…needs_auth出口(UI)needsAuth: needsAuth(info)(中间那一跳)实验当场量出并修掉的一个真实缺陷:实验 3 第一轮只红了 1 条。原因是生产路径的断言拿
CLOUD_MCP_OAUTH_CLIENT_ID常量当基准 —— 一条自指等价链,改常量两边一起动、恒绿。已改成逐字面量断言(commit1a09846f),重跑实验 3 → 红 2 条(常量闸 + 生产路径闸各一)。区分度另有三条负向用例保证:
failed/needs_client_registration/connected三种状态都不得出授权按钮 ⇒ 一个「永远显示授权按钮」的错误实现过不了。needs_client_registration刻意不并入needs_auth(那是缺 clientId,点一百次也没用,给按钮 = 给一颗按了必失败的按钮)。③ 哪一层验了、哪一层等部署
验了(本地门内,可复现)
packages/core的Remote/OAuthschema 解码、真的McpOAuthProvider算出的redirectUrl/clientMetadata/clientInformation()、以及parseRedirectUri()(本机回调服务器就是用它决定 listen 哪个端口和路径)。上游改了它们这里就该红。injectAlphaConfig(真密钥文件 / 真 env),证明纯函数断言全绿而注入面塞回 bearer 的形态过不了。useExtensions+ 真ExtensionHub,唯一替身是 SDK client(它对面是引擎的 HTTP 面)。引擎mcp.status→ 状态投影 → 「需要重新登录」+ 按钮 →ext.authenticateMcp()→POST /mcp/:name/auth/authenticate→ 授权后从引擎重读状态(不是本地乐观改)→ 按钮消失 + 成功提示到达真ToastViewport。等部署(本票做不到,不假装做到了)
真机端到端授权跑不通是预期的。本人实测三个端点全部 404:
⇒ 授权回跳、令牌过期自动刷新、撤销后进
needs_auth这三条只能在部署之后验。本 PR 没有为了让端到端变绿去 mock 掉任何生产接线,也没有声称验过上面这三条。④
ALPHA_CLOUD_TOKEN的消费者枚举与我删了哪些两条独立检索轴交叉(
grep -a全仓字面量 ×materializeCloudMcpConfig/secretFileRef/cloud.dispatch符号轴):alpha-secret-files.ts:36secretEnvVars()—— 登录时写<userData>/alpha-secrets/ALPHA_CLOUD_TOKENalpha-config-injection.ts:86platformPays(ADR-009 B1「平台代付」判据)alpha-config-injection.ts:317MCPAuthorization头sidecar-env.ts密钥不进 sidecar env 的名单alpha-auth/alpha-cloud-jobs(cloud.dispatch令牌,进程内取用,不读文件)packages/ui-mac/src/main/*里其余Bearer {file:…}命中全部属于第三方 MCP 的用户密钥通道(alpha-mcp-secrets.ts等),与云连接器无关,一个字没碰。②里的第 2 条不是靠"我保证没删",而是有闸:
cloud-mcp-oauth.test.ts正反两半都断 —— 有密钥文件 ⇒ 云 server 注册;删掉密钥文件 ⇒ 整个不注册;并另断代付时本地websearch仍是deny。⑤ 我主动没做的事(留痕)
alpha-web#127(owner 裁决),本票只写clientId那个 URL。packages/opencode一个字(north-starUPSTREAM_PATHS)。引擎侧 OAuth 客户端能力本就现成,本票只接线。因此packages/opencode/test/mcp/alpha-cloud-mcp-revival.test.ts:127-128那句「注入面在代付 + kill-switch 下 R4 写出的那份定义(materializeCloudMcpConfig的形状)」的注释现在措辞略陈旧 —— 它描述的是 R4 时代的静态 bearer 形态。该文件在 UPSTREAM_PATH 下,改它会让 north-star 变红;测试本身用自建夹具、行为正确(它证的是「带静态头的定义可被/connect热连」这个历史对照),15 条全绿。登记在此,不顺手改。installCloudMcp的resolveFileRefs。 定义里已无{file:}⇒ 它对云 server 是 no-op。但注入面托管的是一个任意 JSON,含引用时那条 fail-closed 仍是唯一把关;删掉它等于对未来任何带引用的定义默认放行。只更新了那段会说谎的注释。needs_client_registration做补救入口。 它不是用户点一下能修好的状态(缺 clientId 是配置错误),已用负向用例钉住它不得出授权按钮。authenticateMcp里加客户端超时。 上限由引擎回调服务器持有(oauth-callback.ts的 5 分钟);再加一个更短的 race 只会让界面说「超时了」而授权其实还在进行 —— 那是谎报。🤖 Generated with Claude Code
审计闭合(1 Blocker + 1 Major + 1 Minor,全部已修 + 已自证变红)
B1(Blocker)行为闸没走到最终那一跳 —— 已修
问题:kill-switch 下真定义不进
OPENCODE_CONFIG_CONTENT,只经 ARM/DEF 两个 env 托管给 ext,由packages/ext/src/cloud-websearch-kill.ts的installCloudMcp()写回配置。用户拿到的是写回之后那一份,而我原来的闸只断 main 的 config 与 DEF —— 中间产物。审计实测:把写回行变异成「DEF 没有 headers 就补一个 Authorization」⇒ 11 pass / 0 fail 全绿,静态 bearer 可在最终咽喉复活而「删静态 bearer」这条 AC 一个闸都不响。改了什么
cloud-mcp-oauth.test.ts):真injectAlphaConfig(kill-switch)→ 真installCloudMcp—— 从 ui-mac 测试直接 importpackages/ext的生产件本体,不是替身 —— 对最终cfg.mcp.cloud逐字面量断言,外加Object.keys(...).sort()键名单排他 +headers/Authorization/Bearer/{file:/token 值五项缺席。headers.Authorization + oauth:false迁到 OAuth 形态。夹具接受的形状就是这道闸真正的边界 —— 旧夹具自带headers,那条变异分支根本不执行,所以 ext 那 42 条在变异下也全绿。{file:}解析能力拆成独立的通用 JSON 用例(第三方X-Api-Key定义),不再挂在云 server 上给静态 bearer 放行;「读不到就整个不装」的 fail-closed 随之改用带引用的通用夹具守 —— 通用能力没有丢覆盖。自证变红(同一变异,改后复跑):
installCloudMcp写回时「DEF 无 headers 就补Bearer {file:...}」cloud-mcp-oauth11 pass / 0 fail;cloud-websearch-kill42 pass / 0 failcloud-mcp-oauth9 pass / 1 fail;cloud-websearch-kill41 pass / 2 fail —— 共 3 处红红的三条:
B1:真 injectAlphaConfig → 真 installCloudMcp,最终配置里仍然零凭证通道、installCloudMcp 单元判据 > 装的是完整定义,且云 server 那一份零凭证通道(OAuth 形态)、ext 缺席三态 > 基线:ext 真的装载 ⇒ 云 server 被装进配置。M1(Major)授权按钮会对用户说谎 —— 已修
问题:
POST /mcp/:name/auth/authenticate的 success 类型就是MCP.Status(五臂联合),所以 200 +{status:"failed"}是合法响应。原判据「HTTP 无 error 且 不再needsAuth」在这一格成立 ⇒ 用户看到「已重新登录」,而 MCP 仍然不可用。按缺席取反判成功,等于把failed/needs_client_registration/ 读不出全部读成成功。改了什么:判据改成正向的 —— 重读后仅在
store.mcp[name]?.connected === true时返回成功,其余一律auth-failed。补第 8 条 DOM 负向用例(M1:端点 200 但 body 是 failed):断失败提示出现、成功提示不得出现、并断授权按钮不留在原地假装还能救。自证变红:把实现改回
needsAuth ? fail : ok⇒ 子进程 7 pass / 1 fail,失败点正是这条新用例;改回正向判据后 8 pass / 0 fail。N1(Minor)活动合同还在说旧话 —— 已修
后果具体:维护者按当前权威排障,会去轮换
ALPHA_CLOUD_TOKEN,而不是走needs_auth/ 浏览器授权。改了什么(只动这两处,历史验证证据一个字未改):
docs/contracts/platform-integration.md的 MCP facade 段:改述为「sidecar 收到的是 Cloud MCP URL + 一份标准 MCP OAuth 客户端声明(CIMDclientId+ 回环redirectUri),不再收到 bearer;凭证由引擎的 OAuth store 按 server URL 持有并刷新;缺失/被拒表现为needs_auth,桌面端给再授权动作 —— 轮换ALPHA_CLOUD_TOKEN对它没有作用」。ALPHA_CLOUD_TOKEN仍写、仍是 platform-pays 判据(与 Cloud MCP URL 一起门控云注册与 web-search 主权),但不再是 Cloud MCP 的凭证来源,该 server 的配置里既无 token 值也无文件引用。packages/ui-mac/src/main/alpha-config-injection.ts同处生产注释同步,并额外点明:oauth:false从来不是中性设置 —— 它让needs_auth对该 server 结构上不可达(引擎mcp/index.ts:241),凭证缺失只能表现为failed且没有任何用户补救面。ADR-009 就地修订(owner 已批准,前一版「未改·待裁决」作废)
.claude/rules/adrs/ADR-009-websearch-default.md已就地修订(不新开 ADR、不改编号),amended: 2026-07-27 → 2026-08-03,留痕「owner 批准,触发者alpha-code#733」。决策实质不变 —— web search 的默认行为、B1 的登录态门控、B2 的 kill-switch 语义一个字没动;变的只是这条 ADR 下文反复引用的那条凭证传递手段已被取代。改了四处:
ALPHA_CLOUD_TOKEN仍是 B1 判据(B1 那一行不必改),并写下排障含义:云 MCP 未授权时轮换它没有用,正确动作是走needs_auth→ 浏览器授权。这正是 N1 想堵的那条弯路。oauth: false从来不是中性设置 —— 它让needs_auth对该 server 结构上不可达,凭证缺失只能表现为failed,产品面上没有任何补救入口。同时点明 R6 的中和条目WITHHELD_CLOUD_MCP故意保留oauth:false(那里要的正是「连都别想连」),不要一并改掉 —— 两处方向相反。installCloudMcp的{file:}括号、ext 测试断言那句。R5/R6 的收口日志按史料保留,只加标注不重写:那份日志记录的是「当时相信什么、后来被什么证伪」,重写它等于毁掉这条 ADR 最值钱的部分。读者不会据此误行动 —— 顶部修订段已经把当前形态定死。
最终全量门(ADR 修订后,真实数字)
exit=0,7/7 全绿,✅ all local gates green。跑完已核污染守卫:HEAD88e35ef0前后同一个、.git/config无user.*⇒ 本次测量有效。srcplatform-integration.md+ADR-009)审计闭合后的全量门(真实数字)
exit=0,7/7 全绿,✅ all local gates green。跑完已核污染守卫:HEAD 未动(e6d64da2前后同一个)、.git/config无user.*⇒ 本次测量有效(ac#815的污染是间歇 2/5,HEAD 被顶走的那次测量作废)。ffb7e9c6srccloud-mcp-oauth下界 9→10)ext +1 =
{file:}通用用例拆出来的那条;ui-mac +1 = B1 的生产链闸。子进程 DOM 用例 7→8(M1),数字钉在 wrapper 里。