Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/providers/antigravity.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,9 @@ Interestingly, non-Google models (Claude, GPT-OSS) are proxied through Codeium/W

Antigravity stores auth credentials in a VS Code-compatible state database.

- **Path:** `~/Library/Application Support/Antigravity/User/globalStorage/state.vscdb`
- **Paths (tried in order):**
- `~/Library/Application Support/Antigravity IDE/User/globalStorage/state.vscdb` (v2 - renamed app)
- `~/Library/Application Support/Antigravity/User/globalStorage/state.vscdb` (v1 - pre-rename, retained for backwards compatibility)
- **Table:** `ItemTable` (`key` TEXT, `value` TEXT)

### antigravityUnifiedStateSync.oauthToken (sentinel envelope → protobuf)
Expand Down
54 changes: 31 additions & 23 deletions plugins/antigravity/plugin.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
(function () {
var LS_SERVICE = "exa.language_server_pb.LanguageServerService"
var STATE_DB = "~/Library/Application Support/Antigravity/User/globalStorage/state.vscdb"
var STATE_DB_V1 = "~/Library/Application Support/Antigravity/User/globalStorage/state.vscdb"
var STATE_DB_V2 = "~/Library/Application Support/Antigravity IDE/User/globalStorage/state.vscdb"
var CLOUD_CODE_URLS = [
"https://daily-cloudcode-pa.googleapis.com",
"https://cloudcode-pa.googleapis.com",
Expand Down Expand Up @@ -93,29 +94,36 @@
}

function loadOAuthTokens(ctx) {
try {
var rows = ctx.host.sqlite.query(
STATE_DB,
"SELECT value FROM ItemTable WHERE key = '" + OAUTH_TOKEN_KEY + "' LIMIT 1"
)
var parsed = ctx.util.tryParseJson(rows)
if (!parsed || !parsed.length || !parsed[0].value) return null
var inner = unwrapOAuthSentinel(ctx, parsed[0].value)
if (!inner) return null
var fields = readFields(inner)
var accessToken = (fields[1] && fields[1].type === 2) ? fields[1].data : null
var refreshToken = (fields[3] && fields[3].type === 2) ? fields[3].data : null
var expirySeconds = null
if (fields[4] && fields[4].type === 2) {
var ts = readFields(fields[4].data)
if (ts[1] && ts[1].type === 0) expirySeconds = ts[1].value
var dbPaths = [STATE_DB_V2, STATE_DB_V1]
for (var i = 0; i < dbPaths.length; i++) {
var dbPath = dbPaths[i]
try {
if (!ctx.host.fs.exists(dbPath)) {
continue
}
var rows = ctx.host.sqlite.query(
dbPath,
"SELECT value FROM ItemTable WHERE key = '" + OAUTH_TOKEN_KEY + "' LIMIT 1"
)
var parsed = ctx.util.tryParseJson(rows)
if (!parsed || !parsed.length || !parsed[0].value) continue
var inner = unwrapOAuthSentinel(ctx, parsed[0].value)
if (!inner) continue
var fields = readFields(inner)
var accessToken = (fields[1] && fields[1].type === 2) ? fields[1].data : null
var refreshToken = (fields[3] && fields[3].type === 2) ? fields[3].data : null
var expirySeconds = null
if (fields[4] && fields[4].type === 2) {
var ts = readFields(fields[4].data)
if (ts[1] && ts[1].type === 0) expirySeconds = ts[1].value
}
if (!accessToken && !refreshToken) continue
return { accessToken: accessToken, refreshToken: refreshToken, expirySeconds: expirySeconds }
} catch (e) {
ctx.host.log.warn("failed to read unified oauth token from " + dbPath + ": " + String(e))
}
if (!accessToken && !refreshToken) return null
return { accessToken: accessToken, refreshToken: refreshToken, expirySeconds: expirySeconds }
} catch (e) {
ctx.host.log.warn("failed to read unified oauth token: " + String(e))
return null
}
return null
}

// --- Google OAuth token refresh ---
Expand Down Expand Up @@ -189,7 +197,7 @@
function discoverLs(ctx) {
return ctx.host.ls.discover({
processName: "language_server_macos",
markers: ["antigravity"],
markers: ["antigravity", "antigravity-ide"],
csrfFlag: "--csrf_token",
portFlag: "--extension_server_port",
})
Expand Down
108 changes: 108 additions & 0 deletions plugins/antigravity/plugin.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ function setupLsMock(ctx, discovery, responseBody) {
}

function setupSqliteMock(ctx, oauthEnvelopeB64) {
ctx.host.fs.writeText("~/Library/Application Support/Antigravity/User/globalStorage/state.vscdb", "dummy-sqlite")
ctx.host.fs.writeText("~/Library/Application Support/Antigravity IDE/User/globalStorage/state.vscdb", "dummy-sqlite")
ctx.host.sqlite.query.mockImplementation((db, sql) => {
if (sql.includes(OAUTH_TOKEN_KEY) && oauthEnvelopeB64) {
return JSON.stringify([{ value: oauthEnvelopeB64 }])
Expand Down Expand Up @@ -696,6 +698,112 @@ describe("antigravity plugin", () => {
expect(result.lines.length).toBeGreaterThan(0)
})

it("queries the v2 database path if it exists", async () => {
const ctx = makeCtx()
const futureExpiry = Math.floor(Date.now() / 1000) + 3600
const protoB64 = makeOAuthSentinelB64(ctx, { accessToken: "ya29.v2-access", refreshToken: "1//refresh-token", expirySeconds: futureExpiry })

// Simulate v2 DB file existence
const v2Path = "~/Library/Application Support/Antigravity IDE/User/globalStorage/state.vscdb"
ctx.host.fs.writeText(v2Path, "dummy-sqlite")

let queriedDbPath = null
ctx.host.sqlite.query.mockImplementation((db, sql) => {
queriedDbPath = db
if (sql.includes(OAUTH_TOKEN_KEY)) {
return JSON.stringify([{ value: protoB64 }])
}
return "[]"
})
ctx.host.ls.discover.mockReturnValue(null)

ctx.host.http.request.mockImplementation((opts) => {
if (String(opts.url).includes("fetchAvailableModels")) {
return { status: 200, bodyText: JSON.stringify(makeCloudCodeResponse()) }
}
return { status: 500, bodyText: "" }
})

const plugin = await loadPlugin()
plugin.probe(ctx)

expect(queriedDbPath).toBe(v2Path)
})

it("falls back to pre-v2 database path if v2 path does not exist", async () => {
const ctx = makeCtx()
const futureExpiry = Math.floor(Date.now() / 1000) + 3600
const protoB64 = makeOAuthSentinelB64(ctx, { accessToken: "ya29.v1-access", refreshToken: "1//refresh-token", expirySeconds: futureExpiry })

// Do NOT write to v2 path, so it doesn't exist. Simulate v1 DB file existence.
const v1Path = "~/Library/Application Support/Antigravity/User/globalStorage/state.vscdb"
ctx.host.fs.writeText(v1Path, "dummy-sqlite")

let queriedDbPath = null
ctx.host.sqlite.query.mockImplementation((db, sql) => {
queriedDbPath = db
if (sql.includes(OAUTH_TOKEN_KEY)) {
return JSON.stringify([{ value: protoB64 }])
}
return "[]"
})
ctx.host.ls.discover.mockReturnValue(null)

ctx.host.http.request.mockImplementation((opts) => {
if (String(opts.url).includes("fetchAvailableModels")) {
return { status: 200, bodyText: JSON.stringify(makeCloudCodeResponse()) }
}
return { status: 500, bodyText: "" }
})

const plugin = await loadPlugin()
plugin.probe(ctx)

expect(queriedDbPath).toBe(v1Path)
})

it("cascades and falls back to pre-v2 database path if v2 path exists but is corrupt or has no token", async () => {
const ctx = makeCtx()
const futureExpiry = Math.floor(Date.now() / 1000) + 3600
const protoB64 = makeOAuthSentinelB64(ctx, { accessToken: "ya29.v1-fallback-access", refreshToken: "1//refresh-token", expirySeconds: futureExpiry })

const v1Path = "~/Library/Application Support/Antigravity/User/globalStorage/state.vscdb"
const v2Path = "~/Library/Application Support/Antigravity IDE/User/globalStorage/state.vscdb"

// Simulate both files existing on disk
ctx.host.fs.writeText(v1Path, "valid-db")
ctx.host.fs.writeText(v2Path, "corrupt-db")

const queriedPaths = []
ctx.host.sqlite.query.mockImplementation((db, sql) => {
queriedPaths.push(db)
if (db === v2Path) {
// Simulate SQLite query failure/throw or returning empty rows
throw new Error("database is locked or corrupt")
}
if (db === v1Path && sql.includes(OAUTH_TOKEN_KEY)) {
return JSON.stringify([{ value: protoB64 }])
}
return "[]"
})
ctx.host.ls.discover.mockReturnValue(null)

ctx.host.http.request.mockImplementation((opts) => {
if (String(opts.url).includes("fetchAvailableModels")) {
return { status: 200, bodyText: JSON.stringify(makeCloudCodeResponse()) }
}
return { status: 500, bodyText: "" }
})

const plugin = await loadPlugin()
const result = plugin.probe(ctx)

// Should have queried both paths
expect(queriedPaths).toContain(v2Path)
expect(queriedPaths).toContain(v1Path)
expect(result.lines.length).toBeGreaterThan(0)
})

it("throws when unified oauth envelope is missing and no cache", async () => {
const ctx = makeCtx()
setupSqliteMock(ctx, null)
Expand Down
Loading