Skip to content

fix(antigravity): stop the OAuth client scan from eating a neighbouring string - #242

Merged
Nanako0129 merged 2 commits into
mainfrom
fix/antigravity-oauth-client-scan
Aug 25, 2026
Merged

fix(antigravity): stop the OAuth client scan from eating a neighbouring string#242
Nanako0129 merged 2 commits into
mainfrom
fix/antigravity-oauth-client-scan

Conversation

@Nanako0129

Copy link
Copy Markdown
Owner

Problem

With Antigravity installed but not running, the Antigravity card shows Antigravity OAuth client was not found. Install Antigravity.app or configure its OAuth client. — even though the app is installed and the OAuth client id/secret are present in its language_server binary.

The OAuth route does not need the IDE process: discover_client_from_app reads the .app artifacts from disk with std::fs::read. So a closed-but-installed IDE should resolve a client and fetch quota. It did not, and the cause is in our scan, not in the environment.

Root cause

scan_client_ids finds the .apps.googleusercontent.com suffix, then walks backward over token bytes ([A-Za-z0-9_-]) to locate the start of the id. In the shipped language_server (a ~139 MB packed Mach-O), the bytes immediately in front of a client id belong to whatever string was laid down next to it, with no separator. The walk-back absorbs that neighbour's tail into the id's head.

A Google client id is <digits>-<token>, and valid_client_id requires the head before the first - to be all digits. With a neighbour's tail glued on, the head contains letters, so every id is rejected and the scan returns an empty vector — while scan_client_secrets, which reads a fixed-length window forward from its GOCSPX- prefix, is unaffected.

Measured on a real install (/Applications/Antigravity.app/.../bin/language_server): both client ids present in the binary were rejected, scan_client_ids returned 0, and resolve_oauth_client returned None — taking the entire OAuth route down whenever the IDE was not running.

The existing unit test did not catch this because its fixture separates the id with a \x00 byte. NUL is not a token byte, so the walk-back stops on its own — the fixture's anchor made the defect unreachable.

Fix

After the greedy walk-back, re-anchor on the first - in the candidate and keep only the digit run immediately before it. This trims any non-digit neighbour bytes back off the numeric head without disturbing the id itself.

if let Some(dash) = data[start..end].iter().position(|b| *b == b'-') {
    let mut head = start + dash;
    while head > start && data[head - 1].is_ascii_digit() {
        head -= 1;
    }
    start = head;
}

Known ceiling, recorded in the test: if a neighbour's own tail is digits, it is indistinguishable from the id's numeric head and those digits are kept. Nothing in the byte stream marks the boundary. The only stronger fix is parsing Mach-O string sections instead of scanning bytes, which is disproportionate for a case that Google's 12-digit ids make rare.

Validation

Check Result
cargo test -p tb_core_ffi agent_antigravity 27 passed
New regression test scans_client_id_glued_to_a_neighbouring_string passes; fails with left: [] when the fix is reverted (mutation-checked), matching the real-world ids=0
cargo clippy -p tb_core_ffi --all-targets 27 warnings before and after — none new
Real install, IDE closed, swift run TokenBar --smoke error string moves from OAuth client was not found to token refresh was rejected — i.e. the client now resolves and the route reaches the credential stage

The remaining token refresh was rejected on that machine is a stale ~/.gemini/oauth_creds.json (expired refresh token), which is outside this change: it confirms discovery now succeeds and hands off to the refresh step correctly.

Scope

Discovery only. This is independent of #236 (which adds an agy CLI fallback for the not-installed case); it fixes the installed-but-closed case that should already work. No behaviour change for users whose IDE is running — that path never calls this scan.

…ng string

`scan_client_ids` in `crates/tb_core_ffi/src/agent_antigravity.rs` locates a
Google OAuth client id by finding the `.apps.googleusercontent.com` suffix and
walking backward over token bytes (`[A-Za-z0-9_-]`) to the start of the id. In
the shipped Antigravity `language_server` binary — a ~139 MB packed Mach-O —
the bytes immediately in front of a client id belong to whatever string was
laid down next to it, with no separator. The greedy walk-back absorbs that
neighbour's tail into the id's head.

A Google client id is `<digits>-<token>`, and `valid_client_id` requires the
head before the first `-` to be all digits. With a neighbour's tail glued on,
the head contains letters, so every id is rejected and `scan_client_ids`
returns an empty vector. `scan_client_secrets` is unaffected because it reads a
fixed-length window forward from its `GOCSPX-` prefix rather than walking back.

Effect: with Antigravity installed but not running, the OAuth route — which
reads the `.app` from disk via `discover_client_from_app` and does not need the
IDE process — could not resolve a client at all, and the card showed
"Antigravity OAuth client was not found" even though both ids were present in
the binary. The live-IDE path was unaffected because it never calls this scan.

