From 123bfd2637d7032caf1232d182a2b448ab6cfb73 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Tue, 18 Aug 2026 10:48:22 +0900 Subject: [PATCH 1/5] docs(devlog): warn at the top that the header order was overturned A reviewer noted that a reader skimming only the top of this file takes away the pre-reorder chain and the close-#1836-as-superseded line, both of which execution overturned. The original text stays - it is the record of a decision changing - but the reader now meets the correction before the thing corrected. --- .../_plan/260817_wave5_execution/080_wave5d_antigravity.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/devlog/_plan/260817_wave5_execution/080_wave5d_antigravity.md b/devlog/_plan/260817_wave5_execution/080_wave5d_antigravity.md index 0c1eeb6296..b5b440ba43 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) ``` From 1d4e3422d7bb9cbaf04455d3ca48143c3567ef3a Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Tue, 18 Aug 2026 10:56:59 +0900 Subject: [PATCH 2/5] docs(devlog): record the WP9 gate result Full suite green on the promotion candidate: 12805 pass, 10 skip, 0 fail across 826 files, plus typecheck and privacy scan clean. Three issues closed under the close-on-dev-merge decision - #1894, #1843, #1899. Everything else stayed open, and none of it for release-timing reasons, which is the honest verdict on that policy change: it removed a gate that was never what held these back. Also recording what the promotion does not have. Dev's hosted CI has no completed green run on its current head, because the runs at 2b12521ee and aca3c0241 were both cancelled by supersession as later merges landed. The local full suite is the evidence that exists; a hosted run on the exact promotion head is the evidence that does not. --- .../090_wave6_closeout.md | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) 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. From b8e8136f62b74bbb35f7c8066ff509b3944fca20 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Tue, 18 Aug 2026 11:03:56 +0900 Subject: [PATCH 3/5] docs(devlog): record the WP7 outcome Three PRs landed and four are held, each for a reason that belongs to the PR rather than to my schedule. The part worth keeping is the defect I introduced. #1951 fixed #1895's blocker by deciding code mode from freeform metadata rather than the name exec, but my port of the shell-bridge predicate dropped the Cursor original's !tool.namespace requirement - so a namespaced MCP exec_command cancelled code mode on a genuine code-mode turn and silently stripped the guidance. It failed safe, generic rather than false guidance, which is precisely why nothing caught it and why an audit that runs the predicate against adversarial catalogs beats one that reads it. #1953 fixes it, driven red first, and a second reviewer then failed to break the classifier across ten catalog shapes. --- .../070_wave5c_cursor.md | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) 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. From 97a92419dc651480284c2dc7fa29b59975e81566 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Tue, 18 Aug 2026 11:07:10 +0900 Subject: [PATCH 4/5] docs(devlog): record the WP8 outcome Wave 5D was smaller than planned. #1897 had already merged as aca3c0241 and #1836 was already closed, so half the wave was resolved before the phase ran. #1891 I verified rather than took on trust: clean merge onto dev, 75 pass / 0 fail across the three fingerprint suites, typecheck clean. Its description carries a decompiled token sequence and a live round trip, which is the right evidence for a fingerprint change because the failure mode is silent upstream rejection rather than a failing test. It is held only by its own unticked readiness checklist. #1889 is the campaign's second auth-surface block after #1888. It touches src/oauth/, MAINTAINERS.md requires explicit security review there, and the maintainer-sponsored label is the record that the review happened - so applying it to unblock a merge would make the record false rather than skip a step. --- .../080_wave5d_antigravity.md | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/devlog/_plan/260817_wave5_execution/080_wave5d_antigravity.md b/devlog/_plan/260817_wave5_execution/080_wave5d_antigravity.md index b5b440ba43..01ae3fbe0b 100644 --- a/devlog/_plan/260817_wave5_execution/080_wave5d_antigravity.md +++ b/devlog/_plan/260817_wave5_execution/080_wave5d_antigravity.md @@ -157,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. From e9b2a0a63d6499dc977e95596c08a1d24c24e8b8 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Tue, 18 Aug 2026 11:11:55 +0900 Subject: [PATCH 5/5] fix(antigravity): send a bare ide_version, not the whole User-Agent Onboarding was filling metadata.ide_version with antigravityUserAgent(), which returns the entire header - antigravity/ide/2.5.5 (aidev_client; os_type=...; arch=...) - where the real client sends 2.5.5. Nothing failed, and that is the point. The request still succeeds; it just does not look like Antigravity. A fingerprint is only worth having if it matches, and this one had a parenthesized UA string sitting in a version field on every onboarding call. ANTIGRAVITY_IDE_VERSION already existed one import away. The regression pins the field and asserts the shape it must not have, so the next person reaching for the UA helper here gets a failure instead of a silently wrong fingerprint. Driven red: restoring antigravityUserAgent() fails the new test. --- src/oauth/google-antigravity.ts | 9 +++++++-- tests/google-antigravity-oauth.test.ts | 24 ++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) 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 });