Skip to content
Closed
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
57 changes: 57 additions & 0 deletions .github/workflows/ghcr-hotfix.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: ghcr-hotfix

on:
push:
branches:
- fix/antigravity-metadata-alignment
- hotfix/**
pull_request:
branches:
- main
- master
workflow_dispatch:

permissions:
contents: read
packages: write

env:
IMAGE_NAME: ghcr.io/happycastle114/cliproxyapi

jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up QEMU
uses: docker/setup-qemu-action@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.IMAGE_NAME }}
tags: |
type=raw,value=hotfix-latest,enable=${{ github.event_name == 'push' }}
type=sha,prefix=hotfix-

- name: Build and push image
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
16 changes: 4 additions & 12 deletions internal/auth/antigravity/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,11 +154,7 @@ func (o *AntigravityAuth) FetchUserInfo(ctx context.Context, accessToken string)
// FetchProjectID retrieves the project ID for the authenticated user via loadCodeAssist
func (o *AntigravityAuth) FetchProjectID(ctx context.Context, accessToken string) (string, error) {
loadReqBody := map[string]any{
"metadata": map[string]string{
"ideType": "ANTIGRAVITY",
"platform": "PLATFORM_UNSPECIFIED",
"pluginType": "GEMINI",
},
"metadata": antigravityRequestMetadata(),
}

rawBody, errMarshal := json.Marshal(loadReqBody)
Expand All @@ -175,7 +171,7 @@ func (o *AntigravityAuth) FetchProjectID(ctx context.Context, accessToken string
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", APIUserAgent)
req.Header.Set("X-Goog-Api-Client", APIClient)
req.Header.Set("Client-Metadata", ClientMetadata)
req.Header.Set("Client-Metadata", antigravityJSONClientMetadata())

resp, errDo := o.httpClient.Do(req)
if errDo != nil {
Expand Down Expand Up @@ -246,11 +242,7 @@ func (o *AntigravityAuth) OnboardUser(ctx context.Context, accessToken, tierID s
log.Infof("Antigravity: onboarding user with tier: %s", tierID)
requestBody := map[string]any{
"tierId": tierID,
"metadata": map[string]string{
"ideType": "ANTIGRAVITY",
"platform": "PLATFORM_UNSPECIFIED",
"pluginType": "GEMINI",
},
"metadata": antigravityRequestMetadata(),
}

rawBody, errMarshal := json.Marshal(requestBody)
Expand Down Expand Up @@ -279,7 +271,7 @@ func (o *AntigravityAuth) OnboardUser(ctx context.Context, accessToken, tierID s
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", APIUserAgent)
req.Header.Set("X-Goog-Api-Client", APIClient)
req.Header.Set("Client-Metadata", ClientMetadata)
req.Header.Set("Client-Metadata", antigravityJSONClientMetadata())

resp, errDo := o.httpClient.Do(req)
if errDo != nil {
Expand Down
41 changes: 36 additions & 5 deletions internal/auth/antigravity/constants.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
// Package antigravity provides OAuth2 authentication functionality for the Antigravity provider.
package antigravity

import (
"fmt"
"runtime"
)

// OAuth client credentials and configuration
const (
ClientID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com"
Expand All @@ -26,9 +31,35 @@ const (

// Antigravity API configuration
const (
APIEndpoint = "https://cloudcode-pa.googleapis.com"
APIVersion = "v1internal"
APIUserAgent = "google-api-nodejs-client/9.15.1"
APIClient = "google-cloud-sdk vscode_cloudshelleditor/0.1"
ClientMetadata = `{"ideType":"IDE_UNSPECIFIED","platform":"PLATFORM_UNSPECIFIED","pluginType":"GEMINI"}`
APIEndpoint = "https://cloudcode-pa.googleapis.com"
APIVersion = "v1internal"
APIUserAgent = "google-api-nodejs-client/9.15.1"
APIClient = "google-cloud-sdk vscode_cloudshelleditor/0.1"
)

func metadataPlatformForGOOS(goos string) string {
// Antigravity clients are Windows/macOS-oriented; for non-Windows
// environments (including Linux servers), masquerade as macOS.
if goos == "windows" {
return "WINDOWS"
}
return "MACOS"
}

func runtimeMetadataPlatform() string {
return metadataPlatformForGOOS(runtime.GOOS)
}

var antigravityPlatform = runtimeMetadataPlatform()

func antigravityJSONClientMetadata() string {
return fmt.Sprintf(`{"ideType":"ANTIGRAVITY","platform":"%s","pluginType":"GEMINI"}`, antigravityPlatform)
}

func antigravityRequestMetadata() map[string]string {
return map[string]string{
"ideType": "ANTIGRAVITY",
"platform": antigravityPlatform,
"pluginType": "GEMINI",
}
}
Comment on lines +55 to +65
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

There is some duplication between antigravityJSONClientMetadata and antigravityRequestMetadata. Both functions construct similar metadata structures and call runtimeMetadataPlatform(). In functions like FetchProjectID, both helpers are called, resulting in runtimeMetadataPlatform() being executed twice, which is slightly inefficient.

To improve this, you could determine the platform once at package initialization and store it in a package-level variable. This variable can then be reused in both helper functions, making the code more efficient and adhering to the Don't Repeat Yourself (DRY) principle.

var platform = runtimeMetadataPlatform()

func antigravityJSONClientMetadata() string {
	return fmt.Sprintf("{\"ideType\":\"ANTIGRAVITY\",\"platform\":\"%s\",\"pluginType\":\"GEMINI\"}", platform)
}

func antigravityRequestMetadata() map[string]string {
	return map[string]string{
		"ideType":    "ANTIGRAVITY",
		"platform":   platform,
		"pluginType": "GEMINI",
	}
}

38 changes: 38 additions & 0 deletions internal/auth/antigravity/constants_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package antigravity

import (
"strings"
"testing"
)

func TestMetadataPlatformForGOOS(t *testing.T) {
tests := []struct {
name string
goos string
want string
}{
{name: "windows", goos: "windows", want: "WINDOWS"},
{name: "darwin", goos: "darwin", want: "MACOS"},
{name: "linux", goos: "linux", want: "MACOS"},
{name: "unknown", goos: "freebsd", want: "MACOS"},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := metadataPlatformForGOOS(tt.goos)
if got != tt.want {
t.Fatalf("metadataPlatformForGOOS(%q) = %q, want %q", tt.goos, got, tt.want)
}
})
}
}

func TestAntigravityJSONClientMetadata(t *testing.T) {
m := antigravityJSONClientMetadata()
if m == "" {
t.Fatal("metadata should not be empty")
}
if !(strings.Contains(m, `"ideType":"ANTIGRAVITY"`) && strings.Contains(m, `"pluginType":"GEMINI"`)) {
t.Fatalf("unexpected metadata: %s", m)
}
}
42 changes: 37 additions & 5 deletions internal/runtime/executor/antigravity_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,16 @@ const (
antigravityModelsPath = "/v1internal:fetchAvailableModels"
antigravityClientID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com"
antigravityClientSecret = "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"
defaultAntigravityAgent = "antigravity/1.104.0 darwin/arm64"
antigravityAgentVersion = "1.104.0"
defaultAntigravityAgent = "antigravity/" + antigravityAgentVersion + " darwin/arm64"
antigravityAPIClient = "google-cloud-sdk vscode_cloudshelleditor/0.1"
antigravityAuthType = "antigravity"
refreshSkew = 3000 * time.Second
systemInstruction = "You are Antigravity, a powerful agentic AI coding assistant designed by the Google Deepmind team working on Advanced Agentic Coding.You are pair programming with a USER to solve their coding task. The task may require creating a new codebase, modifying or debugging an existing codebase, or simply answering a question.**Absolute paths only****Proactiveness**"
)

var antigravityPlatforms = []string{"windows/amd64", "darwin/arm64", "darwin/amd64"}

var (
randSource = rand.New(rand.NewSource(time.Now().UnixNano()))
randSourceMutex sync.Mutex
Expand Down Expand Up @@ -920,7 +924,7 @@ func (e *AntigravityExecutor) CountTokens(ctx context.Context, auth *cliproxyaut
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+token)
httpReq.Header.Set("User-Agent", resolveUserAgent(auth))
applyAntigravityHeaders(httpReq.Header, auth)
httpReq.Header.Set("Accept", "application/json")
if host := resolveHost(base); host != "" {
httpReq.Host = host
Expand Down Expand Up @@ -1025,7 +1029,7 @@ func FetchAntigravityModels(ctx context.Context, auth *cliproxyauth.Auth, cfg *c
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+token)
httpReq.Header.Set("User-Agent", resolveUserAgent(auth))
applyAntigravityHeaders(httpReq.Header, auth)
if host := resolveHost(baseURL); host != "" {
httpReq.Host = host
}
Expand Down Expand Up @@ -1157,6 +1161,8 @@ func (e *AntigravityExecutor) refreshToken(ctx context.Context, auth *cliproxyau
httpReq.Header.Set("Host", "oauth2.googleapis.com")
httpReq.Header.Set("User-Agent", defaultAntigravityAgent)
httpReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
httpReq.Header.Set("X-Goog-Api-Client", antigravityAPIClient)
httpReq.Header.Set("Client-Metadata", antigravityClientMetadataForUserAgent(defaultAntigravityAgent))

httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
httpResp, errDo := httpClient.Do(httpReq)
Expand Down Expand Up @@ -1322,7 +1328,7 @@ func (e *AntigravityExecutor) buildRequest(ctx context.Context, auth *cliproxyau
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+token)
httpReq.Header.Set("User-Agent", resolveUserAgent(auth))
applyAntigravityHeaders(httpReq.Header, auth)
if stream {
httpReq.Header.Set("Accept", "text/event-stream")
} else {
Expand Down Expand Up @@ -1433,6 +1439,32 @@ func resolveHost(base string) string {
return strings.TrimPrefix(strings.TrimPrefix(base, "https://"), "http://")
}

func randomizedAntigravityUserAgent() string {
randSourceMutex.Lock()
platform := antigravityPlatforms[randSource.Intn(len(antigravityPlatforms))]
randSourceMutex.Unlock()
return "antigravity/" + antigravityAgentVersion + " " + platform
}

func metadataPlatformFromUserAgent(ua string) string {
if strings.Contains(strings.ToLower(ua), "windows/") {
return "WINDOWS"
}
return "MACOS"
}

func antigravityClientMetadataForUserAgent(ua string) string {
platform := metadataPlatformFromUserAgent(ua)
return fmt.Sprintf(`{"ideType":"ANTIGRAVITY","platform":"%s","pluginType":"GEMINI"}`, platform)
}

func applyAntigravityHeaders(h http.Header, auth *cliproxyauth.Auth) {
ua := resolveUserAgent(auth)
h.Set("User-Agent", ua)
h.Set("X-Goog-Api-Client", antigravityAPIClient)
h.Set("Client-Metadata", antigravityClientMetadataForUserAgent(ua))
}

func resolveUserAgent(auth *cliproxyauth.Auth) string {
if auth != nil {
if auth.Attributes != nil {
Expand All @@ -1446,7 +1478,7 @@ func resolveUserAgent(auth *cliproxyauth.Auth) string {
}
}
}
return defaultAntigravityAgent
return randomizedAntigravityUserAgent()
}

func antigravityRetryAttempts(auth *cliproxyauth.Auth, cfg *config.Config) int {
Expand Down
51 changes: 51 additions & 0 deletions internal/runtime/executor/antigravity_executor_headers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package executor

import (
"net/http"
"strings"
"testing"
)

func TestMetadataPlatformFromUserAgent(t *testing.T) {
tests := []struct {
name string
ua string
want string
}{
{name: "windows ua", ua: "antigravity/1.104.0 windows/amd64", want: "WINDOWS"},
{name: "darwin ua", ua: "antigravity/1.104.0 darwin/arm64", want: "MACOS"},
{name: "unknown ua", ua: "antigravity/1.104.0 linux/amd64", want: "MACOS"},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := metadataPlatformFromUserAgent(tt.ua)
if got != tt.want {
t.Fatalf("metadataPlatformFromUserAgent(%q)=%q want=%q", tt.ua, got, tt.want)
}
})
}
}

func TestApplyAntigravityHeaders(t *testing.T) {
h := make(http.Header)
applyAntigravityHeaders(h, nil)

ua := h.Get("User-Agent")
if ua == "" {
t.Fatal("User-Agent should be set")
}
if !strings.HasPrefix(ua, "antigravity/") {
t.Fatalf("unexpected User-Agent: %s", ua)
}
if h.Get("X-Goog-Api-Client") != antigravityAPIClient {
t.Fatalf("unexpected X-Goog-Api-Client: %s", h.Get("X-Goog-Api-Client"))
}
metadata := h.Get("Client-Metadata")
if !strings.Contains(metadata, `"ideType":"ANTIGRAVITY"`) {
t.Fatalf("unexpected Client-Metadata: %s", metadata)
}
if !(strings.Contains(metadata, `"platform":"WINDOWS"`) || strings.Contains(metadata, `"platform":"MACOS"`)) {
t.Fatalf("unexpected Client-Metadata platform: %s", metadata)
}
}
Loading