diff --git a/devlog/_plan/260817_wave5_execution/070_wave5c_cursor.md b/devlog/_plan/260817_wave5_execution/070_wave5c_cursor.md index 7dccf43d65..9dd13db7fd 100644 --- a/devlog/_plan/260817_wave5_execution/070_wave5c_cursor.md +++ b/devlog/_plan/260817_wave5_execution/070_wave5c_cursor.md @@ -68,3 +68,29 @@ its CI could be judged. For #1900 the fork run was approved, waited to `complete `#1866` needs no decision here: it is an issue with no PR, and the structured Computer Use payload it describes is a design task rather than a merge. +## WP7 outcome + +| PR | Outcome | Evidence | +|----|---------|----------| +| #1900 | merged | `2b12521ee` — CI success 01:12:10Z, merged 01:15:18Z | +| #1895 | merged via #1951 | its blocking review finding fixed on top of its commits | +| #1951 | merged | `93e521c80` — CI success 01:33:45Z, merged 01:37:50Z | +| #1953 | merged | `9eb3a101a` — CI success 01:57:51Z, merged 01:59:12Z | +| #1887 | **held** | must migrate five items into #1896 first; closing it as superseded would delete the catalog-derived guard | +| #1896 | **held** | needs #1887's `cursorNativeExecUsesCodeModeBridge` before it can be canonical | +| #1903 | **held** | conflicts alone on `dev`; needs an author rebase, and is a ~32-file review surface | +| #1866 | **not started** | no PR exists; explicitly scoped out of #1900 | + +**The defect I introduced and the audit caught.** #1951 fixed #1895's blocker — code mode is +decided from `freeform` metadata rather than the name `exec` — but my port of the shell-bridge +predicate dropped the Cursor original's `!tool.namespace` requirement. A namespaced MCP tool +(`mcp__docker__exec_command`) then cancelled code mode on a genuine code-mode turn, silently +stripping the guidance. It failed *safe* — generic rather than false guidance — which is exactly +why nothing caught it, and why an audit that runs the predicate against adversarial catalogs +beats one that reads it. Fixed in #1953, driven red first. + +A second reviewer then probed ten catalog shapes — empty-string namespace, non-boolean truthy +`freeform`, mixed namespaced and bare bridges — and found no remaining misclassification. Worth +recording one behavior it judged correct: when `tool_choice` forces `exec`, a catalog holding +both a freeform `exec` and a bare `exec_command` still classifies as code mode, because the +bridge is filtered out of visibility first. Naming an unreachable tool would be the worse answer. diff --git a/devlog/_plan/260817_wave5_execution/080_wave5d_antigravity.md b/devlog/_plan/260817_wave5_execution/080_wave5d_antigravity.md index 0c1eeb6296..01ae3fbe0b 100644 --- a/devlog/_plan/260817_wave5_execution/080_wave5d_antigravity.md +++ b/devlog/_plan/260817_wave5_execution/080_wave5d_antigravity.md @@ -1,5 +1,11 @@ # WP8 — Wave 5D: Antigravity fingerprint and discovery +> **Read the two correction sections below before the original text.** The order and the +> #1836 disposition in this header were both overturned during execution: the real order is +> `#1891 → #1897 → #1889` for merge-cleanliness but **`#1889` must land first** for +> correctness, and #1836 was already closed. The original text is left standing as the record +> of what changed. + ``` #1889 → #1891 → #1897 (then close #1836 as superseded) ``` @@ -151,3 +157,31 @@ type. | #1889 | **blocked** | unsponsored `src/oauth/` surface; draft | | #1836 | already closed | nothing to do | | #1906 | open issue | the undocumented-`v1internal` policy call belongs to the user | +## WP8 outcome — the wave was smaller than planned + +Two of the four items resolved themselves before this phase ran, which the Gate 0 inventory +could not have known: + +| Item | State | Evidence | +|------|-------|----------| +| #1897 | **already merged** | `aca3c0241`, 2026-08-18T01:31:08Z — ancestor of `origin/dev` | +| #1836 | **already closed** | confirmed at WP6; the plan's "close as superseded" was a no-op | +| #1891 | **held** | draft, four readiness boxes unticked — the author's gate | +| #1889 | **blocked** | `unsponsored_surface` on `src/oauth/google-antigravity.ts` | + +**#1891 verified independently rather than taken on trust.** Merged onto current `dev` in a +scratch worktree: clean, then `bun test` across `client-fingerprint`, +`google-antigravity-wire` and `google-antigravity-oauth` gives **75 pass / 0 fail**, with +`tsc --noEmit` clean. Its description carries the kind of evidence a fingerprint change needs — +a decompiled token sequence with an address, and a live `fetchAvailableModels` + +`generateContent` round trip — because the failure mode here is silent upstream rejection, not +a failing test. + +**#1889 is the second auth-surface block of this campaign**, after #1888. +`.github/scripts/pr-sponsored-surface.cjs` lists `src/oauth/` under `RESTRICTED_PREFIXES`, and +`MAINTAINERS.md` requires explicit security review there. The `maintainer-sponsored` label is +the record that the review happened, so an agent applying it to unblock its own merge would +make that record false rather than merely skip a step. Reported, not cleared. + +The planned order (`#1889 → #1891 → #1897`) is therefore moot: #1897 is in, and the remaining +two are gated on a human decision each — one a readiness checklist, one a security review. diff --git a/devlog/_plan/260817_wave5_execution/090_wave6_closeout.md b/devlog/_plan/260817_wave5_execution/090_wave6_closeout.md index bf87a1a790..5d3c8bffd1 100644 --- a/devlog/_plan/260817_wave5_execution/090_wave6_closeout.md +++ b/devlog/_plan/260817_wave5_execution/090_wave6_closeout.md @@ -76,3 +76,31 @@ of them: Neither is affected by the close-on-dev-merge decision: both are blocked *before* merge, so the policy that governs when a merged fix closes its issue never reaches them. +## WP9 gate result + +Run on the promotion candidate (local `dev`, 6 commits ahead of `origin/dev` at the time): + +| Gate | Result | +|------|--------| +| `bun test --isolate tests` | **12805 pass, 10 skip, 0 fail**, 159382 expect() calls across 826 files (452s) | +| `bun run typecheck` | clean | +| `bun run privacy:scan` | passed | + +### What actually closed, under the close-on-dev-merge decision + +| Closed | Landed via | +|--------|-----------| +| #1894 | #1739 through PR #1921 | +| #1843 | #1860, already released in v2.24.0 | +| #1899 | superseded by the ordering assertion in PR #1923 | + +Everything else stayed open, and none of it for release-timing reasons — which is the point +worth making about the policy change. It removed a gate that was never what held these back. + +### Promotion state + +`dev` carries nine merged PRs from this campaign. `preview` and `main` are both behind it, and +`dev`'s own hosted CI has no completed green run on its current head — the runs at `2b12521ee` +and `aca3c0241` were both cancelled by supersession as later merges landed. The local full +suite above is the evidence that exists; a hosted run on the exact promotion head is the +evidence that does not, and promotion should carry that distinction rather than bury it. diff --git a/src/oauth/google-antigravity.ts b/src/oauth/google-antigravity.ts index 0b2d202a96..cc5d6b1938 100644 --- a/src/oauth/google-antigravity.ts +++ b/src/oauth/google-antigravity.ts @@ -12,7 +12,7 @@ import { OAuthCallbackFlow, type OAuthCallbackFlowOptions } from "./callback-server"; import { generatePKCE } from "./pkce"; import type { OAuthController, OAuthCredentials } from "./types"; -import { antigravityUserAgent, ANTIGRAVITY_GOOG_API_CLIENT_UA } from "../adapters/client-fingerprint"; +import { antigravityUserAgent, ANTIGRAVITY_GOOG_API_CLIENT_UA, ANTIGRAVITY_IDE_VERSION } from "../adapters/client-fingerprint"; const CLIENT_ID = process.env.GOOGLE_ANTIGRAVITY_CLIENT_ID || "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com"; @@ -111,7 +111,12 @@ async function onboardProject(accessToken: string, signal?: AbortSignal): Promis const response = await fetch(`${DAILY_API}/${API_VERSION}:onboardUser`, { method: "POST", headers: { Authorization: `Bearer ${accessToken}`, Accept: "*/*", "Content-Type": "application/json", "User-Agent": antigravityUserAgent(), "x-goog-api-client": ANTIGRAVITY_GOOG_API_CLIENT_UA }, - body: JSON.stringify({ tier_id: "free-tier", metadata: { ide_type: "ANTIGRAVITY", ide_name: "antigravity", ide_version: antigravityUserAgent() } }), + // `ide_version` is a version, not a User-Agent. `antigravityUserAgent()` returns the whole + // header — `antigravity/ide/2.5.5 (aidev_client; os_type=...; arch=...)` — so onboarding was + // sending a parenthesized UA string in a field the real client fills with `2.5.5`. It is a + // fingerprint mismatch rather than a crash, which is why nothing failed: the request still + // succeeds, it just does not look like Antigravity. + body: JSON.stringify({ tier_id: "free-tier", metadata: { ide_type: "ANTIGRAVITY", ide_name: "antigravity", ide_version: ANTIGRAVITY_IDE_VERSION } }), signal: requestSignal(signal), }); if (!response.ok) { diff --git a/tests/google-antigravity-oauth.test.ts b/tests/google-antigravity-oauth.test.ts index 961f3c056f..c8fd1337e3 100644 --- a/tests/google-antigravity-oauth.test.ts +++ b/tests/google-antigravity-oauth.test.ts @@ -4,6 +4,7 @@ import { mkdirSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { getCredential, saveCredential } from "../src/oauth/store"; +import { ANTIGRAVITY_IDE_VERSION } from "../src/adapters/client-fingerprint"; const realFetch = globalThis.fetch; afterEach(() => { globalThis.fetch = realFetch; }); @@ -50,6 +51,29 @@ describe("antigravity project discovery", () => { expect(onboardCalls).toBe(2); }); + // `ide_version` was sending `antigravityUserAgent()` — the whole header, parentheses and all — + // where the real client sends a bare version. Nothing failed, because the request still + // succeeds; it just does not look like Antigravity. A fingerprint is only worth having if it + // matches, so pin the field rather than trusting that nobody re-reaches for the UA helper. + test("onboardUser sends a bare ide_version, not the User-Agent string", async () => { + let onboardBody: string | undefined; + routeFetch((url, init) => { + if (url.includes(":loadCodeAssist")) return new Response(JSON.stringify({}), { status: 200 }); + if (url.includes(":onboardUser")) { + onboardBody = typeof init?.body === "string" ? init.body : undefined; + return new Response(JSON.stringify({ done: true, response: { cloudaicompanionProject: "p" } }), { status: 200 }); + } + return new Response("no", { status: 404 }); + }); + + await discoverAntigravityProject("tok"); + + const metadata = JSON.parse(onboardBody ?? "{}").metadata as { ide_version?: string }; + expect(metadata.ide_version).toBe(ANTIGRAVITY_IDE_VERSION); + expect(metadata.ide_version).not.toContain("antigravity/ide/"); + expect(metadata.ide_version).not.toContain("("); + }); + test("returns undefined when onboardUser aborts with a hard 4xx", async () => { routeFetch((url) => { if (url.includes(":loadCodeAssist")) return new Response(JSON.stringify({}), { status: 200 });