Integrating Antigravity and Apache/Maka into Raft via ACP

2026/10/20

Recently I started testing multi-agent collaboration tools in my daily workflow, spending quite a bit of time with both Cumora and Raft. Between the two, I prefer how Raft handles agent definitions, interactions, and channel organization. Naturally, I wanted to plug in all the coding agents I rely on every day, including Codex, Claude Code, Antigravity, and Maka (an open-source coding agent under Apache Incubation, which I use with DeepSeek).

Popular options like Codex and Claude Code work out of the box, but Raft's official runtime list doesn't include Maka, and Antigravity is marked as deprecated. Looking at Raft's runtimeAdmissionService, any attempt to use the antigravity runtime name gets rejected right away with 400 Bad Request.

Could I just patch Raft's source code and add a custom adapter? In practice, that doesn't really work either. Although Raft publishes its code at botiverse/raft-source under FSL-1.1-ALv2 (a Source Available license) which allows personal modifications, the repository is currently a read-only release mirror that doesn't accept external PRs. Maintaining a private fork would mean manually syncing squashed release snapshots on every update, and connecting to the official service would still run into server-side runtime allowlists and client code-signing checks.

While reading through Cumora and Raft's source code to see how they integrate external CLIs, I broke down the three ways to invoke agy (covered in my previous post) and came across ACP (Agent Client Protocol). Then, when I checked Raft's Grok driver (packages/daemon/src/drivers/grok.ts), I realized it also speaks ACP over standard stdio under the hood.

That opened up a clean path that doesn't touch Raft's code at all: place a thin grok proxy script early on PATH, piggyback on Raft's existing Grok adapter, and route requests over ACP to Antigravity, Maka, or any other ACP-compatible agent.

Why Grok Build

Before going down the proxy route, I cloned raft-source locally to run a few experiments.

Raft's old antigravity.deprecated.ts driver spawned a fresh agy chat -m child process on every turn, turning each message into a cold-start single-turn call. Switching to a persistent stream-json pipe kept the process warm across turns and made group chat interactions much faster. Going one step further, copying grok.ts and pointing it directly at agy_acp_server.par worked even better; as analyzed in the previous post, ACP has clear advantages over stream-json.

Since modifying the client app directly wasn't a sustainable option, I looked closely at what packages/daemon/src/drivers/grok.ts actually does:

  1. It calls lookupCommandOnPath("grok") to find the executable on PATH;
  2. It runs two probe commands to verify the CLI is usable: grok --version and grok agent stdio --help;
  3. When a session starts, it spawns grok agent --no-leader --always-approve stdio and exchanges ACP messages over stdio.

There is a neat macOS detail here: GUI apps don't inherit your terminal PATH by default, so Raft's packages/computer/src/shellEnvCapture.ts launches your login shell (-ilc) with __print-env at startup to capture your shell environment.

That means if we symlink our proxy script to ~/.local/bin/grok, Raft's Desktop.app picks it up automatically. To pass the initial probes, the script just prints a version string for --version and help text for agent stdio --help. The real work happens after entering stdio mode to align protocol details. Here are two basic handshake differences:

  • First, Raft's GrokDriver calls session/new right after initialize, whereas agy's ACP server requires an authenticate({ methodId: "oauth-personal" }) call first or it rejects session creation as unauthenticated. Our proxy simply inserts that authentication call before forwarding session/new.
  • Second, Raft passes a channel's Standing Prompt inside _meta.systemPromptOverride on session/new. That field isn't part of standard ACP, so both agy and Maka ignore it. The proxy extracts _meta.systemPromptOverride, holds onto it, and prepends it (along with environment context like the current working directory, OS, and timezone) to the first session/prompt of that session. Maka's Runtime Host already injects environment context on its own, so the extra environment block is only added for agy.

One Adapter for Multiple Agents: Prefix Routing and Coexisting with the Real CLI

Once agy was running smoothly, I moved on to wiring up Maka (maka acp). That immediately raised a routing question: if multiple agents share the same grok entry point, how does the proxy know which backend a session should run?

Raft lets you configure custom environment variables for each agent, so I added a strict rule in the proxy: every agent must explicitly set GROK_SHIM_PROFILE=agy or GROK_SHIM_PROFILE=maka. If that variable is missing, the proxy throws an error on session/new and session/load rather than guessing a default.

Configuring a Maka agent in Raft with Grok Build and GROK_SHIM_PROFILE

