WikifitaGitHub live67e8de5
outro · sdk-test/sdk-test-agent-api

Antigravity Agent API Interception & Wrapper Pattern

How the agentapi binary is intercepted and wrapped to capture all calls, arguments, environment variables, and stdin data for reverse engineering the Antigravity harness

Baixar raw

Antigravity Agent API Interception & Wrapper Pattern

The agentapi binary is the internal command-line interface used by the Antigravity harness ecosystem (IDE, CLI, and SDK) to communicate with the agent backend. This page documents the interception pattern used to capture and analyze all agentapi calls, the wrapper shell script, and how this fits with the broader antigravity-2.0 ecosystem.


What Is agentapi?

The agentapi binary lives at ~/.gemini/antigravity/bin/agentapi and is invoked by the Antigravity application layer (Antigravity.app, IDE plugins, CLI tools) to communicate with the Go-based localharness runtime. It serves as the bridge between the user-facing application and the agent execution engine.

The binary's internal structure -- revealed through reverse engineering of the Go binary at /Users/alefita/probe/sdk-test/reverse_engineering/ -- includes:

  • agentapi_newConversationHandler_Execute -- Creates new conversation sessions
  • agentapi_getConversationMetadataHandler_Metadata -- Retrieves conversation metadata
  • agentapi_sendMessageHandler_Execute -- Sends messages to the agent
  • agentapi_sendMessageHandler_Metadata -- Message metadata endpoint
  • agentapi_resolveModel -- Resolves model configuration
  • agentapi_printUsageAndExit -- Usage/help output
  • agentapi_HandleAgentAPI_csrfUnaryInterceptor_func2 -- CSRF protection interceptor

The agentapi binary is the programmatic interface that the SDK's LocalConnectionStrategy wraps when running in "local harness" mode. It handles:

  1. Conversation lifecycle (create, resume, delete)
  2. Message passing (user input -> model output)
  3. Tool execution routing
  4. Authentication and CSRF protection
  5. Model resolution and configuration

The Interception Pattern

Motivation

To understand how the Antigravity harness communicates with its backend, the agentapi binary was intercepted using a transparent wrapper pattern. This captures:

  1. All command-line arguments (varargs)
  2. All environment variables (filtered and full)
  3. stdin data (if any, with size limits)
  4. The working directory
  5. Timestamps and process metadata
  6. Parent process information

Architecture

sequenceDiagram
    participant App as Antigravity.app / IDE
    participant Wrapper as agentapi (wrapper)
    participant Interceptor as agentapi_interceptor.py
    participant Real as agentapi.bak (original binary)
    participant Harness as localharness (Go)

    App->>Wrapper: exec agentapi [args...]
    Wrapper->>Interceptor: exec python3 agentapi_interceptor.py [args...]
    Note over Interceptor: Capture argv, env, stdin, cwd
    Interceptor->>Interceptor: Write JSONL to /tmp/agentapi_intercept.jsonl
    Interceptor->>Interceptor: Print summary to stderr
    Interceptor->>Real: os.execv(real_binary, [args...])
    Real->>Harness: WebSocket / protobuf communication
    Harness-->>Real: OutputEvent
    Real-->>App: Response

Files

FilePathPurpose
agentapi_interceptor.py/Users/alefita/probe/sdk-test/agentapi_interceptor.pyPython interceptor that captures all data and optionally forwards to real binary
agentapi_wrapper.sh/Users/alefita/probe/sdk-test/agentapi_wrapper.shShell shim that delegates to the Python interceptor
agentapi.bak~/.gemini/antigravity/bin/agentapi.bakBackup of the original agentapi binary

The Interceptor (agentapi_interceptor.py)

A PEP 723 script (run via uv run or standalone Python 3.11+).

Configuration

LOG_FILE = "/tmp/agentapi_intercept.jsonl"      # JSONL append-only log
FORWARD_TO_REAL = True                           # Forward to real binary after logging
REAL_BINARY_CANDIDATES = [
    os.path.expanduser("~/.gemini/antigravity/bin/agentapi.bak"),
    "/Applications/Antigravity.app/Contents/Resources/bin/language_server",
]

