Files
zerogravity/docs/extension-server-analysis.md
Nikketryhard 3d87c04d20 docs: overhaul docs, add architecture and traces, update README/GEMINI
- 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
2026-02-18 01:31:18 -06:00

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.