Right after that came a second problem: when Raft probes which models a runtime supports (detectGrokModelsFromAcp in grok.ts), it spawns a machine-level grok process and sends initialize before any specific agent is started, so no agent environment variables are present. Since we can't rely on environment variables during model detection, I encoded the routing information directly into the model IDs returned by the proxy:

  • During early detection (when GROK_SHIM_PROFILE is unset), the proxy merges the model catalogs from all backends and prefixes every modelId with agy_ or maka_, such as agy_gemini-4-argon-medium or maka_deepseek-flash.
  • When an actual session starts, the proxy checks that the selected model's prefix matches the agent's GROK_SHIM_PROFILE (if a maka agent accidentally selects an agy_ model, it fails immediately with a clear error), strips the prefix, and forwards the real model ID to the backend.

Machine-level model dropdown showing agy_ and maka_ prefixed model IDs

Finally, in case I ever install the official grok CLI on this machine in the future, the proxy also supports transparent passthrough to the real binary (though I don't have a Grok subscription, so I haven't tested that path against a live account).

Model Switching and session/load: Aligning Old and New ACP Versions

With prefix routing in place, the next step to get sessions running was handling model switching and Reasoning Effort.

Raft's GrokDriver switches models by calling session/set_model from an early ACP draft and passing the reasoning effort in _meta.reasoningEffort. agy implements that early method (for personal accounts, it bakes the effort level into three separate model IDs: gemini-4-argon-low, gemini-4-argon-medium, and gemini-4-argon-high).

Maka, on the other hand, follows the strict ACP v1 specification and doesn't support session/set_model (returning -32601). Under ACP v1's Session Config Options design, session settings like the model and reasoning effort live in a unified configOptions array and are updated via session/set_config_option.

