feat: add proxyctl daemon manager, fix standalone LS cleanup
- Add proxyctl CLI script for systemd service management - Add systemd user service file for background operation - Fix standalone LS kill: properly track real LS PID via pgrep and use sudo kill for cross-user cleanup on shutdown - Remove deprecated scripts (dns-redirect, iptables-redirect, mitm-wrapper, standalone-ls, parse-snapshot) - Disable tool stripping in MITM for tool call investigation - Update GEMINI.md with CLI tools documentation
This commit is contained in:
42
GEMINI.md
42
GEMINI.md
@@ -5,24 +5,46 @@ OpenAI-compatible proxy that intercepts and relays requests to Google's Antigrav
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Build
|
||||
cargo build --release
|
||||
|
||||
# First-time setup (creates user + iptables for MITM)
|
||||
sudo ./scripts/mitm-redirect.sh install
|
||||
|
||||
# Run (spawns standalone LS automatically)
|
||||
# Start as daemon (builds if needed)
|
||||
proxyctl start
|
||||
|
||||
# Or run directly
|
||||
RUST_LOG=info ./target/release/antigravity-proxy
|
||||
|
||||
# Custom port
|
||||
RUST_LOG=info ./target/release/antigravity-proxy --port 9000
|
||||
|
||||
# Attach to existing LS instead of spawning standalone
|
||||
RUST_LOG=info ./target/release/antigravity-proxy --no-standalone
|
||||
```
|
||||
|
||||
Default port: **8741**
|
||||
|
||||
## CLI Tools
|
||||
|
||||
### `proxyctl` — Daemon Manager
|
||||
|
||||
Symlinked to `~/.local/bin/proxyctl` for global access. Manages the proxy as a systemd user service.
|
||||
|
||||
| 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 (default 30) + follow |
|
||||
| `proxyctl logs-all` | Full log dump (no follow) |
|
||||
| `proxyctl test [msg]` | Quick test request (gemini-3-flash) |
|
||||
| `proxyctl health` | Health check |
|
||||
|
||||
### `mitm-redirect.sh` — MITM Setup
|
||||
|
||||
One-time setup script for UID-scoped iptables traffic redirection.
|
||||
|
||||
```bash
|
||||
sudo ./scripts/mitm-redirect.sh install # create user + iptables rule
|
||||
sudo ./scripts/mitm-redirect.sh uninstall # remove user + iptables rule
|
||||
sudo ./scripts/mitm-redirect.sh status # check current state
|
||||
```
|
||||
|
||||
## Endpoints
|
||||
|
||||
| Method | Path | Description |
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# ╔═══════════════════════════════════════════════════════════════════════════╗
|
||||
# ║ Antigravity MITM — DNS-based redirect for targeted interception ║
|
||||
# ║ ║
|
||||
# ║ Instead of redirecting ALL port 443 traffic (which breaks everything), ║
|
||||
# ║ this uses /etc/hosts to redirect ONLY the LLM API domain to localhost, ║
|
||||
# ║ then iptables redirects only localhost:443 → MITM port. ║
|
||||
# ║ ║
|
||||
# ║ Also adds the MITM CA to the system trust store so Go trusts it. ║
|
||||
# ╚═══════════════════════════════════════════════════════════════════════════╝
|
||||
set -euo pipefail
|
||||
|
||||
MITM_PORT="${ANTIGRAVITY_MITM_PORT:-8742}"
|
||||
MITM_CA="${HOME}/.config/antigravity-proxy/mitm-ca.pem"
|
||||
# If run with sudo, use SUDO_USER's home
|
||||
if [[ -n "${SUDO_USER:-}" ]]; then
|
||||
MITM_CA="$(eval echo "~${SUDO_USER}")/.config/antigravity-proxy/mitm-ca.pem"
|
||||
fi
|
||||
|
||||
HOSTS_MARKER="# antigravity-mitm"
|
||||
API_DOMAINS=(
|
||||
"daily-cloudcode-pa.googleapis.com"
|
||||
)
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
CYAN='\033[0;36m'
|
||||
BOLD='\033[1m'
|
||||
NC='\033[0m'
|
||||
|
||||
cmd_install() {
|
||||
echo -e "${BOLD}${CYAN}Antigravity MITM DNS Redirect Setup${NC}"
|
||||
echo -e "────────────────────────────────────"
|
||||
echo ""
|
||||
|
||||
# 1. Add MITM CA to system trust store
|
||||
if [[ ! -f "$MITM_CA" ]]; then
|
||||
echo -e " ${RED}✗${NC} MITM CA not found: ${MITM_CA}"
|
||||
echo -e " Start the proxy once first to generate it."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local sys_cert="/usr/local/share/ca-certificates/antigravity-mitm.crt"
|
||||
cp "$MITM_CA" "$sys_cert"
|
||||
update-ca-certificates >/dev/null 2>&1
|
||||
echo -e " ${GREEN}✓${NC} MITM CA added to system trust store"
|
||||
|
||||
# 2. Add /etc/hosts entries for API domains → 127.0.0.1
|
||||
# First, cache the real IPs for the MITM to use later
|
||||
local real_ips_file="/tmp/antigravity-mitm-real-ips"
|
||||
> "$real_ips_file"
|
||||
|
||||
for domain in "${API_DOMAINS[@]}"; do
|
||||
# Remove old entries
|
||||
sed -i "/${domain}.*${HOSTS_MARKER}/d" /etc/hosts
|
||||
|
||||
# Resolve and cache the real IPs BEFORE redirecting
|
||||
local real_ip
|
||||
real_ip=$(dig +short "$domain" 2>/dev/null | grep -E '^[0-9]+\.' | head -1)
|
||||
if [[ -n "$real_ip" ]]; then
|
||||
echo "${domain}=${real_ip}" >> "$real_ips_file"
|
||||
fi
|
||||
|
||||
# Add the /etc/hosts redirect
|
||||
echo "127.0.0.1 ${domain} ${HOSTS_MARKER}" >> /etc/hosts
|
||||
echo -e " ${GREEN}✓${NC} /etc/hosts: ${domain} → 127.0.0.1 (real: ${real_ip:-unknown})"
|
||||
done
|
||||
|
||||
# 3. iptables: redirect ONLY 127.0.0.1:443 → MITM port
|
||||
# This catches only the /etc/hosts redirected domains, nothing else!
|
||||
iptables -t nat -D OUTPUT -d 127.0.0.1 -p tcp --dport 443 \
|
||||
-j REDIRECT --to-port "$MITM_PORT" 2>/dev/null || true
|
||||
iptables -t nat -A OUTPUT -d 127.0.0.1 -p tcp --dport 443 \
|
||||
-j REDIRECT --to-port "$MITM_PORT"
|
||||
echo -e " ${GREEN}✓${NC} iptables: 127.0.0.1:443 → localhost:${MITM_PORT}"
|
||||
|
||||
echo ""
|
||||
echo -e " ${GREEN}Done!${NC}"
|
||||
echo ""
|
||||
echo -e " ${BOLD}How it works:${NC}"
|
||||
echo -e " 1. LS resolves ${API_DOMAINS[0]} → 127.0.0.1 (via /etc/hosts)"
|
||||
echo -e " 2. LS connects to 127.0.0.1:443"
|
||||
echo -e " 3. iptables redirects to MITM proxy on :${MITM_PORT}"
|
||||
echo -e " 4. MITM intercepts, decrypts (CA is trusted), proxies to real Google"
|
||||
echo ""
|
||||
echo -e " Real upstream IPs cached in: ${real_ips_file}"
|
||||
echo -e " Restart Antigravity for changes to take effect."
|
||||
echo -e " Undo: sudo $0 uninstall"
|
||||
echo ""
|
||||
}
|
||||
|
||||
cmd_uninstall() {
|
||||
echo -e "${BOLD}${CYAN}Removing MITM DNS Redirect${NC}"
|
||||
echo ""
|
||||
|
||||
# Remove /etc/hosts entries
|
||||
sed -i "/${HOSTS_MARKER}/d" /etc/hosts
|
||||
echo -e " ${GREEN}✓${NC} Removed /etc/hosts entries"
|
||||
|
||||
# Remove iptables rule
|
||||
iptables -t nat -D OUTPUT -d 127.0.0.1 -p tcp --dport 443 \
|
||||
-j REDIRECT --to-port "$MITM_PORT" 2>/dev/null || true
|
||||
echo -e " ${GREEN}✓${NC} Removed iptables rule"
|
||||
|
||||
# Remove system CA (optional)
|
||||
rm -f /usr/local/share/ca-certificates/antigravity-mitm.crt
|
||||
update-ca-certificates >/dev/null 2>&1
|
||||
echo -e " ${GREEN}✓${NC} Removed MITM CA from system trust store"
|
||||
|
||||
echo ""
|
||||
}
|
||||
|
||||
cmd_status() {
|
||||
echo -e "${BOLD}${CYAN}MITM DNS Redirect Status${NC}"
|
||||
echo ""
|
||||
|
||||
# Check /etc/hosts
|
||||
local hosts_count
|
||||
hosts_count=$(grep -c "$HOSTS_MARKER" /etc/hosts 2>/dev/null || echo 0)
|
||||
if [[ "$hosts_count" -gt 0 ]]; then
|
||||
echo -e " ${GREEN}✓${NC} /etc/hosts: ${hosts_count} domain(s) redirected"
|
||||
grep "$HOSTS_MARKER" /etc/hosts | sed 's/^/ /'
|
||||
else
|
||||
echo -e " ${YELLOW}○${NC} /etc/hosts: no redirects"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
# Check iptables
|
||||
if iptables -t nat -L OUTPUT -n 2>/dev/null | grep -q "127.0.0.1.*REDIRECT.*${MITM_PORT}"; then
|
||||
echo -e " ${GREEN}✓${NC} iptables: 127.0.0.1:443 → :${MITM_PORT}"
|
||||
else
|
||||
echo -e " ${YELLOW}○${NC} iptables: no redirect"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
# Check system CA
|
||||
if [[ -f /usr/local/share/ca-certificates/antigravity-mitm.crt ]]; then
|
||||
echo -e " ${GREEN}✓${NC} System CA: installed"
|
||||
else
|
||||
echo -e " ${YELLOW}○${NC} System CA: not installed"
|
||||
fi
|
||||
echo ""
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
install)
|
||||
cmd_install
|
||||
;;
|
||||
uninstall)
|
||||
cmd_uninstall
|
||||
;;
|
||||
status)
|
||||
cmd_status
|
||||
;;
|
||||
*)
|
||||
echo "Usage: sudo $0 {install|uninstall|status}"
|
||||
echo ""
|
||||
echo "Redirects LLM API domain to localhost via /etc/hosts + iptables."
|
||||
echo "Only intercepts API traffic, everything else is untouched."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -1,168 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# ╔═══════════════════════════════════════════════════════════════════════════╗
|
||||
# ║ Antigravity MITM — iptables redirect for transparent interception ║
|
||||
# ║ ║
|
||||
# ║ Redirects outbound port 443 traffic to the MITM proxy. ║
|
||||
# ║ Uses a dedicated GID to exclude the proxy's own upstream traffic, ║
|
||||
# ║ preventing redirect loops. ║
|
||||
# ║ ║
|
||||
# ║ Usage: sudo ./iptables-redirect.sh install ║
|
||||
# ║ sudo ./iptables-redirect.sh uninstall ║
|
||||
# ║ sudo ./iptables-redirect.sh status ║
|
||||
# ╚═══════════════════════════════════════════════════════════════════════════╝
|
||||
set -euo pipefail
|
||||
|
||||
MITM_PORT="${ANTIGRAVITY_MITM_PORT:-8742}"
|
||||
CHAIN="ANTIGRAVITY_MITM"
|
||||
BYPASS_GROUP="mitm-bypass"
|
||||
|
||||
# Resolve target user (the one whose traffic we redirect)
|
||||
TARGET_USER="${SUDO_USER:-$(whoami)}"
|
||||
TARGET_UID=$(id -u "$TARGET_USER" 2>/dev/null || echo "")
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
CYAN='\033[0;36m'
|
||||
BOLD='\033[1m'
|
||||
NC='\033[0m'
|
||||
|
||||
cmd_install() {
|
||||
echo -e "${BOLD}${CYAN}Antigravity MITM iptables Setup${NC}"
|
||||
echo -e "────────────────────────────────"
|
||||
echo ""
|
||||
|
||||
if [[ -z "$TARGET_UID" ]]; then
|
||||
echo -e " ${RED}✗${NC} Cannot resolve UID for user '${TARGET_USER}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create bypass group (proxy runs with this GID to avoid redirect loop)
|
||||
if ! getent group "$BYPASS_GROUP" >/dev/null 2>&1; then
|
||||
groupadd "$BYPASS_GROUP"
|
||||
echo -e " ${GREEN}✓${NC} Created group: ${BYPASS_GROUP}"
|
||||
else
|
||||
echo -e " ${GREEN}✓${NC} Group exists: ${BYPASS_GROUP}"
|
||||
fi
|
||||
|
||||
# Add user to bypass group (so they can use 'sg' to run proxy)
|
||||
if ! id -nG "$TARGET_USER" 2>/dev/null | grep -qw "$BYPASS_GROUP"; then
|
||||
usermod -aG "$BYPASS_GROUP" "$TARGET_USER"
|
||||
echo -e " ${GREEN}✓${NC} Added ${TARGET_USER} to ${BYPASS_GROUP}"
|
||||
fi
|
||||
|
||||
local bypass_gid
|
||||
bypass_gid=$(getent group "$BYPASS_GROUP" | cut -d: -f3)
|
||||
|
||||
# Check MITM proxy is running
|
||||
if ! ss -tlnp 2>/dev/null | grep -q ":${MITM_PORT}"; then
|
||||
echo -e " ${YELLOW}!${NC} MITM proxy not running on :${MITM_PORT} (will work once started)"
|
||||
else
|
||||
echo -e " ${GREEN}✓${NC} MITM proxy listening on :${MITM_PORT}"
|
||||
fi
|
||||
|
||||
# Create our chain
|
||||
iptables -t nat -N "$CHAIN" 2>/dev/null || true
|
||||
iptables -t nat -F "$CHAIN"
|
||||
|
||||
# THE KEY RULE: redirect port 443 traffic UNLESS it's from the bypass group.
|
||||
# This prevents redirect loops — the proxy runs with GID=mitm-bypass,
|
||||
# so its upstream connections to Google are NOT redirected back to itself.
|
||||
iptables -t nat -A "$CHAIN" \
|
||||
-m owner ! --gid-owner "$bypass_gid" \
|
||||
-p tcp --dport 443 \
|
||||
-j REDIRECT --to-port "$MITM_PORT"
|
||||
|
||||
echo -e " ${GREEN}✓${NC} Redirect rule: tcp/443 → :${MITM_PORT} (skip GID ${bypass_gid})"
|
||||
|
||||
# Hook into OUTPUT for target user only
|
||||
iptables -t nat -D OUTPUT -m owner --uid-owner "$TARGET_UID" \
|
||||
-p tcp --dport 443 -j "$CHAIN" 2>/dev/null || true
|
||||
iptables -t nat -A OUTPUT -m owner --uid-owner "$TARGET_UID" \
|
||||
-p tcp --dport 443 -j "$CHAIN"
|
||||
|
||||
echo -e " ${GREEN}✓${NC} OUTPUT hook: UID ${TARGET_UID} (${TARGET_USER})"
|
||||
|
||||
echo ""
|
||||
echo -e " ${GREEN}Done!${NC}"
|
||||
echo ""
|
||||
echo -e " ${BOLD}IMPORTANT:${NC} Run the proxy with the bypass group to avoid loops:"
|
||||
echo -e " ${CYAN}sg ${BYPASS_GROUP} -c 'RUST_LOG=info ./target/release/antigravity-proxy'${NC}"
|
||||
echo ""
|
||||
echo -e " Then restart Antigravity to re-establish connections."
|
||||
echo -e " Undo: sudo $0 uninstall"
|
||||
echo ""
|
||||
}
|
||||
|
||||
cmd_uninstall() {
|
||||
echo -e "${BOLD}${CYAN}Removing iptables MITM redirect${NC}"
|
||||
echo ""
|
||||
|
||||
local target_uid
|
||||
target_uid=$(id -u "$TARGET_USER" 2>/dev/null || echo "1000")
|
||||
|
||||
# Remove jump from OUTPUT
|
||||
iptables -t nat -D OUTPUT -m owner --uid-owner "$target_uid" \
|
||||
-p tcp --dport 443 -j "$CHAIN" 2>/dev/null || true
|
||||
echo -e " ${GREEN}✓${NC} Removed OUTPUT jump"
|
||||
|
||||
# Flush and delete our chain
|
||||
iptables -t nat -F "$CHAIN" 2>/dev/null || true
|
||||
iptables -t nat -X "$CHAIN" 2>/dev/null || true
|
||||
echo -e " ${GREEN}✓${NC} Removed ${CHAIN} chain"
|
||||
|
||||
echo ""
|
||||
echo -e " ${YELLOW}Note:${NC} Group '${BYPASS_GROUP}' left intact (harmless)."
|
||||
echo -e " Remove with: sudo groupdel ${BYPASS_GROUP}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
cmd_status() {
|
||||
echo -e "${BOLD}${CYAN}iptables MITM Status${NC}"
|
||||
echo ""
|
||||
|
||||
if iptables -t nat -L "$CHAIN" -n 2>/dev/null | grep -q "REDIRECT"; then
|
||||
echo -e " ${GREEN}✓${NC} Chain ${CHAIN}: active"
|
||||
iptables -t nat -L "$CHAIN" -nv --line-numbers 2>/dev/null | \
|
||||
sed 's/^/ /'
|
||||
else
|
||||
echo -e " ${YELLOW}○${NC} Chain ${CHAIN}: not installed"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
if iptables -t nat -L OUTPUT -n 2>/dev/null | grep -q "$CHAIN"; then
|
||||
echo -e " ${GREEN}✓${NC} OUTPUT hook: installed"
|
||||
iptables -t nat -L OUTPUT -n 2>/dev/null | grep "$CHAIN" | sed 's/^/ /'
|
||||
else
|
||||
echo -e " ${YELLOW}○${NC} OUTPUT hook: not installed"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
if getent group "$BYPASS_GROUP" >/dev/null 2>&1; then
|
||||
local gid
|
||||
gid=$(getent group "$BYPASS_GROUP" | cut -d: -f3)
|
||||
echo -e " ${GREEN}✓${NC} Bypass group: ${BYPASS_GROUP} (GID ${gid})"
|
||||
else
|
||||
echo -e " ${YELLOW}○${NC} Bypass group: not created"
|
||||
fi
|
||||
echo ""
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
install)
|
||||
cmd_install
|
||||
;;
|
||||
uninstall)
|
||||
cmd_uninstall
|
||||
;;
|
||||
status)
|
||||
cmd_status
|
||||
;;
|
||||
*)
|
||||
echo "Usage: sudo $0 {install|uninstall|status}"
|
||||
echo ""
|
||||
echo "Redirects outbound port 443 traffic to the MITM proxy."
|
||||
echo "The proxy must be run with 'sg mitm-bypass' to avoid redirect loops."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -1,329 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# ╔═══════════════════════════════════════════════════════════════════════════╗
|
||||
# ║ Antigravity MITM LS Wrapper ║
|
||||
# ║ ║
|
||||
# ║ This script replaces the real Antigravity language server binary. ║
|
||||
# ║ It injects HTTPS_PROXY and NODE_EXTRA_CA_CERTS environment variables ║
|
||||
# ║ so the MITM proxy can intercept LS<->API traffic. ║
|
||||
# ║ ║
|
||||
# ║ Install: ./mitm-wrapper.sh install ║
|
||||
# ║ Uninstall: ./mitm-wrapper.sh uninstall ║
|
||||
# ║ (No args = act as wrapper, exec the real binary with injected env) ║
|
||||
# ╚═══════════════════════════════════════════════════════════════════════════╝
|
||||
set -euo pipefail
|
||||
|
||||
# ── Config ────────────────────────────────────────────────────────────────────
|
||||
# Resolve the real user's home (not /root when running under sudo)
|
||||
if [[ -n "${SUDO_USER:-}" ]]; then
|
||||
REAL_HOME="$(getent passwd "$SUDO_USER" | cut -d: -f6)"
|
||||
else
|
||||
REAL_HOME="$HOME"
|
||||
fi
|
||||
MITM_PORT_FILE="${REAL_HOME}/.config/antigravity-proxy/mitm-port"
|
||||
if [[ -n "${ANTIGRAVITY_MITM_PORT:-}" ]]; then
|
||||
MITM_PORT="$ANTIGRAVITY_MITM_PORT"
|
||||
elif [[ -f "$MITM_PORT_FILE" ]]; then
|
||||
MITM_PORT="$(cat "$MITM_PORT_FILE" 2>/dev/null || echo 8742)"
|
||||
else
|
||||
MITM_PORT="8742"
|
||||
fi
|
||||
CA_PATH="${REAL_HOME}/.config/antigravity-proxy/mitm-ca.pem"
|
||||
|
||||
# Antigravity LS — discovered dynamically from running processes.
|
||||
# Hardcoded paths are only used as a fallback if no LS process is running.
|
||||
LS_FALLBACK_DIRS=(
|
||||
"/usr/share/antigravity/resources/app/extensions/antigravity/bin"
|
||||
"/opt/Antigravity/resources/app/extensions/antigravity/bin"
|
||||
"${REAL_HOME}/.local/share/antigravity/resources/app/extensions/antigravity/bin"
|
||||
)
|
||||
|
||||
BACKUP_SUFFIX=".real"
|
||||
|
||||
# ── Colors ────────────────────────────────────────────────────────────────────
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
CYAN='\033[0;36m'
|
||||
BOLD='\033[1m'
|
||||
NC='\033[0m'
|
||||
|
||||
# ── Find LS binary ───────────────────────────────────────────────────────────
|
||||
find_ls_binary() {
|
||||
# Method 1: Find from running process via /proc
|
||||
if [[ -d /proc ]]; then
|
||||
for pid_dir in /proc/[0-9]*; do
|
||||
local exe_target
|
||||
exe_target="$(readlink "${pid_dir}/exe" 2>/dev/null)" || continue
|
||||
# Strip " (deleted)" suffix that appears when the binary was unlinked
|
||||
exe_target="${exe_target% (deleted)}"
|
||||
if [[ "$exe_target" == *language_server_linux* ]] || \
|
||||
[[ "$exe_target" == *antigravity-language-server* ]]; then
|
||||
# FIX: If the running process is the backup (.real), strip the suffix
|
||||
# so we return the path to the base binary name.
|
||||
echo "${exe_target%$BACKUP_SUFFIX}"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Method 2: Fallback — scan known directories for common binary names
|
||||
local bin_names=("language_server_linux_x64" "language_server_linux_arm64" "antigravity-language-server")
|
||||
for dir_pattern in "${LS_FALLBACK_DIRS[@]}"; do
|
||||
for dir in $dir_pattern; do
|
||||
[[ -d "$dir" ]] || continue
|
||||
for name in "${bin_names[@]}"; do
|
||||
local path="${dir}/${name}"
|
||||
if [[ -f "$path" || -f "${path}${BACKUP_SUFFIX}" ]]; then
|
||||
echo "$path"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
done
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# ── Install ──────────────────────────────────────────────────────────────────
|
||||
cmd_install() {
|
||||
# Find the LS binary first (quiet, just to check permissions)
|
||||
local ls_path
|
||||
ls_path=$(find_ls_binary) || ls_path="${1:-}"
|
||||
|
||||
# Allow override
|
||||
if [[ -n "${1:-}" ]]; then
|
||||
ls_path="$1"
|
||||
fi
|
||||
|
||||
# Check permissions upfront — re-exec with sudo before doing anything
|
||||
if [[ -n "$ls_path" ]]; then
|
||||
local ls_dir
|
||||
ls_dir="$(dirname "$ls_path")"
|
||||
if [[ ! -w "$ls_dir" ]] && [[ "$EUID" -ne 0 ]]; then
|
||||
echo -e " ${RED}✗${NC} ${ls_dir} requires elevated permissions"
|
||||
echo -e " run: sudo $0 install ${1:-}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo -e "${BOLD}${CYAN}Antigravity MITM Wrapper Installer${NC}"
|
||||
echo -e "───────────────────────────────────"
|
||||
echo ""
|
||||
|
||||
# Find the LS binary (for real this time, with output)
|
||||
if [[ -z "$ls_path" ]]; then
|
||||
echo -e " ${RED}✗${NC} Could not find Antigravity language server binary."
|
||||
echo -e " No LS process found in /proc, and fallback paths didn't match."
|
||||
echo ""
|
||||
echo -e " Set the path manually:"
|
||||
echo -e " $0 install /path/to/language_server_linux_x64"
|
||||
exit 1
|
||||
fi
|
||||
echo -e " ${GREEN}✓${NC} Found LS: ${ls_path}"
|
||||
|
||||
local real_path="${ls_path}${BACKUP_SUFFIX}"
|
||||
local wrapper_dir
|
||||
wrapper_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
local wrapper_src="${wrapper_dir}/mitm-wrapper.sh"
|
||||
|
||||
# Verify the binary exists and is not already wrapped
|
||||
if [[ -f "$real_path" ]]; then
|
||||
echo -e " ${YELLOW}!${NC} Already installed (backup exists at ${real_path})"
|
||||
echo -e " Run '$0 uninstall' first to reinstall."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ ! -f "$ls_path" ]]; then
|
||||
echo -e " ${RED}✗${NC} Binary not found: ${ls_path}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify it's a real binary, not already our wrapper
|
||||
if head -c 100 "$ls_path" | grep -q 'ANTIGRAVITY_MITM_PORT'; then
|
||||
echo -e " ${YELLOW}!${NC} Already wrapped (script detected). Run '$0 uninstall' first."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check CA cert
|
||||
if [[ ! -f "$CA_PATH" ]]; then
|
||||
echo -e " ${YELLOW}!${NC} CA cert not found at ${CA_PATH}"
|
||||
echo -e " Start the proxy first to generate it."
|
||||
echo -e " Continuing install anyway..."
|
||||
else
|
||||
echo -e " ${GREEN}✓${NC} CA cert: ${CA_PATH}"
|
||||
fi
|
||||
|
||||
# Back up the real binary
|
||||
cp -p "$ls_path" "$real_path"
|
||||
echo -e " ${GREEN}✓${NC} Backed up real binary to ${real_path}"
|
||||
|
||||
# Remove the original before writing (avoids "Text file busy" if LS is running)
|
||||
rm -f "$ls_path"
|
||||
|
||||
# Create the wrapper script in-place
|
||||
tee "$ls_path" > /dev/null << 'WRAPPER_EOF'
|
||||
#!/usr/bin/env bash
|
||||
# Antigravity MITM LS Wrapper — auto-generated, do not edit.
|
||||
# The LS is a Go binary — it reads HTTPS_PROXY and SSL_CERT_FILE (not NODE_EXTRA_CA_CERTS).
|
||||
# Go's gRPC library also reads GRPC_DEFAULT_SSL_ROOTS_FILE_PATH for root certs.
|
||||
# We build a combined CA bundle (system CAs + MITM CA) and inject it.
|
||||
|
||||
REAL_BINARY="${BASH_SOURCE[0]}.real"
|
||||
|
||||
if [[ ! -f "$REAL_BINARY" ]]; then
|
||||
echo "ERROR: Real LS binary not found at $REAL_BINARY" >&2
|
||||
echo "Run 'mitm-wrapper.sh uninstall' and reinstall." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Inject MITM proxy (don't override if already set)
|
||||
export HTTPS_PROXY="${HTTPS_PROXY:-http://127.0.0.1:__MITM_PORT__}"
|
||||
|
||||
# Build combined CA bundle: system CAs + MITM CA
|
||||
MITM_CA="__CA_PATH__"
|
||||
COMBINED_CA="/tmp/antigravity-mitm-combined-ca.pem"
|
||||
if [[ -f "$MITM_CA" ]]; then
|
||||
# Find system CA bundle
|
||||
SYS_CA=""
|
||||
for candidate in /etc/ssl/certs/ca-certificates.crt /etc/pki/tls/certs/ca-bundle.crt /etc/ssl/cert.pem; do
|
||||
if [[ -f "$candidate" ]]; then
|
||||
SYS_CA="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [[ -n "$SYS_CA" ]]; then
|
||||
cat "$SYS_CA" "$MITM_CA" > "$COMBINED_CA" 2>/dev/null
|
||||
export SSL_CERT_FILE="$COMBINED_CA"
|
||||
# Go's gRPC library may use this instead of SSL_CERT_FILE
|
||||
export GRPC_DEFAULT_SSL_ROOTS_FILE_PATH="$COMBINED_CA"
|
||||
fi
|
||||
fi
|
||||
|
||||
exec "$REAL_BINARY" "$@"
|
||||
WRAPPER_EOF
|
||||
|
||||
# Substitute actual values
|
||||
sed -i "s|__MITM_PORT__|${MITM_PORT}|g" "$ls_path"
|
||||
sed -i "s|__CA_PATH__|${CA_PATH}|g" "$ls_path"
|
||||
|
||||
# Make executable
|
||||
chmod +x "$ls_path"
|
||||
|
||||
echo -e " ${GREEN}✓${NC} Wrapper installed at ${ls_path}"
|
||||
echo ""
|
||||
echo -e " ${BOLD}How it works:${NC}"
|
||||
echo -e " When Antigravity starts the LS, the wrapper will:"
|
||||
echo -e " 1. Set ${CYAN}HTTPS_PROXY${NC}=http://127.0.0.1:${MITM_PORT}"
|
||||
echo -e " 2. Build combined CA bundle (system + MITM) at /tmp/antigravity-mitm-combined-ca.pem"
|
||||
echo -e " 3. Set ${CYAN}SSL_CERT_FILE${NC} to the combined bundle"
|
||||
echo -e " 4. Exec the real LS binary with all original args"
|
||||
echo ""
|
||||
echo -e " ${YELLOW}Note:${NC} Restart Antigravity for the wrapper to take effect."
|
||||
echo ""
|
||||
}
|
||||
|
||||
# ── Uninstall ────────────────────────────────────────────────────────────────
|
||||
cmd_uninstall() {
|
||||
# Check permissions upfront
|
||||
local ls_path
|
||||
ls_path=$(find_ls_binary) || true
|
||||
if [[ -n "$ls_path" ]] && [[ ! -w "$(dirname "$ls_path")" ]] && [[ "$EUID" -ne 0 ]]; then
|
||||
echo -e " ${RED}✗${NC} $(dirname "$ls_path") requires elevated permissions"
|
||||
echo -e " run: sudo $0 uninstall"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${BOLD}${CYAN}Antigravity MITM Wrapper Uninstaller${NC}"
|
||||
echo -e "─────────────────────────────────────"
|
||||
echo ""
|
||||
|
||||
if [[ -n "$ls_path" ]]; then
|
||||
local real_path="${ls_path}${BACKUP_SUFFIX}"
|
||||
if [[ -f "$real_path" ]]; then
|
||||
mv -f "$real_path" "$ls_path"
|
||||
echo -e " ${GREEN}✓${NC} Restored real binary at ${ls_path}"
|
||||
else
|
||||
echo -e " ${YELLOW}!${NC} No backup found at ${real_path}"
|
||||
echo -e " The LS binary may not be wrapped."
|
||||
fi
|
||||
else
|
||||
echo -e " ${RED}✗${NC} Could not find Antigravity language server binary."
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e " ${YELLOW}Note:${NC} Restart Antigravity for the change to take effect."
|
||||
echo ""
|
||||
}
|
||||
|
||||
# ── Status ───────────────────────────────────────────────────────────────────
|
||||
cmd_status() {
|
||||
echo -e "${BOLD}${CYAN}Antigravity MITM Wrapper Status${NC}"
|
||||
echo -e "────────────────────────────────"
|
||||
echo ""
|
||||
|
||||
local ls_path
|
||||
if ls_path=$(find_ls_binary); then
|
||||
echo -e " ${GREEN}✓${NC} LS binary: ${ls_path}"
|
||||
|
||||
local real_path="${ls_path}${BACKUP_SUFFIX}"
|
||||
if [[ -f "$real_path" ]]; then
|
||||
echo -e " ${GREEN}✓${NC} Wrapper: ${BOLD}installed${NC}"
|
||||
echo -e " ${GREEN}✓${NC} Real binary: ${real_path}"
|
||||
|
||||
# Check if wrapper is valid
|
||||
if head -c 200 "$ls_path" | grep -q 'MITM LS Wrapper'; then
|
||||
echo -e " ${GREEN}✓${NC} Wrapper script: valid"
|
||||
else
|
||||
echo -e " ${RED}✗${NC} Wrapper script: ${BOLD}corrupted or replaced${NC}"
|
||||
fi
|
||||
else
|
||||
echo -e " ${YELLOW}○${NC} Wrapper: ${BOLD}not installed${NC}"
|
||||
fi
|
||||
else
|
||||
echo -e " ${RED}✗${NC} LS binary: not found"
|
||||
fi
|
||||
|
||||
# Check CA cert
|
||||
if [[ -f "$CA_PATH" ]]; then
|
||||
echo -e " ${GREEN}✓${NC} CA cert: ${CA_PATH}"
|
||||
else
|
||||
echo -e " ${RED}✗${NC} CA cert: not found (start proxy first)"
|
||||
fi
|
||||
|
||||
# Check MITM port
|
||||
if ss -tlnp 2>/dev/null | grep -q ":${MITM_PORT} "; then
|
||||
echo -e " ${GREEN}✓${NC} MITM proxy: listening on :${MITM_PORT}"
|
||||
else
|
||||
echo -e " ${YELLOW}○${NC} MITM proxy: not running on :${MITM_PORT}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
}
|
||||
|
||||
# ── Main ─────────────────────────────────────────────────────────────────────
|
||||
case "${1:-}" in
|
||||
install)
|
||||
shift
|
||||
cmd_install "${1:-}"
|
||||
;;
|
||||
uninstall)
|
||||
cmd_uninstall
|
||||
;;
|
||||
status)
|
||||
cmd_status
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: $0 {install|uninstall|status}"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " install [path] Install MITM wrapper (auto-detect or specify path)"
|
||||
echo " uninstall Restore original LS binary"
|
||||
echo " status Show wrapper installation status"
|
||||
echo ""
|
||||
echo "Environment:"
|
||||
echo " ANTIGRAVITY_MITM_PORT MITM proxy port (default: 8742)"
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {install|uninstall|status}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -1,475 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Parse Go GODEBUG=http2debug=2 output into a clean, readable snapshot.
|
||||
|
||||
Usage:
|
||||
python3 parse-snapshot.py < raw-http2-dump.log
|
||||
python3 parse-snapshot.py /path/to/logfile
|
||||
"""
|
||||
|
||||
import sys
|
||||
import re
|
||||
import json
|
||||
import gzip
|
||||
from collections import defaultdict
|
||||
from io import BytesIO
|
||||
|
||||
# ── Colors ────────────────────────────────────────────────────────────────────
|
||||
BOLD = "\033[1m"
|
||||
DIM = "\033[2m"
|
||||
RED = "\033[91m"
|
||||
GREEN = "\033[92m"
|
||||
YELLOW = "\033[93m"
|
||||
CYAN = "\033[96m"
|
||||
MAGENTA = "\033[95m"
|
||||
NC = "\033[0m"
|
||||
|
||||
# ── Regexes ───────────────────────────────────────────────────────────────────
|
||||
RE_ENCODING_HEADER = re.compile(
|
||||
r'http2: Transport encoding header "([^"]+)" = "([^"]*)"'
|
||||
)
|
||||
RE_DECODED_HEADER = re.compile(
|
||||
r'http2: decoded hpack field header field "([^"]+)" = "([^"]*)"'
|
||||
)
|
||||
RE_SERVER_ENCODING = re.compile(
|
||||
r'http2: server encoding header "([^"]+)" = "([^"]*)"'
|
||||
)
|
||||
RE_WROTE_DATA = re.compile(
|
||||
r'http2: Framer [^:]+: wrote DATA flags=(\S+) stream=(\d+) len=(\d+) data="(.*?)"'
|
||||
)
|
||||
RE_READ_DATA = re.compile(
|
||||
r'http2: Framer [^:]+: read DATA flags=(\S+) stream=(\d+) len=(\d+) data="(.*?)"'
|
||||
)
|
||||
RE_TRANSPORT_CONN = re.compile(
|
||||
r'http2: Transport creating client conn [^ ]+ to (.+)'
|
||||
)
|
||||
RE_SERVER_READ_DATA = re.compile(
|
||||
r'http2: server read frame DATA flags=(\S+) stream=(\d+) len=(\d+) data="(.*?)"'
|
||||
)
|
||||
RE_WROTE_HEADERS = re.compile(
|
||||
r'http2: Framer [^:]+: wrote HEADERS flags=(\S+) stream=(\d+)'
|
||||
)
|
||||
RE_TIMESTAMP = re.compile(r'^(\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2})')
|
||||
RE_LS_LOG = re.compile(r'^[IWE]\d{4} ')
|
||||
RE_MAXPROCS = re.compile(r'^.*maxprocs:')
|
||||
RE_BYTES_OMITTED = re.compile(r'\((\d+) bytes omitted\)$')
|
||||
|
||||
# Known domain purposes
|
||||
DOMAIN_INFO = {
|
||||
"antigravity-unleash.goog": ("Feature Flags", "Unleash SDK — controls A/B tests, feature rollouts"),
|
||||
"daily-cloudcode-pa.googleapis.com": ("LLM API (gRPC)", "Primary Gemini/Claude API endpoint"),
|
||||
"cloudcode-pa.googleapis.com": ("LLM API (gRPC)", "Production Gemini/Claude API endpoint"),
|
||||
"api.anthropic.com": ("Claude API", "Direct Anthropic API calls"),
|
||||
"lh3.googleusercontent.com": ("Profile Picture", "User avatar image"),
|
||||
"play.googleapis.com": ("Telemetry", "Google Play telemetry/logging"),
|
||||
"firebaseinstallations.googleapis.com": ("Firebase", "Firebase installation tracking"),
|
||||
"oauth2.googleapis.com": ("OAuth", "Token refresh/exchange"),
|
||||
"speech.googleapis.com": ("Speech", "Voice input processing"),
|
||||
"modelarmor.googleapis.com": ("Safety", "Content safety/filtering"),
|
||||
}
|
||||
|
||||
|
||||
class Request:
|
||||
def __init__(self):
|
||||
self.method = ""
|
||||
self.path = ""
|
||||
self.authority = ""
|
||||
self.scheme = ""
|
||||
self.headers = {}
|
||||
self.data = b""
|
||||
self.data_len = 0
|
||||
self.stream_id = None
|
||||
self.timestamp = ""
|
||||
self.direction = "outgoing" # outgoing = LS→upstream, incoming = LS←upstream
|
||||
|
||||
|
||||
class Snapshot:
|
||||
def __init__(self):
|
||||
self.connections = [] # (timestamp, target)
|
||||
self.requests = [] # list of Request
|
||||
self.responses = defaultdict(lambda: {"headers": {}, "data": b"", "data_len": 0})
|
||||
self.ls_logs = []
|
||||
|
||||
def parse(self, lines):
|
||||
current_headers = {}
|
||||
current_direction = "outgoing"
|
||||
current_stream = None
|
||||
|
||||
for line in lines:
|
||||
line = line.rstrip()
|
||||
|
||||
# Skip empty
|
||||
if not line:
|
||||
continue
|
||||
|
||||
# LS process logs
|
||||
if RE_LS_LOG.match(line) or RE_MAXPROCS.match(line):
|
||||
self.ls_logs.append(line)
|
||||
continue
|
||||
|
||||
# New connection
|
||||
m = RE_TRANSPORT_CONN.search(line)
|
||||
if m:
|
||||
ts = ""
|
||||
ts_m = RE_TIMESTAMP.match(line)
|
||||
if ts_m:
|
||||
ts = ts_m.group(1)
|
||||
self.connections.append((ts, m.group(1)))
|
||||
continue
|
||||
|
||||
# Outgoing headers (Transport encoding = LS sending to upstream)
|
||||
m = RE_ENCODING_HEADER.search(line)
|
||||
if m:
|
||||
key, val = m.group(1), m.group(2)
|
||||
if key == ":method":
|
||||
# New request starting
|
||||
if current_headers.get(":path"):
|
||||
self._finalize_request(current_headers, "outgoing", line)
|
||||
current_headers = {}
|
||||
current_direction = "outgoing"
|
||||
current_headers[key] = val
|
||||
ts_m = RE_TIMESTAMP.match(line)
|
||||
if ts_m and "timestamp" not in current_headers:
|
||||
current_headers["timestamp"] = ts_m.group(1)
|
||||
continue
|
||||
|
||||
# Incoming headers (decoded hpack = upstream responding, OR server receiving)
|
||||
m = RE_DECODED_HEADER.search(line)
|
||||
if m:
|
||||
key, val = m.group(1), m.group(2)
|
||||
if key == ":authority" and "server read frame" not in line:
|
||||
# This is a request received by our LS
|
||||
if current_headers.get(":path"):
|
||||
self._finalize_request(current_headers, current_direction, line)
|
||||
current_headers = {}
|
||||
current_direction = "incoming"
|
||||
current_headers[key] = val
|
||||
continue
|
||||
|
||||
# Server encoding (our LS responding)
|
||||
m = RE_SERVER_ENCODING.search(line)
|
||||
if m:
|
||||
continue # Skip server response headers for now
|
||||
|
||||
# Headers frame written (triggers finalization)
|
||||
m = RE_WROTE_HEADERS.search(line)
|
||||
if m:
|
||||
current_stream = m.group(2)
|
||||
if current_headers.get(":path") or current_headers.get(":method"):
|
||||
req = self._finalize_request(current_headers, current_direction, line)
|
||||
if req:
|
||||
req.stream_id = current_stream
|
||||
current_headers = {}
|
||||
continue
|
||||
|
||||
# Data frames (wrote = LS sending, read = LS receiving)
|
||||
for pattern, direction in [
|
||||
(RE_WROTE_DATA, "sent"),
|
||||
(RE_READ_DATA, "received"),
|
||||
(RE_SERVER_READ_DATA, "server_received"),
|
||||
]:
|
||||
m = pattern.search(line)
|
||||
if m:
|
||||
flags, stream, length, data_str = (
|
||||
m.group(1),
|
||||
m.group(2),
|
||||
int(m.group(3)),
|
||||
m.group(4),
|
||||
)
|
||||
# Find matching request by stream
|
||||
for req in reversed(self.requests):
|
||||
if req.stream_id == stream:
|
||||
raw = self._decode_data_str(data_str, line)
|
||||
if direction == "sent" or direction == "server_received":
|
||||
req.data += raw
|
||||
req.data_len = max(req.data_len, length)
|
||||
break
|
||||
# Also check omitted bytes
|
||||
om = RE_BYTES_OMITTED.search(line)
|
||||
if om:
|
||||
pass # length already captured
|
||||
break
|
||||
|
||||
# Finalize any remaining headers
|
||||
if current_headers.get(":path") or current_headers.get(":method"):
|
||||
self._finalize_request(current_headers, current_direction, "")
|
||||
|
||||
def _finalize_request(self, headers, direction, _line):
|
||||
req = Request()
|
||||
req.method = headers.pop(":method", "GET")
|
||||
req.path = headers.pop(":path", "/")
|
||||
req.authority = headers.pop(":authority", "")
|
||||
req.scheme = headers.pop(":scheme", "https")
|
||||
req.timestamp = headers.pop("timestamp", "")
|
||||
req.direction = direction
|
||||
req.headers = {k: v for k, v in headers.items() if not k.startswith(":")}
|
||||
self.requests.append(req)
|
||||
return req
|
||||
|
||||
def _decode_data_str(self, s, full_line):
|
||||
"""Decode escaped string from GODEBUG output back to bytes."""
|
||||
try:
|
||||
# Handle Go's escaped bytes
|
||||
result = bytearray()
|
||||
i = 0
|
||||
while i < len(s):
|
||||
if s[i] == "\\" and i + 1 < len(s):
|
||||
if s[i + 1] == "x" and i + 3 < len(s):
|
||||
result.append(int(s[i + 2 : i + 4], 16))
|
||||
i += 4
|
||||
elif s[i + 1] == "n":
|
||||
result.append(10)
|
||||
i += 2
|
||||
elif s[i + 1] == "r":
|
||||
result.append(13)
|
||||
i += 2
|
||||
elif s[i + 1] == "t":
|
||||
result.append(9)
|
||||
i += 2
|
||||
elif s[i + 1] == "\\":
|
||||
result.append(92)
|
||||
i += 2
|
||||
elif s[i + 1] == '"':
|
||||
result.append(34)
|
||||
i += 2
|
||||
else:
|
||||
result.append(ord(s[i]))
|
||||
i += 1
|
||||
else:
|
||||
result.append(ord(s[i]))
|
||||
i += 1
|
||||
return bytes(result)
|
||||
except Exception:
|
||||
return s.encode("utf-8", errors="replace")
|
||||
|
||||
def render(self):
|
||||
out = []
|
||||
|
||||
# Header
|
||||
out.append(f"\n{BOLD}{CYAN}{'═' * 70}{NC}")
|
||||
out.append(f"{BOLD}{CYAN} STANDALONE LS TRAFFIC SNAPSHOT{NC}")
|
||||
out.append(f"{BOLD}{CYAN}{'═' * 70}{NC}\n")
|
||||
|
||||
# LS Logs
|
||||
if self.ls_logs:
|
||||
out.append(f"{BOLD}▸ Language Server Logs{NC}")
|
||||
out.append(f"{DIM}{'─' * 60}{NC}")
|
||||
for log in self.ls_logs:
|
||||
out.append(f" {DIM}{log}{NC}")
|
||||
out.append("")
|
||||
|
||||
# Connections
|
||||
if self.connections:
|
||||
out.append(f"{BOLD}▸ Outbound Connections{NC}")
|
||||
out.append(f"{DIM}{'─' * 60}{NC}")
|
||||
for ts, target in self.connections:
|
||||
domain = target.split(":")[0] if ":" in target else target
|
||||
info = DOMAIN_INFO.get(domain, ("Unknown", ""))
|
||||
out.append(
|
||||
f" {GREEN}→{NC} {BOLD}{target}{NC} {DIM}({info[0]}){NC}"
|
||||
)
|
||||
if info[1]:
|
||||
out.append(f" {DIM}{info[1]}{NC}")
|
||||
out.append("")
|
||||
|
||||
# Group requests by domain
|
||||
by_domain = defaultdict(list)
|
||||
for req in self.requests:
|
||||
by_domain[req.authority].append(req)
|
||||
|
||||
# Render each domain's requests
|
||||
for domain, reqs in by_domain.items():
|
||||
if domain.startswith("127.0.0.1"):
|
||||
label = "Local (our requests to LS)"
|
||||
color = DIM
|
||||
else:
|
||||
info = DOMAIN_INFO.get(domain, ("External", ""))
|
||||
label = info[0]
|
||||
color = YELLOW if "API" in info[0] else CYAN
|
||||
|
||||
out.append(f"{BOLD}{'═' * 70}{NC}")
|
||||
out.append(f"{BOLD}{color} {domain}{NC} {DIM}— {label}{NC}")
|
||||
out.append(f"{BOLD}{'═' * 70}{NC}")
|
||||
|
||||
for i, req in enumerate(reqs):
|
||||
arrow = "→" if req.direction == "outgoing" else "←"
|
||||
method_color = GREEN if req.method == "GET" else YELLOW
|
||||
|
||||
out.append(f"\n {BOLD}{arrow} {method_color}{req.method}{NC} {req.path}")
|
||||
|
||||
# Important headers
|
||||
interesting = [
|
||||
"authorization",
|
||||
"content-type",
|
||||
"user-agent",
|
||||
"unleash-appname",
|
||||
"unleash-instanceid",
|
||||
"unleash-sdk",
|
||||
"x-goog-api-key",
|
||||
"x-goog-api-client",
|
||||
"grpc-encoding",
|
||||
"te",
|
||||
]
|
||||
shown = False
|
||||
for key in interesting:
|
||||
if key in req.headers:
|
||||
val = req.headers[key]
|
||||
# Mask tokens partially
|
||||
if key == "authorization" and len(val) > 30:
|
||||
if val.startswith("Bearer "):
|
||||
val = f"Bearer {val[7:20]}...{val[-10:]}"
|
||||
elif len(val) > 40:
|
||||
val = f"{val[:30]}...{val[-10:]}"
|
||||
out.append(f" {DIM}{key}:{NC} {val}")
|
||||
shown = True
|
||||
|
||||
# All other headers (collapsed)
|
||||
other = {
|
||||
k: v
|
||||
for k, v in req.headers.items()
|
||||
if k not in interesting and not k.startswith(":")
|
||||
}
|
||||
if other:
|
||||
if not shown:
|
||||
out.append(f" {DIM}Headers:{NC}")
|
||||
for k, v in other.items():
|
||||
out.append(f" {DIM}{k}:{NC} {v}")
|
||||
|
||||
# Body
|
||||
if req.data:
|
||||
out.append(self._render_body(req.data, req.data_len))
|
||||
|
||||
out.append("")
|
||||
|
||||
return "\n".join(out)
|
||||
|
||||
def _render_body(self, data, total_len):
|
||||
"""Render body data in the most readable format possible."""
|
||||
lines = []
|
||||
|
||||
# Try JSON
|
||||
try:
|
||||
text = data.decode("utf-8")
|
||||
obj = json.loads(text)
|
||||
pretty = json.dumps(obj, indent=2, ensure_ascii=False)
|
||||
lines.append(f" {BOLD}Body ({len(data)} bytes, JSON):{NC}")
|
||||
for l in pretty.split("\n")[:30]:
|
||||
lines.append(f" {GREEN}{l}{NC}")
|
||||
if len(pretty.split("\n")) > 30:
|
||||
lines.append(f" {DIM}... ({len(pretty.split(chr(10))) - 30} more lines){NC}")
|
||||
return "\n".join(lines)
|
||||
except (json.JSONDecodeError, UnicodeDecodeError):
|
||||
pass
|
||||
|
||||
# Try gzip
|
||||
if data[:2] == b"\x1f\x8b":
|
||||
try:
|
||||
decompressed = gzip.decompress(data)
|
||||
try:
|
||||
text = decompressed.decode("utf-8")
|
||||
try:
|
||||
obj = json.loads(text)
|
||||
pretty = json.dumps(obj, indent=2, ensure_ascii=False)
|
||||
lines.append(
|
||||
f" {BOLD}Body ({len(data)} bytes gzip → {len(decompressed)} bytes, JSON):{NC}"
|
||||
)
|
||||
for l in pretty.split("\n")[:50]:
|
||||
lines.append(f" {GREEN}{l}{NC}")
|
||||
if len(pretty.split("\n")) > 50:
|
||||
lines.append(
|
||||
f" {DIM}... ({len(pretty.split(chr(10))) - 50} more lines){NC}"
|
||||
)
|
||||
return "\n".join(lines)
|
||||
except json.JSONDecodeError:
|
||||
lines.append(
|
||||
f" {BOLD}Body ({len(data)} bytes gzip → {len(decompressed)} bytes, text):{NC}"
|
||||
)
|
||||
for l in text.split("\n")[:20]:
|
||||
lines.append(f" {l[:200]}")
|
||||
return "\n".join(lines)
|
||||
except UnicodeDecodeError:
|
||||
lines.append(
|
||||
f" {BOLD}Body ({len(data)} bytes gzip → {len(decompressed)} bytes, binary):{NC}"
|
||||
)
|
||||
lines.append(f" {DIM}{self._extract_strings(decompressed)}{NC}")
|
||||
return "\n".join(lines)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Try protobuf (extract readable strings)
|
||||
if data[:1] in (b"\x08", b"\x0a", b"\x10", b"\x12", b"\x18", b"\x1a", b"\x20", b"\x22"):
|
||||
strings = self._extract_strings(data)
|
||||
if strings:
|
||||
lines.append(f" {BOLD}Body ({total_len} bytes, protobuf):{NC}")
|
||||
lines.append(f" {DIM}Extracted strings:{NC}")
|
||||
for s in strings.split(" | ")[:20]:
|
||||
s = s.strip()
|
||||
if len(s) > 3:
|
||||
lines.append(f" {MAGENTA}{s}{NC}")
|
||||
return "\n".join(lines)
|
||||
|
||||
# Try plain text
|
||||
try:
|
||||
text = data.decode("utf-8")
|
||||
lines.append(f" {BOLD}Body ({len(data)} bytes, text):{NC}")
|
||||
for l in text.split("\n")[:10]:
|
||||
lines.append(f" {l[:200]}")
|
||||
return "\n".join(lines)
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
|
||||
# PNG
|
||||
if data[:4] == b"\x89PNG":
|
||||
lines.append(f" {BOLD}Body ({total_len} bytes, PNG image){NC}")
|
||||
return "\n".join(lines)
|
||||
|
||||
# Binary fallback
|
||||
lines.append(f" {BOLD}Body ({total_len} bytes, binary):{NC}")
|
||||
strings = self._extract_strings(data)
|
||||
if strings:
|
||||
lines.append(f" {DIM}Extracted strings:{NC}")
|
||||
for s in strings.split(" | ")[:15]:
|
||||
s = s.strip()
|
||||
if len(s) > 3:
|
||||
lines.append(f" {MAGENTA}{s}{NC}")
|
||||
else:
|
||||
lines.append(f" {DIM}(no readable strings){NC}")
|
||||
return "\n".join(lines)
|
||||
|
||||
def _extract_strings(self, data, min_len=4):
|
||||
"""Extract printable ASCII strings from binary data."""
|
||||
strings = []
|
||||
current = bytearray()
|
||||
for b in data:
|
||||
if 32 <= b <= 126:
|
||||
current.append(b)
|
||||
else:
|
||||
if len(current) >= min_len:
|
||||
strings.append(current.decode("ascii"))
|
||||
current = bytearray()
|
||||
if len(current) >= min_len:
|
||||
strings.append(current.decode("ascii"))
|
||||
# Deduplicate while preserving order
|
||||
seen = set()
|
||||
unique = []
|
||||
for s in strings:
|
||||
if s not in seen:
|
||||
seen.add(s)
|
||||
unique.append(s)
|
||||
return " | ".join(unique[:30])
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) > 1:
|
||||
with open(sys.argv[1]) as f:
|
||||
lines = f.readlines()
|
||||
else:
|
||||
lines = sys.stdin.readlines()
|
||||
|
||||
snap = Snapshot()
|
||||
snap.parse(lines)
|
||||
print(snap.render())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
130
scripts/proxyctl
Executable file
130
scripts/proxyctl
Executable file
@@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env bash
|
||||
# proxyctl — manage the antigravity proxy daemon
|
||||
set -euo pipefail
|
||||
|
||||
SERVICE="antigravity-proxy"
|
||||
PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
PORT="${PROXY_PORT:-8741}"
|
||||
BASE_URL="http://localhost:${PORT}"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
DIM='\033[2m'
|
||||
BOLD='\033[1m'
|
||||
NC='\033[0m'
|
||||
|
||||
usage() {
|
||||
echo -e "${BOLD}proxyctl${NC} — antigravity proxy daemon manager"
|
||||
echo ""
|
||||
echo -e " ${CYAN}start${NC} Start the proxy daemon"
|
||||
echo -e " ${CYAN}stop${NC} Stop the proxy daemon"
|
||||
echo -e " ${CYAN}restart${NC} Rebuild + restart"
|
||||
echo -e " ${CYAN}rebuild${NC} Build release binary only"
|
||||
echo -e " ${CYAN}status${NC} Service status + quota + usage"
|
||||
echo -e " ${CYAN}logs${NC} [N] Tail last N lines (default 30) + follow"
|
||||
echo -e " ${CYAN}logs-all${NC} Full log dump (no follow)"
|
||||
echo -e " ${CYAN}test${NC} [msg] Quick test request (gemini-3-flash)"
|
||||
echo -e " ${CYAN}health${NC} Health check"
|
||||
echo ""
|
||||
}
|
||||
|
||||
do_build() {
|
||||
echo -e "${YELLOW}Building release binary...${NC}"
|
||||
cd "$PROJECT_DIR"
|
||||
cargo build --release 2>&1
|
||||
echo -e "${GREEN}Build complete.${NC}"
|
||||
}
|
||||
|
||||
do_start() {
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user start "$SERVICE"
|
||||
echo -e "${GREEN}Started.${NC} Waiting for ready..."
|
||||
# Wait up to 10s for health
|
||||
for i in $(seq 1 20); do
|
||||
if curl -sf "${BASE_URL}/health" >/dev/null 2>&1; then
|
||||
echo -e "${GREEN}Proxy is up on port ${PORT}.${NC}"
|
||||
return 0
|
||||
fi
|
||||
sleep 0.5
|
||||
done
|
||||
echo -e "${RED}Proxy didn't become healthy in 10s. Check logs:${NC}"
|
||||
journalctl --user -u "$SERVICE" --no-pager -n 20
|
||||
return 1
|
||||
}
|
||||
|
||||
do_stop() {
|
||||
systemctl --user stop "$SERVICE" 2>/dev/null || true
|
||||
echo -e "${YELLOW}Stopped.${NC}"
|
||||
}
|
||||
|
||||
do_restart() {
|
||||
echo -e "${YELLOW}Stopping...${NC}"
|
||||
do_stop
|
||||
do_build
|
||||
do_start
|
||||
}
|
||||
|
||||
do_status() {
|
||||
echo -e "${BOLD}── Service ──${NC}"
|
||||
systemctl --user status "$SERVICE" --no-pager 2>/dev/null | head -6 || echo -e "${RED}Not running${NC}"
|
||||
echo ""
|
||||
|
||||
# Health check
|
||||
if ! curl -sf "${BASE_URL}/health" >/dev/null 2>&1; then
|
||||
echo -e "${RED}Proxy is not responding on port ${PORT}.${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo -e "${BOLD}── Quota ──${NC}"
|
||||
curl -sf "${BASE_URL}/v1/quota" 2>/dev/null | jq '.' 2>/dev/null || echo -e "${DIM}(no quota data)${NC}"
|
||||
echo ""
|
||||
|
||||
echo -e "${BOLD}── Usage ──${NC}"
|
||||
curl -sf "${BASE_URL}/v1/usage" 2>/dev/null | jq '.' 2>/dev/null || echo -e "${DIM}(no usage data)${NC}"
|
||||
echo ""
|
||||
|
||||
echo -e "${BOLD}── Sessions ──${NC}"
|
||||
curl -sf "${BASE_URL}/v1/sessions" 2>/dev/null | jq '.' 2>/dev/null || echo -e "${DIM}(no sessions)${NC}"
|
||||
}
|
||||
|
||||
do_logs() {
|
||||
local lines="${1:-30}"
|
||||
journalctl --user -u "$SERVICE" --no-pager -n "$lines" -f
|
||||
}
|
||||
|
||||
do_logs_all() {
|
||||
journalctl --user -u "$SERVICE" --no-pager
|
||||
}
|
||||
|
||||
do_test() {
|
||||
local msg="${1:-Say hello in exactly 3 words}"
|
||||
echo -e "${CYAN}Testing:${NC} ${msg}"
|
||||
curl -sf "${BASE_URL}/v1/responses" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"model\": \"gemini-3-flash\",
|
||||
\"input\": \"${msg}\",
|
||||
\"stream\": false,
|
||||
\"timeout\": 30
|
||||
}" | jq '.'
|
||||
}
|
||||
|
||||
do_health() {
|
||||
curl -sf "${BASE_URL}/health" | jq '.' 2>/dev/null && echo -e "${GREEN}Healthy${NC}" || echo -e "${RED}Not responding${NC}"
|
||||
}
|
||||
|
||||
# ── Main ──
|
||||
case "${1:-}" in
|
||||
start) do_start ;;
|
||||
stop) do_stop ;;
|
||||
restart) do_restart ;;
|
||||
rebuild) do_build ;;
|
||||
status) do_status ;;
|
||||
logs) do_logs "${2:-30}" ;;
|
||||
logs-all) do_logs_all ;;
|
||||
test) do_test "${2:-}" ;;
|
||||
health) do_health ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
@@ -1,277 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# ╔═══════════════════════════════════════════════════════════════════════════╗
|
||||
# ║ Standalone Language Server Launcher ║
|
||||
# ║ ║
|
||||
# ║ Launches an isolated LS instance that: ║
|
||||
# ║ - Shares OAuth via the main app's extension server ║
|
||||
# ║ - Has its own HTTPS port, data dir, and cascades ║
|
||||
# ║ - Optionally routes traffic through our MITM proxy ║
|
||||
# ║ - Can capture a clean traffic snapshot ║
|
||||
# ║ ║
|
||||
# ║ Usage: ║
|
||||
# ║ ./standalone-ls.sh # Launch, test, exit ║
|
||||
# ║ ./standalone-ls.sh --fg # Foreground (stay alive) ║
|
||||
# ║ ./standalone-ls.sh --mitm # Route through MITM proxy ║
|
||||
# ║ ./standalone-ls.sh --snapshot # Capture clean traffic dump ║
|
||||
# ║ ./standalone-ls.sh --snapshot --prompt "Say hello" ║
|
||||
# ╚═══════════════════════════════════════════════════════════════════════════╝
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# ── Defaults ──────────────────────────────────────────────────────────────────
|
||||
LS_BIN="/usr/share/antigravity/resources/app/extensions/antigravity/bin/language_server_linux_x64"
|
||||
HTTPS_PORT="42200"
|
||||
DATA_DIR="/tmp/antigravity-standalone"
|
||||
FOREGROUND=false
|
||||
USE_MITM=false
|
||||
SNAPSHOT=false
|
||||
TIMEOUT=15
|
||||
PROMPT=""
|
||||
MODEL="MODEL_PLACEHOLDER_M3"
|
||||
|
||||
# ── Parse args ────────────────────────────────────────────────────────────────
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--port) HTTPS_PORT="$2"; shift 2 ;;
|
||||
--mitm) USE_MITM=true; shift ;;
|
||||
--fg) FOREGROUND=true; shift ;;
|
||||
--timeout) TIMEOUT="$2"; shift 2 ;;
|
||||
--snapshot) SNAPSHOT=true; TIMEOUT=30; shift ;;
|
||||
--prompt) PROMPT="$2"; shift 2 ;;
|
||||
--model) MODEL="$2"; shift 2 ;;
|
||||
-h|--help)
|
||||
echo "Usage: $0 [OPTIONS]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --port PORT HTTPS port for standalone LS (default: 42200)"
|
||||
echo " --mitm Route traffic through MITM proxy"
|
||||
echo " --fg Run in foreground (stay alive)"
|
||||
echo " --timeout SECS Background mode timeout (default: 15)"
|
||||
echo " --snapshot Capture clean traffic snapshot"
|
||||
echo " --prompt TEXT Prompt to send (snapshot mode)"
|
||||
echo " --model MODEL Model alias (default: MODEL_PLACEHOLDER_M3)"
|
||||
exit 0 ;;
|
||||
*) echo "Unknown option: $1"; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ── Discover main LS config ──────────────────────────────────────────────────
|
||||
MAIN_PID=$(pgrep -f 'language_server_linux_x64' | head -1 || true)
|
||||
if [[ -z "$MAIN_PID" ]]; then
|
||||
echo "[-] No main LS process found. Main Antigravity must be running."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MAIN_CSRF=$(tr '\0' '\n' < /proc/"$MAIN_PID"/cmdline | grep -A1 'csrf_token' | tail -1)
|
||||
EXT_PORT=$(tr '\0' '\n' < /proc/"$MAIN_PID"/cmdline | grep -A1 'extension_server_port' | tail -1)
|
||||
|
||||
echo "[*] Main LS PID: $MAIN_PID"
|
||||
echo "[*] CSRF: $MAIN_CSRF"
|
||||
echo "[*] Extension server: $EXT_PORT"
|
||||
|
||||
# ── Build protobuf metadata for stdin ─────────────────────────────────────────
|
||||
TS=$(date +%s)
|
||||
METADATA=$(python3 -c "
|
||||
import sys
|
||||
def v(n):
|
||||
r = bytearray()
|
||||
while n > 0x7f:
|
||||
r.append((n & 0x7f) | 0x80)
|
||||
n >>= 7
|
||||
r.append(n & 0x7f)
|
||||
return bytes(r)
|
||||
def s(f, val):
|
||||
t = v((f << 3) | 2)
|
||||
d = val.encode()
|
||||
return t + v(len(d)) + d
|
||||
buf = bytearray()
|
||||
buf += s(1, 'standalone-api-key-$TS')
|
||||
buf += s(3, 'antigravity')
|
||||
buf += s(4, '1.15.8')
|
||||
buf += s(5, '1.16.39')
|
||||
buf += s(6, 'en_US')
|
||||
buf += s(10, 'standalone-session-$TS')
|
||||
buf += s(11, 'antigravity')
|
||||
sys.stdout.buffer.write(bytes(buf))
|
||||
" | base64)
|
||||
|
||||
# ── Setup data directory ──────────────────────────────────────────────────────
|
||||
mkdir -p "$DATA_DIR/.gemini"
|
||||
|
||||
# ── MITM environment ─────────────────────────────────────────────────────────
|
||||
MITM_ENV=()
|
||||
if $USE_MITM; then
|
||||
REAL_HOME="${SUDO_USER:+$(getent passwd "$SUDO_USER" | cut -d: -f6)}"
|
||||
REAL_HOME="${REAL_HOME:-$HOME}"
|
||||
MITM_PORT_FILE="${REAL_HOME}/.config/antigravity-proxy/mitm-port"
|
||||
CA_PATH="${REAL_HOME}/.config/antigravity-proxy/mitm-ca.pem"
|
||||
|
||||
if [[ -f "$MITM_PORT_FILE" ]]; then
|
||||
MITM_PORT=$(cat "$MITM_PORT_FILE")
|
||||
else
|
||||
MITM_PORT="8742"
|
||||
fi
|
||||
|
||||
if [[ ! -f "$CA_PATH" ]]; then
|
||||
echo "[-] MITM CA cert not found at $CA_PATH"
|
||||
echo " Start the proxy first to generate it."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
COMBINED_CA="/tmp/antigravity-mitm-combined-ca.pem"
|
||||
SYS_CA=""
|
||||
for candidate in /etc/ssl/certs/ca-certificates.crt /etc/pki/tls/certs/ca-bundle.crt /etc/ssl/cert.pem; do
|
||||
if [[ -f "$candidate" ]]; then SYS_CA="$candidate"; break; fi
|
||||
done
|
||||
if [[ -n "$SYS_CA" ]]; then
|
||||
cat "$SYS_CA" "$CA_PATH" > "$COMBINED_CA"
|
||||
else
|
||||
echo "[-] No system CA bundle found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MITM_ENV=(
|
||||
"HTTPS_PROXY=http://127.0.0.1:${MITM_PORT}"
|
||||
"SSL_CERT_FILE=${COMBINED_CA}"
|
||||
"GRPC_DEFAULT_SSL_ROOTS_FILE_PATH=${COMBINED_CA}"
|
||||
)
|
||||
echo "[*] MITM: enabled (port $MITM_PORT)"
|
||||
else
|
||||
echo "[*] MITM: disabled (use --mitm to enable)"
|
||||
fi
|
||||
|
||||
# ── LS args ───────────────────────────────────────────────────────────────────
|
||||
LS_ARGS=(
|
||||
-enable_lsp
|
||||
-extension_server_port "$EXT_PORT"
|
||||
-csrf_token "$MAIN_CSRF"
|
||||
-server_port "$HTTPS_PORT"
|
||||
-workspace_id "standalone_$TS"
|
||||
-cloud_code_endpoint "https://daily-cloudcode-pa.googleapis.com"
|
||||
-app_data_dir "antigravity-standalone"
|
||||
-gemini_dir "$DATA_DIR/.gemini"
|
||||
)
|
||||
|
||||
# ── Extra env for snapshot mode ───────────────────────────────────────────────
|
||||
EXTRA_ENV=()
|
||||
if $SNAPSHOT; then
|
||||
EXTRA_ENV=("GODEBUG=http2debug=2")
|
||||
echo "[*] Snapshot: enabled (HTTP/2 debug tracing)"
|
||||
fi
|
||||
|
||||
# ── Banner ────────────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "========================================="
|
||||
echo " Standalone LS"
|
||||
echo " Port: $HTTPS_PORT (HTTPS)"
|
||||
echo " Data: $DATA_DIR"
|
||||
echo " Mode: $($FOREGROUND && echo "foreground" || echo "background ($TIMEOUT s)")"
|
||||
echo "========================================="
|
||||
echo ""
|
||||
|
||||
# ── Foreground mode ───────────────────────────────────────────────────────────
|
||||
if $FOREGROUND; then
|
||||
echo "$METADATA" | base64 -d | \
|
||||
env "${MITM_ENV[@]+"${MITM_ENV[@]}"}" \
|
||||
"${EXTRA_ENV[@]+"${EXTRA_ENV[@]}"}" \
|
||||
ANTIGRAVITY_EDITOR_APP_ROOT="/usr/share/antigravity/resources/app" \
|
||||
exec "$LS_BIN" "${LS_ARGS[@]}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── Background mode ──────────────────────────────────────────────────────────
|
||||
LOG="/tmp/standalone-ls.log"
|
||||
rm -f "$LOG"
|
||||
|
||||
echo "$METADATA" | base64 -d | \
|
||||
env "${MITM_ENV[@]+"${MITM_ENV[@]}"}" \
|
||||
"${EXTRA_ENV[@]+"${EXTRA_ENV[@]}"}" \
|
||||
ANTIGRAVITY_EDITOR_APP_ROOT="/usr/share/antigravity/resources/app" \
|
||||
timeout "$TIMEOUT" "$LS_BIN" "${LS_ARGS[@]}" \
|
||||
> "$LOG" 2>&1 &
|
||||
|
||||
LS_PID=$!
|
||||
echo "[*] PID: $LS_PID"
|
||||
|
||||
# Wait for init
|
||||
for i in $(seq 1 5); do
|
||||
sleep 1
|
||||
if ! kill -0 "$LS_PID" 2>/dev/null; then
|
||||
echo "[-] LS died after ${i}s"
|
||||
echo "=== LOGS ==="
|
||||
cat "$LOG"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo "[+] LS alive and initialized"
|
||||
|
||||
# ── Snapshot mode: send a prompt and capture traffic ──────────────────────────
|
||||
if $SNAPSHOT; then
|
||||
if [[ -z "$PROMPT" ]]; then
|
||||
PROMPT="Say exactly: Hello standalone world"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "[*] Sending cascade: \"$PROMPT\""
|
||||
CASCADE_ID=$(curl -sk --max-time 10 \
|
||||
"https://127.0.0.1:${HTTPS_PORT}/exa.language_server_pb.LanguageServerService/StartCascade" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "x-codeium-csrf-token: $MAIN_CSRF" \
|
||||
-H "Origin: vscode-file://vscode-app" \
|
||||
-d "{
|
||||
\"prompt\": \"$PROMPT\",
|
||||
\"modelOrAlias\": {\"model\": \"$MODEL\"},
|
||||
\"workspaceRootPaths\": [\"$DATA_DIR\"]
|
||||
}" 2>/dev/null | python3 -c "import json,sys; print(json.load(sys.stdin).get('cascadeId',''))" 2>/dev/null || true)
|
||||
|
||||
echo "[*] Cascade: $CASCADE_ID"
|
||||
echo "[*] Waiting 15s for upstream API calls..."
|
||||
sleep 15
|
||||
|
||||
# Kill LS to flush logs
|
||||
kill "$LS_PID" 2>/dev/null
|
||||
wait "$LS_PID" 2>/dev/null || true
|
||||
|
||||
# Parse and display
|
||||
echo ""
|
||||
python3 "$SCRIPT_DIR/parse-snapshot.py" "$LOG"
|
||||
|
||||
# Also save raw log
|
||||
SNAPSHOT_FILE="/tmp/standalone-snapshot-$(date +%Y%m%d-%H%M%S).log"
|
||||
cp "$LOG" "$SNAPSHOT_FILE"
|
||||
echo ""
|
||||
echo "[*] Raw log saved to: $SNAPSHOT_FILE"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── Normal mode: test and report ──────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "=== GetUserStatus ==="
|
||||
curl -sk "https://127.0.0.1:${HTTPS_PORT}/exa.language_server_pb.LanguageServerService/GetUserStatus" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "x-codeium-csrf-token: $MAIN_CSRF" \
|
||||
-H "Origin: vscode-file://vscode-app" \
|
||||
-d '{}' 2>/dev/null | python3 -c "
|
||||
import json, sys
|
||||
try:
|
||||
d = json.load(sys.stdin)
|
||||
us = d.get('userStatus', {})
|
||||
ps = us.get('planStatus', {})
|
||||
pi = ps.get('planInfo', {})
|
||||
print(f'Plan: {pi.get(\"planName\",\"?\")}, Prompt: {ps.get(\"availablePromptCredits\",\"?\")}, Flow: {ps.get(\"availableFlowCredits\",\"?\")}')
|
||||
ut = us.get('userTier', {})
|
||||
print(f'Tier: {ut.get(\"name\",\"?\")}')
|
||||
models = us.get('cascadeModelConfigData', {}).get('clientModelConfigs', [])
|
||||
print(f'Models: {len(models)}')
|
||||
for m in models[:5]:
|
||||
qi = m.get('quotaInfo', {})
|
||||
print(f' - {m.get(\"label\")}: remaining={qi.get(\"remainingFraction\",\"?\")}')
|
||||
except Exception as e:
|
||||
print(f'Error: {e}')
|
||||
" 2>/dev/null
|
||||
|
||||
echo ""
|
||||
kill "$LS_PID" 2>/dev/null || true
|
||||
wait "$LS_PID" 2>/dev/null || true
|
||||
echo "[*] Done"
|
||||
@@ -9,7 +9,8 @@ use serde_json::Value;
|
||||
use tracing::info;
|
||||
|
||||
/// Strip ALL tool definitions.
|
||||
const STRIP_ALL_TOOLS: bool = true;
|
||||
/// Set to false to allow tools through (for tool call testing).
|
||||
const STRIP_ALL_TOOLS: bool = false;
|
||||
|
||||
/// Modify a streamGenerateContent request body in-place.
|
||||
/// Returns the modified JSON bytes, or None if modification wasn't possible.
|
||||
|
||||
@@ -29,6 +29,10 @@ const LS_USER: &str = "antigravity-ls";
|
||||
/// A running standalone LS process.
|
||||
pub struct StandaloneLS {
|
||||
child: Child,
|
||||
/// The actual LS process PID (may differ from child PID when spawned via sudo).
|
||||
ls_pid: Option<u32>,
|
||||
/// Whether the LS was spawned via sudo (needs sudo kill).
|
||||
use_sudo: bool,
|
||||
pub port: u16,
|
||||
pub csrf: String,
|
||||
}
|
||||
@@ -196,8 +200,25 @@ impl StandaloneLS {
|
||||
|
||||
info!(pid = child.id(), port, "Standalone LS spawned");
|
||||
|
||||
// When spawned via sudo, the child is the sudo process which exits after
|
||||
// launching the LS as the target user. We need the actual LS PID for cleanup.
|
||||
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_ls_pid_for_user(LS_USER).ok()
|
||||
} else {
|
||||
Some(child.id())
|
||||
};
|
||||
|
||||
if let Some(pid) = ls_pid {
|
||||
info!(ls_pid = pid, sudo = use_sudo, "Discovered actual LS process");
|
||||
}
|
||||
|
||||
Ok(StandaloneLS {
|
||||
child,
|
||||
ls_pid,
|
||||
use_sudo,
|
||||
port,
|
||||
csrf: main_config.csrf.clone(),
|
||||
})
|
||||
@@ -239,9 +260,26 @@ impl StandaloneLS {
|
||||
/// Kill the standalone LS process.
|
||||
pub fn kill(&mut self) {
|
||||
info!("Killing standalone LS");
|
||||
|
||||
if self.use_sudo {
|
||||
// The child is sudo which already exited. Kill the actual LS via sudo.
|
||||
if let Some(pid) = self.ls_pid {
|
||||
info!(pid, "Killing LS process via sudo");
|
||||
let _ = std::process::Command::new("sudo")
|
||||
.args(["-n", "kill", "-TERM", &pid.to_string()])
|
||||
.status();
|
||||
// Give it a moment to exit gracefully
|
||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||
// Force kill if still alive
|
||||
let _ = std::process::Command::new("sudo")
|
||||
.args(["-n", "kill", "-KILL", &pid.to_string()])
|
||||
.status();
|
||||
}
|
||||
} else {
|
||||
let _ = self.child.kill();
|
||||
let _ = self.child.wait();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for StandaloneLS {
|
||||
@@ -366,6 +404,23 @@ fn has_ls_user() -> bool {
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Find the PID of a language_server process owned by a specific user.
|
||||
///
|
||||
/// Used to discover the actual LS process after sudo spawns it as a different user.
|
||||
fn find_ls_pid_for_user(user: &str) -> Result<u32, String> {
|
||||
let output = Command::new("pgrep")
|
||||
.args(["-u", user, "-f", "language_server_linux"])
|
||||
.output()
|
||||
.map_err(|e| format!("pgrep failed: {e}"))?;
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
stdout
|
||||
.lines()
|
||||
.next()
|
||||
.and_then(|line| line.trim().parse::<u32>().ok())
|
||||
.ok_or_else(|| format!("No LS process found for user {user}"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
Reference in New Issue
Block a user