diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 17b4505a0..0f136570e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -73,6 +73,7 @@ jobs: - { suffix: "-opencode", dockerfile: "Dockerfile.opencode", artifact: "opencode" } - { suffix: "-cursor", dockerfile: "Dockerfile.cursor", artifact: "cursor" } - { suffix: "-hermes", dockerfile: "Dockerfile.hermes", artifact: "hermes" } + - { suffix: "-openclaw", dockerfile: "Dockerfile.openclaw", artifact: "openclaw" } platform: - { os: linux/amd64, runner: ubuntu-latest } - { os: linux/arm64, runner: ubuntu-24.04-arm } @@ -137,6 +138,7 @@ jobs: - { suffix: "-opencode", artifact: "opencode" } - { suffix: "-cursor", artifact: "cursor" } - { suffix: "-hermes", artifact: "hermes" } + - { suffix: "-openclaw", artifact: "openclaw" } runs-on: ubuntu-latest permissions: contents: read @@ -188,6 +190,7 @@ jobs: - { suffix: "-opencode" } - { suffix: "-cursor" } - { suffix: "-hermes" } + - { suffix: "-openclaw" } runs-on: ubuntu-latest permissions: contents: read diff --git a/.github/workflows/docker-smoke-test.yml b/.github/workflows/docker-smoke-test.yml index 0fd0c2385..0bf0b984a 100644 --- a/.github/workflows/docker-smoke-test.yml +++ b/.github/workflows/docker-smoke-test.yml @@ -21,6 +21,7 @@ jobs: - { dockerfile: Dockerfile.opencode, suffix: "-opencode", agent: "opencode", agent_args: "acp" } - { dockerfile: Dockerfile.cursor, suffix: "-cursor", agent: "cursor-agent", agent_args: "acp" } - { dockerfile: Dockerfile.hermes, suffix: "-hermes", agent: "hermes-acp", agent_args: "" } + - { dockerfile: Dockerfile.openclaw, suffix: "-openclaw", agent: "openclaw", agent_args: "acp" } runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 diff --git a/Dockerfile.openclaw b/Dockerfile.openclaw new file mode 100644 index 000000000..19bffe7f1 --- /dev/null +++ b/Dockerfile.openclaw @@ -0,0 +1,54 @@ +# --- 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 --- +# node:22-bookworm-slim mirrors the base image used by Dockerfile.claude, +# Dockerfile.codex, Dockerfile.gemini, and Dockerfile.opencode, keeping the +# project on a single consistent runtime base. +# +# OpenClaw requires Node >= 22.16 (enforced by openclaw.mjs at startup). +# +# openclaw is published to npm as a single global package — `openclaw acp` +# is a thin WebSocket bridge to a separately-running openclaw gateway; the +# gateway itself is NOT bundled in this image. See docs/openclaw.md for the +# required external gateway setup. +# +# Version is pinned for reproducible builds. Bump via a dedicated PR. +FROM node:22-bookworm-slim +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates curl procps ripgrep tini && rm -rf /var/lib/apt/lists/* + +# Install openclaw +# Note: openclaw does not publish SHA256 checksums for its npm releases, +# so checksum verification is not performed. +ARG OPENCLAW_VERSION=latest +RUN npm install -g openclaw@${OPENCLAW_VERSION} --retry 3 + +# Install gh CLI (matches Dockerfile.claude / Dockerfile.gemini / Dockerfile.codex) +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 + +# Pre-create bridge state dir with correct ownership (mirrors the .opencode +# pattern in Dockerfile.opencode — exec-based config writes shouldn't end up +# root-owned). +RUN mkdir -p /home/node/.openclaw && chown -R node:node /home/node/.openclaw + +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/README.md b/README.md index ae2079b38..01e71e82d 100644 --- a/README.md +++ b/README.md @@ -169,6 +169,7 @@ The bot creates a thread. After that, just type in the thread — no @mention ne | Copilot CLI ⚠️ | `copilot --acp --stdio` | Native | [docs/copilot.md](docs/copilot.md) | | Cursor | `cursor-agent acp` | Native | [docs/cursor.md](docs/cursor.md) | | Hermes Agent | `hermes-acp` | Native | [docs/hermes.md](docs/hermes.md) | +| OpenClaw | `openclaw acp` | Native (requires external gateway) | [docs/openclaw.md](docs/openclaw.md) | > 🔧 Running multiple agents? See [docs/multi-agent.md](docs/multi-agent.md) diff --git a/config.toml.example b/config.toml.example index 20ccff34f..df543a86d 100644 --- a/config.toml.example +++ b/config.toml.example @@ -110,6 +110,14 @@ working_dir = "/home/agent" # # Supports 30+ providers (xAI Grok OAuth, Anthropic, OpenAI Codex, Gemini, etc.) # # Provider switching: kubectl exec -it -- hermes model +# [agent] +# command = "openclaw" +# args = ["acp", "--url", "ws://openclaw-gateway:18789", "--session", "agent:main:main"] +# working_dir = "/home/node" +# env = { OPENCLAW_GATEWAY_TOKEN = "${OPENCLAW_GATEWAY_TOKEN}" } +# # Requires a separately-running openclaw gateway — see docs/openclaw.md. +# # Model selection lives on the gateway side, not via OpenAB's /model command. + [pool] max_sessions = 10 session_ttl_hours = 24 diff --git a/docs/openclaw.md b/docs/openclaw.md new file mode 100644 index 000000000..7281e15cd --- /dev/null +++ b/docs/openclaw.md @@ -0,0 +1,167 @@ +# OpenClaw + +[OpenClaw](https://github.com/openclaw/openclaw) is a self-hosted AI agent +gateway. OpenAB connects to it via the `openclaw acp` bridge, which speaks ACP +over stdio and forwards prompts to a running OpenClaw gateway over WebSocket. + +Unlike other ACP backends, OpenClaw requires a **separately-running gateway +service** — the OpenAB container does not embed the gateway. Provider API keys +(OpenAI, Anthropic, etc.), agent definitions, and model selection all live in +the gateway, not in OpenAB. + +## Architecture + +``` +Discord ──► openab ──stdio──► openclaw acp ──WS──► openclaw gateway ──HTTPS──► LLM + (inside container) (separate service) +``` + +## Prerequisites + +- A running OpenClaw gateway, reachable from the OpenAB container. The + upstream [Quick Start](https://github.com/openclaw/openclaw#quick-start) + walks through `openclaw onboard --install-daemon` + `openclaw gateway`. +- A gateway token. Generated on first gateway start; persisted at + `~/.openclaw/gateway.token` on the gateway host. + +## Docker Image + +```bash +docker build -f Dockerfile.openclaw -t openab-openclaw:latest . +``` + +The image installs the `openclaw` npm package globally and requires Node 22.16+. + +## Helm Install + +```bash +helm install openab openab/openab \ + --set agents.kiro.enabled=false \ + --set agents.openclaw.discord.enabled=true \ + --set agents.openclaw.discord.botToken="$DISCORD_BOT_TOKEN" \ + --set-string 'agents.openclaw.discord.allowedChannels[0]=YOUR_CHANNEL_ID' \ + --set agents.openclaw.image=ghcr.io/openabdev/openab-openclaw:latest \ + --set agents.openclaw.command=openclaw \ + --set-json 'agents.openclaw.args=["acp","--url","ws://openclaw-gateway:18789","--session","agent:main:main"]' \ + --set agents.openclaw.workingDir=/home/node \ + --set agents.openclaw.env.OPENCLAW_GATEWAY_TOKEN="$OPENCLAW_GATEWAY_TOKEN" +``` + +> Set `agents.kiro.enabled=false` to disable the default Kiro agent. + +## Manual config.toml + +```toml +[agent] +command = "openclaw" +args = [ + "acp", + "--url", "ws://openclaw-gateway:18789", + "--session", "agent:main:main", +] +working_dir = "/home/node" +env = { OPENCLAW_GATEWAY_TOKEN = "${OPENCLAW_GATEWAY_TOKEN}" } +``` + +### Required flags + +| Flag | Purpose | +|---|---| +| `--url ` | Gateway WebSocket URL. Use `wss://` for TLS. | +| `--session ` | OpenClaw session key — see [Sessions and Models](#sessions-and-models). | +| `--token-file ` *(alt. to env var)* | Path to a file holding the gateway token. Useful for Kubernetes Secret mounts. | + +### Gateway authentication + +Provide the shared secret via **one** of: + +- Env var: `OPENCLAW_GATEWAY_TOKEN` +- Token file: `--token-file /path/to/token` +- CLI flag: `--token ` (avoid — visible in process list) + +## Sessions and Models + +OpenAB's `/model` slash command **does not** select LLM models for the OpenClaw +backend. + +OpenClaw routes by **session key** (e.g., `agent:main:main`), and each session +resolves to an **agent definition** on the gateway side. The agent definition +determines which provider and which model to use. + +To switch models or providers: + +1. Edit the agent definition in the gateway's `~/.openclaw/openclaw.json`, or +2. Change `--session` in `config.toml` to point at a different agent and + restart the pod. + +In-band ACP options the bridge does pass through: + +| Option | Effect | +|---|---| +| `thought_level` | Verbosity of agent thinking output | +| `reasoning_level` | Reasoning effort hint to the model | +| `verbose_level` / `trace_level` | Diagnostic detail | +| `fast_mode` | Latency-optimized routing | +| `response_usage` | Include token usage in responses | +| `timeout_seconds` | Per-prompt timeout | + +## Capabilities and Limits + +| Feature | Supported | +|---|---| +| Text prompts | ✅ | +| Image attachments (inbound) | ✅ | +| Audio attachments | ❌ | +| Embedded context resources | ✅ | +| `session/load` | ✅ (only for sessions created through the bridge) | +| Per-session MCP servers | ❌ — rejected by the bridge; configure on the gateway | +| `/reset`, `/agent` slash commands | ✅ | +| `/model` slash command | ⚠️ See [Sessions and Models](#sessions-and-models) | +| Prompt size cap | 2 MB | + +## Persisted Paths (PVC) + +| Path | Contents | +|------|----------| +| `/home/node/.openclaw/` | Bridge state (small — token file if used) | + +The bulk of OpenClaw state — provider API keys, agent definitions, session +transcripts, event ledger — lives on the **gateway side**, not in the OpenAB +container. + +## Troubleshooting + +### Bridge exits immediately with `gateway closed before ready` + +The gateway is not reachable at the configured `--url`. Check: + +- The gateway service is running and listening on the expected port + (default 18789). +- DNS resolution for the gateway hostname works from inside the OpenAB pod. +- The token (env var or `--token-file`) matches the gateway's configured token. + +### `invalid token` / authentication failed + +Token mismatch. Regenerate the token on the gateway host per the upstream +docs, then update `OPENCLAW_GATEWAY_TOKEN` (or the file referenced by +`--token-file`) and restart the OpenAB pod. + +### Messages take a long time, then return empty + +Likely a gateway-side issue — the agent definition references a provider +without an API key, or the model name is invalid. The bridge cannot see this; +only the gateway logs it: + +```bash +kubectl logs deployment/openclaw-gateway --tail=200 +``` + +### `/model gpt-4o` has no effect + +Expected — see [Sessions and Models](#sessions-and-models). Change the +gateway-side agent definition or use a different `--session` key. + +### Per-session MCP servers don't work + +The bridge rejects per-session `mcpServers` in `session/new`. Configure MCP +servers at the gateway level instead — see the upstream OpenClaw docs.