Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ jobs:
- { suffix: "-codex", dockerfile: "Dockerfile.codex", artifact: "codex" }
- { suffix: "-claude", dockerfile: "Dockerfile.claude", artifact: "claude" }
- { suffix: "-gemini", dockerfile: "Dockerfile.gemini", artifact: "gemini" }
- { suffix: "-opencode", dockerfile: "Dockerfile.opencode", artifact: "opencode" }
platform:
- { os: linux/amd64, runner: ubuntu-latest }
- { os: linux/arm64, runner: ubuntu-24.04-arm }
Expand Down Expand Up @@ -105,6 +106,7 @@ jobs:
- { suffix: "-codex", artifact: "codex" }
- { suffix: "-claude", artifact: "claude" }
- { suffix: "-gemini", artifact: "gemini" }
- { suffix: "-opencode", artifact: "opencode" }
runs-on: ubuntu-latest
permissions:
contents: read
Expand Down
35 changes: 35 additions & 0 deletions Dockerfile.opencode
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# --- 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 && rm -rf /var/lib/apt/lists/*

# Install OpenCode CLI (native ACP support via `opencode acp`)
RUN npm install -g opencode-ai --retry 3

# 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/*

RUN mkdir -p /home/node/.local/share/opencode && \
chown -R node:node /home/node
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 ["openab"]
CMD ["/etc/openab/config.toml"]
46 changes: 34 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# OpenAB — Open Agent Broker

