# Extension Server Protocol — Deep Analysis

Source: `strings` analysis of `/usr/share/antigravity/resources/app/extensions/antigravity/bin/language_server_linux_x64`

---

## Architecture Overview

```mermaid
graph TD
    subgraph "Antigravity App (Electron)"
        EXT["Extension (TypeScript)"]
        EXTSRV["Extension Server<br/>(ConnectRPC, HTTP/1.1)"]
    end

    subgraph "Language Server (Go binary)"
        LS["LanguageServerService<br/>(API Server, ConnectRPC)"]
        ESC["ExtensionServerClient<br/>(ConnectRPC client)"]
        STATESYNC["UnifiedStateSyncClient"]
        CAC["CodeAssistClient"]
    end

    subgraph "Google Cloud"
        GOOGLE["daily-cloudcode-pa.googleapis.com"]
    end

    EXT -->|"starts"| EXTSRV
    ESC -->|"HTTP/1.1<br/>application/connect+proto<br/>(ServerStream RPC)"| EXTSRV
    LS -->|"HTTPS<br/>application/connect+proto"| GOOGLE
    STATESYNC -->|"via ESC"| EXTSRV
    CAC -->|"GetOAuthToken()"| STATESYNC

    style EXTSRV fill:#f96,stroke:#333
    style STATESYNC fill:#69f,stroke:#333
```

### Communication Paths

| From → To             | Protocol | Content-Type                   | Direction            |
| --------------------- | -------- | ------------------------------ | -------------------- |
| Proxy → LS API Server | HTTP/1.1 | `application/json` (JSON-RPC)  | Unary                |
| Proxy → LS API Server | HTTP/1.1 | `application/proto` (protobuf) | Unary                |
| LS → Extension Server | HTTP/1.1 | `application/connect+proto`    | **Server-Streaming** |
| LS → Google API       | HTTPS/H2 | `application/connect+proto`    | Streaming            |

> [!IMPORTANT]
> **ALL ExtensionServerService methods use Server-Streaming Connect RPC**, even methods like `GetSecretValue` and `LanguageServerStarted` that are semantically unary.
> This means every response must use Connect streaming envelope framing.

---

## Connect Streaming Protocol

The LS uses `connect-go` v1 (`connectrpc.com/connect/v1`). All extension server calls use **ServerStream**, which requires:

### Request Format (LS → Stub)

```
POST /exa.extension_server_pb.ExtensionServerService/{Method} HTTP/1.1
Content-Type: application/connect+proto
Connect-Protocol-Version: 1
```

Body: raw protobuf (NOT envelope-framed for client→server in ServerStream)

### Response Format (Stub → LS)

```
HTTP/1.1 200 OK
Content-Type: application/connect+proto
```

Body: sequence of **envelope frames**:

```
┌─────────┬──────────────────┬───────────────┐
│ Flags   │ Message Length    │ Message Data  │
│ (1 byte)│ (4 bytes, BE)    │ (N bytes)     │
├─────────┼──────────────────┼───────────────┤
│ 0x00    │ protobuf length  │ protobuf data │  ← data message (optional, 0 or more)
│ 0x02    │ trailer length   │ JSON trailer  │  ← end-of-stream (required, exactly 1)
└─────────┴──────────────────┴───────────────┘
```

### Examples

**Empty success** (most methods):

```
\x02 \x00\x00\x00\x02 {}
```

= flag(0x02) + length(2) + trailer("{}")

**Success with data** (e.g., GetSecretValue):

```
\x00 \x00\x00\x00\x0e \x0a\x0c ya29.a0A...   ← data: protobuf response
\x02 \x00\x00\x00\x02 {}                       ← end-of-stream trailer
```

**Error response** (e.g., not found):

```
\x02 \x00\x00\x00\x22 {"error":{"code":"not_found"}}
```

> [!WARNING]
> Sending `Content-Length: 0` with `Content-Type: application/connect+proto` causes `unexpected EOF` — the client expects at least the end-of-stream envelope.

---

