- 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
11 KiB
Extension Server Protocol — Deep Analysis
Source: strings analysis of /usr/share/antigravity/resources/app/extensions/antigravity/bin/language_server_linux_x64
Architecture Overview
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
GetSecretValueandLanguageServerStartedthat 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: 0withContent-Type: application/connect+protocausesunexpected 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:
*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
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):
CodeAssistClient.GetOAuthToken()— entry point for getting OAuth- →
UnifiedStateSyncClient.GetOAuthTokenInfo()— state sync layer - →
ExtensionServerClient.GetSecretValue(key)— calls extension server - → Our stub must respond with the token value
Proto Structures
GetSecretValueRequest (from binary):
message GetSecretValueRequest {
string key = 1; // protobuf:"bytes,1,opt,name=key"
}
GetSecretValueResponse (from binary):
message GetSecretValueResponse {
string value = 1; // protobuf:"bytes,1,opt,name=value"
}
OAuthTokenInfo (from binary):
message OAuthTokenInfo {
string access_token = ?; // .GetAccessToken()
string expiry = ?; // .GetExpiry()
string refresh_token = ?; // .GetRefreshToken()
string token_type = ?; // .GetTokenType()
}
SaveOAuthTokenInfoRequest contains:
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:
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 tokenGetSecureModeEnabled()— security settingsGetBrowserToolsEnabled()— browser tool accessGetBrowserAllowlist()— allowed browser URLsGetBrowserJsExecutionPolicy()— JS execution policyGetTerminalAutoExecutionPolicy()— terminal auto-run policyGetTerminalAllowedCommands()— allowed terminal commandsGetTerminalDeniedCommands()— denied terminal commandsGetGcpProjectID()— GCP project bindingGetAllowAgentAccessNonWorkspaceFiles()— file access policyGetAllowCascadeAccessGitignoreFiles()— gitignore policyGetAutoContinueOnMaxGeneratorInvocations()— auto-continueGetDisableCascadeAutoFixLints()— lint autofixGetTabGitignoreAccess()— 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:
- Accept the subscription request
- Send an end-of-stream trailer immediately (empty data)
- 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/jsonorapplication/proto. This is different from the Extension Server which expects streamingapplication/connect+proto.
What the Headless Stub Must Implement
Critical (blocks all requests):
-
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
- Request body contains
-
LanguageServerStarted — Must acknowledge startup
- Empty success response (just end-of-stream trailer)
Important (blocks some features):
-
SubscribeToUnifiedStateSyncTopic — Settings subscription
- Can return empty end-of-stream (no data messages)
- LS will retry but won't crash
-
PushUnifiedStateSyncUpdate — State push
- Can return empty success
Nice-to-have (non-blocking):
- All other methods — return empty success
GetChromeDevtoolsMcpUrl,ShowAnnotation,OpenFilePointer, etc.