feat: rebrand to ZeroGravity, replace proxyctl with zg Rust binary
Phase 1 - Rename: - Crate: antigravity-proxy -> zerogravity - Env: ANTIGRAVITY_OAUTH_TOKEN -> ZEROGRAVITY_TOKEN - Paths: ~/.config/antigravity-proxy -> ~/.config/zerogravity - Paths: /tmp/antigravity-* -> /tmp/zerogravity-* - User: antigravity-ls -> zerogravity-ls - Service: antigravity-proxy -> zerogravity Phase 2 - zg daemon manager: - New Rust binary src/bin/zg.rs replaces scripts/proxyctl bash - Commands: start, stop, restart, rebuild, status, logs, test, health - Auto-resolves project dir from binary location - All commands exit immediately (safe for agent fast-bash)
This commit is contained in:
74
Cargo.lock
generated
74
Cargo.lock
generated
@@ -103,43 +103,6 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "antigravity-proxy"
|
||||
version = "3.0.0"
|
||||
dependencies = [
|
||||
"async-stream",
|
||||
"axum",
|
||||
"base64",
|
||||
"brotli 7.0.0",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"clap",
|
||||
"flate2",
|
||||
"http",
|
||||
"http-body-util",
|
||||
"httparse",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"rand",
|
||||
"rcgen",
|
||||
"regex",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
"rustls-pemfile",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"time",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-stream",
|
||||
"tower-http",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"uuid",
|
||||
"wreq",
|
||||
"wreq-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-stream"
|
||||
version = "0.3.6"
|
||||
@@ -2396,6 +2359,43 @@ dependencies = [
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerogravity"
|
||||
version = "3.0.0"
|
||||
dependencies = [
|
||||
"async-stream",
|
||||
"axum",
|
||||
"base64",
|
||||
"brotli 7.0.0",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"clap",
|
||||
"flate2",
|
||||
"http",
|
||||
"http-body-util",
|
||||
"httparse",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"rand",
|
||||
"rcgen",
|
||||
"regex",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
"rustls-pemfile",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"time",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-stream",
|
||||
"tower-http",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"uuid",
|
||||
"wreq",
|
||||
"wreq-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.8.2"
|
||||
|
||||
10
Cargo.toml
10
Cargo.toml
@@ -1,8 +1,16 @@
|
||||
[package]
|
||||
name = "antigravity-proxy"
|
||||
name = "zerogravity"
|
||||
version = "3.0.0"
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
name = "zerogravity"
|
||||
path = "src/main.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "zg"
|
||||
path = "src/bin/zg.rs"
|
||||
|
||||
[dependencies]
|
||||
axum = { version = "0.8", features = ["json"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
|
||||
38
GEMINI.md
38
GEMINI.md
@@ -27,41 +27,41 @@ This "LS as dumb relay" pattern keeps the LS interactions minimal and predictabl
|
||||
|
||||
## Agent Quick Reference
|
||||
|
||||
### `proxyctl` — Daemon Manager
|
||||
### `zg` — Daemon Manager
|
||||
|
||||
`proxyctl` commands exit immediately (not foreground) — safe for agent use via fast-bash MCP.
|
||||
`zg` commands exit immediately (not foreground) — safe for agent use via fast-bash MCP.
|
||||
|
||||
```bash
|
||||
# Rebuild and restart after code changes
|
||||
proxyctl restart
|
||||
zg restart
|
||||
|
||||
# Quick test
|
||||
proxyctl test "say hi in 3 words"
|
||||
zg test "say hi in 3 words"
|
||||
|
||||
# Check status
|
||||
proxyctl status
|
||||
zg status
|
||||
|
||||
# Check health
|
||||
proxyctl health
|
||||
zg health
|
||||
```
|
||||
|
||||
| Command | Description |
|
||||
| --------------------- | ----------------------------------- |
|
||||
| `proxyctl start` | Start the proxy daemon |
|
||||
| `proxyctl stop` | Stop the proxy daemon |
|
||||
| `proxyctl restart` | Rebuild + restart |
|
||||
| `proxyctl rebuild` | Build release binary only |
|
||||
| `proxyctl status` | Service status + quota + usage |
|
||||
| `proxyctl logs [N]` | Tail last N lines + follow |
|
||||
| `proxyctl logs-all` | Full log dump (no follow) |
|
||||
| `proxyctl test [msg]` | Quick test request (gemini-3-flash) |
|
||||
| `proxyctl health` | Health check |
|
||||
| `zg start` | Start the proxy daemon |
|
||||
| `zg stop` | Stop the proxy daemon |
|
||||
| `zg restart` | Rebuild + restart |
|
||||
| `zg rebuild` | Build release binary only |
|
||||
| `zg status` | Service status + quota + usage |
|
||||
| `zg logs [N]` | Tail last N lines + follow |
|
||||
| `zg logs-all` | Full log dump (no follow) |
|
||||
| `zg test [msg]` | Quick test request (gemini-3-flash) |
|
||||
| `zg health` | Health check |
|
||||
|
||||
### Testing After Changes
|
||||
|
||||
```bash
|
||||
# 1. Rebuild + restart
|
||||
proxyctl restart
|
||||
zg restart
|
||||
|
||||
# 2. Test an endpoint
|
||||
curl -s http://localhost:8741/v1/chat/completions \
|
||||
@@ -69,7 +69,7 @@ curl -s http://localhost:8741/v1/chat/completions \
|
||||
-d '{"model": "gemini-3-flash", "messages": [{"role": "user", "content": "Say hi"}]}' | jq .
|
||||
|
||||
# 3. Inspect latest trace
|
||||
TRACE_DIR=~/.config/antigravity-proxy/traces/$(date +%Y-%m-%d)
|
||||
TRACE_DIR=~/.config/zerogravity/traces/$(date +%Y-%m-%d)
|
||||
cat "$TRACE_DIR/$(ls -t "$TRACE_DIR" | head -1)/summary.md"
|
||||
```
|
||||
|
||||
@@ -99,8 +99,8 @@ cat "$TRACE_DIR/$(ls -t "$TRACE_DIR" | head -1)/summary.md"
|
||||
|
||||
The proxy needs an OAuth token:
|
||||
|
||||
1. **Env var**: `ANTIGRAVITY_OAUTH_TOKEN=ya29.xxx`
|
||||
2. **Token file**: `~/.config/antigravity-proxy-token`
|
||||
1. **Env var**: `ZEROGRAVITY_TOKEN=ya29.xxx`
|
||||
2. **Token file**: `~/.config/zerogravity-token`
|
||||
3. **Runtime**: `curl -X POST http://localhost:8741/v1/token -d '{"token":"ya29.xxx"}'`
|
||||
|
||||
## CLI Flags
|
||||
|
||||
26
README.md
26
README.md
@@ -23,10 +23,10 @@ graph LR
|
||||
|
||||
```bash
|
||||
# Headless mode (no running Antigravity app needed)
|
||||
RUST_LOG=info ./target/release/antigravity-proxy --headless
|
||||
RUST_LOG=info ./target/release/zerogravity --headless
|
||||
|
||||
# Or use the daemon manager
|
||||
proxyctl start
|
||||
zg start
|
||||
```
|
||||
|
||||
## Endpoints
|
||||
@@ -50,22 +50,22 @@ proxyctl start
|
||||
|
||||
The proxy needs an OAuth token:
|
||||
|
||||
1. **Env var**: `ANTIGRAVITY_OAUTH_TOKEN=ya29.xxx`
|
||||
2. **Token file**: `~/.config/antigravity-proxy-token`
|
||||
1. **Env var**: `ZEROGRAVITY_TOKEN=ya29.xxx`
|
||||
2. **Token file**: `~/.config/zerogravity-token`
|
||||
3. **Runtime**: `curl -X POST http://localhost:8741/v1/token -d '{"token":"ya29.xxx"}'`
|
||||
|
||||
## `proxyctl` Commands
|
||||
## `zg` Commands
|
||||
|
||||
| Command | Description |
|
||||
| --------------------- | ------------------------------ |
|
||||
| `proxyctl start` | Start the proxy daemon |
|
||||
| `proxyctl stop` | Stop the proxy daemon |
|
||||
| `proxyctl restart` | Rebuild + restart |
|
||||
| `proxyctl rebuild` | Build release binary only |
|
||||
| `proxyctl status` | Service status + quota + usage |
|
||||
| `proxyctl logs [N]` | Tail last N lines + follow |
|
||||
| `proxyctl test [msg]` | Quick test request |
|
||||
| `proxyctl health` | Health check |
|
||||
| `zg start` | Start the proxy daemon |
|
||||
| `zg stop` | Stop the proxy daemon |
|
||||
| `zg restart` | Rebuild + restart |
|
||||
| `zg rebuild` | Build release binary only |
|
||||
| `zg status` | Service status + quota + usage |
|
||||
| `zg logs [N]` | Tail last N lines + follow |
|
||||
| `zg test [msg]` | Quick test request |
|
||||
| `zg health` | Health check |
|
||||
|
||||
## Documentation
|
||||
|
||||
|
||||
@@ -132,11 +132,11 @@ Events dispatched through `tokio::sync::mpsc` channels from MITM → API handler
|
||||
### UID-Scoped iptables (Classic Mode)
|
||||
|
||||
```bash
|
||||
# One-time setup — creates antigravity-ls user + iptables rule
|
||||
# One-time setup — creates zerogravity-ls user + iptables rule
|
||||
sudo ./scripts/mitm-redirect.sh install
|
||||
|
||||
# Run proxy (standalone LS + MITM both enabled by default)
|
||||
RUST_LOG=info ./target/release/antigravity-proxy
|
||||
RUST_LOG=info ./target/release/zerogravity
|
||||
|
||||
# Check intercepted usage
|
||||
curl -s http://localhost:8741/v1/usage | jq .
|
||||
@@ -150,7 +150,7 @@ sudo ./scripts/mitm-redirect.sh uninstall
|
||||
No iptables or sudo needed. The LS connects through `HTTPS_PROXY` instead:
|
||||
|
||||
```bash
|
||||
RUST_LOG=info ./target/release/antigravity-proxy --headless
|
||||
RUST_LOG=info ./target/release/zerogravity --headless
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -5,7 +5,7 @@ Per-call debug traces for inspecting request/response flow. Every API call write
|
||||
## Location
|
||||
|
||||
```
|
||||
~/.config/antigravity-proxy/traces/{YYYY-MM-DD}/{HH-MM-SS.sss}_{cascade_short}/
|
||||
~/.config/zerogravity/traces/{YYYY-MM-DD}/{HH-MM-SS.sss}_{cascade_short}/
|
||||
```
|
||||
|
||||
Disable with `--no-trace`.
|
||||
@@ -108,11 +108,11 @@ Traces are designed for LLM consumption. To inspect the last trace:
|
||||
|
||||
```bash
|
||||
# Find latest trace
|
||||
ls -t ~/.config/antigravity-proxy/traces/$(date +%Y-%m-%d)/ | head -1
|
||||
ls -t ~/.config/zerogravity/traces/$(date +%Y-%m-%d)/ | head -1
|
||||
|
||||
# Read the summary
|
||||
cat ~/.config/antigravity-proxy/traces/$(date +%Y-%m-%d)/$(ls -t ~/.config/antigravity-proxy/traces/$(date +%Y-%m-%d)/ | head -1)/summary.md
|
||||
cat ~/.config/zerogravity/traces/$(date +%Y-%m-%d)/$(ls -t ~/.config/zerogravity/traces/$(date +%Y-%m-%d)/ | head -1)/summary.md
|
||||
|
||||
# Grep for failures
|
||||
grep 'outcome=.*error\|outcome=.*timeout' ~/.config/antigravity-proxy/traces/$(date +%Y-%m-%d)/*/meta.txt
|
||||
grep 'outcome=.*error\|outcome=.*timeout' ~/.config/zerogravity/traces/$(date +%Y-%m-%d)/*/meta.txt
|
||||
```
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
# modification, no system-wide changes.
|
||||
#
|
||||
# Flow:
|
||||
# 1. Standalone LS runs as 'antigravity-ls' user (via sudo -u)
|
||||
# 1. Standalone LS runs as 'zerogravity-ls' user (via sudo -u)
|
||||
# 2. iptables catches :443 traffic from that UID only → REDIRECT to MITM port
|
||||
# 3. MITM terminates TLS (Go client trusts our CA via SSL_CERT_FILE)
|
||||
# 4. MITM forwards upstream, captures usage
|
||||
@@ -24,10 +24,10 @@
|
||||
set -euo pipefail
|
||||
|
||||
MITM_PORT="${2:-8742}"
|
||||
LS_USER="antigravity-ls"
|
||||
LS_USER="zerogravity-ls"
|
||||
DATA_DIR="/tmp/antigravity-standalone"
|
||||
LS_BINARY="/usr/share/antigravity/resources/app/extensions/antigravity/bin/language_server_linux_x64"
|
||||
SUDOERS_FILE="/etc/sudoers.d/antigravity-ls"
|
||||
SUDOERS_FILE="/etc/sudoers.d/zerogravity-ls"
|
||||
|
||||
install() {
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
@@ -54,7 +54,7 @@ install() {
|
||||
echo " + data dir: $DATA_DIR (mode 1777, writable by all)"
|
||||
|
||||
# ── 3. Sudoers entry ────────────────────────────────────────────────
|
||||
# Allow the invoking user (SUDO_USER) to run ANY command as antigravity-ls.
|
||||
# Allow the invoking user (SUDO_USER) to run ANY command as zerogravity-ls.
|
||||
# This is needed for the proxy to spawn the LS binary.
|
||||
local REAL_USER="${SUDO_USER:-$(logname 2>/dev/null || whoami)}"
|
||||
cat > "$SUDOERS_FILE" <<EOF
|
||||
@@ -78,7 +78,7 @@ EOF
|
||||
echo
|
||||
echo "[mitm-redirect] ✓ Installed (only affects uid=$LS_UID)"
|
||||
echo " Restart the proxy to take effect:"
|
||||
echo " RUST_LOG=info ./target/release/antigravity-proxy --standalone"
|
||||
echo " RUST_LOG=info ./target/release/zerogravity --standalone"
|
||||
}
|
||||
|
||||
uninstall() {
|
||||
|
||||
@@ -390,7 +390,7 @@ pub(crate) async fn handle_completions(
|
||||
if token.is_empty() {
|
||||
return err_response(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"No OAuth token. POST to /v1/token or set ANTIGRAVITY_OAUTH_TOKEN env var.".into(),
|
||||
"No OAuth token. POST to /v1/token or set ZEROGRAVITY_TOKEN env var.".into(),
|
||||
"authentication_error",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ async fn handle_gemini_inner(
|
||||
if token.is_empty() {
|
||||
return err_response(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"No OAuth token. POST to /v1/token or set ANTIGRAVITY_OAUTH_TOKEN env var.".into(),
|
||||
"No OAuth token. POST to /v1/token or set ZEROGRAVITY_TOKEN env var.".into(),
|
||||
"authentication_error",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -226,7 +226,7 @@ pub(crate) async fn handle_responses(
|
||||
if token.is_empty() {
|
||||
return err_response(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"No OAuth token. POST to /v1/token or set ANTIGRAVITY_OAUTH_TOKEN env var.".into(),
|
||||
"No OAuth token. POST to /v1/token or set ZEROGRAVITY_TOKEN env var.".into(),
|
||||
"authentication_error",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ impl Backend {
|
||||
}
|
||||
|
||||
// Then env var
|
||||
if let Ok(env_token) = std::env::var("ANTIGRAVITY_OAUTH_TOKEN") {
|
||||
if let Ok(env_token) = std::env::var("ZEROGRAVITY_TOKEN") {
|
||||
if !env_token.is_empty() {
|
||||
let mut guard = self.inner.write().await;
|
||||
if guard.oauth_token != env_token {
|
||||
@@ -607,12 +607,12 @@ fn discover() -> Result<BackendInner, String> {
|
||||
https_port = "3100".to_string();
|
||||
}
|
||||
|
||||
let oauth_token = std::env::var("ANTIGRAVITY_OAUTH_TOKEN")
|
||||
let oauth_token = std::env::var("ZEROGRAVITY_TOKEN")
|
||||
.ok()
|
||||
.filter(|s| !s.is_empty())
|
||||
.or_else(|| {
|
||||
let home = std::env::var("HOME").unwrap_or_default();
|
||||
let path = format!("{home}/.config/antigravity-proxy-token");
|
||||
let path = format!("{home}/.config/zerogravity/token");
|
||||
fs::read_to_string(&path)
|
||||
.ok()
|
||||
.map(|s| s.trim().to_string())
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
fn main() {
|
||||
antigravity_proxy::snapshot::run_cli();
|
||||
zerogravity::snapshot::run_cli();
|
||||
}
|
||||
|
||||
273
src/bin/zg.rs
Normal file
273
src/bin/zg.rs
Normal file
@@ -0,0 +1,273 @@
|
||||
//! `zg` — ZeroGravity daemon manager.
|
||||
//!
|
||||
//! All commands exit immediately (safe for agent use via fast-bash MCP).
|
||||
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
const SERVICE: &str = "zerogravity";
|
||||
const PORT: u16 = 8741;
|
||||
|
||||
// ANSI colors
|
||||
const RED: &str = "\x1b[0;31m";
|
||||
const GREEN: &str = "\x1b[0;32m";
|
||||
const YELLOW: &str = "\x1b[1;33m";
|
||||
const CYAN: &str = "\x1b[0;36m";
|
||||
const BOLD: &str = "\x1b[1m";
|
||||
const DIM: &str = "\x1b[2m";
|
||||
const NC: &str = "\x1b[0m";
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let cmd = args.get(1).map(|s| s.as_str()).unwrap_or("");
|
||||
let arg = args.get(2).map(|s| s.as_str());
|
||||
|
||||
match cmd {
|
||||
"start" => do_start(),
|
||||
"stop" => do_stop(),
|
||||
"restart" => do_restart(),
|
||||
"rebuild" => do_build(),
|
||||
"status" => do_status(),
|
||||
"logs" => do_logs(arg.unwrap_or("30"), false),
|
||||
"logs-follow" => do_logs(arg.unwrap_or("30"), true),
|
||||
"logs-all" => do_logs_all(),
|
||||
"test" => do_test(arg.unwrap_or("Say hello in exactly 3 words")),
|
||||
"health" => do_health(),
|
||||
_ => usage(),
|
||||
}
|
||||
}
|
||||
|
||||
fn usage() {
|
||||
println!("{BOLD}zg{NC} — ZeroGravity daemon manager\n");
|
||||
println!(" {CYAN}start{NC} Start the proxy daemon");
|
||||
println!(" {CYAN}stop{NC} Stop the proxy daemon");
|
||||
println!(" {CYAN}restart{NC} Rebuild + restart");
|
||||
println!(" {CYAN}rebuild{NC} Build release binary only");
|
||||
println!(" {CYAN}status{NC} Service status + quota + usage");
|
||||
println!(" {CYAN}logs{NC} [N] Show last N lines (default 30)");
|
||||
println!(" {CYAN}logs-follow{NC} [N] Tail last N lines + follow");
|
||||
println!(" {CYAN}logs-all{NC} Full log dump");
|
||||
println!(" {CYAN}test{NC} [msg] Quick test request (gemini-3-flash)");
|
||||
println!(" {CYAN}health{NC} Health check");
|
||||
}
|
||||
|
||||
fn project_dir() -> String {
|
||||
// Resolve project dir from binary location:
|
||||
// binary is at <project>/target/release/zg or <project>/target/debug/zg
|
||||
let exe = std::env::current_exe().expect("cannot resolve exe path");
|
||||
let exe_dir = exe.parent().expect("no parent dir");
|
||||
|
||||
// Walk up from target/release/ or target/debug/
|
||||
if let Some(target_dir) = exe_dir.parent() {
|
||||
if let Some(project) = target_dir.parent() {
|
||||
return project.to_string_lossy().to_string();
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: use current dir
|
||||
std::env::current_dir()
|
||||
.expect("no cwd")
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn base_url() -> String {
|
||||
let port = std::env::var("PROXY_PORT")
|
||||
.ok()
|
||||
.and_then(|p| p.parse::<u16>().ok())
|
||||
.unwrap_or(PORT);
|
||||
format!("http://localhost:{port}")
|
||||
}
|
||||
|
||||
fn systemctl(args: &[&str]) -> bool {
|
||||
Command::new("systemctl")
|
||||
.arg("--user")
|
||||
.args(args)
|
||||
.status()
|
||||
.map(|s| s.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn curl_get(path: &str) -> Option<String> {
|
||||
let url = format!("{}{}", base_url(), path);
|
||||
Command::new("curl")
|
||||
.args(["-sf", &url])
|
||||
.output()
|
||||
.ok()
|
||||
.filter(|o| o.status.success())
|
||||
.map(|o| String::from_utf8_lossy(&o.stdout).to_string())
|
||||
}
|
||||
|
||||
fn curl_post(path: &str, body: &str) -> Option<String> {
|
||||
let url = format!("{}{}", base_url(), path);
|
||||
Command::new("curl")
|
||||
.args(["-sf", &url, "-H", "Content-Type: application/json", "-d", body])
|
||||
.output()
|
||||
.ok()
|
||||
.filter(|o| o.status.success())
|
||||
.map(|o| String::from_utf8_lossy(&o.stdout).to_string())
|
||||
}
|
||||
|
||||
fn health_ok() -> bool {
|
||||
curl_get("/health").is_some()
|
||||
}
|
||||
|
||||
fn jq_print(json: &str) {
|
||||
let mut child = Command::new("jq")
|
||||
.arg(".")
|
||||
.stdin(Stdio::piped())
|
||||
.spawn()
|
||||
.expect("jq not found");
|
||||
if let Some(stdin) = child.stdin.as_mut() {
|
||||
use std::io::Write;
|
||||
let _ = stdin.write_all(json.as_bytes());
|
||||
}
|
||||
let _ = child.wait();
|
||||
}
|
||||
|
||||
// ── Commands ──
|
||||
|
||||
fn do_build() {
|
||||
let dir = project_dir();
|
||||
println!("{YELLOW}Building release binary...{NC}");
|
||||
let status = Command::new("cargo")
|
||||
.args(["build", "--release"])
|
||||
.current_dir(&dir)
|
||||
.stdin(Stdio::inherit())
|
||||
.stdout(Stdio::inherit())
|
||||
.stderr(Stdio::inherit())
|
||||
.status()
|
||||
.expect("failed to run cargo");
|
||||
|
||||
if status.success() {
|
||||
println!("{GREEN}Build complete.{NC}");
|
||||
} else {
|
||||
eprintln!("{RED}Build failed.{NC}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn do_start() {
|
||||
systemctl(&["daemon-reload"]);
|
||||
systemctl(&["start", SERVICE]);
|
||||
println!("{GREEN}Started.{NC} Waiting for ready...");
|
||||
|
||||
for _ in 0..20 {
|
||||
if health_ok() {
|
||||
println!("{GREEN}ZeroGravity is up on port {PORT}.{NC}");
|
||||
return;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||
}
|
||||
|
||||
eprintln!("{RED}Proxy didn't become healthy in 10s. Check logs:{NC}");
|
||||
let _ = Command::new("journalctl")
|
||||
.args(["--user", "-u", SERVICE, "--no-pager", "-n", "20"])
|
||||
.status();
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
fn do_stop() {
|
||||
let _ = systemctl(&["stop", SERVICE]);
|
||||
println!("{YELLOW}Stopped.{NC}");
|
||||
}
|
||||
|
||||
fn do_restart() {
|
||||
println!("{YELLOW}Stopping...{NC}");
|
||||
do_stop();
|
||||
do_build();
|
||||
do_start();
|
||||
}
|
||||
|
||||
fn do_status() {
|
||||
println!("{BOLD}── Service ──{NC}");
|
||||
let output = Command::new("systemctl")
|
||||
.args(["--user", "status", SERVICE, "--no-pager"])
|
||||
.output();
|
||||
match output {
|
||||
Ok(o) => {
|
||||
let text = String::from_utf8_lossy(&o.stdout);
|
||||
// Print first 6 lines
|
||||
for (i, line) in text.lines().enumerate() {
|
||||
if i >= 6 { break; }
|
||||
println!("{line}");
|
||||
}
|
||||
}
|
||||
Err(_) => println!("{RED}Not running{NC}"),
|
||||
}
|
||||
println!();
|
||||
|
||||
if !health_ok() {
|
||||
println!("{RED}Proxy is not responding on port {PORT}.{NC}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
println!("{BOLD}── Quota ──{NC}");
|
||||
match curl_get("/v1/quota") {
|
||||
Some(json) => jq_print(&json),
|
||||
None => println!("{DIM}(no quota data){NC}"),
|
||||
}
|
||||
println!();
|
||||
|
||||
println!("{BOLD}── Usage ──{NC}");
|
||||
match curl_get("/v1/usage") {
|
||||
Some(json) => jq_print(&json),
|
||||
None => println!("{DIM}(no usage data){NC}"),
|
||||
}
|
||||
println!();
|
||||
|
||||
println!("{BOLD}── Sessions ──{NC}");
|
||||
match curl_get("/v1/sessions") {
|
||||
Some(json) => jq_print(&json),
|
||||
None => println!("{DIM}(no sessions){NC}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn do_logs(n: &str, follow: bool) {
|
||||
let mut args = vec!["--user", "-u", SERVICE, "--no-pager", "-n", n];
|
||||
if follow {
|
||||
args.push("-f");
|
||||
}
|
||||
let _ = Command::new("journalctl")
|
||||
.args(&args)
|
||||
.stdin(Stdio::inherit())
|
||||
.stdout(Stdio::inherit())
|
||||
.stderr(Stdio::inherit())
|
||||
.status();
|
||||
}
|
||||
|
||||
fn do_logs_all() {
|
||||
let _ = Command::new("journalctl")
|
||||
.args(["--user", "-u", SERVICE, "--no-pager"])
|
||||
.stdin(Stdio::inherit())
|
||||
.stdout(Stdio::inherit())
|
||||
.stderr(Stdio::inherit())
|
||||
.status();
|
||||
}
|
||||
|
||||
fn do_test(msg: &str) {
|
||||
println!("{CYAN}Testing:{NC} {msg}");
|
||||
let body = format!(
|
||||
r#"{{"model":"gemini-3-flash","input":"{}","stream":false,"timeout":30}}"#,
|
||||
msg.replace('"', r#"\""#)
|
||||
);
|
||||
match curl_post("/v1/responses", &body) {
|
||||
Some(json) => jq_print(&json),
|
||||
None => {
|
||||
eprintln!("{RED}Request failed. Is the proxy running?{NC}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn do_health() {
|
||||
match curl_get("/health") {
|
||||
Some(json) => {
|
||||
jq_print(&json);
|
||||
println!("{GREEN}Healthy{NC}");
|
||||
}
|
||||
None => {
|
||||
println!("{RED}Not responding{NC}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -185,7 +185,7 @@ pub fn log_base() -> String {
|
||||
/// Token file path.
|
||||
pub fn token_file_path() -> String {
|
||||
let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string());
|
||||
format!("{home}/.config/antigravity-proxy-token")
|
||||
format!("{home}/.config/zerogravity/token")
|
||||
}
|
||||
|
||||
/// User-Agent string matching the Electron webview — computed once.
|
||||
|
||||
22
src/main.rs
22
src/main.rs
@@ -1,4 +1,4 @@
|
||||
//! Antigravity OpenAI Proxy — Rust edition v3 (stealth hardened).
|
||||
//! ZeroGravity — Rust edition v3 (stealth hardened).
|
||||
//!
|
||||
//! Single-binary replacement for server.py. BoringSSL TLS impersonation,
|
||||
//! byte-exact protobuf encoding, Chrome header fingerprinting, cascade
|
||||
@@ -26,8 +26,8 @@ use mitm::store::MitmStore;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(
|
||||
name = "antigravity-proxy",
|
||||
about = "Antigravity OpenAI Proxy (stealth)"
|
||||
name = "zerogravity",
|
||||
about = "ZeroGravity — stealth LLM proxy"
|
||||
)]
|
||||
struct Cli {
|
||||
/// Port to listen on
|
||||
@@ -64,7 +64,7 @@ struct Cli {
|
||||
#[arg(long, conflicts_with = "headless")]
|
||||
classic: bool,
|
||||
|
||||
/// Disable per-call debug traces (on by default, writes JSON to ~/.config/antigravity-proxy/traces/)
|
||||
/// Disable per-call debug traces (on by default, writes JSON to ~/.config/zerogravity/traces/)
|
||||
#[arg(long)]
|
||||
no_trace: bool,
|
||||
}
|
||||
@@ -236,12 +236,12 @@ async fn main() {
|
||||
|
||||
let backend = Arc::new(if let Some((_, port, ref csrf)) = standalone_ls {
|
||||
// Build backend pointing at standalone LS
|
||||
let oauth = std::env::var("ANTIGRAVITY_OAUTH_TOKEN")
|
||||
let oauth = std::env::var("ZEROGRAVITY_TOKEN")
|
||||
.ok()
|
||||
.filter(|s| !s.is_empty())
|
||||
.or_else(|| {
|
||||
let home = std::env::var("HOME").unwrap_or_default();
|
||||
let path = format!("{home}/.config/antigravity-proxy-token");
|
||||
let path = format!("{home}/.config/zerogravity/token");
|
||||
std::fs::read_to_string(&path)
|
||||
.ok()
|
||||
.map(|s| s.trim().to_string())
|
||||
@@ -282,7 +282,7 @@ async fn main() {
|
||||
let trace_collector = trace::TraceCollector::new(trace_enabled);
|
||||
if trace_enabled {
|
||||
trace_collector.cleanup_old_traces(7);
|
||||
info!("Debug tracing enabled → ~/.config/antigravity-proxy/traces/");
|
||||
info!("Debug tracing enabled → ~/.config/zerogravity/traces/");
|
||||
}
|
||||
|
||||
let state = Arc::new(AppState {
|
||||
@@ -391,7 +391,7 @@ fn print_banner(
|
||||
let ver = crate::constants::antigravity_version();
|
||||
|
||||
println!();
|
||||
println!(" \x1b[1;35m>> antigravity-proxy\x1b[0m \x1b[2mv{ver}\x1b[0m");
|
||||
println!(" \x1b[1;35m>> zerogravity\x1b[0m \x1b[2mv{ver}\x1b[0m");
|
||||
println!(" \x1b[2m────────────────────────────────────────────────\x1b[0m");
|
||||
println!();
|
||||
println!(" \x1b[1mcore\x1b[0m");
|
||||
@@ -462,11 +462,11 @@ fn print_banner(
|
||||
|
||||
if token == "NOT SET" {
|
||||
println!(" \x1b[1;33m[!]\x1b[0m no oauth token");
|
||||
println!(" export ANTIGRAVITY_OAUTH_TOKEN=ya29.xxx");
|
||||
println!(" export ZEROGRAVITY_TOKEN=ya29.xxx");
|
||||
println!(
|
||||
" curl -X POST http://127.0.0.1:{port}/v1/token -d '{{\"token\":\"ya29.xxx\"}}'"
|
||||
);
|
||||
println!(" echo 'ya29.xxx' > ~/.config/antigravity-proxy-token");
|
||||
println!(" echo 'ya29.xxx' > ~/.config/zerogravity/token");
|
||||
println!();
|
||||
}
|
||||
}
|
||||
@@ -522,5 +522,5 @@ fn dirs_data_dir() -> std::path::PathBuf {
|
||||
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
|
||||
std::path::PathBuf::from(home)
|
||||
.join(".config")
|
||||
.join("antigravity-proxy")
|
||||
.join("zerogravity")
|
||||
}
|
||||
|
||||
@@ -24,13 +24,13 @@ const LS_BINARY_PATH: &str =
|
||||
const APP_ROOT: &str = "/usr/share/antigravity/resources/app";
|
||||
|
||||
/// Data directory for the standalone LS.
|
||||
const DATA_DIR: &str = "/tmp/antigravity-standalone";
|
||||
const DATA_DIR: &str = "/tmp/zerogravity-standalone";
|
||||
|
||||
/// System user for UID-scoped iptables isolation.
|
||||
const LS_USER: &str = "antigravity-ls";
|
||||
const LS_USER: &str = "zerogravity-ls";
|
||||
|
||||
/// Path for the compiled dns_redirect.so preload library.
|
||||
const DNS_REDIRECT_SO_PATH: &str = "/tmp/antigravity-dns-redirect.so";
|
||||
const DNS_REDIRECT_SO_PATH: &str = "/tmp/zerogravity-dns-redirect.so";
|
||||
|
||||
/// Source file for the DNS redirect preload library (relative to binary).
|
||||
const DNS_REDIRECT_C_SOURCE: &str = include_str!("../mitm/dns_redirect.c");
|
||||
|
||||
@@ -57,9 +57,9 @@ impl StandaloneLS {
|
||||
1, // DETECT_AND_USE_PROXY_ENABLED
|
||||
);
|
||||
|
||||
// Setup data dir (mode 1777 so both current user and antigravity-ls can write)
|
||||
// Setup data dir (mode 1777 so both current user and zerogravity-ls can write)
|
||||
let gemini_dir = format!("{DATA_DIR}/.gemini");
|
||||
let app_data_dir = format!("{DATA_DIR}/.gemini/antigravity-standalone");
|
||||
let app_data_dir = format!("{DATA_DIR}/.gemini/zerogravity-standalone");
|
||||
let annotations_dir = format!("{app_data_dir}/annotations");
|
||||
let brain_dir = format!("{app_data_dir}/brain");
|
||||
for dir in [
|
||||
@@ -77,7 +77,7 @@ impl StandaloneLS {
|
||||
}
|
||||
}
|
||||
// Check if data dir is writable by writing a test file.
|
||||
// Old runs as `antigravity-ls` user leave dirs owned by that user.
|
||||
// Old runs as `zerogravity-ls` user leave dirs owned by that user.
|
||||
let test_path = format!("{app_data_dir}/.write_test");
|
||||
if std::fs::write(&test_path, b"ok").is_err() {
|
||||
eprintln!(
|
||||
@@ -138,12 +138,12 @@ impl StandaloneLS {
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
// Fall back to env var / token file
|
||||
let token = std::env::var("ANTIGRAVITY_OAUTH_TOKEN")
|
||||
let token = std::env::var("ZEROGRAVITY_TOKEN")
|
||||
.ok()
|
||||
.filter(|s| !s.is_empty())
|
||||
.or_else(|| {
|
||||
let home = std::env::var("HOME").unwrap_or_default();
|
||||
let path = format!("{home}/.config/antigravity-proxy-token");
|
||||
let path = format!("{home}/.config/zerogravity/token");
|
||||
std::fs::read_to_string(&path)
|
||||
.ok()
|
||||
.map(|s| s.trim().to_string())
|
||||
@@ -153,7 +153,7 @@ impl StandaloneLS {
|
||||
if !token.is_empty() {
|
||||
info!("Loaded OAuth token from env/file (no refresh token — manual refresh needed)");
|
||||
} else {
|
||||
eprintln!("[headless] ⚠ No OAuth token found. Login to Antigravity first, or set ANTIGRAVITY_OAUTH_TOKEN");
|
||||
eprintln!("[headless] ⚠ No OAuth token found. Login to Antigravity first, or set ZEROGRAVITY_TOKEN");
|
||||
}
|
||||
(token, None)
|
||||
});
|
||||
@@ -224,7 +224,7 @@ impl StandaloneLS {
|
||||
"https://daily-cloudcode-pa.googleapis.com".to_string()
|
||||
},
|
||||
"-app_data_dir".to_string(),
|
||||
"antigravity-standalone".to_string(),
|
||||
"zerogravity-standalone".to_string(),
|
||||
"-gemini_dir".to_string(),
|
||||
gemini_dir,
|
||||
];
|
||||
@@ -240,16 +240,16 @@ impl StandaloneLS {
|
||||
if let Some(mitm) = mitm_config {
|
||||
// Go's SSL_CERT_FILE replaces the entire system cert pool, so we
|
||||
// need a combined bundle: system CAs + our MITM CA
|
||||
// Write to /tmp — accessible by antigravity-ls user
|
||||
// Write to /tmp — accessible by zerogravity-ls user
|
||||
// (user's ~/.config/ is not traversable by other UIDs)
|
||||
let combined_ca_path = "/tmp/antigravity-mitm-combined-ca.pem".to_string();
|
||||
let combined_ca_path = "/tmp/zerogravity-mitm-ca.pem".to_string();
|
||||
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}"))
|
||||
.map_err(|e| format!("Failed to write combined CA bundle: {e}"))?;
|
||||
// Make readable by antigravity-ls user
|
||||
// Make readable by zerogravity-ls user
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
@@ -286,18 +286,18 @@ impl StandaloneLS {
|
||||
env_vars.push(("LD_PRELOAD".into(), so));
|
||||
env_vars.push((
|
||||
"DNS_REDIRECT_LOG".into(),
|
||||
"/tmp/antigravity-dns-redirect.log".into(),
|
||||
"/tmp/zerogravity-dns-redirect.log".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// In headless mode, never use sudo — run as current user
|
||||
// In normal mode, use sudo if 'antigravity-ls' user exists
|
||||
// In normal mode, use sudo if 'zerogravity-ls' user exists
|
||||
let use_sudo = !headless && has_ls_user();
|
||||
|
||||
let mut cmd = if use_sudo {
|
||||
info!("Using UID isolation: spawning LS as 'antigravity-ls' user");
|
||||
info!("Using UID isolation: spawning LS as 'zerogravity-ls' user");
|
||||
let mut c = Command::new("sudo");
|
||||
c.args(["-n", "-u", LS_USER, "--", "/usr/bin/env"]);
|
||||
for (k, v) in &env_vars {
|
||||
@@ -317,7 +317,7 @@ impl StandaloneLS {
|
||||
};
|
||||
|
||||
// Capture stderr for debugging — logs to /tmp so we can diagnose LS failures
|
||||
let stderr_file = std::fs::File::create("/tmp/antigravity-ls-debug.log")
|
||||
let stderr_file = std::fs::File::create("/tmp/zerogravity-ls-debug.log")
|
||||
.map_err(|e| format!("Failed to create LS debug log: {e}"))?;
|
||||
cmd.stdin(Stdio::piped())
|
||||
.stdout(Stdio::null())
|
||||
@@ -342,7 +342,7 @@ impl StandaloneLS {
|
||||
let ls_pid = if use_sudo {
|
||||
// Give sudo a moment to spawn the real process
|
||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||
// Find the LS process owned by antigravity-ls user
|
||||
// Find the LS process owned by zerogravity-ls user
|
||||
find_ls_pid_for_user(LS_USER).ok()
|
||||
} else {
|
||||
Some(child.id())
|
||||
@@ -424,7 +424,7 @@ impl StandaloneLS {
|
||||
// The child is sudo which already exited. Kill the actual LS.
|
||||
if let Some(pid) = self.ls_pid {
|
||||
info!(pid, "Killing LS process via sudo -u {}", LS_USER);
|
||||
// Run kill AS the antigravity-ls user (same UID can signal)
|
||||
// Run kill AS the zerogravity-ls user (same UID can signal)
|
||||
let ok = std::process::Command::new("sudo")
|
||||
.args(["-n", "-u", LS_USER, "kill", "-TERM", &pid.to_string()])
|
||||
.stdout(Stdio::null())
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Per-call debug trace system.
|
||||
//!
|
||||
//! Every API call gets a structured JSON trace file written to
|
||||
//! `~/.config/antigravity-proxy/traces/{YYYY-MM-DD}/{HH-MM-SS}_{cascade_short}.json`.
|
||||
//! `~/.config/zerogravity/traces/{YYYY-MM-DD}/{HH-MM-SS}_{cascade_short}.json`.
|
||||
//!
|
||||
//! Designed for LLM consumption: compact, structured, no raw bodies.
|
||||
|
||||
@@ -23,7 +23,7 @@ impl TraceCollector {
|
||||
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
|
||||
let traces_dir = PathBuf::from(home)
|
||||
.join(".config")
|
||||
.join("antigravity-proxy")
|
||||
.join("zerogravity")
|
||||
.join("traces");
|
||||
Self {
|
||||
enabled,
|
||||
|
||||
Reference in New Issue
Block a user