---
name: sdk-test-localharness
type: analysis
title: "Localharness Architecture — Go Binary Subprocess Deep-Dive"
description: "Complete architectural analysis of the localharness Go binary: subprocess lifecycle, WebSocket protocol, handshake sequence, binary discovery chain, and connection management"
tags: [localharness, golang, websocket, protobuf, subprocess, architecture, antigravity]
timestamp: 2026-07-22
---

# 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

```mermaid
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).

```mermaid
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

```python
# 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.

```mermaid
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

```mermaid
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

```mermaid
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.

```mermaid
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.

```mermaid
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

```python
# 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

```python
# 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

```python
# Fallback for editable installs or non-standard layouts
import importlib.resources
# Attempts to locate within package resources
```

### Step 4: System PATH

```python
# 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:

```python
# 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:

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

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

```python
# LocalConnection internal state
_active_subagent_ids: set[str] = set()
_subagent_responses: dict[str, str] = {}
```

### Idle Detection

The connection is considered idle ONLY when:
1. The parent trajectory is in IDLE state, AND
2. 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:
1. Captures the subagent's last model response
2. Dispatches `PostToolCallHook` with the response content
3. 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-key` header 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_file` paths 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).*
