# Antigravity CLI `agy` 1.0.2 — Unauthenticated Local Language Server `RunCommand` RPC (regression vs 2.0.1 Desktop)

## Affected

- **Product**: Google Antigravity CLI (`agy`)
- **Version**: 1.0.2 (build `6109799369277440`)
- **Platform tested**: macOS 25.5.0 Apple Silicon
- **Manifest**: `https://antigravity-cli-auto-updater-974169037036.us-central1.run.app/manifests/darwin_arm64.json`

## Summary

Running `agy` starts a Language Server that listens for gRPC-over-HTTP and gRPC-over-HTTPS on random `127.0.0.1` TCP ports for the lifetime of the agy session. The port numbers are logged at `~/.gemini/antigravity-cli/log/cli-*.log` (file mode `0644`).

A 9-byte gRPC-web POST to `/exa.language_server_pb.LanguageServerService/RunCommand` with `cmd="id"` executes the shell command and returns stdout in the HTTP response body. No `x-codeium-csrf-token`, no `Authorization`, no `Cookie` is required.

The Antigravity Desktop 2.0.1 build of the same Language Server did require `x-codeium-csrf-token`. The CLI 1.0.2 build does not. This is a regression.

## Steps to Reproduce

```bash
chmod +x evidence/run_unauth_runcommand_poc.sh
./evidence/run_unauth_runcommand_poc.sh
```

Expected (3-of-3 successful runs in `evidence/repro-runs/`):

```text
[5] response strings:
    HTTP/2 200
    content-type: application/grpc-web+proto
    uid=501(REDACTED-USER) gid=20(staff) groups=20(staff),12(everyone),...
    grpc-status: 0

MARKER: AGY_UNAUTH_RUNCOMMAND_FIRED
```

The request payload is 9 bytes: `00 00 00 00 04 0a 02 69 64` (gRPC-web framing + protobuf `cmd="id"`).

## Attack Scenario

The attacker is any same-UID local process. The attack uses only Python stdlib (`os`, `re`, `glob`, `struct`, `ssl`, `http.client`); no shell, no subprocess.

1. Read `~/.gemini/antigravity-cli/log/cli-*.log` (mode `0644`) and grep for the LS port.
2. Build the 9-byte gRPC-web payload.
3. POST to `https://127.0.0.1:<port>/exa.language_server_pb.LanguageServerService/RunCommand` with `Content-Type: application/grpc-web+proto` and no auth headers.
4. Read the response body — contains `uid=...gid=...groups=...`.

End-to-end demonstration: `evidence/CHAIN-DEMO-20260524/11-non-shell-attacker.py` (final log line: `RESULT: ATTACK SUCCEEDED`).

## Demonstrated Impact

The same single unauthenticated `RunCommand` primitive performed each of the following operations on disk and over the network during testing.

| Operation | Evidence file |
|---|---|
| Read first 80 bytes of `~/.ssh/google_compute_engine` (returns base64 of the OpenSSH private key file header) | `evidence/CHAIN-DEMO-20260524/08-read-ssh-key-partial.bin` |
| List `~/.ssh/` (returns real directory listing including private key file, ssh-agent socket, config) | `evidence/CHAIN-DEMO-20260524/02-list-ssh.bin` |
| Confirm presence + size of `~/.aws/credentials` and `~/.config/gcloud/application_default_credentials.json` | `evidence/CHAIN-DEMO-20260524/03-cloud-creds.bin` |
| Write a file at `/tmp/AGY-PERSISTENCE-PROOF.txt` with attacker content; response echoes the written bytes back | `evidence/CHAIN-DEMO-20260524/05-write-persistence.bin` |
| Enumerate sensitive env var names of the agy process; values stripped by attacker's own `sed` filter | `evidence/CHAIN-DEMO-20260524/06-read-env.bin` |
| Send a POST to an attacker-controlled HTTP server on the same machine with real victim metadata in the body (87 bytes) | `evidence/CHAIN-DEMO-20260524/09-exfil-server.log` |
| Spawn a `nohup` background process that survives the request and writes `/tmp/AGY-BG-PROOF.txt` 30 seconds later | `evidence/CHAIN-DEMO-20260524/10-background-marker.txt` |

## What agy LS Adds Beyond Same-UID Baseline

The standard "same-user attacker already has shell" objection does not apply when the attacker runs inside a tighter ambient sandbox. To show this, the same attacker was run under `sandbox-exec` (profile at `evidence/DIFFERENTIAL-20260524/sandbox-profile.sb`) that denies process-exec of any binary except python, denies all file writes, and denies non-loopback network. The sandbox allows file-read everywhere (concedes same-UID file reads as a non-novel capability).