## Extension Server Service — Method Reference

### All Methods Are ServerStream

From binary analysis, every method on `ExtensionServerService` is wrapped as:

```go
*connect.ServerStreamForClient[...Response]
```

This includes ALL of these methods:

| Method                             | Request Proto                             | Response Proto                       | Purpose            |
| ---------------------------------- | ----------------------------------------- | ------------------------------------ | ------------------ |
| `LanguageServerStarted`            | `LanguageServerStartedRequest`            | `LanguageServerStartedResponse`      | Init notification  |
| `GetSecretValue`                   | `GetSecretValueRequest{Key}`              | `GetSecretValueResponse{Value}`      | Secret store read  |
| `StoreSecretValue`                 | `StoreSecretValueRequest{Key,Value}`      | `StoreSecretValueResponse`           | Secret store write |
| `SubscribeToUnifiedStateSyncTopic` | `SubscribeToUnifiedStateSyncTopicRequest` | `UnifiedStateSyncUpdate` (stream)    | Real-time state    |
| `PushUnifiedStateSyncUpdate`       | `PushUnifiedStateSyncUpdateRequest`       | `PushUnifiedStateSyncUpdateResponse` | Push state updates |
| `GetChromeDevtoolsMcpUrl`          | `GetChromeDevtoolsMcpUrlRequest`          | `GetChromeDevtoolsMcpUrlResponse`    | Chrome DevTools    |
| `FetchMCPAuthToken`                | `FetchMCPAuthTokenRequest`                | `FetchMCPAuthTokenResponse`          | MCP auth           |
| `AddAnnotation`                    | `AddAnnotationRequest`                    | `AddAnnotationResponse`              | IDE annotation     |
| `OpenFilePointer`                  | `OpenFilePointerRequest`                  | `OpenFilePointerResponse`            | Open file in IDE   |
| `ExecuteCommand`                   | `ExecuteCommandRequest`                   | `TerminalShellCommandStreamChunk`    | Run terminal cmd   |
| ...                                | ...                                       | ...                                  | 53+ total methods  |

---

## OAuth Token Flow

```mermaid
sequenceDiagram
    participant LS as Language Server
    participant ESC as ExtensionServerClient
    participant STUB as Stub Extension Server
    participant SYNC as UnifiedStateSyncClient

    Note over LS: Startup
    LS->>ESC: LanguageServerStarted()
    ESC->>STUB: POST .../LanguageServerStarted
    STUB-->>ESC: 200 OK (empty envelope)

    Note over LS: OAuth Token Request
    LS->>SYNC: GetOAuthTokenInfo()
    SYNC->>ESC: GetSecretValue({key: "oauth_token"})
    ESC->>STUB: POST .../GetSecretValue
    STUB-->>ESC: 200 OK (envelope with {value: "ya29.a0..."})
    ESC-->>SYNC: {value: "ya29.a0..."}
    SYNC-->>LS: OAuthTokenInfo{access_token: "ya29.a0..."}

    Note over LS: Using Token
    LS->>LS: Set auth_token on API requests
    LS->>LS: GetUserStatus, LoadCodeAssist, etc.
```

### Key Insight: How the LS Gets OAuth Tokens

The call chain at `server.go:558` (`Failed to get OAuth token`):

1. **`CodeAssistClient.GetOAuthToken()`** — entry point for getting OAuth
2. → **`UnifiedStateSyncClient.GetOAuthTokenInfo()`** — state sync layer
3. → **`ExtensionServerClient.GetSecretValue(key)`** — calls extension server
4. → **Our stub** must respond with the token value

### Proto Structures

**GetSecretValueRequest** (from binary):

```protobuf
message GetSecretValueRequest {
  string key = 1;  // protobuf:"bytes,1,opt,name=key"
}
```

**GetSecretValueResponse** (from binary):

```protobuf
message GetSecretValueResponse {
  string value = 1;  // protobuf:"bytes,1,opt,name=value"
}
```

**OAuthTokenInfo** (from binary):

