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(()) }