A lightweight, secure, cloud-native ACP harness that bridges Discord and any [Agent Client Protocol](https://github.com/anthropics/agent-protocol)-compatible coding CLI (Kiro CLI, Claude Code, Codex, Gemini, etc.) over stdio JSON-RPC — delivering the next-generation development experience.
A lightweight, secure, cloud-native ACP harness that bridges Discord and any [Agent Client Protocol](https://github.com/anthropics/agent-protocol)-compatible coding CLI (Kiro CLI, Claude Code, Codex, Gemini, OpenCode, etc.) over stdio JSON-RPC — delivering the next-generation development experience.

🪼 **Join our community!** Come say hi on Discord — we'd love to have you: **[🪼 OpenAB — Official](https://discord.gg/YNksK9M6)** 🎉

Expand All @@ -17,7 +17,7 @@ A lightweight, secure, cloud-native ACP harness that bridges Discord and any [Ag

## Features

- **Pluggable agent backend** — swap between Kiro CLI, Claude Code, Codex, Gemini via config
- **Pluggable agent backend** — swap between Kiro CLI, Claude Code, Codex, Gemini, OpenCode via config
- **@mention trigger** — mention the bot in an allowed channel to start a conversation
- **Thread-based multi-turn** — auto-creates threads; no @mention needed for follow-ups
- **Edit-streaming** — live-updates the Discord message every 1.5s as tokens arrive
Expand Down Expand Up @@ -84,14 +84,15 @@ The bot creates a thread. After that, just type in the thread — no @mention ne

## Pluggable Agent Backends

Supports Kiro CLI, Claude Code, Codex, Gemini, and any ACP-compatible CLI.
Supports Kiro CLI, Claude Code, Codex, Gemini, OpenCode, and any ACP-compatible CLI.

| Agent key | CLI | ACP Adapter | Auth |
|-----------|-----|-------------|------|
| `kiro` (default) | Kiro CLI | Native `kiro-cli acp` | `kiro-cli login --use-device-flow` |
| `codex` | Codex | [@zed-industries/codex-acp](https://github.com/zed-industries/codex-acp) | `codex login --device-auth` |
| `claude` | Claude Code | [@agentclientprotocol/claude-agent-acp](https://github.com/agentclientprotocol/claude-agent-acp) | `claude setup-token` |
| `gemini` | Gemini CLI | Native `gemini --acp` | Google OAuth or `GEMINI_API_KEY` |
| `opencode` | OpenCode | Native `opencode acp` | `opencode auth login` |

### Helm Install (recommended)

Expand All @@ -115,18 +116,25 @@ helm install openab openab/openab \
--set agents.claude.command=claude-agent-acp \
--set agents.claude.workingDir=/home/node

# Multi-agent (kiro + claude in one release)
# OpenCode only (disable default kiro)
helm install openab openab/openab \
--set agents.kiro.enabled=false \
--set agents.opencode.preset=opencode \
--set agents.opencode.discord.botToken="$DISCORD_BOT_TOKEN" \
--set-string 'agents.opencode.discord.allowedChannels[0]=YOUR_CHANNEL_ID'

# Multi-agent (kiro + opencode in one release)
helm install openab openab/openab \
--set agents.kiro.discord.botToken="$KIRO_BOT_TOKEN" \
--set-string 'agents.kiro.discord.allowedChannels[0]=KIRO_CHANNEL_ID' \
--set agents.claude.discord.botToken="$CLAUDE_BOT_TOKEN" \
--set-string 'agents.claude.discord.allowedChannels[0]=CLAUDE_CHANNEL_ID' \
--set agents.claude.image=ghcr.io/openabdev/openab-claude:78f8d2c \
--set agents.claude.command=claude-agent-acp \
--set agents.claude.workingDir=/home/node
--set agents.opencode.preset=opencode \
--set agents.opencode.discord.botToken="$OPENCODE_BOT_TOKEN" \
--set-string 'agents.opencode.discord.allowedChannels[0]=OPENCODE_CHANNEL_ID'
```

Each agent key in `agents` map creates its own Deployment, ConfigMap, Secret, and PVC. Set `agents.<name>.enabled: false` to skip creating resources for an agent.
Each agent key in `agents` map creates its own Deployment, ConfigMap, Secret, and PVC. Set `agents.<name>.enabled: false` to skip creating resources for an agent. Set `agents.<name>.preset` to `kiro`, `codex`, `claude`, `gemini`, or `opencode` to auto-configure the bundled image, command, args, and working directory.

For OpenCode auth, run `kubectl exec -it deployment/openab-opencode -- opencode auth login`, then `kubectl rollout restart deployment/openab-opencode`.

### Manual config.toml

Expand Down Expand Up @@ -157,6 +165,12 @@ command = "gemini"
args = ["--acp"]
working_dir = "/home/node"
env = { GEMINI_API_KEY = "${GEMINI_API_KEY}" }

# OpenCode
[agent]
command = "opencode"
args = ["acp"]
working_dir = "/home/node"
```

## Configuration Reference
Expand Down Expand Up @@ -200,7 +214,7 @@ error_hold_ms = 2500 # keep error emoji for 2.5s

## Kubernetes Deployment

The Docker image bundles both `openab` and `kiro-cli` in a single container (openab spawns kiro-cli as a child process).
The default Docker image bundles both `openab` and `kiro-cli` in a single container (openab spawns kiro-cli as a child process).

### Pod Architecture

Expand Down Expand Up @@ -251,7 +265,15 @@ The Docker image bundles both `openab` and `kiro-cli` in a single container (ope

### Install with Your Coding CLI

See the **[Helm chart docs](https://openabdev.github.io/openab)** for per-agent install commands (Kiro CLI, Claude Code, Codex, Gemini) and values reference.
Use one of these prompts with any coding CLI (Kiro CLI, Claude Code, Codex, Gemini, OpenCode, etc.) on the host that has `helm` and `kubectl` access to your cluster:

**Kiro CLI (default):**
> Install openab on my local k8s cluster using the Helm chart from https://openabdev.github.io/openab. Set `agents.kiro.discord.botToken` from the `DISCORD_BOT_TOKEN` environment variable and `agents.kiro.discord.allowedChannels[0]` to <REPLACE_WITH_YOUR_CHANNEL_ID>. After install, follow the NOTES output to authenticate, then restart the deployment.

**OpenCode:**
> Install openab on my local k8s cluster using the Helm chart from https://openabdev.github.io/openab. Disable the default `kiro` agent, create an `opencode` agent with `agents.opencode.preset=opencode`, set `agents.opencode.discord.botToken` from the `DISCORD_BOT_TOKEN` environment variable, and set `agents.opencode.discord.allowedChannels[0]` to <REPLACE_WITH_YOUR_CHANNEL_ID>. After install, run `kubectl exec -it deployment/openab-opencode -- opencode auth login`, then restart `deployment/openab-opencode`.

For Codex, Claude Code, and Gemini, use the same `agents.<name>.preset=<preset>` pattern.

### Build & Push

Expand Down
8 changes: 5 additions & 3 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Users running `helm install` only see stable versions. Beta versions require `--
┌─────────────┐ ┌──────────────────┐ ┌─────────────────────┐
│ CI: Build │────>│ CI: Bump PR │────>│ Merge bump PR │
3 images │ │ 0.2.1-beta.12345 │ │ → publishes beta │
5 images │ │ 0.2.1-beta.12345 │ │ → publishes beta │
└─────────────┘ └──────────────────┘ └─────────────────────┘
┌───────────────────────────────────────────────┘
Expand All @@ -35,7 +35,7 @@ Users running `helm install` only see stable versions. Beta versions require `--
┌─────────────┐ ┌──────────────────┐ ┌─────────────────────┐
│ CI: Build │────>│ CI: Bump PR │────>│ Merge bump PR │
3 images │ │ 0.2.1 │ │ → publishes stable │
5 images │ │ 0.2.1 │ │ → publishes stable │
└─────────────┘ └──────────────────┘ └─────────────────────┘
┌───────────────────────────────────────────────┘
Expand All @@ -45,12 +45,14 @@ Users running `helm install` only see stable versions. Beta versions require `--

## Image Tags

Each build produces three multi-arch images tagged with the git short SHA:
Each build produces five multi-arch images tagged with the git short SHA:

```
ghcr.io/openabdev/openab:<sha> # kiro-cli
ghcr.io/openabdev/openab-codex:<sha> # codex
ghcr.io/openabdev/openab-claude:<sha> # claude
ghcr.io/openabdev/openab-gemini:<sha> # gemini
ghcr.io/openabdev/openab-opencode:<sha> # opencode
```

The `latest` tag always points to the most recent build.
2 changes: 1 addition & 1 deletion charts/openab/Chart.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
apiVersion: v2
name: openab
description: Discord ↔ ACP coding CLI bridge (Kiro CLI, Claude Code, Codex, Gemini)
description: Discord ↔ ACP coding CLI bridge (Kiro CLI, Claude Code, Codex, Gemini, OpenCode)
type: application
version: 0.6.1-beta.1
appVersion: "52ac30a"
28 changes: 17 additions & 11 deletions charts/openab/templates/NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,34 @@ openab {{ .Chart.AppVersion }} has been installed!
Agents deployed:
{{- range $name, $cfg := .Values.agents }}
{{- if ne (include "openab.agentEnabled" $cfg) "false" }}
• {{ $name }} ({{ $cfg.command }})
{{- $d := dict "ctx" $ "agent" $name "cfg" $cfg }}
{{- $deployment := include "openab.agentFullname" $d }}
{{- $cmd := include "openab.agentCommand" $d | trim }}
• {{ $name }} ({{ $cmd }})
{{- if not $cfg.discord.botToken }}
⚠️ No bot token provided. Create the secret manually:
kubectl create secret generic {{ include "openab.agentFullname" (dict "ctx" $ "agent" $name) }} \
kubectl create secret generic {{ $deployment }} \
--from-literal=discord-bot-token="YOUR_TOKEN"
{{- end }}

{{- if eq $cfg.command "kiro-cli" }}
{{- if eq $cmd "kiro-cli" }}
Authenticate:
kubectl exec -it deployment/{{ include "openab.agentFullname" (dict "ctx" $ "agent" $name) }} -- kiro-cli login --use-device-flow
{{- else if eq $cfg.command "codex-acp" }}
kubectl exec -it deployment/{{ $deployment }} -- kiro-cli login --use-device-flow
{{- else if eq $cmd "codex-acp" }}
Authenticate:
kubectl exec -it deployment/{{ include "openab.agentFullname" (dict "ctx" $ "agent" $name) }} -- codex login --device-auth
{{- else if eq $cfg.command "claude-agent-acp" }}
kubectl exec -it deployment/{{ $deployment }} -- codex login --device-auth
{{- else if eq $cmd "claude-agent-acp" }}
Authenticate:
kubectl exec -it deployment/{{ include "openab.agentFullname" (dict "ctx" $ "agent" $name) }} -- claude setup-token
{{- else if eq $cfg.command "gemini" }}
kubectl exec -it deployment/{{ $deployment }} -- claude setup-token
{{- else if eq $cmd "gemini" }}
Authenticate:
kubectl exec -it deployment/{{ include "openab.agentFullname" (dict "ctx" $ "agent" $name) }} -- gemini
kubectl exec -it deployment/{{ $deployment }} -- gemini
{{- else if eq $cmd "opencode" }}
Authenticate:
kubectl exec -it deployment/{{ $deployment }} -- opencode auth login
{{- end }}

Restart after auth:
kubectl rollout restart deployment/{{ include "openab.agentFullname" (dict "ctx" $ "agent" $name) }}
kubectl rollout restart deployment/{{ $deployment }}
{{- end }}
{{- end }}
61 changes: 57 additions & 4 deletions charts/openab/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -41,27 +41,80 @@ app.kubernetes.io/component: {{ .agent }}
{{- printf "%s-%s" (include "openab.fullname" .ctx) .agent | trunc 63 | trimSuffix "-" }}
{{- end }}

{{/* Resolve image: agent-level string override global default (repository:tag, tag defaults to appVersion) */}}
{{/* Resolve image: agent-level string override -> preset image -> global default (repository:tag). */}}
{{- define "openab.agentImage" -}}
{{- if and .cfg.image (kindIs "string" .cfg.image) (ne .cfg.image "") }}
{{- .cfg.image }}
{{- else }}
{{- $tag := default .ctx.Chart.AppVersion .ctx.Values.image.tag }}
{{- $preset := default "" .cfg.preset }}
{{- if eq $preset "" }}
{{- printf "%s:%s" .ctx.Values.image.repository $tag }}
{{- else if eq $preset "kiro" }}
{{- printf "%s:%s" .ctx.Values.image.repository $tag }}
{{- else if eq $preset "codex" }}
{{- printf "%s-codex:%s" .ctx.Values.image.repository $tag }}
{{- else if eq $preset "claude" }}
{{- printf "%s-claude:%s" .ctx.Values.image.repository $tag }}
{{- else if eq $preset "gemini" }}
{{- printf "%s-gemini:%s" .ctx.Values.image.repository $tag }}
{{- else if eq $preset "opencode" }}
{{- printf "%s-opencode:%s" .ctx.Values.image.repository $tag }}
{{- else }}
{{- fail (printf "unsupported agents.%s.preset %q" .agent $preset) }}
{{- end }}
{{- end }}
{{- end }}

{{/* Resolve command: preset wins when set; otherwise use explicit command. */}}
{{- define "openab.agentCommand" -}}
{{- $preset := default "" .cfg.preset }}
{{- if eq $preset "" }}
{{- required (printf "agents.%s.command is required when preset is empty" .agent) .cfg.command }}
{{- else if eq $preset "kiro" }}kiro-cli
{{- else if eq $preset "codex" }}codex-acp
{{- else if eq $preset "claude" }}claude-agent-acp
{{- else if eq $preset "gemini" }}gemini
{{- else if eq $preset "opencode" }}opencode
{{- else }}{{- fail (printf "unsupported agents.%s.preset %q" .agent $preset) }}
{{- end }}
{{- end }}

{{/* Resolve args: preset wins when set; otherwise use explicit args. */}}
{{- define "openab.agentArgs" -}}
{{- $preset := default "" .cfg.preset }}
{{- if eq $preset "" }}
{{- if .cfg.args }}{{ .cfg.args | toJson }}{{ else }}[]{{ end }}
{{- else if eq $preset "kiro" }}["acp","--trust-all-tools"]
{{- else if or (eq $preset "codex") (eq $preset "claude") }}[]
{{- else if eq $preset "gemini" }}["--acp"]
{{- else if eq $preset "opencode" }}["acp"]
{{- else }}{{- fail (printf "unsupported agents.%s.preset %q" .agent $preset) }}
{{- end }}
{{- end }}

{{/* Resolve working directory: preset wins when set; otherwise use explicit value. */}}
{{- define "openab.agentWorkingDir" -}}
{{- $preset := default "" .cfg.preset }}
{{- if eq $preset "" }}
{{- .cfg.workingDir | default "/home/agent" }}
{{- else if eq $preset "kiro" }}/home/agent
{{- else if or (eq $preset "codex") (eq $preset "claude") (eq $preset "gemini") (eq $preset "opencode") }}/home/node
{{- else }}{{- fail (printf "unsupported agents.%s.preset %q" .agent $preset) }}
{{- end }}
{{- end }}

{{/* Resolve imagePullPolicy: global default (per-agent image string has no pullPolicy) */}}
{{/* Resolve imagePullPolicy: global default (per-agent image string has no pullPolicy). */}}
{{- define "openab.agentImagePullPolicy" -}}
{{- .ctx.Values.image.pullPolicy }}
{{- end }}

{{/* Agent enabled: default true unless explicitly set to false */}}
{{/* Agent enabled: default true unless explicitly set to false. */}}
{{- define "openab.agentEnabled" -}}
{{- if eq (.enabled | toString) "false" }}false{{ else }}true{{ end }}
{{- end }}

{{/* Persistence enabled: default true unless explicitly set to false */}}
{{/* Persistence enabled: default true unless explicitly set to false. */}}
{{- define "openab.persistenceEnabled" -}}
{{- if and . .persistence (eq (.persistence.enabled | toString) "false") }}false{{ else }}true{{ end }}
{{- end }}
9 changes: 6 additions & 3 deletions charts/openab/templates/configmap.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
{{- range $name, $cfg := .Values.agents }}
{{- if ne (include "openab.agentEnabled" $cfg) "false" }}
{{- $d := dict "ctx" $ "agent" $name "cfg" $cfg }}
{{- $command := include "openab.agentCommand" $d | trim }}
{{- $args := include "openab.agentArgs" $d | trim }}
{{- $workingDir := include "openab.agentWorkingDir" $d | trim }}
---
apiVersion: v1
kind: ConfigMap
Expand All @@ -26,9 +29,9 @@ data:
allowed_users = {{ $cfg.discord.allowedUsers | default list | toJson }}

[agent]
command = "{{ $cfg.command }}"
args = {{ if $cfg.args }}{{ $cfg.args | toJson }}{{ else }}[]{{ end }}
working_dir = "{{ $cfg.workingDir | default "/home/agent" }}"
command = "{{ $command }}"
args = {{ $args }}
working_dir = "{{ $workingDir }}"
{{- if $cfg.env }}
env = { {{ range $k, $v := $cfg.env }}{{ $k }} = "{{ $v }}", {{ end }} }
{{- end }}
Expand Down
7 changes: 4 additions & 3 deletions charts/openab/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
{{- if ne (include "openab.agentEnabled" $cfg) "false" }}
{{- $d := dict "ctx" $ "agent" $name "cfg" $cfg }}
{{- $pvcEnabled := not (eq (include "openab.persistenceEnabled" $cfg) "false") }}
{{- $workingDir := include "openab.agentWorkingDir" $d | trim }}
---
apiVersion: apps/v1
kind: Deployment
Expand Down Expand Up @@ -46,7 +47,7 @@ spec:
key: discord-bot-token
{{- end }}
- name: HOME
value: {{ $cfg.workingDir | default "/home/agent" }}
value: {{ $workingDir | quote }}
{{- range $k, $v := $cfg.env }}
- name: {{ $k }}
value: {{ $v | quote }}
Expand All @@ -65,11 +66,11 @@ spec:
readOnly: true
{{- if $pvcEnabled }}
- name: data
mountPath: {{ $cfg.workingDir | default "/home/agent" }}
mountPath: {{ $workingDir | quote }}
{{- end }}
{{- if $cfg.agentsMd }}
- name: config
mountPath: {{ $cfg.workingDir | default "/home/agent" }}/AGENTS.md
mountPath: {{ printf "%s/AGENTS.md" $workingDir | quote }}
subPath: AGENTS.md
{{- end }}
{{- with $cfg.nodeSelector }}
Expand Down
Loading