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
|
## Quick Start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Build
|
|
||||||
cargo build --release
|
|
||||||
|
|
||||||
# First-time setup (creates user + iptables for MITM)
|
# First-time setup (creates user + iptables for MITM)
|
||||||
sudo ./scripts/mitm-redirect.sh install
|
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
|
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**
|
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
|
## Endpoints
|
||||||
|
|
||||||
| Method | Path | Description |
|
| 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;
|
use tracing::info;
|
||||||
|
|
||||||
/// Strip ALL tool definitions.
|
/// 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.
|
/// Modify a streamGenerateContent request body in-place.
|
||||||
/// Returns the modified JSON bytes, or None if modification wasn't possible.
|
/// 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.
|
/// A running standalone LS process.
|
||||||
pub struct StandaloneLS {
|
pub struct StandaloneLS {
|
||||||
child: Child,
|
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 port: u16,
|
||||||
pub csrf: String,
|
pub csrf: String,
|
||||||
}
|
}
|
||||||
@@ -196,8 +200,25 @@ impl StandaloneLS {
|
|||||||
|
|
||||||
info!(pid = child.id(), port, "Standalone LS spawned");
|
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 {
|
Ok(StandaloneLS {
|
||||||
child,
|
child,
|
||||||
|
ls_pid,
|
||||||
|
use_sudo,
|
||||||
port,
|
port,
|
||||||
csrf: main_config.csrf.clone(),
|
csrf: main_config.csrf.clone(),
|
||||||
})
|
})
|
||||||
@@ -239,8 +260,25 @@ impl StandaloneLS {
|
|||||||
/// Kill the standalone LS process.
|
/// Kill the standalone LS process.
|
||||||
pub fn kill(&mut self) {
|
pub fn kill(&mut self) {
|
||||||
info!("Killing standalone LS");
|
info!("Killing standalone LS");
|
||||||
let _ = self.child.kill();
|
|
||||||
let _ = self.child.wait();
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,6 +404,23 @@ fn has_ls_user() -> bool {
|
|||||||
.unwrap_or(false)
|
.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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
Reference in New Issue
Block a user