Maka's backend daemon already had the ability to switch models, and when contributing to an open-source project like Maka, we obviously shouldn't hardcode non-standard or vendor-private methods like session/set_model or _x.ai/* into its ACP server just to please one client. So I opened a PR (apache/maka#6189) to expose the standard model option in configOptions (listing only the models currently enabled by the user, and relaxing the static option allowlist at the adapter entry point).

This also illustrates the three-layer division of responsibilities we use across the whole integration (including Steer in the next section):

  • On the Maka side, we only touch its ACP adapter (packages/cli/src/acp/) to expose standard capabilities that are already defined in the ACP spec and supported by its runtime, making the changes clean upstream contributions;
  • On the Raft side, we don't touch a single line of code, leaving GrokDriver to send session/set_model and _x.ai/* requests as usual;
  • In the middle, the grok proxy handles all protocol translation, including model probing, prefix stripping, effort splitting, and error message rewriting.

Neither side has to pollute its codebase for the other, and all translation logic stays in the proxy:

  • Probing phase: Raft expects the model list during initialize (before any session exists), whereas Maka returns configOptions on session/new. During initialization, the proxy opens a brief probe session against Maka, reads the model and thinking-level lists, and immediately calls session/close so no orphan sessions are left behind.
  • Switching phase: when Raft sends session/set_model, the proxy splits it into one or two session/set_config_option calls to update Maka's model and thinking_level.

Session resumption (session/load) had a small catch as well: per the ACP v1 spec, a successful session/load returns configuration options without repeating the sessionId field. Our proxy initially shared validation logic with session/new and required sessionId in the response, causing history resumption to fail. Passing the requested sessionId through and rewriting unknown-session errors into the "session not found" string that GrokDriver looks for made session resume and fallback creation work smoothly.

Mid-Turn Steering: Protocol Hooks and Maka Changes

In a multi-agent group chat like Raft, a very common action is sending a follow-up message while an agent is still working on a turn, whether to add context, adjust requirements, or tell it to stop.

Supporting this experience looked very different across the two agents:

agy is a closed-source binary (agy_acp_server.par), and testing showed it doesn't support mid-turn steering at the bottom layer. Its ACP service defines no steering methods; if you force a second concurrent session/prompt before the current turn finishes, the underlying connection throws a concurrency exception, and the outer handler catches it and streams the error text into the chat window as a normal assistant reply. Worse yet, its Python base class acp.interfaces.Agent defines the extension entry point ext_method with a default ... (Ellipsis) body, which causes any _-prefixed extension request to return result: null (pretending to succeed while silently dropping the message). For agy, the proxy has to use a queue-and-hold workaround: buffer incoming busy messages in memory, hold back the completion response when the current turn ends, and send the queued messages as sequential session/prompt calls.

Maka, however, is open source. Checking its codebase showed that its background daemon (Runtime Host) already had scheduling logic for inserting messages mid-turn; the ACP adapter under packages/cli/src/acp/ simply only ever called turn.start and hadn't exposed mid-turn steering yet. With access to the source, we could wire up true Steer support in Maka directly (apache/maka#6192).

Protocol Boundary: Translating Grok's Private Interface to Standard ACP

Before writing code, we needed to sort out how the protocol works on each side.

When a session is busy, Raft's grok.ts doesn't send session/prompt; instead, it sends a Grok-private extension request _x.ai/interject, falling back to session/steer only if the backend returns -32601 Method not found.

Following the same approach we took for model and thinking_level, instead of teaching Maka to handle _x.ai/interject, we followed the ACP design proposals for Mid-turn Injection (RFD #1261, Schema #2043) and exposed a pair of standard methods on Maka's ACP adapter:

  • session/inject: accepts sessionId, content, and mode: "steer" | "queue". steer hands the new message to the model at the next step boundary inside the current turn (such as right after a tool call finishes), while queue waits until the current turn ends. It returns a server-generated messageId.
  • session/revoke_inject: allows the client to withdraw a queued message by its messageId before the model consumes it.

On the grok proxy side, we simply intercept _x.ai/interject from Raft and translate it into session/inject { mode: "steer" } for Maka.

Issues Encountered While Wiring Up Steer in Maka

Maka's architecture splits into a lightweight maka acp CLI process and a persistent Runtime Host background daemon. Mapping stateless protocol requests onto a stateful background execution stream surfaced three practical engineering issues:

  1. Timing gaps when the session is already idle: In asynchronous communication, the client may think an agent is still busy and send a steer request just as the server finishes the last step of the turn. When Maka's background daemon receives a mid-turn submission while no turn is running, its default behavior is to start a brand-new turn automatically. Because that turn wasn't started through a normal session/prompt call, the ACP adapter has no output listener attached to it, so the client never sees the reply. To prevent this, the adapter verifies that the session actually has an active turn before forwarding, and rejects the request with a precondition error if the session is already idle.
  2. Concurrency race when revoking a message: When a client tries to revoke a queued message, the model might have picked up that exact message for execution just milliseconds earlier. In that window, the daemon returns a state conflict error rather than "not found." From the client's perspective, both outcomes mean the message has already been consumed and can no longer be withdrawn, so the adapter catches both cases and normalizes them into the same status code.
  3. Keeping connection state in sync across daemon restarts: Every time Maka's background daemon starts, it generates a new process identifier (hostEpoch) and requires write requests to carry it so stale commands from before a restart are rejected. Initially, the ACP adapter copied hostEpoch into a static property when creating the connection wrapper. If the daemon restarted mid-session and auto-reconnected, subsequent session/inject calls kept sending the old identifier and failed. Switching the property on the connection wrapper to a dynamic getter that reads the active connection's current identifier fixed the issue.

A Raft Bug Caught Along the Way

After all offline proxy tests passed, I switched back to the release build of Raft v1.21.24 to test in the UI. When I clicked scan in Settings -> Runtimes, every local agent on my machine (including Claude Code, Codex, and Grok Build) showed up as not installed.

Checking ~/.slock/computer/logs/service.log and runtimeDetectionRound.ts in raft-source revealed why: when the daemon spawns a child process to probe runtimes, it assumes process.argv[1] is the entry script path. In the packaged Electron app, however, argv[1] is actually the launch mode flag "__service", causing the child process under ELECTRON_RUN_AS_NODE=1 to try loading "__service" as a file from the root directory and exit immediately with Cannot find module '/__service'.

I wrote up a reproduction report and sent it to the Raft developers. Until an official release fixes that argv[1] check, starting the compiled raft-computer.js daemon directly with node works around the problem.

Wrap-Up

The entire grok proxy came out to around 1,400 lines of plain Node.js code with zero third-party dependencies. For daily use, all it takes is:

  1. Download and set up the Antigravity ACP binary;
  2. Make sure Maka's background daemon is running (maka runtime-host status);
  3. Create two Grok Build agents in Raft configured with GROK_SHIM_PROFILE=agy and GROK_SHIM_PROFILE=maka, and pull them into the same channel to collaborate.

Antigravity and Maka replying in the same Raft channel

Even if the protocol evolves later (for instance, if grok adopts standard ACP mid-turn steering), the changes needed on our side will be tiny, making this a low-maintenance way to stay compatible.

Looking back on this whole exercise, two thoughts stand out:

  • First, although ACP is still rough around the edges (such as the split between early drafts and the v1 spec, and capabilities like Steer still living in proposals), it is already a practical common interface today. Previously, every desktop client that wanted to support a new coding agent had to write a custom driver for its specific CLI flags and output format; once both ends converge on ACP, even version and extension differences can be smoothed over at the process boundary with a lightweight script.
  • Second, making source code available really does let the community help out. Both adding ACP Steer and model selection to Maka and tracking down the runtime detection bug in packaged Raft were only possible because the source code was right there to read. Right now raft-source doesn't accept issues or open discussions yet, and I hope once the Raft team figures out their community collaboration model, they'll open up more room for discussion and contributions.

For comments and further discussion, mail to [email protected]