Data Capture

The interceptor captures the following per-invocation record:

record = {
    "timestamp": 1753000000.0,
    "timestamp_iso": "2026-07-21T12:00:00-0300",
    "pid": 12345,
    "ppid": 12340,
    "uid": 501,
    "cwd": "/Users/alefita/workdir/project",
    "argv": ["agentapi", "sendMessage", "--json", "..."],
    "argc": 4,
    "argv_detail": {
        "script": "agentapi",
        "subcommand": "sendMessage",
        "remaining_args": ["--json", "..."],
    },
    "env_interesting": {
        "GEMINI_API_KEY": "AIza...",
        "ANTIGRAVITY_HARNESS_PATH": "/path/to/localharness",
        "HOME": "/Users/alefita",
        # ... filtered env vars
    },
    "env_count_total": 87,
    "stdin_data": None,  # or {"encoding": "utf-8", "data": "..."}
    "parent_process": "Antigravity --type=zygote",
}

Environment Variable Filtering

Only environment variables with specific prefixes are captured in the "interesting" subset:

interesting_prefixes = (
    "GEMINI", "GOOGLE", "ANTIGRAVITY", "AGY", "HOME", "USER",
    "PATH", "SHELL", "TERM", "XDG", "OAUTH", "TOKEN", "AUTH",
    "API_KEY", "CLOUD", "GCP", "VERTEX", "MLX", "GRPC",
)

The full environment is also available in the record under env_full for complete analysis.

Stdin Capture

def capture_stdin_if_available(max_bytes=8192):
    """Non-blocking read of stdin if data is available."""
    if sys.stdin.isatty():
        return None
    readable, _, _ = select.select([sys.stdin], [], [], 0.1)
    if readable:
        data = sys.stdin.buffer.read(max_bytes)
        try:
            return {"encoding": "utf-8", "data": data.decode("utf-8")}
        except UnicodeDecodeError:
            return {"encoding": "hex", "data": data.hex(), "size_bytes": len(data)}
    return None

Uses select() with a 0.1s timeout for non-blocking detection. Limits capture to 8 KiB to avoid blocking on large payloads.

Forward-to-Real Mechanism

After logging, the interceptor optionally forwards the call to the real binary using os.execv():

if "language_server" in real_binary:
    cmd = [real_binary, "agentapi"] + sys.argv[1:]
else:
    cmd = [real_binary] + sys.argv[1:]

os.execv(cmd[0], cmd)

This replaces the current process with the real binary, making the interception transparent to the caller. The language_server binary requires an agentapi subcommand prefix, while the backup agentapi.bak does not.

Parent Process Detection

ps_result = subprocess.run(
    ["ps", "-p", str(ppid), "-o", "comm=,args="],
    capture_output=True, text=True, timeout=2
)
record["parent_process"] = ps_result.stdout.strip()

Identifies the calling process (e.g., Antigravity --type=zygote, Google Chrome, python3).


The Wrapper (agentapi_wrapper.sh)

A minimal shell shim that replaces the original agentapi binary:

#!/bin/sh
exec /usr/bin/python3 /Users/alefita/probe/sdk-test/agentapi_interceptor.py "$@"

Installation

# 1. Backup original
cp ~/.gemini/antigravity/bin/agentapi ~/.gemini/antigravity/bin/agentapi.bak

# 2. Install wrapper
cp agentapi_wrapper.sh ~/.gemini/antigravity/bin/agentapi
chmod +x ~/.gemini/antigravity/bin/agentapi

Usage

Once installed, every call to agentapi from the Antigravity ecosystem is transparently intercepted:

# From Antigravity.app, IDE, or CLI:
agentapi sendMessage --json '{"message": "hello"}'

# Interceptor captures, then forwards to real binary
# Log written to /tmp/agentapi_intercept.jsonl

Reading Captured Data

# Human-readable summary (printed to stderr during each call)
# Look at the stderr output from the intercepted call

