use serde_json::Value;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
const CONFIG_DIR_ENV: &str = "ANTIGRAVITY_BRIDGE_CONFIG_DIR";
const BINARY_PATH_ENV: &str = "ANTIGRAVITY_BRIDGE_BINARY_PATH";
const LEGACY_BINARY_PATH_ENV: &str = "AGY_BINARY_PATH";
#[allow(dead_code)]
#[derive(Clone)]
pub struct CachedSessionHashes {
pub trajectory_id: String,
pub hashes: Vec<(serde_json::Value, String)>,
}
pub struct BridgeState {
pub client_id: String,
pub client_secret: String,
pub cached_token: Option<String>,
pub token_expiry: u64,
pub project_id: Option<String>,
pub thought_signature_cache: HashMap<String, String>,
pub sessions_cache: HashMap<String, (String, String)>,
pub hash_cache: Vec<CachedSessionHashes>,
}
pub type SharedState = std::sync::Arc<tokio::sync::RwLock<BridgeState>>;
pub fn agy_binary_path() -> anyhow::Result<PathBuf> {
std::env::var_os(BINARY_PATH_ENV)
.or_else(|| std::env::var_os(LEGACY_BINARY_PATH_ENV))
.map(PathBuf::from)
.map(Ok)
.unwrap_or_else(|| Ok(home_dir()?.join(".local/bin/agy")))
}
fn antigravity_config_dir() -> anyhow::Result<PathBuf> {
std::env::var_os(CONFIG_DIR_ENV)
.map(PathBuf::from)
.map(Ok)
.unwrap_or_else(|| Ok(home_dir()?.join(".gemini/antigravity-cli")))
}
fn home_dir() -> anyhow::Result<PathBuf> {
dirs::home_dir().ok_or_else(|| anyhow::anyhow!("could not resolve home directory"))
}
fn antigravity_config_path(file_name: &str) -> anyhow::Result<PathBuf> {
Ok(antigravity_config_dir()?.join(file_name))
}
pub fn load_expected_sha256() -> anyhow::Result<String> {
let path = antigravity_config_path("agy.sha256")?;
if !path.exists() {
anyhow::bail!(
"CRITICAL: expected SHA256 file not found at {}. Please write the expected agy binary SHA256 into it.",
path.display()
);
}
let content = std::fs::read_to_string(&path)?;
Ok(content.trim().to_string())
}
pub fn get_binary_sha256(path: impl AsRef<Path>) -> anyhow::Result<String> {
use sha2::{Digest, Sha256};
let data = std::fs::read(path)?;
let mut hasher = Sha256::new();
hasher.update(&data);
Ok(hex::encode(hasher.finalize()))
}
pub fn extract_credentials_candidates(
path: impl AsRef<Path>,
) -> anyhow::Result<(Vec<String>, Vec<String>)> {
let data = std::fs::read(path)?;
let client_id_regex =
regex::bytes::Regex::new(r"\d{10,15}-[a-z0-9_-]{20,50}\.apps\.googleusercontent\.com")?;
let mut client_ids = Vec::new();
for mat in client_id_regex.find_iter(&data) {
if let Ok(id) = std::str::from_utf8(mat.as_bytes()) {
client_ids.push(id.to_string());
}
}
client_ids.sort();
client_ids.dedup();
let client_secret_regex = regex::bytes::Regex::new(r"GOCSPX-[A-Za-z0-9_-]{28}")?;
let mut client_secrets = Vec::new();
for mat in client_secret_regex.find_iter(&data) {
if let Ok(secret) = std::str::from_utf8(mat.as_bytes()) {
client_secrets.push(secret.to_string());
}
}
client_secrets.sort();
client_secrets.dedup();
Ok((client_ids, client_secrets))
}
pub fn load_refresh_token() -> anyhow::Result<String> {
let path = antigravity_config_path("antigravity-oauth-token")?;
let content = std::fs::read_to_string(path)?;
let val: Value = serde_json::from_str(&content)?;
let token = val["token"]["refresh_token"]
.as_str()
.ok_or_else(|| anyhow::anyhow!("No refresh_token found in antigravity-oauth-token JSON"))?
.to_string();
Ok(token)
}
pub async fn verify_and_bind_credentials(
refresh_token: &str,
client_ids: &[String],
client_secrets: &[String],
) -> anyhow::Result<(String, String)> {
let client = reqwest::Client::new();
let url = "https://oauth2.googleapis.com/token";
for client_id in client_ids {
for client_secret in client_secrets {
let params = [
("client_id", client_id.as_str()),
("client_secret", client_secret.as_str()),
("refresh_token", refresh_token),
("grant_type", "refresh_token"),
];
let res = client.post(url).form(¶ms).send().await;
if let Ok(response) = res {
if response.status() == 200 {
tracing::info!(
"Dynamic credential binding successful with client_id start: {}",
&client_id[..15]
);
return Ok((client_id.clone(), client_secret.clone()));
}
}
}
}
anyhow::bail!("Dynamic credential extraction failed to verify working pair.")
}
pub async fn fetch_project_id(token: &str) -> anyhow::Result<String> {
let client = reqwest::Client::new();
let url = "https://daily-cloudcode-pa.googleapis.com/v1internal:loadCodeAssist";
let payload = serde_json::json!({
"metadata": {
"ideType": "ANTIGRAVITY"
}
});
let response = client
.post(url)
.header("Authorization", format!("Bearer {}", token))
.header("Content-Type", "application/json")
.header("User-Agent", "antigravity/cli/1.0.10 linux/amd64")
.json(&payload)
.send()
.await?;
let status = response.status();
if !status.is_success() {
let err_text = response.text().await?;
anyhow::bail!("fetch_project_id returned status {}: {}", status, err_text);
}
let val: Value = response.json().await?;
let proj = val["cloudaicompanionProject"]
.as_str()
.ok_or_else(|| {
anyhow::anyhow!(
"cloudaicompanionProject field missing in loadCodeAssist response payload: {:?}",
val
)
})?
.to_string();
Ok(proj)
}
pub async fn get_valid_token_and_project(
state_arc: &SharedState,
) -> anyhow::Result<(String, String)> {
let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
{
let state = state_arc.read().await;
if let (Some(token), Some(project)) = (&state.cached_token, &state.project_id) {
if now < state.token_expiry.saturating_sub(60) {
return Ok((token.clone(), project.clone()));
}
}
}
let mut state = state_arc.write().await;
if let (Some(token), Some(project)) = (&state.cached_token, &state.project_id) {
if now < state.token_expiry.saturating_sub(60) {
return Ok((token.clone(), project.clone()));
}
}
let refresh_token = load_refresh_token()?;
let client = reqwest::Client::new();
let url = "https://oauth2.googleapis.com/token";
let params = [
("client_id", state.client_id.as_str()),
("client_secret", state.client_secret.as_str()),
("refresh_token", &refresh_token),
("grant_type", "refresh_token"),
];
let response = client.post(url).form(¶ms).send().await?;
if !response.status().is_success() {
let err_text = response.text().await?;
anyhow::bail!("Failed to refresh OAuth token: {}", err_text);
}
let res_val: Value = response.json().await?;
let token = res_val["access_token"]
.as_str()
.ok_or_else(|| anyhow::anyhow!("access_token missing in oauth response"))?
.to_string();
let expires_in = res_val["expires_in"].as_u64().unwrap_or(3600);
state.cached_token = Some(token.clone());
state.token_expiry = now + expires_in;
let project = fetch_project_id(&token).await?;
state.project_id = Some(project.clone());
Ok((token, project))
}
pub fn load_signatures() -> HashMap<String, String> {
let Ok(path) = antigravity_config_path("signatures.json") else {
return HashMap::new();
};
if let Ok(content) = std::fs::read_to_string(path) {
serde_json::from_str(&content).unwrap_or_default()
} else {
HashMap::new()
}
}
pub fn save_signatures(cache: &HashMap<String, String>) {
let Ok(dir) = antigravity_config_dir() else {
return;
};
let _ = std::fs::create_dir_all(&dir);
let path = dir.join("signatures.json");
if let Ok(json_str) = serde_json::to_string(cache) {
let _ = std::fs::write(path, json_str);
}
}
pub fn load_sessions() -> HashMap<String, (String, String)> {
let Ok(path) = antigravity_config_path("sessions.json") else {
return HashMap::new();
};
if let Ok(content) = std::fs::read_to_string(path) {
serde_json::from_str(&content).unwrap_or_default()
} else {
HashMap::new()
}
}
pub fn save_sessions(cache: &HashMap<String, (String, String)>) {
let Ok(dir) = antigravity_config_dir() else {
return;
};
let _ = std::fs::create_dir_all(&dir);
let path = dir.join("sessions.json");
if let Ok(json_str) = serde_json::to_string(cache) {
let _ = std::fs::write(path, json_str);
}
}