fix: block ALL LS follow-up requests across connections
Move the in-flight blocking check to the top of the LLM request flow, BEFORE request modification. This catches follow-ups on ALL connections (the LS opens multiple parallel TLS connections). Only the very first modified request reaches Google — all others get fake STOP responses. Previously, each new connection independently allowed one request through before blocking, letting 4-5 requests leak per turn.
This commit is contained in:
@@ -108,7 +108,10 @@ pub struct MainLSConfig {
|
||||
/// and CSRF is a random UUID.
|
||||
pub fn generate_standalone_config() -> MainLSConfig {
|
||||
let csrf = Uuid::new_v4().to_string();
|
||||
info!(csrf_len = csrf.len(), "Generated standalone config (headless)");
|
||||
info!(
|
||||
csrf_len = csrf.len(),
|
||||
"Generated standalone config (headless)"
|
||||
);
|
||||
MainLSConfig {
|
||||
extension_server_port: "0".to_string(), // disables extension server
|
||||
csrf,
|
||||
@@ -159,7 +162,13 @@ impl StandaloneLS {
|
||||
let app_data_dir = format!("{DATA_DIR}/.gemini/antigravity-standalone");
|
||||
let annotations_dir = format!("{app_data_dir}/annotations");
|
||||
let brain_dir = format!("{app_data_dir}/brain");
|
||||
for dir in [DATA_DIR, &gemini_dir, &app_data_dir, &annotations_dir, &brain_dir] {
|
||||
for dir in [
|
||||
DATA_DIR,
|
||||
&gemini_dir,
|
||||
&app_data_dir,
|
||||
&annotations_dir,
|
||||
&brain_dir,
|
||||
] {
|
||||
let _ = std::fs::create_dir_all(dir);
|
||||
#[cfg(unix)]
|
||||
{
|
||||
@@ -194,7 +203,10 @@ impl StandaloneLS {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let _ = std::fs::set_permissions(&settings_path, std::fs::Permissions::from_mode(0o0666));
|
||||
let _ = std::fs::set_permissions(
|
||||
&settings_path,
|
||||
std::fs::Permissions::from_mode(0o0666),
|
||||
);
|
||||
}
|
||||
tracing::info!("Pre-seeded user_settings.pb (detect_and_use_proxy=ENABLED)");
|
||||
}
|
||||
@@ -203,10 +215,7 @@ impl StandaloneLS {
|
||||
// The LS connects to this port and calls LanguageServerStarted — without it,
|
||||
// the LS never fully initializes and won't accept connections on its server_port.
|
||||
let _stub_listener = if headless {
|
||||
let stub_port: u16 = main_config
|
||||
.extension_server_port
|
||||
.parse()
|
||||
.unwrap_or(0);
|
||||
let stub_port: u16 = main_config.extension_server_port.parse().unwrap_or(0);
|
||||
if stub_port == 0 {
|
||||
// Create a real listener so the LS can connect
|
||||
let listener = TcpListener::bind("127.0.0.1:0")
|
||||
@@ -215,7 +224,10 @@ impl StandaloneLS {
|
||||
.local_addr()
|
||||
.map_err(|e| format!("Failed to get stub port: {e}"))?
|
||||
.port();
|
||||
info!(port = actual_port, "Stub extension server listening (headless)");
|
||||
info!(
|
||||
port = actual_port,
|
||||
"Stub extension server listening (headless)"
|
||||
);
|
||||
// Read OAuth state from Antigravity's state.vscdb if available.
|
||||
// The DB stores the exact Topic proto (access_token + refresh_token + expiry)
|
||||
// which lets the LS auto-refresh tokens via its built-in Google OAuth2 client.
|
||||
@@ -306,10 +318,7 @@ impl StandaloneLS {
|
||||
// 3. MITM proxy intercepts the transparent TLS connection via SNI
|
||||
if let Some(mitm) = mitm_config {
|
||||
// Extract port from proxy_addr (e.g. "http://127.0.0.1:8742" → "8742")
|
||||
let mitm_port = mitm.proxy_addr
|
||||
.rsplit(':')
|
||||
.next()
|
||||
.unwrap_or("8742");
|
||||
let mitm_port = mitm.proxy_addr.rsplit(':').next().unwrap_or("8742");
|
||||
format!("https://daily-cloudcode-pa.googleapis.com:{mitm_port}")
|
||||
} else {
|
||||
"https://daily-cloudcode-pa.googleapis.com".to_string()
|
||||
@@ -324,9 +333,8 @@ impl StandaloneLS {
|
||||
debug!(?args, "LS args");
|
||||
|
||||
// Build env vars for the LS process
|
||||
let mut env_vars: Vec<(String, String)> = vec![
|
||||
("ANTIGRAVITY_EDITOR_APP_ROOT".into(), APP_ROOT.into()),
|
||||
];
|
||||
let mut env_vars: Vec<(String, String)> =
|
||||
vec![("ANTIGRAVITY_EDITOR_APP_ROOT".into(), APP_ROOT.into())];
|
||||
|
||||
// If MITM is enabled, add SSL + proxy env vars
|
||||
if let Some(mitm) = mitm_config {
|
||||
@@ -335,8 +343,8 @@ impl StandaloneLS {
|
||||
// Write to /tmp — accessible by antigravity-ls user
|
||||
// (user's ~/.config/ is not traversable by other UIDs)
|
||||
let combined_ca_path = "/tmp/antigravity-mitm-combined-ca.pem".to_string();
|
||||
let system_ca = std::fs::read_to_string("/etc/ssl/certs/ca-certificates.crt")
|
||||
.unwrap_or_default();
|
||||
let system_ca =
|
||||
std::fs::read_to_string("/etc/ssl/certs/ca-certificates.crt").unwrap_or_default();
|
||||
let mitm_ca = std::fs::read_to_string(&mitm.ca_cert_path)
|
||||
.map_err(|e| format!("Failed to read MITM CA cert: {e}"))?;
|
||||
std::fs::write(&combined_ca_path, format!("{system_ca}\n{mitm_ca}"))
|
||||
@@ -441,7 +449,11 @@ impl StandaloneLS {
|
||||
};
|
||||
|
||||
if let Some(pid) = ls_pid {
|
||||
info!(ls_pid = pid, sudo = use_sudo, "Discovered actual LS process");
|
||||
info!(
|
||||
ls_pid = pid,
|
||||
sudo = use_sudo,
|
||||
"Discovered actual LS process"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(StandaloneLS {
|
||||
@@ -617,8 +629,7 @@ fn find_main_ls_pid() -> Result<String, String> {
|
||||
return Err("No /proc filesystem".to_string());
|
||||
}
|
||||
|
||||
let entries = std::fs::read_dir(proc)
|
||||
.map_err(|e| format!("Cannot read /proc: {e}"))?;
|
||||
let entries = std::fs::read_dir(proc).map_err(|e| format!("Cannot read /proc: {e}"))?;
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let name = entry.file_name();
|
||||
@@ -704,12 +715,10 @@ fn cleanup_orphaned_ls() {
|
||||
.output();
|
||||
|
||||
let pids: Vec<u32> = match output {
|
||||
Ok(out) => {
|
||||
String::from_utf8_lossy(&out.stdout)
|
||||
.lines()
|
||||
.filter_map(|l| l.trim().parse().ok())
|
||||
.collect()
|
||||
}
|
||||
Ok(out) => String::from_utf8_lossy(&out.stdout)
|
||||
.lines()
|
||||
.filter_map(|l| l.trim().parse().ok())
|
||||
.collect(),
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
@@ -717,7 +726,11 @@ fn cleanup_orphaned_ls() {
|
||||
return;
|
||||
}
|
||||
|
||||
info!(count = pids.len(), ?pids, "Cleaning up orphaned standalone LS processes");
|
||||
info!(
|
||||
count = pids.len(),
|
||||
?pids,
|
||||
"Cleaning up orphaned standalone LS processes"
|
||||
);
|
||||
|
||||
// Kill each PID by running `kill` AS the antigravity-ls user.
|
||||
// This works because same-UID processes can signal each other,
|
||||
@@ -870,7 +883,8 @@ fn extract_access_token_from_topic(topic_bytes: &[u8]) -> Option<String> {
|
||||
// Simple approach: convert to string and find base64 pattern
|
||||
let as_str = String::from_utf8_lossy(topic_bytes);
|
||||
// The base64 OAuthTokenInfo starts with "Co" (0x0A = field 1, len-delimited)
|
||||
for segment in as_str.split(|c: char| !c.is_alphanumeric() && c != '+' && c != '/' && c != '=') {
|
||||
for segment in as_str.split(|c: char| !c.is_alphanumeric() && c != '+' && c != '/' && c != '=')
|
||||
{
|
||||
if segment.len() > 50 {
|
||||
if let Ok(decoded) = base64::engine::general_purpose::STANDARD.decode(segment) {
|
||||
// Try to extract field 1 (access_token) from the OAuthTokenInfo proto
|
||||
@@ -951,7 +965,11 @@ fn decode_varint_at(buf: &[u8], offset: usize) -> Option<(u64, usize)> {
|
||||
/// IMPORTANT: `SubscribeToUnifiedStateSyncTopic` is a long-lived stream.
|
||||
/// If we immediately close it, the LS reconnects in a tight loop and never
|
||||
/// proceeds to fetch OAuth tokens. We keep subscription connections OPEN.
|
||||
fn stub_handle_connection(conn: std::net::TcpStream, oauth_token: &str, oauth_topic_bytes: &Option<Vec<u8>>) {
|
||||
fn stub_handle_connection(
|
||||
conn: std::net::TcpStream,
|
||||
oauth_token: &str,
|
||||
oauth_topic_bytes: &Option<Vec<u8>>,
|
||||
) {
|
||||
use std::io::{BufRead, BufReader, Read, Write};
|
||||
|
||||
let mut reader = BufReader::new(match conn.try_clone() {
|
||||
@@ -1028,7 +1046,7 @@ fn stub_handle_connection(conn: std::net::TcpStream, oauth_token: &str, oauth_to
|
||||
i += 1;
|
||||
if i + len <= proto_body.len() {
|
||||
if field_num == 1 {
|
||||
topic_name = String::from_utf8_lossy(&proto_body[i..i+len]).to_string();
|
||||
topic_name = String::from_utf8_lossy(&proto_body[i..i + len]).to_string();
|
||||
}
|
||||
i += len;
|
||||
} else {
|
||||
@@ -1084,7 +1102,10 @@ fn stub_handle_connection(conn: std::net::TcpStream, oauth_token: &str, oauth_to
|
||||
// This includes access_token + refresh_token + expiry, so the
|
||||
// LS can auto-refresh tokens via its built-in Google OAuth2 client.
|
||||
initial_state_bytes = topic_bytes.clone();
|
||||
eprintln!("[stub-ext] using state.vscdb topic ({} bytes)", topic_bytes.len());
|
||||
eprintln!(
|
||||
"[stub-ext] using state.vscdb topic ({} bytes)",
|
||||
topic_bytes.len()
|
||||
);
|
||||
} else if !oauth_token.is_empty() {
|
||||
// Manual token fallback — construct OAuthTokenInfo with far-future expiry
|
||||
// (no refresh_token, so the LS can't auto-refresh)
|
||||
@@ -1155,7 +1176,10 @@ fn stub_handle_connection(conn: std::net::TcpStream, oauth_token: &str, oauth_to
|
||||
if !send_chunk(&mut writer, &initial_env) {
|
||||
return;
|
||||
}
|
||||
eprintln!("[stub-ext] STREAM → sent initial_state ({} bytes)", initial_state_bytes.len());
|
||||
eprintln!(
|
||||
"[stub-ext] STREAM → sent initial_state ({} bytes)",
|
||||
initial_state_bytes.len()
|
||||
);
|
||||
|
||||
// (applied_update removed — data is in initial_state)
|
||||
|
||||
@@ -1197,7 +1221,10 @@ fn stub_handle_connection(conn: std::net::TcpStream, oauth_token: &str, oauth_to
|
||||
if !oauth_token.is_empty() {
|
||||
// Build protobuf: GetSecretValueResponse { string value = 1 }
|
||||
let proto = encode_proto_string(1, oauth_token.as_bytes());
|
||||
eprintln!("[stub-ext] → serving token ({} bytes) for key={key:?}", oauth_token.len());
|
||||
eprintln!(
|
||||
"[stub-ext] → serving token ({} bytes) for key={key:?}",
|
||||
oauth_token.len()
|
||||
);
|
||||
|
||||
// Data envelope: flag=0x00, length, data
|
||||
envelope.push(0x00u8);
|
||||
|
||||
Reference in New Issue
Block a user