# Machine-readable log
cat /tmp/agentapi_intercept.jsonl | python3 -m json.tool

# Filter by subcommand
cat /tmp/agentapi_intercept.jsonl | jq 'select(.argv_detail.subcommand == "sendMessage")'

# Find all environment variables across calls
cat /tmp/agentapi_intercept.jsonl | jq '.env_interesting | keys[]' | sort -u

# Timeline of calls
cat /tmp/agentapi_intercept.jsonl | jq '[.timestamp_iso, .argv_detail.subcommand, .cwd]'

How This Fits with the SDK

The agentapi interceptor pattern and the Antigravity Python SDK are complementary tools for understanding and extending the harness:

graph TB
    subgraph SDK["Python SDK (google-antigravity)"]
        Agent["Agent"]
        LConn["LocalConnection"]
        LStrat["LocalConnectionStrategy"]
        PB["localharness_pb2.py<br/>(protobuf bindings)"]
    end

    subgraph Intercept["Interception Layer"]
        Wrapper["agentapi_wrapper.sh"]
        Interceptor["agentapi_interceptor.py"]
    end

    subgraph Harness["Go Harness"]
        LH["localharness binary"]
        WS["WebSocket server"]
        AG["Agent runtime"]
    end

    Agent --> LStrat
    LStrat -->|"spawns subprocess"| LH
    LH --> WS
    WS --> AG

    Wrapper -->|"replaces"| agentapi
    Interceptor -->|"captures + forwards"| agentapi.bak
    agentapi.bak -->|"communicates with"| LH

SDK Path vs. Interception Path

AspectSDK PathInterception Path
Entry pointAgent(config)agentapi [subcommand] [args]
TransportWebSocket (direct to harness)Binary stdin/stdout + WebSocket
ConfigLocalAgentConfig (Pydantic)Command-line arguments + env vars
Proto layerlocalharness_pb2.py (Python)Internal Go protobuf
PurposeProgrammatic agent developmentReverse engineering and analysis

What the SDK Reveals

The SDK's LocalConnectionStrategy bypasses the agentapi binary entirely -- it spawns localharness directly and communicates via WebSocket. This means:

  1. The SDK's protobuf bindings (localharness_pb2.py) are the authoritative schema definition
  2. The agentapi binary is a thin CLI wrapper around the same harness
  3. Intercepting agentapi reveals the CLI-level API surface that the SDK abstracts away

Key Findings from Interception

The interception pattern revealed:

  1. Environment-driven configuration: The harness reads GEMINI_API_KEY, ANTIGRAVITY_HARNESS_PATH, and other env vars directly
  2. Subcommand structure: agentapi sendMessage, agentapi newConversation, agentapi getConversationMetadata
  3. CSRF protection: Internal interceptor chain includes CSRF unary interceptors
  4. Model resolution: agentapi_resolveModel handles model name -> endpoint mapping
  5. Parent process diversity: Calls originate from Electron (Antigravity.app), Chrome extensions, and Python scripts

Related Reverse Engineering

The interception pattern is part of a broader reverse engineering effort documented in /Users/alefita/probe/sdk-test/reverse_engineering/:

ComponentDecompiled FunctionsPurpose
agy/interceptor_*, agentapi_*Main agent process interceptors
localharness/agentapi_ptrnewConversationHandler_ExecuteConversation creation
language_server_ide/interceptor_*, agentapi_*IDE-specific language server
language_server_hub/interceptor_*, agentapi_*Hub language server
CLIProxyAPI/interceptor.go, handlers_interceptors_test.goGo proxy API with interceptor patterns

The Go binary's interceptor chain includes:

  • CSRF interceptor -- Cross-site request forgery protection (wraps unary, streaming client, and streaming handler)
  • Latency interceptor -- Request timing metrics
  • Mock data interceptor -- Test data injection
  • Request ID interceptor -- Correlation ID propagation
  • Trace task interceptor -- Distributed tracing

Cross-References


Source: antigravity-sdk-python v0.1.2 + manual reverse engineering at /Users/alefita/probe/sdk-test/.