| Operation | Direct in-sandbox | Via agy LS |
|---|---|---|
| Execute `/bin/cat /etc/passwd` | `BLOCKED — PermissionError: '/bin/cat'` | `HTTP 200`, body contains real `/etc/passwd` content |
| Open `/tmp/AGY-DIFF-WRITE-PROOF-<pid>.txt` for write | `BLOCKED — PermissionError` | `HTTP 200`, file present on disk afterwards |
| `urllib.request.urlopen('https://example.com/')` | `BLOCKED — URLError: nodename nor servname provided` | `HTTP 200`, body contains real `<title>Example Domain</title>` |

Transcript: `evidence/DIFFERENTIAL-20260524/differential-test.log`.

The sandbox blocked all three operations directly. The unauthenticated agy LS RPC restored all three to the same attacker. The agy LS RPC is therefore a novel privilege grant for: arbitrary shell execution, arbitrary file write, and arbitrary outbound network.

## Cross-Origin Reachability — Browser

The agy LS reflects `Access-Control-Allow-Origin` only for `http(s)://localhost(:port)?`, `http(s)://127.0.0.1(:port)?`, and `http://[::1](:port)?` (`evidence/DRIVE-BY-WEB-20260524/`). Origins such as `https://attacker.example.com`, `null`, and `http://localhost.attacker.com` are rejected. The server also requires a gRPC-specific `Content-Type` (returns `415 Unsupported Media Type` for `text/plain` or `application/x-www-form-urlencoded`), which forces CORS preflight and blocks any "simple-request" bypass.

A page served from `http://localhost:8080` was empirically able to fetch the unauthenticated `RunCommand` from a browser (`evidence/DRIVE-BY-WEB-20260524/server.log` shows the page captured the `uid=` line). A page served from any non-localhost origin cannot. The browser-reachable attack surface therefore requires the attacker to control content served from a localhost origin (e.g. XSS in a local development server the developer is running, or HTML served by a locally installed application).

## Regression Evidence

The Antigravity Desktop 2.0.1 Language Server was tested by the same researcher on 2026-05-21. The Desktop LS did require `x-codeium-csrf-token`:

> "Mocked LS runtime found the accepted CSRF header is `x-codeium-csrf-token`; with that token, LS can read/write/list and delete local files and run commands. CORS/no token is refuted for the tested methods: preflight returned no allow-origin headers, no-token/bad-token POSTs returned 401."
> — `recon/desktop-2.0.1-20260521/runtime-mocked-ls-token-cors-r4/analysis.md`

In agy CLI 1.0.2, the same `RunCommand` method returns `grpc-status: 0` (success) without any auth header. A re-verification sent a deliberately malformed payload and received `grpc-status: 3` (INVALID_ARGUMENT — body parser failure), not `grpc-status: 16` (UNAUTHENTICATED). The auth gate is never reached. Capture: `evidence/RE-VERIFY-AUTH-BYPASS-20260524T140400Z/RE-VERIFY-NOTE.md`.

## Suggested Fix

1. Re-enable `x-codeium-csrf-token` enforcement on `RunCommand` and every other method on `LanguageServerService`, matching the 2.0.1 Desktop hardening.
2. Move the Language Server to a Unix domain socket (e.g. in `$XDG_RUNTIME_DIR`) with mode `0600`. Remove the TCP loopback transport.
3. Stop logging the LS port in plaintext at a user-readable path (`~/.gemini/antigravity-cli/log/cli-*.log`, mode `0644`).

## Attachments

Single archive: `attachments.zip` (sha256 in `attachments.zip.sha256`).

Key files:

- `REPORT.md` (this file)
- `evidence/run_unauth_runcommand_poc.sh` — one-script reproduction
- `evidence/repro-runs/run-1.txt`, `evidence/repro-runs/run-2.txt`, `evidence/repro-runs/run-3.txt` — three successful runs
- `evidence/CLEAN-POC-RUNCOMMAND-id/02-request-cmd-id.bin` — exact 9-byte payload
- `evidence/CLEAN-POC-RUNCOMMAND-id/03-response.bin` — captured response with uid line
- `evidence/CHAIN-DEMO-20260524/` — chain demonstrations + pure-Python no-shell attacker
- `evidence/DIFFERENTIAL-20260524/` — sandbox profile and differential transcript showing novel capabilities
- `evidence/DRIVE-BY-WEB-20260524/` — CORS origin allowlist test results
- `evidence/RE-VERIFY-AUTH-BYPASS-20260524T140400Z/RE-VERIFY-NOTE.md` — `grpc-status: 3 vs 16` confirmation
- `evidence/01-rpc-enum.txt` — full 241-method enumeration
- `evidence/poc-runcommand-id-cross-origin.bin` — same request with attacker `Origin` header
- `evidence/poc-options-preflight.txt` — OPTIONS preflight with no `Access-Control-Allow-Origin`
- `SHA256SUMS.txt`
