- Add docs/architecture.md with 4 mermaid diagrams - Add docs/mitm.md with 3 mermaid diagrams (replaces mitm-interception-status) - Add docs/traces.md documenting per-call trace system - Rewrite README.md to be concise with mermaid and doc refs - Rewrite GEMINI.md for core philosophy and agent usage - Clean extension-server-analysis.md (remove stale debug sections) - Delete temp docs: standalone-ls-todo, panel-stream-investigation, endpoint-gap-analysis, request-comparison
307 lines
11 KiB
Markdown
307 lines
11 KiB
Markdown
# 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.
|