Localharness Architecture — Go Binary Subprocess Deep-Dive
Complete architectural analysis of the localharness Go binary: subprocess lifecycle, WebSocket protocol, handshake sequence, binary discovery chain, and connection management
Localharness Architecture — Go Binary Subprocess Deep-Dive
What Is Localharness?
Localharness is a pre-compiled Go binary that ships inside platform-specific wheels of the google-antigravity Python SDK. It is the actual agent runtime -- the Python layer is an orchestrator that manages its lifecycle. The SDK spawns it as a child process and communicates over a WebSocket connection using Protocol Buffers serialized as JSON.
The binary is the bridge between the Python SDK (google.antigravity) and the Gemini model backend. It handles:
- Model API calls (Gemini, Gemma, Custom backends)
- Built-in tool execution (file ops, shell, subagents, questions)
- MCP server management
- Trajectory persistence
- Context compaction
- Browser automation (CDP)
Architecture Overview
graph TB
subgraph SDK_Process["Python SDK Process"]
Agent["Agent<br/>async context manager"]
Conv["Conversation<br/>session state"]
LConn["LocalConnection<br/>WebSocket client"]
TR["ToolRunner<br/>Python tools"]
HR["HookRunner<br/>lifecycle hooks"]
TGR["TriggerRunner<br/>background tasks"]
Agent --> Conv
Conv --> LConn
Agent --> TR
Agent --> HR
Agent --> TGR
LConn -.-> TR
LConn -.-> HR
end
subgraph Transport["Transport Layer"]
WS["WebSocket<br/>ws://localhost:{port}/"]
STDIN["stdin pipe<br/>length-prefixed protobuf"]
STDOUT["stdout pipe<br/>length-prefixed protobuf"]
STDERR["stderr pipe<br/>log stream"]
end
subgraph Harness_Process["localharness (Go binary)"]
WSS["WebSocket Server"]
AL["Agent Loop<br/>(Cortex)"]
MT["Model Transport<br/>gRPC to Gemini"]
BTools["Built-in Tools<br/>file/shell/subagent"]
MCPM["MCP Manager"]
TPS["Trajectory Persistence"]
SR["Stderr Logger"]
WSS --> AL
AL --> MT
AL --> BTools
AL --> MCPM
AL --> TPS
end
LConn -->|"JSON-serialized protobuf"| WS
Agent -->|"InputConfig (4-byte LE + pb)"| STDIN
STDIN -->|"OutputConfig (port + api_key)"| STDOUT
STDOUT --> LConn
STDERR --> SR
WS <--> WSS
MT -->|"gRPC + Bearer"| GCloud["Gemini API<br/>cloudcode-pa.googleapis.com"]
Connection Lifecycle
The connection lifecycle has 7 distinct phases. Understanding each phase is critical for debugging, extending, or building alternative harness implementations.
Phase 1: Binary Discovery
The SDK locates the localharness binary through a 4-step fallback chain. This ensures the binary is found regardless of installation method (wheel, development, system PATH).
flowchart TD
Start["LocalConnectionStrategy.__aenter__()"] --> EV{"ANTIGRAVITY_HARNESS_PATH<br/>env var set?"}
EV -->|YES| UseEnv["Use env var path"]
EV -->|NO| Meta{"importlib.metadata<br/>wheel discovery?"}
Meta -->|FOUND| UseMeta["Use wheel binary<br/>google/antigravity/bin/localharness"]
Meta -->|NOT FOUND| Res{"importlib.resources<br/>fallback?"}
Res -->|FOUND| UseRes["Use resource binary"]
Res -->|NOT FOUND| PATH{"shutil.which<br/>'localharness' in PATH?"}
PATH -->|FOUND| UsePATH["Use system PATH binary"]
PATH -->|NOT FOUND| FAIL["Raise FileNotFoundError"]
UseEnv --> Validate["Validate binary exists + executable"]
UseMeta --> Validate
UseRes --> Validate
UsePATH --> Validate
Validate --> Phase2["Proceed to Phase 2: Spawn"]
Implementation detail: The environment variable path takes absolute precedence. In development, symlinking to a local build via ANTIGRAVITY_HARNESS_PATH is the standard pattern.
Phase 2: Process Spawn
# Pseudocode from LocalConnectionStrategy.__aenter__()
process = subprocess.Popen(
[binary_path],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
# No shell=True -- direct exec for security
)
The harness binary starts and immediately blocks on stdin, waiting for the InputConfig message.
Phase 3: stdin/stdout Handshake
This is a synchronous, length-prefixed protobuf exchange over the process's stdin and stdout pipes. It happens BEFORE the WebSocket connection is established.
sequenceDiagram
participant SDK as Python SDK
participant Harness as localharness (Go)
participant WSS as WebSocket Server
Note over SDK,Harness: Phase 3: stdin/stdout Handshake
SDK->>Harness: [4 bytes LE length][InputConfig protobuf]
Note right of Harness: Parses InputConfig:<br/>- storage_directory<br/>- port (0 = auto)<br/>- bind_address (localhost)<br/>- client_info
Harness->>WSS: Start WebSocket server on port
WSS-->>Harness: Server ready, port assigned
Harness->>SDK: [4 bytes LE length][OutputConfig protobuf]
Note left of SDK: Parses OutputConfig:<br/>- port (actual port)<br/>- api_key (ephemeral)
Wire format for stdin/stdout:
[uint32 little-endian: message_length][protobuf bytes: message_body]
The api_key in OutputConfig is an ephemeral key generated by the harness for WebSocket authentication. It is NOT the Gemini API key -- it is a local-only credential to prevent other processes on the same machine from connecting.
Phase 4: WebSocket Connection
sequenceDiagram
participant SDK as Python SDK
participant Harness as localharness (Go)
Note over SDK,Harness: Phase 4: WebSocket Connect
SDK->>Harness: WebSocket upgrade request
Note right of Harness: Header: x-goog-api-key: {api_key}
alt Connection succeeds
Harness-->>SDK: 101 Switching Protocols
else Connection refused (port mismatch)
SDK->>Harness: Retry with exponential backoff
Note left of SDK: 5 attempts, max delay ~2s
end
Note over SDK,Harness: WebSocket established at ws://localhost:{port}/
Retry strategy: The SDK retries the WebSocket connection up to 5 times with exponential backoff. This handles the race condition where the harness hasn't finished starting its WebSocket server by the time the SDK tries to connect.
Phase 5: Initialize Conversation
sequenceDiagram
participant SDK as Python SDK
participant Harness as localharness (Go)
Note over SDK,Harness: Phase 5: Initialize
SDK->>Harness: InitializeConversationEvent (JSON-serialized protobuf)
Note right of Harness: HarnessConfig contains:<br/>- cascade_id (session ID)<br/>- model_config (oneof: Gemini/Gemma/Custom)<br/>- system_instructions<br/>- tools (Python tools as schemas)<br/>- harness_side_tools (toggles)<br/>- workspaces<br/>- mcp_servers<br/>- skills_paths<br/>- compaction_threshold<br/>- initial_trajectory (if resuming)
Harness-->>SDK: TrajectoryStateUpdate(state=RUNNING)
The InitializeConversationEvent is the single most important message in the protocol. It carries the entire agent configuration as a HarnessConfig protobuf. See sdk-test-protobuf for the complete field reference.
Phase 6: Message Loop
The bidirectional message loop is the core runtime. Messages flow as JSON-serialized protobuf over the WebSocket.
sequenceDiagram
participant SDK as Python SDK
participant Harness as localharness (Go)
participant Gemini as Gemini API
Note over SDK,Gemini: Phase 6: Message Loop
SDK->>Harness: InputEvent{user_input: "What files are here?"}
Harness->>Gemini: gRPC Chat request
Gemini-->>Harness: Streaming response chunks
loop For each step
Harness-->>SDK: OutputEvent{step_update: {text_delta: "..."}}
Harness-->>SDK: OutputEvent{step_update: {thinking_delta: "..."}}
end
alt Tool call required
Harness-->>SDK: OutputEvent{step_update: {action: list_directory{...}}}
Note over SDK: SDK dispatches PreToolCallDecideHook
SDK->>Harness: InputEvent{tool_confirmation: {accepted: true}}
Harness-->>SDK: OutputEvent{step_update: {state: DONE}}
end
alt Host-side tool (Python)
Harness-->>SDK: OutputEvent{tool_call: {name: "get_weather", args: ...}}
Note over SDK: SDK executes via ToolRunner
SDK->>Harness: InputEvent{tool_response: {response_json: "..."}}
end
alt Subagent spawned
Harness-->>SDK: OutputEvent{step_update: {action: invoke_subagent{}}}
Note over Harness: New trajectory created
Harness-->>SDK: OutputEvent{step_update: {trajectory_id: "sub-1", ...}}
Harness-->>SDK: TrajectoryStateUpdate{trajectory_id: "sub-1", state: IDLE}
end
Harness-->>SDK: OutputEvent{step_update: {state: DONE, action: finish{...}}}
Harness-->>SDK: TrajectoryStateUpdate{state: IDLE}
Message serialization: All WebSocket messages are JSON-serialized protobuf. The SDK uses google.protobuf.json_format for serialization/deserialization. This is NOT binary protobuf over WebSocket -- it is the JSON mapping, which enables easy debugging with standard WebSocket tools.
Phase 7: Shutdown
The shutdown sequence is a 7-step escalation designed to handle graceful and ungraceful termination.
sequenceDiagram
participant SDK as Python SDK
participant Harness as localharness (Go)
Note over SDK,Harness: Phase 7: Shutdown
SDK->>SDK: 1. Dispatch on_session_end hook
SDK->>SDK: 2. Cancel background tasks (triggers)
SDK->>SDK: 3. Cancel WebSocket reader task
SDK->>Harness: 4. Close WebSocket (graceful, 0.5s timeout)
alt WebSocket close succeeds
Harness-->>SDK: WebSocket close frame
else Timeout
Note over SDK: Proceed to step 5
end
SDK->>Harness: 5. Close stdin pipe (EOF)
Note right of Harness: Go detects EOF on stdin<br/>→ os.Exit(0)
alt Process exits within 5s
Harness-->>SDK: Process exit code
else Timeout
SDK->>Harness: 6. SIGTERM
alt Process exits within 1s
Harness-->>SDK: Process terminated
else Timeout
SDK->>Harness: 7. SIGKILL
Note over Harness: Force killed
end
end
Why stdin EOF triggers exit: The Go harness blocks on stdin reading in its main goroutine. When the SDK closes the stdin pipe, the read returns io.EOF, which the harness treats as a shutdown signal. This is a deliberate design -- it means the harness always exits when the SDK disconnects, even if the WebSocket teardown fails.
Binary Discovery Chain — Implementation Detail
Step 1: Environment Variable
# Highest priority: explicit override
path = os.environ.get("ANTIGRAVITY_HARNESS_PATH")
if path and os.path.isfile(path) and os.access(path, os.X_OK):
return path
Step 2: Wheel Metadata
# Installed via pip/uv: binary is inside the wheel
import importlib.metadata
dist = importlib.metadata.distribution("google-antigravity")
# Looks for: google/antigravity/bin/localharness
# Platform-specific wheel: google_antigravity-0.1.2-cp310-cp310-macosx_11_0_arm64.whl
Step 3: Resource Fallback
# Fallback for editable installs or non-standard layouts
import importlib.resources
# Attempts to locate within package resources
Step 4: System PATH
# Last resort: system PATH
import shutil
path = shutil.which("localharness")
Default Data Paths
| Path | Purpose | Configurable Via |
|---|---|---|
~/.gemini/antigravity/ | App data (config, logs, trajectories) | app_data_dir in HarnessConfig |
/tmp/antigravity_* | Trajectory save (temporary) | save_dir in AgentConfig |
os.getcwd() | Default workspace | workspaces in AgentConfig |
Workspace Scoping
When workspaces are configured (which they are by default -- os.getcwd()), the SDK automatically applies workspace_only() policies. This restricts file tools (view_file, create_file, edit_file) to the specified directories. The harness also enforces workspace validation via the PermissionsConfig protobuf message.
Stderr Handling
The harness writes diagnostic logs to stderr. The SDK starts a dedicated reader thread:
# In LocalConnectionStrategy.__aenter__()
stderr_thread = threading.Thread(
target=_read_stderr,
args=(process.stderr,),
daemon=True, # Don't block shutdown
)
stderr_thread.start()
Stderr lines are logged at DEBUG level. They are NOT surfaced to the user by default. The harness logs include:
- WebSocket connection events
- Model API call latencies
- Tool execution traces
- MCP server lifecycle events
- Trajectory persistence operations
Built-in Tools
The harness executes 11 built-in tools natively (in Go). These are NOT Python tools -- they run in the harness process.
| Tool | Execution | Confirmation Required |
|---|---|---|
list_directory | Harness-side | No |
search_directory | Harness-side | No |
find_file | Harness-side | No |
view_file | Harness-side | No |
create_file | Harness-side | Yes (configurable) |
edit_file | Harness-side | Yes (configurable) |
run_command | Harness-side | Yes (default policy) |
ask_question | Host-side (SDK) | N/A |
start_subagent | Harness-side | Yes (configurable) |
generate_image | Harness-side | No |
finish | Harness-side | No |
Confirmation Flow
When a tool requires confirmation, the flow is:
sequenceDiagram
participant Model as Gemini Model
participant Harness as localharness
participant SDK as Python SDK
participant User as User
Model->>Harness: Tool call: run_command("rm -rf /tmp/test")
Harness-->>SDK: StepUpdate{tool_confirmation_request, action: run_command}
SDK->>SDK: Dispatch PreToolCallDecideHook
alt Policy: allow
SDK->>Harness: ToolConfirmation(accepted=true)
else Policy: deny
SDK->>Harness: ToolConfirmation(accepted=false)
else Policy: ask_user
SDK->>User: "Allow run_command?"
User-->>SDK: yes/no
SDK->>Harness: ToolConfirmation(accepted=user_answer)
end
Harness->>Model: Continue with result
Host-Side Tool Flow (Python Tools)
Python tools registered via the SDK run in the SDK process, not the harness. The flow is:
sequenceDiagram
participant Model as Gemini Model
participant Harness as localharness
participant SDK as Python SDK
Model->>Harness: Needs custom tool "get_weather"
Harness-->>SDK: OutputEvent{tool_call: {id: "tc_1", name: "get_weather", args: {city: "Tokyo"}}}
SDK->>SDK: PreToolCallDecideHook (policy check)
SDK->>SDK: ToolRunner.execute("get_weather", city="Tokyo")
Note over SDK: Runs sync tool in asyncio.to_thread()
SDK->>SDK: PostToolCallHook or OnToolErrorHook
SDK->>Harness: InputEvent{tool_response: {id: "tc_1", response_json: '{"result": "sunny"}'}}
Harness->>Model: Tool result appended to context
Parallel execution: When the model requests multiple tools in one turn, ToolRunner.process_tool_calls() uses asyncio.gather() for concurrent execution. Each call is wrapped in try/except for error isolation.
Subagent Management
The harness supports spawning subagents as separate trajectories. The SDK tracks them:
# LocalConnection internal state
_active_subagent_ids: set[str] = set()
_subagent_responses: dict[str, str] = {}
Idle Detection
The connection is considered idle ONLY when:
- The parent trajectory is in IDLE state, AND
- ALL subagent trajectories are in IDLE state
This prevents premature session termination while subagents are still running.
Subagent Completion
When a subagent trajectory reaches IDLE state, the SDK:
- Captures the subagent's last model response
- Dispatches
PostToolCallHookwith the response content - Removes the trajectory from
_active_subagent_ids
Security Model
Ephemeral WebSocket API Key
The OutputConfig.api_key is generated fresh for each harness spawn. It is:
- A random string generated by the Go harness
- Used ONLY for the
x-goog-api-keyheader in the WebSocket handshake - NOT the Gemini API key or any cloud credential
- Valid only for the lifetime of the harness process
Workspace Validation
The PermissionsConfig.enforce_workspace_validation flag controls whether the harness validates that file operations stay within configured workspaces. When enabled:
view_file,create_file,edit_filepaths must resolve within a workspace- Symlink traversal is checked
- Path traversal attacks (
../../../etc/passwd) are blocked
Tool Safety Guard
The Agent class enforces at startup: if write tools or MCP servers are enabled but no policies and no PreToolCallDecideHook are registered, it raises ValueError. This prevents accidentally deploying an unguarded agent with destructive capabilities.
Differences: Localharness vs LocalConnection
| Aspect | Localharness (Go) | LocalConnection (Python) |
|---|---|---|
| Role | Agent runtime | Orchestrator |
| Language | Go | Python |
| Model calls | Direct gRPC to Gemini | Delegated to harness |
| Built-in tools | Executes natively | Dispatches confirmation only |
| Python tools | Receives results via WebSocket | Executes and returns results |
| MCP servers | Manages connections | Configures only |
| Trajectory persistence | Writes to disk | Reads initial_trajectory for resume |
| Context compaction | Performs in-process | Notified via compaction event |
| Browser automation | CDP via Go | Not involved |
| Subagent spawning | Creates new trajectories | Tracks trajectory state |
Performance Characteristics
| Metric | Value |
|---|---|
| Binary size | ~99 MB (ARM64, includes Go runtime) |
| Startup time | ~200-500ms (spawn + handshake) |
| WebSocket latency | <1ms (localhost) |
| Handshake overhead | ~50-100ms (stdin/stdout exchange) |
| Memory baseline | ~50-80MB (Go runtime + WebSocket server) |
Troubleshooting
Binary Not Found
FileNotFoundError: localharness binary not found
Fix: Set ANTIGRAVITY_HARNESS_PATH to the binary location, or reinstall the SDK wheel for your platform.
WebSocket Connection Refused
ConnectionRefusedError: [Errno 61] Connection refused
Fix: The harness may need more time to start. The SDK retries 5 times with exponential backoff. If persistent, check that the port is not in use.
Process Hangs on Shutdown
If the harness doesn't exit on stdin EOF, the SDK escalates to SIGTERM (1s) then SIGKILL (1s). Check stderr logs for the cause.
Cross-References
- sdk-test-antigravity-sdk — Full SDK reference (Agent, Conversation, types)
- sdk-test-protobuf — Complete protobuf message reference
- sdk-test-hooks-deep — Hook system architecture
- sdk-test-auth-chain — Authentication chain
- sdk-test-architecture — JETSKY ecosystem overview
- sdk-test-decompiled-patterns — Decompiled function patterns from the Go binary
Source: antigravity-sdk-python v0.1.2, local_connection.py (1792 lines), local_connection_config.py (194 lines). Decompiled binary: language_server_hub (3305 functions).