From 9d0ef8440b90bb044d6bab2572c56461f9f4670a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E6=B8=A1=E6=B3=95=E5=B8=AB?= Date: Thu, 14 May 2026 21:42:30 +0000 Subject: [PATCH] feat: add Grok Build as ACP backend Add xAI Grok Build support via native ACP protocol (`grok agent stdio`). Changes: - Dockerfile.grok: runtime image with Grok Build CLI installed - connection.rs: auto-authenticate when initialize returns authMethods - config.toml.example: add Grok Build config example The authenticate step is triggered automatically when the agent's initialize response includes authMethods (as Grok Build does), so existing backends (Kiro, Claude, Codex, Gemini) are unaffected. Closes #821 --- .github/workflows/build.yml | 3 ++ .github/workflows/docker-smoke-test.yml | 1 + Dockerfile.grok | 40 +++++++++++++++++++++++++ config.toml.example | 6 ++++ src/acp/connection.rs | 35 ++++++++++++++++++++++ 5 files changed, 85 insertions(+) create mode 100644 Dockerfile.grok diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7b360417..50f54fcc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -72,6 +72,7 @@ jobs: - { suffix: "-copilot", dockerfile: "Dockerfile.copilot", artifact: "copilot" } - { suffix: "-opencode", dockerfile: "Dockerfile.opencode", artifact: "opencode" } - { suffix: "-cursor", dockerfile: "Dockerfile.cursor", artifact: "cursor" } + - { suffix: "-grok", dockerfile: "Dockerfile.grok", artifact: "grok" } platform: - { os: linux/amd64, runner: ubuntu-latest } - { os: linux/arm64, runner: ubuntu-24.04-arm } @@ -135,6 +136,7 @@ jobs: - { suffix: "-copilot", artifact: "copilot" } - { suffix: "-opencode", artifact: "opencode" } - { suffix: "-cursor", artifact: "cursor" } + - { suffix: "-grok", artifact: "grok" } runs-on: ubuntu-latest permissions: contents: read @@ -185,6 +187,7 @@ jobs: - { suffix: "-copilot" } - { suffix: "-opencode" } - { suffix: "-cursor" } + - { suffix: "-grok" } runs-on: ubuntu-latest permissions: contents: read diff --git a/.github/workflows/docker-smoke-test.yml b/.github/workflows/docker-smoke-test.yml index 64b4653a..91d4312f 100644 --- a/.github/workflows/docker-smoke-test.yml +++ b/.github/workflows/docker-smoke-test.yml @@ -20,6 +20,7 @@ jobs: - { dockerfile: Dockerfile.copilot, suffix: "-copilot", agent: "copilot", agent_args: "--acp" } - { dockerfile: Dockerfile.opencode, suffix: "-opencode", agent: "opencode", agent_args: "acp" } - { dockerfile: Dockerfile.cursor, suffix: "-cursor", agent: "cursor-agent", agent_args: "acp" } + - { dockerfile: Dockerfile.grok, suffix: "-grok", agent: "grok", agent_args: "agent stdio" } runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 diff --git a/Dockerfile.grok b/Dockerfile.grok new file mode 100644 index 00000000..eaed0816 --- /dev/null +++ b/Dockerfile.grok @@ -0,0 +1,40 @@ +# --- Build stage --- +FROM rust:1-bookworm AS builder +WORKDIR /build +COPY Cargo.toml Cargo.lock ./ +RUN mkdir src && echo 'fn main() {}' > src/main.rs && cargo build --release && rm -rf src +COPY src/ src/ +RUN touch src/main.rs && cargo build --release + +# --- Runtime stage --- +FROM node:22-bookworm-slim +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl procps tini && rm -rf /var/lib/apt/lists/* + +# Install Grok Build CLI as node user (installs to ~/.grok/bin/grok) +# Pin version via ARG; update when upgrading. +ARG GROK_VERSION=0.1.210 +USER node +RUN curl -fsSL https://x.ai/cli/install.sh | bash -s ${GROK_VERSION} +USER root + +# Ensure grok is on PATH for all users +ENV PATH="/home/node/.grok/bin:$PATH" + +# Install gh CLI +RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \ + -o /usr/share/keyrings/githubcli-archive-keyring.gpg && \ + echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \ + > /etc/apt/sources.list.d/github-cli.list && \ + apt-get update && apt-get install -y --no-install-recommends gh && \ + rm -rf /var/lib/apt/lists/* + +ENV HOME=/home/node +WORKDIR /home/node + +COPY --from=builder --chown=node:node /build/target/release/openab /usr/local/bin/openab + +USER node +HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ + CMD pgrep -x openab || exit 1 +ENTRYPOINT ["tini", "--"] +CMD ["openab", "run", "-c", "/etc/openab/config.toml"] diff --git a/config.toml.example b/config.toml.example index d33a0902..7793c64c 100644 --- a/config.toml.example +++ b/config.toml.example @@ -103,6 +103,12 @@ working_dir = "/home/agent" # working_dir = "/home/agent" # env = {} # Auth via: kubectl exec -it -- cursor-agent login +# [agent] +# command = "grok" +# args = ["agent", "stdio"] +# working_dir = "/home/node" +# env = { GROK_CODE_XAI_API_KEY = "${GROK_CODE_XAI_API_KEY}" } + [pool] max_sessions = 10 session_ttl_hours = 24 diff --git a/src/acp/connection.rs b/src/acp/connection.rs index 90c0eae2..18830349 100644 --- a/src/acp/connection.rs +++ b/src/acp/connection.rs @@ -446,6 +446,41 @@ impl AcpConnection { load_session = self.supports_load_session, "initialized" ); + + // If the agent requires authentication (e.g. Grok Build), handle it + if let Some(auth_methods) = result.and_then(|r| r.get("authMethods")) { + self.authenticate(auth_methods).await?; + } + Ok(()) + } + + async fn authenticate(&mut self, auth_methods: &Value) -> Result<()> { + let methods: Vec<&str> = auth_methods + .as_array() + .map(|arr| { + arr.iter() + .filter_map(|m| m.get("id").and_then(|id| id.as_str())) + .collect() + }) + .unwrap_or_default(); + + // Prefer API key auth, fall back to cached token + let method_id = if methods.contains(&"xai.api_key") { + "xai.api_key" + } else if methods.contains(&"cached_token") { + "cached_token" + } else { + return Err(anyhow!("no supported auth method (available: {methods:?})")); + }; + + info!(method = method_id, "authenticating"); + let resp = self.send_request( + "authenticate", + Some(json!({"methodId": method_id, "meta": {"headless": true}})), + ) + .await?; + debug!(result = ?resp.result, "authenticate response"); + info!("authenticated"); Ok(()) }