diff --git a/content/posts/reverse-engineering-antigravity-ide.md b/content/posts/reverse-engineering-antigravity-ide.md new file mode 100644 index 0000000..80210fe --- /dev/null +++ b/content/posts/reverse-engineering-antigravity-ide.md @@ -0,0 +1,70 @@ +--- +title: "How I Built a Blog Agent that Writes About Itself" +date: 2026-01-16 +draft: false +--- + + +I've been spending a lot of time "vibe coding" in the Antigravity IDE lately. It's an incredible flow state—intense, iterative, and fast. But it has a major flaw: the context is ephemeral. Once the session is over, that rich history of decisions, wrong turns, and "aha!" moments is locked away in an opaque, internal format. + +I wanted to capture that value. I wanted a system that could take my chaotic coding sessions and distill them into structured, technical blog posts (like the one you're reading right now). + +But getting the data out turned into a much deeper rabbit hole than I expected. + +## The Challenge: Check the Database? +My first instinct was simple: It's an Electron app, so there's probably a SQLite database. + +I found it easily enough at `~/Library/Application Support/Antigravity/User/globalStorage/state.vscdb`. But when I opened it up, I hit a wall. The data wasn't plain text; it was stored in the `ItemTable` under keys like `antigravityUnifiedStateSync.trajectorySummaries` as Base64-encoded strings. + +Decoding them revealed raw Protobuf wire formats, not JSON. + +### The "Wire-Walking" Dead End +I spent a few hours writing a Python script to "wire-walk" the Protobuf data without a schema. I managed to extract some human-readable strings, but it was a mess: +1. **Missing Context**: I got fragments of text, but the user prompts and cohesive flow were gone. +2. **Encryption**: The actual conversation files (ending in `.pb`) in `~/.gemini/antigravity/conversations/` were encrypted. + +It turns out Antigravity uses Electron’s `safeStorage` API, which interfaces directly with the macOS Keychain. Without the app's private key (which is hardware-bound), that data is effectively random noise. I even tried using Frida to hook `safeStorage.decryptString()`, but macOS SIP (System Integrity Protection) and code signing shut that down immediately. + +I was stuck. I couldn't decrypt the local files, and I couldn't parse the database effectively. + +## The Breakthrough: Living Off the Land +When you can't break the front door, look for the side entrance. I realized I wasn't the only one trying to read this state—the official extensions had to do it too. + +I started poking around the source code of the `vscode-antigravity-cockpit` extension, specifically a file named `local_auth_importer.ts`. That's where I found the golden ticket. + +The extension *doesn't* decrypt the local files. Instead, it reads a specific key from the SQLite database: `jetskiStateSync.agentManagerInitState`. + +When I decoded field #6 of this Protobuf structure, I found an `OAuthTokenInfo` message. It contained the user’s active `accessToken` and `refreshToken`. + +### Shifting Strategy: Don't Crack it, Join it +This changed everything. I didn't need to reverse-engineer the local storage encryption; I just needed to impersonate the IDE. + +By "piggybacking" on this existing auth mechanism, I could extract a valid OAuth token directly from the local state. But I still needed the endpoints. + +Instead of guessing, I opened the **Developer Tools** inside Antigravity itself (it is Electron, after all). By enabling the Chrome network tracing tools and triggering an export manually, I caught the request in the act. + +I saw the exact call to `exa.language_server_pb.LanguageServerService/ConvertTrajectoryToMarkdown`. + +It was perfect. By sending a gRPC-over-HTTP request to this endpoint using the stolen token, the server—which *does* have access to the unencrypted history—returned a perfectly formatted Markdown document of my entire coding session. + +## The Architecture: The Blog-Agent +Once I had the data extraction solved, building the rest of the "blog-agent" was straightforward. I built a **Node.js** stack to automate the pipeline: + +* **Backend**: An **Express** server handles the routing, session imports, and post generation. +* **Frontend**: A clean **EJS** interface to list sessions, view summaries, and "publish" them to the filesystem. +* **Storage**: A local SQLite database (`data/sessions.sqlite`) acts as a cache. (I learned my lesson: always cache your LLM inputs). +* **The Brain**: I use the **OpenAI SDK** (pointing to a LiteLLM proxy) to interface with `gemini-3-flash`. I wrote a map-reduce style prompt that first extracts technical decisions from the raw conversation log, then synthesizes them into a narrative. +* **Persistence**: The final posts are saved with YAML front matter into a `generated_posts/` directory. + +## Key Insights + +* **Don't Fight the OS**: Trying to break macOS Keychain/SIP encryption is a losing battle for a weekend project. +* **Follow the Tokens**: Applications often store auth tokens in less-secure places (like plain SQLite or weaker encryption) than the user content itself. +* **Extensions are Open Books**: If an app has extensions, their source code is often the best documentation for the internal API. + +In a satisfying detailed loop, **this very article was generated by the blog-agent itself**, analyzing the "vibe coding" session where I built it. + +## References +- `server.js`: The Express server and API implementation. +- `services/antigravity.js`: The client for the Antigravity gRPC-over-HTTP API. +- [vscode-antigravity-cockpit](https://github.com/jlcodes99/vscode-antigravity-cockpit): The extension that leaked the auth logic.