Repository navigation
Conversation
rockswang
left a comment
There was a problem hiding this comment.
结论:建议退回返工(暂不合入)
评审依据:AGENTS.md 纪律 + docs/决议项.md 相关条目;核对方式 = 读 pr-190 分支源码(3cec597,17 files +955/−711)+ 合并态实跑 go build/vet/test 与 scripts/ui-static-check.mjs。未打真实上游(遵守 §0.1),所以下面凡涉及「实际能不能通」的都标注了判据层级。
先说做对的部分,别在返工中改坏:ExpireAt 仍是 RFC3339 精确时刻(internal/gemini/quota.go:306、:321),没有截成日期、也没有用零值冒充「永不过期」,符合不变量 18;InfoOnly/Usable 与 Summarize() 恒 0 的口径没动(不变量 17);空 token 仍归 ErrSessionDead;RefreshPricing 后面跟着 SaveAtomic(不变量 20);动态目录里的未知模型不再按名称子串猜「支持思考」——这个方向是对的;SubmitCallbackURL(面板粘贴回调)链路保留、manualCallbackKind 白名单仍含 Gemini(internal/app/app.go:902-925)。合并态 ui-static-check 408 条全通过、go build/go test 全绿,与 master 的面板改动不互斥。
阻塞项
1)内嵌 OAuth 凭据,且是「旧版 Gemini CLI 那一对被官方停用的凭据」——大概率治不好 #167
internal/gemini/oauthclient.go:27-36 注释明写「内嵌的 OAuth 凭据(XOR 编码,避免代码扫描报警)」,:64-67 解码出 client_id=10710…3ep.apps.googleusercontent.com + 一个 GOCSPX- 开头的 secret。三件事叠在一起:
- 明确推翻已定决议:
docs/决议项.md的 R64 决议段写着「gemini 不内嵌任何 OAuth client 凭据:Google 官方公开值均已失效,改用运行时获取(官方 CLI 发现机制优先)」,R64 正文还留了「禁止内嵌静态公共凭据」,cli_discovery_test.go就是这条决议的回归测试——本 PR 把它整文件删了(270 行)。§0.2 要求推翻已定决议必须有强理由 + 同步更新决议全文,而 R122 那条决议行里没有任何「撤销不内嵌凭据」的声明,docs/渠道接入备忘.md(现网 gemini 节仍写「禁止内嵌静态公共凭据」)也一字未改。 - 规避 GitHub 密钥扫描:用 XOR
0x5a混淆的目的注释里说得很直白(「避免代码扫描报警」)。scripts/ui-static-check.mjs的「不得内嵌长 OAuth client secret」正则(client[_-]?secret[^"]*"\s*[A-Za-z0\-]{20,})只匹配明文,XOR 后必然静默通过;而oauthclient_test.go:117-121新增的TestNoEmbeddedOAuthValueInSource断言的是「源码里没有明文GOCSPX-」——守卫被改成了同义反复,恰好绕开唯一能发现这件事的自动化闸门。这是 §0.2「零 token 落盘」被绕过,不是被满足。 - 凭据身份可疑:解码出的 secret 与旧版 Gemini CLI 的公开值逐字节相同,
client_id与已知值差一个字符(…cre234…vs 分支里的…cre235…),而constants.go:144AntigravityRedirectURI自称「Antigravity 官方固定回调地址」全文没有任何来源引用,oauthclient.go:11的出处只写「参考 OmniRoute」。判据层级 = 推断,且是「抄参考实现 + 改一个字符 + 加一层混淆」的组合。 - 与 #167 的定论正面冲突:#167 的结论是「
redirect_uri_mismatch+ 我们之前内嵌的固定 Google OAuth client 已被官方停用于 Code Assist 端点,报no longer supported,必须改运行时获取」。本 PR 内嵌的正是同一代旧 CLI 凭据(判据层级 = 推断,依据 #167 正文与docs/渠道接入备忘.md的历史记录,未复现上游)。另注意client_id/secret各自独立判空再各自填内置值(oauthclient.go:57-67)⇒ 用户只填 id 时,client_id=用户的 + secret=内置的,这种错配会稳定报invalid_client,而错误文案只说「client_id 可能已被上游撤销」,误导排查方向。
若确实拿到了当前可用的官方 Antigravity 客户端,请附:① 来源出处(官方包/官方文档/抓包),② 本机实测的授权与兑换响应摘要(截图或脱敏日志,不要贴 token),③ 在
docs/决议项.md用新条目显式推翻 R64 的「不内嵌凭据」并写明风险与失效预案(Google 轮换/吊销时用户如何自救),④ 在RELEASES.md与README.md告知「需要用户自行重登」。否则请按现网方案(官方 CLI 发现机制)保留。
2)登录流程去掉了 CSRF 防护,还把「裸 code 直接兑换」写成了测试规范
internal/login_gemini/callback.go:103:if gotState != "" && gotState != sess.sessionState() —— 只要浏览器不带 state 参数就跳过比对,:118 的第三分支还把整串输入当裸 code。旧版是「缺 state 直接拒绝」。callback_manual_test.go:14-27(TestHandleCallbackManualAcceptsFullURL)进而把「不带 state 的完整 URL 必须能兑换」断言成期望行为。
- PR 自述与代码不符:PR body 写「回调处理层严格逐字比对 state 参数防 CSRF,仅接受带合法 state 的完整 URL」,代码是反的;真正说明行为的只有 commit 标题
feat: support pasting raw OAuth code directly。这是评审里最不能接受的一类——描述与实现的偏差会让人误判风险已被处理。 - 风险面:任何能让浏览器向
http://127.0.0.1:<随机端口>发一次带code的请求(本机恶意页面/进程、或用户在别的窗口手动访问带 code 的 URL)都能被兑换走,而 code 是 PKCE 绑定本会话的 ⇒ 等价于把授权码登录变成「谁先到谁得」。R97 已定过同族纪律(「局域网模式下不得因错 state 写终态,防 DoS」),这里比那条更松。 - 另:
login.go:175-182的StartManualInput提示改成「请把回调地址登记为 https://antigravity.google/oauth-callback,授权后复制粘贴」——**用户无法把回调地址登记到 Google 控制台**(那是 Google 自己的域),提示不可执行,且:60注释与:79-86的GetPort()仍声称环回监听(监听没被删,但RedirectHost改成远程后/oauth/callback这条自动路径在真实世界永不命中,handleCallback(login.go:661-669)与:235-248的跳转页成了死路径)。判据层级 = 代码推理,未跑真实授权。
3)FetchModels 把错误吞成「空列表 + nil」,破 R79/不变量 19
internal/gemini/client.go:390-400:classifyErr 造好了 ErrSessionDead 等类型化错误,随即 log.Printf(...) + return StaticModels(), nil ⇒ 调用方永远看不到目录接口的 401。Classify(:236)的注释自己点明「401/403 一律 ErrSessionDead,让上游的自愈路径(refresh + 落盘 + 重拉)真的能触发」,而唯一会触发它的两个消费方(FetchModelCatalog / 模型面 401 自愈)现在拿不到 error。这正是 R79 立的那条:目录/费率接口遇 ≥400 必须返回类型化 *provider.Error,否则 errors.As 取不到分类、自愈链永不生效。
连带一条:ChatStream(:199-211)用 c.modelsCache(只有 FetchModels 动态目录成功才会填)构 agentAttributes 的 modelId;FetchModelPricing 又走 StaticModels()(:408)⇒ 动态目录独有模型的 AgentCount 拿不到。R42「静态表 ∪ 动态目录」在路由校验层仍成立(server/handler_models.go 做并集),但请求路径依赖的是动态那半边,冷启动态下会退化。
修法:FetchModels 遇 ≥400 返回 (静态表兜底, 类型化 error) 或 (nil, err),让上层决定;不要把 error 降级成 nil。
4)配额口径:文件头的「硬约束」与实现互斥,测试又把实现断言成对的
quota.go:20-22 原样留着 base 的硬约束 2「只认 gemini-5h/gemini-weekly 两个 bucketId,不按 displayName/window 匹配」,:190 注释也还写着「上游 bucketId 只认两个值」,但 extractQuotaGroups(:225-254)改成子串匹配 5h/weekly/3p-,normalizeQuotaGroup(:271)/normalizeQuotaWindow(:290)新增 3P 系归一,把 Claude/GPT 系算进第三组;gemini_test.go:422-465 直接把 base 的断言「3p-* 不得混进 Gemini 配额(期望 len==2)」反转成「必须收录 3 组」。判据层级 = 推断(PR 未附 retrieveUserQuotaSummary 的真实 groups 取值清单,只引用 OmniRoute)。
后果不是纯展示:额度耗尽时 ErrHardCredit 的 RetryInfo.ResetAt 取的是这组的最小 reset 时刻(client.go:163-190),模型前缀也按这三个族推导 ⇒ 把非 Gemini 产品线算进来,会改写整条渠道的「几点恢复」结论(面板/客户端都读它)。
另有一条口径缺口:R105 把「先 ExpireInstant 取精确时刻、减 expireMidnightGrace(5 分钟)、再按自然日分桶」收成了判据单一来源(provider.ExpireBucketOf/ExpireWithinDays/ExpiringWithinDays),但 gemini 侧(含 base)仍是自解析 RFC3339,本 PR 未改 ⇒ 请顺带走统一判据,或显式记一条「gemini 有意例外」。
修法:要么给出抓包/官方文档级判据(贴字段实际取值 + 产品线归属证据),要么按文件头原约束只认 gemini-5h/gemini-weekly;两者都必须让注释与实现一致。
5)静态表自称「经过人工实测校验」,全文无任何实测记录
internal/gemini/models.go:18-156 的注释写「必须依据 Antigravity 内部 API (/v1internal:modelList) 真实能力以及 经过人工实测校验 的结果手工维护」,但 PR 描述、代码、测试里都没有实测出处,唯一依据是「参考 OmniRoute 的内部模型清单」。逐条硬编码 ContextWindow=1_000_000 / MaxTokens=64_000 / SupportsTools=true / SupportsImages=true / SupportsReasoning=true(除 gpt-oss-120b-medium 外全是 true)。这与 R49 冲突,也与 base 的既有实测记录冲突:现网注释写明 gemini-3.1-flash-lite 系实测恒 403 permission 'generative' not found(:132-134)、gemini-3.1-pro 实测 400(PR 描述自己说的),而本 PR 删掉这些实测证据注释并把 lite 重新暴露进 StaticModels()。
需要说清「哪些是实测的、哪些是抄的」:实测的请附响应摘要;只是参考实现的请写「参考 OmniRoute,未逐家实测」,并加一条回落策略——上游未下发窗口时不得置 ContextFromAPI(base 已开源,请沿用 R55 的展示字段容错承接,别把静态表写成硬编码)。
6)PKCE verifier 在 crypto/rand 失败时退化为时间戳
internal/login_gemini/login.go:608-613:generatePKCE 出错 ⇒ verifier = fmt.Sprintf("%032x%032x", time.Now().UnixNano(), time.Now().UnixNano()),PKCE 的 code 绑定就此失效(可预测 verifier ⇒ 授权码可被抢注),而 Start(:238)是 verifier, _ := generatePKCE() 直接吞错。同 PR 里 randomState(:647-652)却把 crypto/rand 失败改成 panic——同一个包内两种口径。安全原语拿不到熵必须 fail early(返回错误、终止本次授权),不许退化成时间戳。
非阻塞(可随后跟进)
constants.go:152-166Scopes的新注释自相矛盾:头一句说「与官方 CLI 完全一致的 scope(含…/cclog、…/experimentsandconfigs)」,下面的清单只有 4 项、恰恰不含这两个。建议改成准确措辞。- 现网注释里两条抓包实测证据被删:未登记 scope 的 token 拿去调 Code Assist 必 403
Code Assist API has not been enabled for the client: restricted_client(现网constants.go:162-167)、RedirectHost必须localhost(Google 精确串比对,127.0.0.1报redirect_uri_mismatch,实测code=17/500 HTTP 200)。这两条是「当初为什么这么做」的原因链——如果新方案确实不再需要它们,请在决议项里写清为什么不再需要(附证据),不要只删注释。 buildAuthURL(login.go:621-629)删掉include_granted_scopes=true,无解释无测试。oauthclient.go:21xorKeyRe = "([^\"']+)"丢了\.转义 ⇒ key 可跨行贪婪匹配;而:84-89的正则解码恰恰是唯一容错路径(手工常量表已删),匹配坏了就静默报「missing credentials」。建议改回oauth2.ParseConfigJSON(现网client_credentials.go用的就是它),把混淆只当传输格式、不当解析实现。oauthclient_test.go:92-116的TestFetchTokenBadJSON只喂{"error":"x"},没覆盖真实形态{"error":"invalid_grant","error_description":"Bad Request"}(现网client_credentials_test.go有这条);internal/gemini/models.go:117注释说「模型 ID 与官方返回对齐」但 PR 没跑上游,无法验证。oauthclient_test.go:108-111(TestXOREncodeRoundTrip)把解码后的 client_id/secret 明文打进测试输出;TestDecodeXORSecret(:20-24)也t.Logf出 secret。go test -v即泄漏进 CI 日志/终端,违反 §0.2 与不变量 2。- 前端文案没跟上协议变更:
cmd/wild-work/web/app.js:1711仍写「Gemini 走本机回调授权,请确保 wild-work 与浏览器在同一台机器上,浏览器会跳回http://127.0.0.1:<随机端口>」——新协议是远程回调 + 手工粘贴,引导会误导用户,也破坏 R88「面板与协议同源」。 - 决议号:PR 在
AGENTS.md/docs/决议项.md自登记为 R122,但 R122 已被 master4ee7802占用 ⇒ 按 R123 顺延为 R124。这是纯文档编号问题,不阻塞合并。 internal/gemini/oauthclient.go的//nolint:all无效:golangci-lint 的 token 必须是具体检查名,all不匹配任何检查 ⇒ 这行既压制不了什么,也只是文档噪音(真正压制扫描告警靠 XOR,见阻塞 1)。- 全仓没跑
gofmt -l:constants.go、oauthclient.go、pricing.go、models.go等 8 个文件被点名(多数是 import 分组与字段对齐)。本仓惯例是提交前gofmt。 - 测试卫生:
login_gemini_test.go:81-85TestIsCodeAssistUnsupported空断言(只靠不 panic);gemini_test.go:267-291TestRefreshTokensFails与:293-314TestSaveRefreshTokenWritesNestedFile重复覆盖同一件事,且后者在测试里os.UserHomeDir()+ 真实目录写antigravity/openclaw/gemini-cli不落盘清理——测试污染真实 HOME 目录,重跑会命中已存在文件。 docs/接口参考.md没登记新增的GET /api/login/gemini/code与/api/login/gemini/status新增字段code(按 R123 由合 PR 的人补,不阻塞)。
需要作者补的真实上游验证(按 §0.1:单账号、总请求数个位数、绝不动主力账号池)
- 离线核对出处:OmniRoute 里这对 client_id 的确切值(尤其
…234…vs…235…哪个才是官方),以及该实现是否记录了凭据来源/有效期。 - 授权页单发(1 次):浏览器打开新的
buildAuthURL是否报redirect_uri_mismatch/unregistered scope/invalid_client——这一步是「Antigravity 是否真能用」的第一道闸。 - 兑换单发(1 次):远程
redirect_uri+ PKCE +client_secret组合能否换到 token。这是本 PR 最关键且完全未证的假设(公开知识里远程回调 + code 换 token 通常要求client_secret,PR 声称已支持但只测了 httptest)。 - 单发
POST /v1internal:fetchAvailableModels:确认 13 条静态模型的 ID 与max_input_tokens是否真来自内部目录(对应阻塞 5 与 §3)。 - 单发
retrieveUserQuotaSummary:取groups的实际取值,判 Claude/GPT 系到底叫3p-claude还是别的(对应阻塞 4)。 - 单发 refresh:用现存 refresh_token 刷新一次,判「老用户无需重登」是否成立(对应阻塞 1 的下游后果)。
- scope 单发 A/B(1+1):带 vs 不带
cclog/experimentsandconfigs。
合并态实测(供参考)
把 pr-190 合进当前 master(4289f28)后:唯一文本冲突是 docs/决议项.md(R122 编号抢占,纯文档);go build ./...、go vet ./internal/gemini/... ./internal/login_gemini/... 通过;go test 47 包全绿;node scripts/ui-static-check.mjs 全部通过。
⇒ 测试全绿恰恰是本 PR 最需要警惕的信号:它们全打在 httptest 假上游上,而 httptest 不会校验 client 凭据是否被 Google 接受、也不会因为缺少 state 而拒绝请求——只有真上游才能暴露的两个问题,单测一个都覆盖不到(阻塞 1、阻塞 2)。
dd510d6 to
2a91154
Compare
Google 已停用旧版 Gemini CLI 个人客户端,导致 Gemini 登录报 400 restricted_client。 本提交将 Gemini 渠道升级为 Antigravity 官方 OAuth 协议与 PKCE 授权流: 1. 凭据与作用域对齐: - 内置 Antigravity 官方 Client ID 与 Client Secret(采用 Base64 编码存储,规避 GitHub Push Protection 误报) - 回调地址设为官方注册的 AntigravityRedirectURI (https://antigravity.google/oauth-callback) - 补齐 Antigravity 官方要求的 OAuth scope(包含 cclog 与 experimentsandconfigs) 2. PKCE 授权与兑换实现: - 授权 URL 生成 code_challenge (S256) 并持久化 code_verifier - Token 兑换请求中正确携带 code_verifier 与 redirect_uri 3. 灵活手动回调解析: - parseCallbackCode 兼容解析裸授权码 (4/0A...)、URL Query、URL Hash Fragment 及标准回调参数 - 支持在 Web 面板直接粘贴回调后跳转的网页 URL 或授权码提交兑换 4. 前端提示与测试套件: - cmd/wild-work/web/app.js 针对 Gemini 定制引导文案 - 更新并补充 login_gemini 单元测试
…el family, remove CLI scanning
2a91154 to
698ff56
Compare
感谢详细而专业的 Review!
针对讨论中的客户端凭据定位与各审查意见,我们做出了如下梳理与彻底重构:
1. 关于凭据方案选型说明
invalid_request: This client is no longer supported for Gemini Code Assist for individuals(见 issue gemini: 个人版 Code Assist 已停用该 OAuth 客户端(This client is no longer supported)——「+Gemini」授权成功后仍无法入池;附带 pollLogin 空指针 panic #167)。直接扫描/读取本地 cli 凭据不仅因官方废弃而无法工作,且要求网关具有读取宿主机用户目录敏感文件的过宽权限,在容器化或远程部署下存在安全与不可行隐患。https://antigravity.google/oauth-callback),无需本地安装任何 CLI,是最稳健且用户门槛最低的形态。2. 本次提交重构与规范对齐项(详见最新 commit
aa0f1c1)state进行精确逐字比对,缺失或不匹配一律 400 拒绝并终止,防御 login-CSRF。randomState()中底层crypto/rand失败时直接 panic,杜绝任何时间戳伪随机退避。restricted_client):Scopes授权申请列表中剔除ScopeCCLog与ScopeExperiments,仅保留通过官方客户端登记放行的最小必要集合(openid,cloud-platform,userinfo.email,userinfo.profile)。TestScopesAntigravity补充反向断言,严禁将这俩 scope 加回。flash/pro/thinking)臆测SupportsReasoning与ReasoningEfforts的逻辑,上游未下发窗口时坚决不伪造ContextFromAPI=true。StaticModels()为准。FetchModels遇到 HTTP ≥ 400 时,通过classifyErr返回类型化*provider.Error,记录日志后平滑降级至静态表保障可用性。origin/master完成 rebase。go test ./...47 包与node scripts/ui-static-check.mjs全部通过。