Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Required: password used to log into the web admin UI.
# Pick something long and random.
AGM_ADMIN_PASSWORD=change-me-please

# Optional: if set, requests to the OpenAI/Anthropic proxy must include this
# key in `Authorization: Bearer …`, `x-api-key`, or `x-goog-api-key`.
# Leave empty/unset to run the proxy in open mode (anyone on the network can
# call it — only safe behind a firewall).
AGM_API_KEY=

# Optional: where to put the management API server.
# Defaults: 8045 for the proxy (changed via the config UI), 8046 for management.
AGM_MANAGEMENT_PORT=8046

# Optional: which interface the proxy and management server bind to.
# Default is 0.0.0.0 (all interfaces). Set to 127.0.0.1 for production
# behind nginx so the Node servers are only reachable via the reverse proxy.
# AGM_BIND_HOST=127.0.0.1

# Optional: relocate persistent state (master key, sqlite, logs).
# Defaults to ~/.antigravity-manager-server when not running under Electron.
# AGM_DATA_DIR=/var/lib/antigravity-manager
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -125,4 +125,10 @@ cli/aliases.json
accounts_backup.json

# Bun lock file
bun.lock
bun.lock
# Antigravity Manager standalone build outputs
dist-web/
dist-server/

# Claude Code internal
.claude/
54 changes: 54 additions & 0 deletions deploy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Deployment

Reference configs for hosting the standalone server (`npm run server:dev`
or `npm run server:start`) on a Linux VM behind nginx with TLS.

## Files

- `nginx/agm.conf` — HTTP-only site config that fronts both ports on a
single domain. `certbot --nginx` rewrites it in place to add TLS,
the 443 server block, and the 80 → 443 redirect. Routes:
- `/admin/*` → management UI (port 8046)
- `/api/*` → management API (port 8046, with login rate limit)
- `/v1/*`, `/v1beta/*` → OpenAI/Anthropic/Gemini proxy (port 8045)
- `/` → 302 redirect to `/admin/`

## Quick deploy

```bash
# Replace YOUR_DOMAIN below.
sudo cp deploy/nginx/agm.conf /etc/nginx/sites-available/agm.conf
sudo sed -i 's/agm.example.com/YOUR_DOMAIN/g' /etc/nginx/sites-available/agm.conf
sudo ln -s /etc/nginx/sites-available/agm.conf /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

# TLS via Let's Encrypt — rewrites agm.conf with the 443 block.
sudo certbot --nginx -d YOUR_DOMAIN
```

## Lock the upstreams to loopback

The Node servers default to `0.0.0.0`, which means they're reachable
directly on `:8045` and `:8046` even with nginx in front. Two fixes —
ideally both:

1. **Set `AGM_BIND_HOST=127.0.0.1`** in `.env`. Both the proxy and the
management server then bind to loopback only and are only reachable
via nginx.
2. **Firewall:** allow only ports 80 and 443 inbound.
```bash
sudo ufw allow 22 && sudo ufw allow 80 && sudo ufw allow 443
sudo ufw enable
```

## Hardening checklist before pointing DNS at the VM

- [ ] Strong `AGM_ADMIN_PASSWORD` (24+ random chars) in `.env`
- [ ] `AGM_API_KEY` set to a long random secret (clients send it as
`Authorization: Bearer …`)
- [ ] `.env` permissions: `chmod 600 .env`
- [ ] Run as a non-login system user (e.g. `adduser --system --group agm`)
- [ ] Backup `~/.antigravity-agent/cloud_accounts.db` and `~/.antigravity-agent/.mk`
(the master key — without it, the DB is unrecoverable)
- [ ] Process supervisor in place (systemd/pm2) so a crash auto-restarts
- [ ] HTTPS verified end-to-end (`curl -I https://YOUR_DOMAIN/admin/`)
114 changes: 114 additions & 0 deletions deploy/nginx/agm.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Antigravity Manager — nginx site config (HTTP only).
#
# Drop in /etc/nginx/sites-available/agm.conf, symlink it into
# /etc/nginx/sites-enabled/, then:
#
# nginx -t && systemctl reload nginx
# certbot --nginx -d agm.example.com
#
# certbot will rewrite this file in place to add the 443 server block,
# the TLS cert paths, and the 80 -> 443 redirect.
#
# Replace `agm.example.com` with your domain.
#
# Routing:
# /admin/, /admin/*, /api/* -> 127.0.0.1:8046 (management API + web UI)
# /v1/*, /v1beta/* -> 127.0.0.1:8045 (OpenAI/Anthropic proxy)
# / -> redirect to /admin/
#
# Bind both Node servers to 127.0.0.1 so they're only reachable through
# nginx. In your .env:
# AGM_BIND_HOST=127.0.0.1

# Per-IP rate limits. The login bucket is intentionally tight; the proxy
# bucket should be generous enough not to throttle a real client.
limit_req_zone $binary_remote_addr zone=agm_login:10m rate=5r/m;
limit_req_zone $binary_remote_addr zone=agm_api:10m rate=20r/s;
limit_req_zone $binary_remote_addr zone=agm_proxy:10m rate=60r/s;

upstream agm_management { server 127.0.0.1:8046; keepalive 16; }
upstream agm_proxy { server 127.0.0.1:8045; keepalive 16; }

server {
listen 80;
listen [::]:80;
server_name agm.example.com;

# Allow image uploads to /v1/images/edits and audio to /v1/audio/transcriptions.
client_max_body_size 25m;

# ---------- Web UI ----------
location = / { return 302 /admin/; }

location /admin/ {
proxy_pass http://agm_management;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}

# ---------- Management API ----------
location = /api/auth/login {
# Brute-force gate. Burst lets a typo or two through, then queues.
limit_req zone=agm_login burst=3 nodelay;

proxy_pass http://agm_management;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}

location /api/ {
limit_req zone=agm_api burst=40 nodelay;

proxy_pass http://agm_management;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 120s;
}

# ---------- OpenAI / Anthropic proxy ----------
# Streaming SSE responses need buffering off and a long read timeout.
location /v1/ {
limit_req zone=agm_proxy burst=80 nodelay;

proxy_pass http://agm_proxy;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Connection "";
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
}

# Gemini-native surface (used by some SDKs).
location /v1beta/ {
limit_req zone=agm_proxy burst=80 nodelay;

proxy_pass http://agm_proxy;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Connection "";
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
}

# Anything else: drop. (No bare /api or /v1 endpoints are public.)
location / { return 404; }
}
Loading