// Package misc provides miscellaneous utility functions for the CLI Proxy API server. package misc import ( "context" "errors" "fmt" "io" "net/http" "strings" "sync" "time" log "github.com/sirupsen/logrus" "gopkg.in/yaml.v3" ) const ( antigravityFallbackVersion = "2.2.1" antigravityHubPlatform = "darwin/arm64" antigravityVersionCacheTTL = 6 * time.Hour antigravityFetchTimeout = 10 * time.Second AntigravityNodeAPIClientUA = "google-api-nodejs-client/10.3.0" AntigravityGoogAPIClientUA = "gl-node/22.21.1" ) var ( antigravityHubLatestManifestURL = "https://antigravity-hub-auto-updater-974169037036.us-central1.run.app/manifest/latest-arm64-mac.yml" ) type antigravityHubUpdaterManifest struct { Version string `yaml:"version"` } var ( cachedAntigravityVersion = antigravityFallbackVersion antigravityVersionMu sync.RWMutex antigravityVersionExpiry time.Time antigravityUpdaterOnce sync.Once ) // StartAntigravityVersionUpdater starts a background goroutine that periodically refreshes the cached antigravity version. // This is intentionally decoupled from request execution to avoid blocking executors on version lookups. func StartAntigravityVersionUpdater(ctx context.Context) { antigravityUpdaterOnce.Do(func() { go runAntigravityVersionUpdater(ctx) }) } func runAntigravityVersionUpdater(ctx context.Context) { if ctx == nil { ctx = context.Background() } ticker := time.NewTicker(antigravityVersionCacheTTL / 2) defer ticker.Stop() log.Infof("periodic antigravity version refresh started (interval=%s)", antigravityVersionCacheTTL/2) refreshAntigravityVersion(ctx) for { select { case <-ctx.Done(): return case <-ticker.C: refreshAntigravityVersion(ctx) } } } func refreshAntigravityVersion(ctx context.Context) { version, errFetch := fetchAntigravityLatestVersion(ctx) antigravityVersionMu.Lock() defer antigravityVersionMu.Unlock() now := time.Now() if errFetch == nil { cachedAntigravityVersion = version antigravityVersionExpiry = now.Add(antigravityVersionCacheTTL) log.WithField("version", version).Info("fetched latest antigravity version") return } if cachedAntigravityVersion == "" || now.After(antigravityVersionExpiry) { cachedAntigravityVersion = antigravityFallbackVersion antigravityVersionExpiry = now.Add(antigravityVersionCacheTTL) log.WithError(errFetch).Warn("failed to refresh antigravity version, using fallback version") return } log.WithError(errFetch).Debug("failed to refresh antigravity version, keeping cached value") } // AntigravityLatestVersion returns the cached antigravity version refreshed by StartAntigravityVersionUpdater. // It falls back to antigravityFallbackVersion if the cache is empty or stale. func AntigravityLatestVersion() string { antigravityVersionMu.RLock() if cachedAntigravityVersion != "" && time.Now().Before(antigravityVersionExpiry) { v := cachedAntigravityVersion antigravityVersionMu.RUnlock() return v } antigravityVersionMu.RUnlock() return antigravityFallbackVersion } // AntigravityUserAgent returns the User-Agent string used by the Antigravity Hub family. func AntigravityUserAgent() string { return fmt.Sprintf("antigravity/hub/%s %s", AntigravityLatestVersion(), antigravityHubPlatform) } func isAntigravityFamilyUserAgent(lower string) bool { return strings.HasPrefix(lower, "antigravity/hub/") || strings.HasPrefix(lower, "antigravity/") } func antigravityBaseUserAgent(userAgent string) string { userAgent = strings.TrimSpace(userAgent) if userAgent == "" { return AntigravityUserAgent() } lower := strings.ToLower(userAgent) if isAntigravityFamilyUserAgent(lower) { if idx := strings.Index(lower, " google-api-nodejs-client/"); idx >= 0 { trimmed := strings.TrimSpace(userAgent[:idx]) if trimmed != "" { return trimmed } } } return userAgent } // AntigravityRequestUserAgent returns the short Antigravity runtime UA used by // generate/stream/model-list requests. func AntigravityRequestUserAgent(userAgent string) string { return antigravityBaseUserAgent(userAgent) } // AntigravityLoadCodeAssistUserAgent returns the short Antigravity UA used by // loadCodeAssist requests. func AntigravityLoadCodeAssistUserAgent(userAgent string) string { return AntigravityRequestUserAgent(userAgent) } // AntigravityOnboardUserUserAgent returns the long Antigravity control-plane UA // used by onboardUser requests. func AntigravityOnboardUserUserAgent(userAgent string) string { userAgent = strings.TrimSpace(userAgent) if userAgent == "" { return AntigravityUserAgent() + " " + AntigravityNodeAPIClientUA } lower := strings.ToLower(userAgent) if !isAntigravityFamilyUserAgent(lower) { return userAgent } if strings.Contains(lower, "google-api-nodejs-client/") { return userAgent } return antigravityBaseUserAgent(userAgent) + " " + AntigravityNodeAPIClientUA } // AntigravityVersionFromUserAgent extracts the Antigravity version prefix from // either the short or long Antigravity UA forms. func AntigravityVersionFromUserAgent(userAgent string) string { base := antigravityBaseUserAgent(userAgent) lower := strings.ToLower(base) if strings.HasPrefix(lower, "antigravity/hub/") { rest := base[len("antigravity/hub/"):] if idx := strings.IndexAny(rest, " "); idx >= 0 { rest = rest[:idx] } rest = strings.TrimSpace(rest) if rest == "" { return AntigravityLatestVersion() } return rest } const legacyPrefix = "antigravity/" if !strings.HasPrefix(lower, legacyPrefix) { return AntigravityLatestVersion() } rest := base[len(legacyPrefix):] if idx := strings.IndexAny(rest, " "); idx >= 0 { rest = rest[:idx] } rest = strings.TrimSpace(rest) if rest == "" { return AntigravityLatestVersion() } return rest } func fetchAntigravityLatestVersion(ctx context.Context) (string, error) { if ctx == nil { ctx = context.Background() } client := &http.Client{Timeout: antigravityFetchTimeout} return fetchAntigravityHubLatestManifestVersion(ctx, client) } func fetchAntigravityHubLatestManifestVersion(ctx context.Context, client *http.Client) (string, error) { httpReq, errReq := http.NewRequestWithContext(ctx, http.MethodGet, antigravityHubLatestManifestURL, nil) if errReq != nil { return "", fmt.Errorf("build antigravity Hub updater manifest request: %w", errReq) } httpReq.Header.Set("User-Agent", "electron-builder") httpReq.Header.Set("Cache-Control", "no-cache") resp, errDo := client.Do(httpReq) if errDo != nil { return "", fmt.Errorf("fetch antigravity Hub updater manifest: %w", errDo) } defer func() { if errClose := resp.Body.Close(); errClose != nil { log.WithError(errClose).Warn("antigravity Hub updater manifest response body close error") } }() if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("antigravity Hub updater manifest returned status %d", resp.StatusCode) } raw, errRead := io.ReadAll(io.LimitReader(resp.Body, 4096)) if errRead != nil { return "", fmt.Errorf("read antigravity Hub updater manifest: %w", errRead) } var manifest antigravityHubUpdaterManifest if errDecode := yaml.Unmarshal(raw, &manifest); errDecode != nil { return "", fmt.Errorf("decode antigravity Hub updater manifest: %w", errDecode) } version := strings.TrimSpace(manifest.Version) if version == "" { return "", errors.New("antigravity Hub updater manifest returned empty version") } if !isValidAntigravitySemVersion(version) { return "", fmt.Errorf("antigravity Hub updater manifest returned invalid version %q", version) } return version, nil } func isValidAntigravitySemVersion(version string) bool { parts := strings.Split(version, ".") if len(parts) != 3 { return false } for _, part := range parts { if part == "" { return false } for _, ch := range part { if ch < '0' || ch > '9' { return false } } } return true }