Fix: after the walk-back, re-anchor on the first `-` in the candidate and keep
only the digit run immediately before it, trimming any non-digit neighbour
bytes off the numeric head without disturbing the id.

Ceiling (recorded in the regression test): a neighbour whose own tail is digits
is indistinguishable from the id's numeric head, so those digits are kept.
Nothing in the byte stream marks the boundary; the only stronger fix is parsing
Mach-O string sections, which is disproportionate given Google's 12-digit ids.

The existing `scans_and_pairs_oauth_client_from_bytes` test missed this because
its fixture separates the id with `\x00`, which is not a token byte, so the
walk-back stopped on its own — the anchor made the defect unreachable. The new
`scans_client_id_glued_to_a_neighbouring_string` test packs the id against a
neighbour with no separator and fails (`left: []`) when the fix is reverted,
matching the observed `ids=0` on a real install.

Verified: `cargo test -p tb_core_ffi agent_antigravity` (27 passed);
`cargo clippy -p tb_core_ffi --all-targets` (27 warnings before and after, none
new); and on a real install with the IDE closed, `swift run TokenBar --smoke`
moves the Antigravity error from "OAuth client was not found" to "token refresh
was rejected" — the client now resolves and the route reaches the credential
stage (the remaining failure is a separately-stale ~/.gemini/oauth_creds.json).

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ceb2195799

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/tb_core_ffi/src/agent_antigravity.rs Outdated
Addresses a P2 from Codex review on the scan fix. The re-anchor step chose the
first `-` in the walked-back candidate and kept the digit run before it. A
Google client id is `<digits>-<token>` with exactly one hyphen — the token and
the `.apps.googleusercontent.com` suffix carry none — so when a neighbour's
packed tail ends in `…<letters><digits>-`, the first hyphen is the neighbour's,
not the id's.

Because `valid_client_id` only checks that the digits before the *first* hyphen
are numeric, that produced a fabricated-but-accepted id: `label123-beta456-real.
apps…` yielded `123-beta456-real.apps…` (head `123`, accepted) instead of the
real `456-real.apps…`. In the installed-but-closed IDE path that id is submitted
to Google's token endpoint and the refresh fails, defeating the fix for such
artifact layouts.

Re-anchor on the last hyphen (`rposition`) instead. Since the id's token is
hyphen-free, the last hyphen is always the id's delimiter, and the maximal digit
run before it is the project number. This is strictly more correct than the
first-hyphen choice: it recovers the real id in both the single-neighbour-hyphen
case and the multi-hyphen case, and leaves the documented ceiling unchanged — a
neighbour whose digits are glued directly onto the project number with no
intervening hyphen still merges into the head, because nothing in the byte
stream marks that boundary.

New assertion in `scans_client_id_glued_to_a_neighbouring_string` locks Codex's
counterexample: it yields `456-real.apps…` and fails with the fabricated
`123-beta456-real.apps…` when `rposition` is reverted to `position`.

Verified: `cargo test -p tb_core_ffi agent_antigravity` (27 passed);
`cargo clippy -p tb_core_ffi --all-targets` (27 warnings before and after);
real install with the IDE closed, `swift run TokenBar --smoke` still reports
`token refresh was rejected` — the two single-hyphen ids in the shipped binary
resolve exactly as before, confirming this only changes the multi-hyphen case.
@Nanako0129
Nanako0129 merged commit 052f549 into main Aug 25, 2026
1 check passed
Nanako0129 added a commit that referenced this pull request Aug 25, 2026
Replace the stale v1.14.1 override notes with v1.14.2's, covering the
Antigravity "quota when the IDE is closed" work.

`scripts/release_notes.sh` ships `release-notes.override.txt` verbatim to the
Sparkle dialog / appcast / latest.json and `release-notes.override.md` verbatim
as the GitHub Release body, so these files are the source of truth for this
release's notes; DeepSeek generation is skipped when they are present. The
deterministic "Thanks: @…" line (txt) and GitHub's "New Contributors" / "Full
Changelog" tail (md) are still appended automatically from PR authorship, which
credits @iF2007 for #236; the md also carries an explicit per-change credit.

Change set since v1.14.1:
- #236 (@iF2007): fetch Antigravity quota when the IDE is closed or not
  installed — OAuth remote plus an `agy` CLI fallback.
- #242: fix the OAuth client scan absorbing a neighbouring value in the
  installed language_server binary, which returned zero client ids and
  disabled the OAuth route for an installed-but-closed IDE.

No literal <, &, or > in either file, so the appcast CDATA/HTML escaping path
is not exercised.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant