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

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 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:

*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):

  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):

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 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):

  1. SubscribeToUnifiedStateSyncTopic — Settings subscription

    • Can return empty end-of-stream (no data messages)
    • LS will retry but won't crash
  2. PushUnifiedStateSyncUpdate — State push

    • Can return empty success

Nice-to-have (non-blocking):

  1. All other methods — return empty success
    • GetChromeDevtoolsMcpUrl, ShowAnnotation, OpenFilePointer, etc.