# 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
(ConnectRPC, HTTP/1.1)"] end subgraph "Language Server (Go binary)" LS["LanguageServerService
(API Server, ConnectRPC)"] ESC["ExtensionServerClient
(ConnectRPC client)"] STATESYNC["UnifiedStateSyncClient"] CAC["CodeAssistClient"] end subgraph "Google Cloud" GOOGLE["daily-cloudcode-pa.googleapis.com"] end EXT -->|"starts"| EXTSRV ESC -->|"HTTP/1.1
application/connect+proto
(ServerStream RPC)"| EXTSRV LS -->|"HTTPS
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.