```protobuf
message OAuthTokenInfo {
  string access_token = ?;  // .GetAccessToken()
  string expiry = ?;        // .GetExpiry()
  string refresh_token = ?; // .GetRefreshToken()
  string token_type = ?;    // .GetTokenType()
}
```

**SaveOAuthTokenInfoRequest** contains:

```protobuf
message SaveOAuthTokenInfoRequest {
  OAuthTokenInfo token_info = 1;  // protobuf:"bytes,1,opt,name=token_info"
}
```

---

## UnifiedStateSyncTopic

The state sync system is central to the LS's operation:

```mermaid
graph LR
    LSS["LS State Sync"] -->|"Subscribe"| EXT["Extension Server"]
    EXT -->|"Stream updates"| LSS
    LSS -->|"Push"| EXT

    LSS --> |"Reads"| OAuth["OAuth Token"]
    LSS --> |"Reads"| Settings["User Settings"]
    LSS --> |"Reads"| Models["Available Models"]
    LSS --> |"Reads"| Trajectories["Trajectory Summaries"]
```

### What the LS reads via state sync:

From `statesync.(*UnifiedStateSyncClient)` methods:

- `GetOAuthTokenInfo()` — OAuth access token
- `GetSecureModeEnabled()` — security settings
- `GetBrowserToolsEnabled()` — browser tool access
- `GetBrowserAllowlist()` — allowed browser URLs
- `GetBrowserJsExecutionPolicy()` — JS execution policy
- `GetTerminalAutoExecutionPolicy()` — terminal auto-run policy
- `GetTerminalAllowedCommands()` — allowed terminal commands
- `GetTerminalDeniedCommands()` — denied terminal commands
- `GetGcpProjectID()` — GCP project binding
- `GetAllowAgentAccessNonWorkspaceFiles()` — file access policy
- `GetAllowCascadeAccessGitignoreFiles()` — gitignore policy
- `GetAutoContinueOnMaxGeneratorInvocations()` — auto-continue
- `GetDisableCascadeAutoFixLints()` — lint autofix
- `GetTabGitignoreAccess()` — tab-level gitignore

### SubscribeToUnifiedStateSyncTopic

This is a **long-lived server stream**. The LS subscribes at startup and expects to receive `UnifiedStateSyncUpdate` messages over time. If the stub closes the connection, the LS will reconnect.

For a minimal stub, we can:

1. Accept the subscription request
2. Send an end-of-stream trailer immediately (empty data)
3. Close the connection

The LS will poll/retry, but this won't break functionality — it just means settings won't be dynamically updated (which is fine for headless mode).

---

## Proxy → LS Communication

The proxy communicates with the LS API server (`LanguageServerService`) differently:

| Method                            | Content-Type        | Protocol      |
| --------------------------------- | ------------------- | ------------- |
| `call_json()` (most methods)      | `application/json`  | Unary Connect |
| `call_proto()` (protobuf methods) | `application/proto` | Unary Connect |

Both use `Connect-Protocol-Version: 1` header.

> [!NOTE]
> The LS API server accepts unary Connect calls with `application/json` or `application/proto`.
> This is different from the Extension Server which expects streaming `application/connect+proto`.

---

## What the Headless Stub Must Implement

### Critical (blocks all requests):

1. **GetSecretValue** — Must return the OAuth token when requested
   - Request body contains `key` (field 1, string)
   - Response must contain `value` (field 1, string) = the OAuth token
   - Must use Connect streaming envelope framing

2. **LanguageServerStarted** — Must acknowledge startup
   - Empty success response (just end-of-stream trailer)

### Important (blocks some features):

3. **SubscribeToUnifiedStateSyncTopic** — Settings subscription
   - Can return empty end-of-stream (no data messages)
   - LS will retry but won't crash

4. **PushUnifiedStateSyncUpdate** — State push
   - Can return empty success

### Nice-to-have (non-blocking):

5. All other methods — return empty success
   - `GetChromeDevtoolsMcpUrl`, `ShowAnnotation`, `OpenFilePointer`, etc.
