From 5e51940cb023ef663874a940cf9e34c0d908a639 Mon Sep 17 00:00:00 2001 From: thepagent Date: Sat, 4 Apr 2026 06:26:44 +0800 Subject: [PATCH 01/80] perf: cache deps layer + drop arm64 QEMU build (#2) * perf: cache dependency build layer in Dockerfile * perf: native multi-arch build with matrix runners --------- Co-authored-by: thepagent --- .github/workflows/build.yml | 81 +++++++++++++++++++++++++++++++------ Dockerfile | 3 +- 2 files changed, 70 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9ed390ba..1e784a90 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,15 +32,75 @@ env: jobs: build-image: + strategy: + matrix: + include: + - platform: linux/amd64 + runner: ubuntu-latest + - platform: linux/arm64 + runner: ubuntu-24.04-arm + runs-on: ${{ matrix.runner }} + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-buildx-action@v3 + + - uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + + - name: Build and push by digest + id: build + uses: docker/build-push-action@v6 + with: + context: . + platforms: ${{ matrix.platform }} + outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=${{ inputs.dry_run != true }} + cache-from: type=gha,scope=${{ matrix.platform }} + cache-to: type=gha,scope=${{ matrix.platform }},mode=max + + - name: Export digest + if: inputs.dry_run != true + run: | + mkdir -p /tmp/digests + digest="${{ steps.build.outputs.digest }}" + touch "/tmp/digests/${digest#sha256:}" + + - name: Upload digest + if: inputs.dry_run != true + uses: actions/upload-artifact@v4 + with: + name: digests-${{ matrix.runner }} + path: /tmp/digests/* + retention-days: 1 + + merge-manifests: + needs: build-image + if: inputs.dry_run != true runs-on: ubuntu-latest permissions: contents: read packages: write outputs: version: ${{ steps.meta.outputs.version }} - digest: ${{ steps.build.outputs.digest }} steps: - - uses: actions/checkout@v4 + - name: Download digests + uses: actions/download-artifact@v4 + with: + path: /tmp/digests + pattern: digests-* + merge-multiple: true - uses: docker/setup-buildx-action@v3 @@ -59,19 +119,14 @@ jobs: type=sha,prefix= type=raw,value=latest - - name: Build and push - id: build - uses: docker/build-push-action@v6 - with: - context: . - platforms: linux/amd64,linux/arm64 - push: ${{ inputs.dry_run != true }} - tags: ${{ steps.meta.outputs.tags }} - cache-from: type=gha - cache-to: type=gha,mode=max + - name: Create manifest list + working-directory: /tmp/digests + run: | + docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *) bump-chart: - needs: build-image + needs: merge-manifests if: inputs.dry_run != true runs-on: ubuntu-latest permissions: diff --git a/Dockerfile b/Dockerfile index 08159bc5..4a8679d9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,8 +2,9 @@ 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 cargo build --release +RUN touch src/main.rs && cargo build --release # --- Runtime stage --- FROM debian:bookworm-slim From 96627304678de419f9cb882dbf48f01d8d87878b Mon Sep 17 00:00:00 2001 From: thepagent Date: Sat, 4 Apr 2026 07:31:33 +0800 Subject: [PATCH 02/80] fix: use app bot identity for chart bump commits (#3) Co-authored-by: thepagent --- .github/workflows/build.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1e784a90..1859f971 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -179,8 +179,8 @@ jobs: env: GH_TOKEN: ${{ steps.app-token.outputs.token }} run: | - git config user.name "thepagent" - git config user.email "thepagent@users.noreply.github.com" + git config user.name "openclaw-helm-bot[bot]" + git config user.email "3185992+openclaw-helm-bot[bot]@users.noreply.github.com" git remote set-url origin https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }} git add charts/agent-broker/Chart.yaml charts/agent-broker/values.yaml git commit -m "chore: release chart ${{ steps.bump.outputs.new_version }}" @@ -191,3 +191,4 @@ jobs: GH_TOKEN: ${{ steps.app-token.outputs.token }} run: | gh workflow run release.yml --repo ${{ github.repository }} + From 27e58e694e9a233f3ce666e14f204ead8e88aca3 Mon Sep 17 00:00:00 2001 From: thepagent Date: Sat, 4 Apr 2026 07:34:01 +0800 Subject: [PATCH 03/80] fix: pull --rebase before push in bump-chart (#4) * fix: use app bot identity for chart bump commits * fix: pull --rebase before push in bump-chart --------- Co-authored-by: thepagent --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1859f971..f8eb12ec 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -184,6 +184,7 @@ jobs: git remote set-url origin https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }} git add charts/agent-broker/Chart.yaml charts/agent-broker/values.yaml git commit -m "chore: release chart ${{ steps.bump.outputs.new_version }}" + git pull --rebase git push - name: Trigger chart release From 2b3430545ccec0108940463cbb3a9803848ca8c0 Mon Sep 17 00:00:00 2001 From: thepagent Date: Sat, 4 Apr 2026 07:50:00 +0800 Subject: [PATCH 04/80] fix: create PR for chart bump instead of direct push (#8) Co-authored-by: thepagent --- .github/workflows/build.yml | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f8eb12ec..1142eef4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -131,8 +131,7 @@ jobs: runs-on: ubuntu-latest permissions: contents: write - packages: write - pages: write + pull-requests: write steps: - name: Generate App token id: app-token @@ -175,21 +174,19 @@ jobs: sed -i "s|repository: .*|repository: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}|" charts/agent-broker/values.yaml sed -i "s/tag: .*/tag: \"${SHORT_SHA}\"/" charts/agent-broker/values.yaml - - name: Commit and push + - name: Create bump PR env: GH_TOKEN: ${{ steps.app-token.outputs.token }} run: | + BRANCH="chore/chart-${{ steps.bump.outputs.new_version }}" git config user.name "openclaw-helm-bot[bot]" git config user.email "3185992+openclaw-helm-bot[bot]@users.noreply.github.com" - git remote set-url origin https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }} + git checkout -b "$BRANCH" git add charts/agent-broker/Chart.yaml charts/agent-broker/values.yaml - git commit -m "chore: release chart ${{ steps.bump.outputs.new_version }}" - git pull --rebase - git push - - - name: Trigger chart release - env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} - run: | - gh workflow run release.yml --repo ${{ github.repository }} + git commit -m "chore: bump chart to ${{ steps.bump.outputs.new_version }}" + git push origin "$BRANCH" + gh pr create \ + --title "chore: bump chart to ${{ steps.bump.outputs.new_version }}" \ + --body "Auto-generated chart version bump for image \`${{ github.sha }}\`." \ + --base main --head "$BRANCH" From a87fdffea16d0991fda3230db1b3df1aca19c5ab Mon Sep 17 00:00:00 2001 From: "openclaw-helm-bot[bot]" <271055092+openclaw-helm-bot[bot]@users.noreply.github.com> Date: Sat, 4 Apr 2026 07:56:45 +0800 Subject: [PATCH 05/80] chore: bump chart to 0.1.6 (#9) Co-authored-by: openclaw-helm-bot[bot] <3185992+openclaw-helm-bot[bot]@users.noreply.github.com> --- charts/agent-broker/Chart.yaml | 4 ++-- charts/agent-broker/values.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/charts/agent-broker/Chart.yaml b/charts/agent-broker/Chart.yaml index 17bf32ac..c7d6790a 100644 --- a/charts/agent-broker/Chart.yaml +++ b/charts/agent-broker/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: agent-broker description: Discord ↔ ACP coding CLI bridge (Kiro CLI, Claude Code, Codex, Gemini) type: application -version: 0.1.5 -appVersion: "1e66133" +version: 0.1.6 +appVersion: "7d215de" diff --git a/charts/agent-broker/values.yaml b/charts/agent-broker/values.yaml index e28c1eb8..3fbf9302 100644 --- a/charts/agent-broker/values.yaml +++ b/charts/agent-broker/values.yaml @@ -1,6 +1,6 @@ image: repository: ghcr.io/thepagent/agent-broker - tag: "1e66133" + tag: "7d215de" pullPolicy: IfNotPresent replicas: 1 From 8c302884a12685cf4e02bb7eed5582896c6135ff Mon Sep 17 00:00:00 2001 From: thepagent Date: Sat, 4 Apr 2026 08:09:22 +0800 Subject: [PATCH 06/80] fix: set working_dir to /home/agent (#10) Co-authored-by: thepagent --- charts/agent-broker/values.yaml | 2 +- config.toml.example | 8 ++++---- k8s/configmap.yaml | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/charts/agent-broker/values.yaml b/charts/agent-broker/values.yaml index 3fbf9302..d3ff52e6 100644 --- a/charts/agent-broker/values.yaml +++ b/charts/agent-broker/values.yaml @@ -25,7 +25,7 @@ agent: args: - acp - --trust-all-tools - workingDir: /tmp + workingDir: /home/agent env: {} # ANTHROPIC_API_KEY: "${ANTHROPIC_API_KEY}" diff --git a/config.toml.example b/config.toml.example index 619236a6..c4227dc6 100644 --- a/config.toml.example +++ b/config.toml.example @@ -5,24 +5,24 @@ allowed_channels = ["1234567890"] [agent] command = "kiro-cli" args = ["acp", "--trust-all-tools"] -working_dir = "/tmp" +working_dir = "/home/agent" # [agent] # command = "claude" # args = ["--acp"] -# working_dir = "/tmp" +# working_dir = "/home/agent" # env = { ANTHROPIC_API_KEY = "${ANTHROPIC_API_KEY}" } # [agent] # command = "codex" # args = ["--acp"] -# working_dir = "/tmp" +# working_dir = "/home/agent" # env = { OPENAI_API_KEY = "${OPENAI_API_KEY}" } # [agent] # command = "gemini" # args = ["--acp"] -# working_dir = "/tmp" +# working_dir = "/home/agent" # env = { GEMINI_API_KEY = "${GEMINI_API_KEY}" } [pool] diff --git a/k8s/configmap.yaml b/k8s/configmap.yaml index 6af4f32a..f180017e 100644 --- a/k8s/configmap.yaml +++ b/k8s/configmap.yaml @@ -11,7 +11,7 @@ data: [agent] command = "kiro-cli" args = ["acp", "--trust-all-tools"] - working_dir = "/tmp" + working_dir = "/home/agent" [pool] max_sessions = 10 From 85ce57f9dde6b7762c9b23981197763ad6458b6d Mon Sep 17 00:00:00 2001 From: "openclaw-helm-bot[bot]" <271055092+openclaw-helm-bot[bot]@users.noreply.github.com> Date: Sat, 4 Apr 2026 08:11:24 +0800 Subject: [PATCH 07/80] chore: bump chart to 0.1.7 (#11) Co-authored-by: openclaw-helm-bot[bot] <3185992+openclaw-helm-bot[bot]@users.noreply.github.com> --- charts/agent-broker/Chart.yaml | 4 ++-- charts/agent-broker/values.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/charts/agent-broker/Chart.yaml b/charts/agent-broker/Chart.yaml index c7d6790a..c757201c 100644 --- a/charts/agent-broker/Chart.yaml +++ b/charts/agent-broker/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: agent-broker description: Discord ↔ ACP coding CLI bridge (Kiro CLI, Claude Code, Codex, Gemini) type: application -version: 0.1.6 -appVersion: "7d215de" +version: 0.1.7 +appVersion: "fea0445" diff --git a/charts/agent-broker/values.yaml b/charts/agent-broker/values.yaml index d3ff52e6..3511143c 100644 --- a/charts/agent-broker/values.yaml +++ b/charts/agent-broker/values.yaml @@ -1,6 +1,6 @@ image: repository: ghcr.io/thepagent/agent-broker - tag: "7d215de" + tag: "fea0445" pullPolicy: IfNotPresent replicas: 1 From 4057078e81828ea12717d2426881f10919071e07 Mon Sep 17 00:00:00 2001 From: Neil Kuan <46012524+neilkuan@users.noreply.github.com> Date: Sat, 4 Apr 2026 12:50:59 +0800 Subject: [PATCH 08/80] feat: support AGENTS.md injection via helm values (#13) * feat: support AGENTS.md injection via helm values Add agentsMd value to chart so AGENTS.md can be configured at install time via values.yaml instead of manually exec-ing into the pod. * chore: bump chart to 0.1.8 --- charts/agent-broker/Chart.yaml | 2 +- charts/agent-broker/templates/configmap.yaml | 4 ++++ charts/agent-broker/templates/deployment.yaml | 5 +++++ charts/agent-broker/values.yaml | 6 ++++++ 4 files changed, 16 insertions(+), 1 deletion(-) diff --git a/charts/agent-broker/Chart.yaml b/charts/agent-broker/Chart.yaml index c757201c..2020ebf6 100644 --- a/charts/agent-broker/Chart.yaml +++ b/charts/agent-broker/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: agent-broker description: Discord ↔ ACP coding CLI bridge (Kiro CLI, Claude Code, Codex, Gemini) type: application -version: 0.1.7 +version: 0.1.8 appVersion: "fea0445" diff --git a/charts/agent-broker/templates/configmap.yaml b/charts/agent-broker/templates/configmap.yaml index cf5af16b..c0b3b12e 100644 --- a/charts/agent-broker/templates/configmap.yaml +++ b/charts/agent-broker/templates/configmap.yaml @@ -25,3 +25,7 @@ data: [reactions] enabled = {{ .Values.reactions.enabled }} remove_after_reply = {{ .Values.reactions.removeAfterReply }} + {{- if .Values.agentsMd }} + AGENTS.md: | + {{- .Values.agentsMd | nindent 4 }} + {{- end }} diff --git a/charts/agent-broker/templates/deployment.yaml b/charts/agent-broker/templates/deployment.yaml index 08674b8f..fee9c419 100644 --- a/charts/agent-broker/templates/deployment.yaml +++ b/charts/agent-broker/templates/deployment.yaml @@ -56,6 +56,11 @@ spec: mountPath: /home/agent/.local/share/kiro-cli subPath: kiro-cli-data {{- end }} + {{- if .Values.agentsMd }} + - name: config + mountPath: /home/agent/AGENTS.md + subPath: AGENTS.md + {{- end }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} diff --git a/charts/agent-broker/values.yaml b/charts/agent-broker/values.yaml index 3511143c..da3b9f87 100644 --- a/charts/agent-broker/values.yaml +++ b/charts/agent-broker/values.yaml @@ -37,6 +37,12 @@ reactions: enabled: true removeAfterReply: false +agentsMd: "" + # agentsMd: | + # IDENTITY - your agent identity + # SOUL - your agent personality + # USER - how agent should address the user + env: {} envFrom: [] From 33252910d3c9bae08653531acd7d336037e1df67 Mon Sep 17 00:00:00 2001 From: thepagent Date: Sat, 4 Apr 2026 19:30:43 +0800 Subject: [PATCH 09/80] feat: add Codex and Claude Code support via ACP adapters (#20) * feat: add Codex support via codex-acp adapter - Add Dockerfile.codex (node:22 + codex-acp + codex CLI for OAuth) - Simplify PVC mount: persist entire /home/agent instead of subPaths This ensures auth tokens for any CLI backend (kiro, codex, etc.) survive restarts - Bump chart to 0.1.9 Closes #14 * ci: build codex image alongside kiro in CI - Add variant matrix to build both Dockerfile and Dockerfile.codex - Produces ghcr.io/thepagent/agent-broker (kiro) and ghcr.io/thepagent/agent-broker-codex - Trigger on Dockerfile.* changes - Separate cache scopes per variant+platform * feat: add Claude Code support via claude-agent-acp adapter - Add Dockerfile.claude (node:22 + @agentclientprotocol/claude-agent-acp@0.25.0 + claude CLI) - Add claude variant to CI build matrix - Produces ghcr.io/thepagent/agent-broker-claude Note: @zed-industries/claude-agent-acp is deprecated, using @agentclientprotocol/claude-agent-acp Closes #16 * docs: dynamic NOTES.txt auth instructions per backend Show kiro-cli, codex, or claude auth steps based on agent.command value. * chore: remove unused matrix field, add token expiry note --------- Co-authored-by: thepagent --- .github/workflows/build.yml | 41 +++++++++++-------- Dockerfile.claude | 23 +++++++++++ Dockerfile.codex | 23 +++++++++++ charts/agent-broker/Chart.yaml | 2 +- charts/agent-broker/templates/NOTES.txt | 20 +++++++++ charts/agent-broker/templates/deployment.yaml | 6 +-- 6 files changed, 93 insertions(+), 22 deletions(-) create mode 100644 Dockerfile.claude create mode 100644 Dockerfile.codex diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1142eef4..f242a02e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,6 +9,7 @@ on: - "Cargo.toml" - "Cargo.lock" - "Dockerfile" + - "Dockerfile.*" workflow_dispatch: inputs: chart_bump: @@ -34,12 +35,14 @@ jobs: build-image: strategy: matrix: - include: - - platform: linux/amd64 - runner: ubuntu-latest - - platform: linux/arm64 - runner: ubuntu-24.04-arm - runs-on: ${{ matrix.runner }} + variant: + - { suffix: "", dockerfile: "Dockerfile" } + - { suffix: "-codex", dockerfile: "Dockerfile.codex" } + - { suffix: "-claude", dockerfile: "Dockerfile.claude" } + platform: + - { os: linux/amd64, runner: ubuntu-latest } + - { os: linux/arm64, runner: ubuntu-24.04-arm } + runs-on: ${{ matrix.platform.runner }} permissions: contents: read packages: write @@ -58,17 +61,18 @@ jobs: id: meta uses: docker/metadata-action@v5 with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}${{ matrix.variant.suffix }} - name: Build and push by digest id: build uses: docker/build-push-action@v6 with: context: . - platforms: ${{ matrix.platform }} - outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=${{ inputs.dry_run != true }} - cache-from: type=gha,scope=${{ matrix.platform }} - cache-to: type=gha,scope=${{ matrix.platform }},mode=max + file: ${{ matrix.variant.dockerfile }} + platforms: ${{ matrix.platform.os }} + outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}${{ matrix.variant.suffix }},push-by-digest=true,name-canonical=true,push=${{ inputs.dry_run != true }} + cache-from: type=gha,scope=${{ matrix.variant.suffix }}-${{ matrix.platform.os }} + cache-to: type=gha,scope=${{ matrix.variant.suffix }}-${{ matrix.platform.os }},mode=max - name: Export digest if: inputs.dry_run != true @@ -81,13 +85,19 @@ jobs: if: inputs.dry_run != true uses: actions/upload-artifact@v4 with: - name: digests-${{ matrix.runner }} + name: digests${{ matrix.variant.suffix }}-${{ matrix.platform.runner }} path: /tmp/digests/* retention-days: 1 merge-manifests: needs: build-image if: inputs.dry_run != true + strategy: + matrix: + variant: + - { suffix: "" } + - { suffix: "-codex" } + - { suffix: "-claude" } runs-on: ubuntu-latest permissions: contents: read @@ -99,7 +109,7 @@ jobs: uses: actions/download-artifact@v4 with: path: /tmp/digests - pattern: digests-* + pattern: digests${{ matrix.variant.suffix }}-* merge-multiple: true - uses: docker/setup-buildx-action@v3 @@ -114,7 +124,7 @@ jobs: id: meta uses: docker/metadata-action@v5 with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}${{ matrix.variant.suffix }} tags: | type=sha,prefix= type=raw,value=latest @@ -123,7 +133,7 @@ jobs: working-directory: /tmp/digests run: | docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ - $(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *) + $(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}${{ matrix.variant.suffix }}@sha256:%s ' *) bump-chart: needs: merge-manifests @@ -189,4 +199,3 @@ jobs: --title "chore: bump chart to ${{ steps.bump.outputs.new_version }}" \ --body "Auto-generated chart version bump for image \`${{ github.sha }}\`." \ --base main --head "$BRANCH" - diff --git a/Dockerfile.claude b/Dockerfile.claude new file mode 100644 index 00000000..b88ac38d --- /dev/null +++ b/Dockerfile.claude @@ -0,0 +1,23 @@ +# --- 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 claude-agent-acp adapter and Claude Code CLI +RUN npm install -g @agentclientprotocol/claude-agent-acp@0.25.0 @anthropic-ai/claude-code + +RUN mkdir -p /home/agent && chown node:node /home/agent +ENV HOME=/home/agent +WORKDIR /home/agent + +COPY --from=builder /build/target/release/agent-broker /usr/local/bin/agent-broker + +ENTRYPOINT ["agent-broker"] +CMD ["/etc/agent-broker/config.toml"] diff --git a/Dockerfile.codex b/Dockerfile.codex new file mode 100644 index 00000000..cf10ecae --- /dev/null +++ b/Dockerfile.codex @@ -0,0 +1,23 @@ +# --- 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/* + +# Pre-install codex-acp and codex CLI globally +RUN npm install -g @zed-industries/codex-acp@0.9.5 @openai/codex + +RUN mkdir -p /home/agent && chown node:node /home/agent +ENV HOME=/home/agent +WORKDIR /home/agent + +COPY --from=builder /build/target/release/agent-broker /usr/local/bin/agent-broker + +ENTRYPOINT ["agent-broker"] +CMD ["/etc/agent-broker/config.toml"] diff --git a/charts/agent-broker/Chart.yaml b/charts/agent-broker/Chart.yaml index 2020ebf6..b00bc12b 100644 --- a/charts/agent-broker/Chart.yaml +++ b/charts/agent-broker/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: agent-broker description: Discord ↔ ACP coding CLI bridge (Kiro CLI, Claude Code, Codex, Gemini) type: application -version: 0.1.8 +version: 0.1.9 appVersion: "fea0445" diff --git a/charts/agent-broker/templates/NOTES.txt b/charts/agent-broker/templates/NOTES.txt index 9cc1c57d..4ca8e467 100644 --- a/charts/agent-broker/templates/NOTES.txt +++ b/charts/agent-broker/templates/NOTES.txt @@ -8,9 +8,29 @@ agent-broker {{ .Chart.AppVersion }} has been installed! --from-literal=discord-bot-token="YOUR_TOKEN" {{- end }} +{{- if eq .Values.agent.command "kiro-cli" }} + Authenticate kiro-cli (first time only): kubectl exec -it deployment/{{ include "agent-broker.fullname" . }} -- kiro-cli login --use-device-flow +{{- else if eq .Values.agent.command "codex-acp" }} + +Authenticate Codex (first time only): + + kubectl exec -it deployment/{{ include "agent-broker.fullname" . }} -- codex login --device-auth +{{- else if eq .Values.agent.command "claude-agent-acp" }} + +Authenticate Claude Code (first time only): + + kubectl exec -it deployment/{{ include "agent-broker.fullname" . }} -- claude setup-token + +Then set the token as an env var (token is valid for 1 year): + + helm upgrade {{ .Release.Name }} agent-broker/agent-broker --set env.CLAUDE_CODE_OAUTH_TOKEN="" +{{- else }} + +Authenticate your agent CLI (first time only) — refer to your CLI's documentation. +{{- end }} Then restart the pod: diff --git a/charts/agent-broker/templates/deployment.yaml b/charts/agent-broker/templates/deployment.yaml index fee9c419..e07636b3 100644 --- a/charts/agent-broker/templates/deployment.yaml +++ b/charts/agent-broker/templates/deployment.yaml @@ -50,11 +50,7 @@ spec: readOnly: true {{- if .Values.persistence.enabled }} - name: data - mountPath: /home/agent/.kiro - subPath: dot-kiro - - name: data - mountPath: /home/agent/.local/share/kiro-cli - subPath: kiro-cli-data + mountPath: /home/agent {{- end }} {{- if .Values.agentsMd }} - name: config From 3159bc303240de73b1eab595a806bf063c7bf625 Mon Sep 17 00:00:00 2001 From: thepagent Date: Sat, 4 Apr 2026 19:37:42 +0800 Subject: [PATCH 10/80] debug: add logging for thread detection (#22) Co-authored-by: thepagent --- src/discord.rs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/discord.rs b/src/discord.rs index c46d60d5..32c495ab 100644 --- a/src/discord.rs +++ b/src/discord.rs @@ -37,10 +37,21 @@ impl EventHandler for Handler { let in_thread = if !in_allowed_channel { match msg.channel_id.to_channel(&ctx.http).await { - Ok(serenity::model::channel::Channel::Guild(gc)) => gc - .parent_id - .map_or(false, |pid| self.allowed_channels.contains(&pid.get())), - _ => false, + Ok(serenity::model::channel::Channel::Guild(gc)) => { + let result = gc + .parent_id + .map_or(false, |pid| self.allowed_channels.contains(&pid.get())); + tracing::debug!(channel_id = %msg.channel_id, parent_id = ?gc.parent_id, result, "thread check"); + result + } + Ok(other) => { + tracing::debug!(channel_id = %msg.channel_id, kind = ?other, "not a guild channel"); + false + } + Err(e) => { + tracing::debug!(channel_id = %msg.channel_id, error = %e, "to_channel failed"); + false + } } } else { false From a8a3340d951b09d18a07329f58d4125eabeb3ac3 Mon Sep 17 00:00:00 2001 From: thepagent Date: Sat, 4 Apr 2026 19:42:48 +0800 Subject: [PATCH 11/80] fix: disambiguate CI artifact names to prevent cross-variant digest collision (#23) The default variant (suffix='') produced artifact names like 'digests-ubuntu-latest' which matched the download pattern 'digests-codex-*' prefix. Use explicit artifact keys (default, codex, claude) instead of suffix-based naming. Co-authored-by: thepagent --- .github/workflows/build.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f242a02e..7a587372 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -36,9 +36,9 @@ jobs: strategy: matrix: variant: - - { suffix: "", dockerfile: "Dockerfile" } - - { suffix: "-codex", dockerfile: "Dockerfile.codex" } - - { suffix: "-claude", dockerfile: "Dockerfile.claude" } + - { suffix: "", dockerfile: "Dockerfile", artifact: "default" } + - { suffix: "-codex", dockerfile: "Dockerfile.codex", artifact: "codex" } + - { suffix: "-claude", dockerfile: "Dockerfile.claude", artifact: "claude" } platform: - { os: linux/amd64, runner: ubuntu-latest } - { os: linux/arm64, runner: ubuntu-24.04-arm } @@ -85,7 +85,7 @@ jobs: if: inputs.dry_run != true uses: actions/upload-artifact@v4 with: - name: digests${{ matrix.variant.suffix }}-${{ matrix.platform.runner }} + name: digests-${{ matrix.variant.artifact }}-${{ matrix.platform.runner }} path: /tmp/digests/* retention-days: 1 @@ -95,9 +95,9 @@ jobs: strategy: matrix: variant: - - { suffix: "" } - - { suffix: "-codex" } - - { suffix: "-claude" } + - { suffix: "", artifact: "default" } + - { suffix: "-codex", artifact: "codex" } + - { suffix: "-claude", artifact: "claude" } runs-on: ubuntu-latest permissions: contents: read @@ -109,7 +109,7 @@ jobs: uses: actions/download-artifact@v4 with: path: /tmp/digests - pattern: digests${{ matrix.variant.suffix }}-* + pattern: digests-${{ matrix.variant.artifact }}-* merge-multiple: true - uses: docker/setup-buildx-action@v3 From e195461caf0d0a6058ac416c61b3e2db68ceaab6 Mon Sep 17 00:00:00 2001 From: "openclaw-helm-bot[bot]" <271055092+openclaw-helm-bot[bot]@users.noreply.github.com> Date: Sat, 4 Apr 2026 19:50:21 +0800 Subject: [PATCH 12/80] chore: bump chart to 0.1.10 (#24) Co-authored-by: openclaw-helm-bot[bot] <3185992+openclaw-helm-bot[bot]@users.noreply.github.com> --- charts/agent-broker/Chart.yaml | 4 ++-- charts/agent-broker/values.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/charts/agent-broker/Chart.yaml b/charts/agent-broker/Chart.yaml index b00bc12b..0c2cfe6a 100644 --- a/charts/agent-broker/Chart.yaml +++ b/charts/agent-broker/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: agent-broker description: Discord ↔ ACP coding CLI bridge (Kiro CLI, Claude Code, Codex, Gemini) type: application -version: 0.1.9 -appVersion: "fea0445" +version: 0.1.10 +appVersion: "ef19c20" diff --git a/charts/agent-broker/values.yaml b/charts/agent-broker/values.yaml index da3b9f87..cdb21f6e 100644 --- a/charts/agent-broker/values.yaml +++ b/charts/agent-broker/values.yaml @@ -1,6 +1,6 @@ image: repository: ghcr.io/thepagent/agent-broker - tag: "fea0445" + tag: "ef19c20" pullPolicy: IfNotPresent replicas: 1 From 3b935e46f6f64541f6f338f5b56400e7594aaeab Mon Sep 17 00:00:00 2001 From: thepagent Date: Sat, 4 Apr 2026 20:09:11 +0800 Subject: [PATCH 13/80] feat: add agent.preset for simplified install UX (#28) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Users can now install with just: helm install agent-broker agent-broker/agent-broker \ --set discord.botToken=$DISCORD_BOT_TOKEN \ --set discord.allowedChannels[0]=CHANNEL_ID \ --set agent.preset=codex Presets: kiro (default), codex, claude — auto-configures image + command. Closes #25 Co-authored-by: thepagent --- charts/agent-broker/templates/NOTES.txt | 7 ++-- charts/agent-broker/templates/_helpers.tpl | 38 +++++++++++++++++++ charts/agent-broker/templates/configmap.yaml | 4 +- charts/agent-broker/templates/deployment.yaml | 2 +- charts/agent-broker/values.yaml | 1 + 5 files changed, 46 insertions(+), 6 deletions(-) diff --git a/charts/agent-broker/templates/NOTES.txt b/charts/agent-broker/templates/NOTES.txt index 4ca8e467..6f012e65 100644 --- a/charts/agent-broker/templates/NOTES.txt +++ b/charts/agent-broker/templates/NOTES.txt @@ -8,17 +8,18 @@ agent-broker {{ .Chart.AppVersion }} has been installed! --from-literal=discord-bot-token="YOUR_TOKEN" {{- end }} -{{- if eq .Values.agent.command "kiro-cli" }} +{{- $cmd := include "agent-broker.agent.command" . | trim }} +{{- if eq $cmd "kiro-cli" }} Authenticate kiro-cli (first time only): kubectl exec -it deployment/{{ include "agent-broker.fullname" . }} -- kiro-cli login --use-device-flow -{{- else if eq .Values.agent.command "codex-acp" }} +{{- else if eq $cmd "codex-acp" }} Authenticate Codex (first time only): kubectl exec -it deployment/{{ include "agent-broker.fullname" . }} -- codex login --device-auth -{{- else if eq .Values.agent.command "claude-agent-acp" }} +{{- else if eq $cmd "claude-agent-acp" }} Authenticate Claude Code (first time only): diff --git a/charts/agent-broker/templates/_helpers.tpl b/charts/agent-broker/templates/_helpers.tpl index 2ca6992f..708df752 100644 --- a/charts/agent-broker/templates/_helpers.tpl +++ b/charts/agent-broker/templates/_helpers.tpl @@ -32,3 +32,41 @@ app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/name: {{ include "agent-broker.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} + +{{/* +Resolve agent preset → image repository +*/}} +{{- define "agent-broker.image.repository" -}} +{{- if .Values.agent.preset }} + {{- if eq .Values.agent.preset "codex" }}ghcr.io/thepagent/agent-broker-codex + {{- else if eq .Values.agent.preset "claude" }}ghcr.io/thepagent/agent-broker-claude + {{- else }}{{ .Values.image.repository }} + {{- end }} +{{- else }}{{ .Values.image.repository }} +{{- end }} +{{- end }} + +{{/* +Resolve agent preset → command +*/}} +{{- define "agent-broker.agent.command" -}} +{{- if .Values.agent.preset }} + {{- if eq .Values.agent.preset "codex" }}codex-acp + {{- else if eq .Values.agent.preset "claude" }}claude-agent-acp + {{- else }}{{ .Values.agent.command }} + {{- end }} +{{- else }}{{ .Values.agent.command }} +{{- end }} +{{- end }} + +{{/* +Resolve agent preset → args +*/}} +{{- define "agent-broker.agent.args" -}} +{{- if .Values.agent.preset }} + {{- if or (eq .Values.agent.preset "codex") (eq .Values.agent.preset "claude") }}[] + {{- else }}{{ .Values.agent.args | toJson }} + {{- end }} +{{- else }}{{ .Values.agent.args | toJson }} +{{- end }} +{{- end }} diff --git a/charts/agent-broker/templates/configmap.yaml b/charts/agent-broker/templates/configmap.yaml index c0b3b12e..77a16525 100644 --- a/charts/agent-broker/templates/configmap.yaml +++ b/charts/agent-broker/templates/configmap.yaml @@ -11,8 +11,8 @@ data: allowed_channels = [{{ range $i, $ch := .Values.discord.allowedChannels }}{{ if $i }}, {{ end }}"{{ $ch }}"{{ end }}] [agent] - command = "{{ .Values.agent.command }}" - args = [{{ range $i, $a := .Values.agent.args }}{{ if $i }}, {{ end }}"{{ $a }}"{{ end }}] + command = "{{ include "agent-broker.agent.command" . | trim }}" + args = {{ include "agent-broker.agent.args" . | trim }} working_dir = "{{ .Values.agent.workingDir }}" {{- if .Values.agent.env }} env = { {{ range $k, $v := .Values.agent.env }}{{ $k }} = "{{ $v }}", {{ end }} } diff --git a/charts/agent-broker/templates/deployment.yaml b/charts/agent-broker/templates/deployment.yaml index e07636b3..8335d3b1 100644 --- a/charts/agent-broker/templates/deployment.yaml +++ b/charts/agent-broker/templates/deployment.yaml @@ -22,7 +22,7 @@ spec: spec: containers: - name: agent-broker - image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + image: "{{ include "agent-broker.image.repository" . | trim }}:{{ .Values.image.tag }}" imagePullPolicy: {{ .Values.image.pullPolicy }} env: - name: DISCORD_BOT_TOKEN diff --git a/charts/agent-broker/values.yaml b/charts/agent-broker/values.yaml index cdb21f6e..4b083cb3 100644 --- a/charts/agent-broker/values.yaml +++ b/charts/agent-broker/values.yaml @@ -21,6 +21,7 @@ discord: - "YOUR_CHANNEL_ID" agent: + preset: "" # kiro (default), codex, or claude — auto-configures image + command command: kiro-cli args: - acp From 3555249a3bb04d1f02332e31ea2fc0a551a43862 Mon Sep 17 00:00:00 2001 From: thepagent Date: Sat, 4 Apr 2026 20:16:33 +0800 Subject: [PATCH 14/80] docs: update README with preset install and tested backends (#30) * docs: update README with preset install instructions and tested backends * docs: add per-backend CLI install prompts * docs: simplify prompts, defer auth to NOTES.txt --------- Co-authored-by: thepagent --- README.md | 77 ++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 65 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 829b8ba5..9ca3ce80 100644 --- a/README.md +++ b/README.md @@ -81,9 +81,57 @@ The bot creates a thread. After that, just type in the thread — no @mention ne ## Pluggable Agent Backends -> **Note:** Currently only **Kiro CLI** is supported and tested. Other ACP-compatible CLIs (Claude Code, Codex, Gemini) should work in theory but are untested. Contributions and bug reports welcome. +Swap backends using the `agent.preset` Helm value or manual config. Tested backends: -Swap the `[agent]` block to use any ACP-compatible CLI. The `env` field supports `${VAR}` expansion from the process environment. +| Preset | CLI | ACP Adapter | Auth | +|--------|-----|-------------|------| +| (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` | + +### Helm Install (recommended) + +```bash +helm repo add agent-broker https://thepagent.github.io/agent-broker +helm repo update + +# Kiro CLI (default) +helm install agent-broker agent-broker/agent-broker \ + --set discord.botToken="$DISCORD_BOT_TOKEN" \ + --set discord.allowedChannels[0]="YOUR_CHANNEL_ID" + +# Codex +helm install agent-broker agent-broker/agent-broker \ + --set discord.botToken="$DISCORD_BOT_TOKEN" \ + --set discord.allowedChannels[0]="YOUR_CHANNEL_ID" \ + --set agent.preset=codex + +# Claude Code +helm install agent-broker agent-broker/agent-broker \ + --set discord.botToken="$DISCORD_BOT_TOKEN" \ + --set discord.allowedChannels[0]="YOUR_CHANNEL_ID" \ + --set agent.preset=claude +``` + +Then authenticate inside the pod (first time only): + +```bash +# Kiro CLI +kubectl exec -it deployment/agent-broker -- kiro-cli login --use-device-flow + +# Codex +kubectl exec -it deployment/agent-broker -- codex login --device-auth + +# Claude Code +kubectl exec -it deployment/agent-broker -- claude setup-token +# Then: helm upgrade agent-broker agent-broker/agent-broker --set env.CLAUDE_CODE_OAUTH_TOKEN="" +``` + +Restart after auth: `kubectl rollout restart deployment agent-broker` + +### Manual config.toml + +For non-Helm deployments, swap the `[agent]` block: ```toml # Kiro CLI (default) @@ -92,19 +140,17 @@ command = "kiro-cli" args = ["acp", "--trust-all-tools"] working_dir = "/tmp" -# Claude Code +# Codex (requires codex-acp in PATH) [agent] -command = "claude" -args = ["--acp"] +command = "codex-acp" +args = [] working_dir = "/tmp" -env = { ANTHROPIC_API_KEY = "${ANTHROPIC_API_KEY}" } -# Codex +# Claude Code (requires claude-agent-acp in PATH) [agent] -command = "codex" -args = ["--acp"] +command = "claude-agent-acp" +args = [] working_dir = "/tmp" -env = { OPENAI_API_KEY = "${OPENAI_API_KEY}" } # Gemini [agent] @@ -205,9 +251,16 @@ The Docker image bundles both `agent-broker` and `kiro-cli` in a single containe ### Install with Your Coding CLI -Use this prompt with any coding CLI (Kiro CLI, Claude Code, Codex, Gemini, etc.) on the host that has `helm` and `kubectl` access to your cluster: +Use one of these prompts with any coding CLI (Kiro CLI, Claude Code, Codex, Gemini, etc.) on the host that has `helm` and `kubectl` access to your cluster: + +**Kiro CLI (default):** +> Install agent-broker on my local k8s cluster using the Helm chart from https://thepagent.github.io/agent-broker. My Discord bot token is in the environment variable DISCORD_BOT_TOKEN and my channel ID is . After install, follow the NOTES output to authenticate, then restart the deployment. + +**Codex:** +> Install agent-broker on my local k8s cluster using the Helm chart from https://thepagent.github.io/agent-broker with `--set agent.preset=codex`. My Discord bot token is in the environment variable DISCORD_BOT_TOKEN and my channel ID is . After install, follow the NOTES output to authenticate, then restart the deployment. -> Install agent-broker on my local k8s cluster using the Helm chart from https://thepagent.github.io/agent-broker. My Discord bot token is in the environment variable DISCORD_BOT_TOKEN and my channel ID is . After install, authenticate kiro-cli inside the pod using kiro-cli login --use-device-flow, then restart the deployment. +**Claude Code:** +> Install agent-broker on my local k8s cluster using the Helm chart from https://thepagent.github.io/agent-broker with `--set agent.preset=claude`. My Discord bot token is in the environment variable DISCORD_BOT_TOKEN and my channel ID is . After install, follow the NOTES output to authenticate, then restart the deployment. ### Build & Push From 0774525273e291be7db4a33a3822cc3a1791f972 Mon Sep 17 00:00:00 2001 From: "openclaw-helm-bot[bot]" <271055092+openclaw-helm-bot[bot]@users.noreply.github.com> Date: Sat, 4 Apr 2026 20:17:50 +0800 Subject: [PATCH 15/80] chore: bump chart to 0.1.11 (#29) Co-authored-by: openclaw-helm-bot[bot] <3185992+openclaw-helm-bot[bot]@users.noreply.github.com> --- charts/agent-broker/Chart.yaml | 4 ++-- charts/agent-broker/values.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/charts/agent-broker/Chart.yaml b/charts/agent-broker/Chart.yaml index 0c2cfe6a..6a13fbf6 100644 --- a/charts/agent-broker/Chart.yaml +++ b/charts/agent-broker/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: agent-broker description: Discord ↔ ACP coding CLI bridge (Kiro CLI, Claude Code, Codex, Gemini) type: application -version: 0.1.10 -appVersion: "ef19c20" +version: 0.1.11 +appVersion: "c9bb6ee" diff --git a/charts/agent-broker/values.yaml b/charts/agent-broker/values.yaml index 4b083cb3..aea1dea7 100644 --- a/charts/agent-broker/values.yaml +++ b/charts/agent-broker/values.yaml @@ -1,6 +1,6 @@ image: repository: ghcr.io/thepagent/agent-broker - tag: "ef19c20" + tag: "c9bb6ee" pullPolicy: IfNotPresent replicas: 1 From 4c6f79c228dd13caabe044b575d5e0a1b5dfa86b Mon Sep 17 00:00:00 2001 From: "openclaw-helm-bot[bot]" <271055092+openclaw-helm-bot[bot]@users.noreply.github.com> Date: Sat, 4 Apr 2026 20:20:40 +0800 Subject: [PATCH 16/80] chore: bump chart to 0.1.12 (#31) Co-authored-by: openclaw-helm-bot[bot] <3185992+openclaw-helm-bot[bot]@users.noreply.github.com> --- charts/agent-broker/Chart.yaml | 4 ++-- charts/agent-broker/values.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/charts/agent-broker/Chart.yaml b/charts/agent-broker/Chart.yaml index 6a13fbf6..ae6b9f95 100644 --- a/charts/agent-broker/Chart.yaml +++ b/charts/agent-broker/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: agent-broker description: Discord ↔ ACP coding CLI bridge (Kiro CLI, Claude Code, Codex, Gemini) type: application -version: 0.1.11 -appVersion: "c9bb6ee" +version: 0.1.12 +appVersion: "7a6fe69" diff --git a/charts/agent-broker/values.yaml b/charts/agent-broker/values.yaml index aea1dea7..9f21c096 100644 --- a/charts/agent-broker/values.yaml +++ b/charts/agent-broker/values.yaml @@ -1,6 +1,6 @@ image: repository: ghcr.io/thepagent/agent-broker - tag: "c9bb6ee" + tag: "7a6fe69" pullPolicy: IfNotPresent replicas: 1 From ffe7e57e233ccd6667a18ac4fbd6984ef2b9651a Mon Sep 17 00:00:00 2001 From: thepagent Date: Sat, 4 Apr 2026 20:52:31 +0800 Subject: [PATCH 17/80] chore: bump chart to 0.2.0 (#32) Co-authored-by: thepagent --- charts/agent-broker/Chart.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/agent-broker/Chart.yaml b/charts/agent-broker/Chart.yaml index ae6b9f95..5fb575e0 100644 --- a/charts/agent-broker/Chart.yaml +++ b/charts/agent-broker/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: agent-broker description: Discord ↔ ACP coding CLI bridge (Kiro CLI, Claude Code, Codex, Gemini) type: application -version: 0.1.12 +version: 0.2.0 appVersion: "7a6fe69" From 96e3de7b39f25a0732fbfeb35fbfdac79f56c6ba Mon Sep 17 00:00:00 2001 From: thepagent Date: Sat, 4 Apr 2026 21:54:27 +0800 Subject: [PATCH 18/80] ci: default to beta chart versions, stable via workflow_dispatch (#33) * ci: default to beta chart versions, stable via workflow_dispatch - Push-triggered builds produce beta versions (e.g. 0.2.1-beta.12345) - workflow_dispatch adds 'release' toggle for stable versions - Beta charts are invisible to `helm install` (requires --devel or --version) - Bump PRs labeled as beta or stable * docs: add RELEASING.md for maintainers * docs: add ASCII flow diagrams to RELEASING.md --------- Co-authored-by: thepagent --- .github/workflows/build.yml | 30 ++++++++++++++++---- RELEASING.md | 56 +++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 6 deletions(-) create mode 100644 RELEASING.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7a587372..f8a934d8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,6 +21,11 @@ on: - minor - major default: patch + release: + description: 'Stable release (no beta suffix)' + required: false + type: boolean + default: false dry_run: description: 'Dry run (show changes without committing)' required: false @@ -164,7 +169,9 @@ jobs: id: bump run: | current="${{ steps.current.outputs.chart_version }}" - IFS='.' read -r major minor patch <<< "$current" + # Strip any existing pre-release suffix for base version + base="${current%%-*}" + IFS='.' read -r major minor patch <<< "$base" bump_type="${{ inputs.chart_bump }}" bump_type="${bump_type:-patch}" case "$bump_type" in @@ -172,7 +179,12 @@ jobs: minor) minor=$((minor + 1)); patch=0 ;; patch) patch=$((patch + 1)) ;; esac - new_version="${major}.${minor}.${patch}" + # Stable release: clean version. Otherwise: beta with run number. + if [ "${{ inputs.release }}" = "true" ]; then + new_version="${major}.${minor}.${patch}" + else + new_version="${major}.${minor}.${patch}-beta.${GITHUB_RUN_NUMBER}" + fi echo "new_version=$new_version" >> "$GITHUB_OUTPUT" - name: Update Chart.yaml and values.yaml @@ -188,14 +200,20 @@ jobs: env: GH_TOKEN: ${{ steps.app-token.outputs.token }} run: | - BRANCH="chore/chart-${{ steps.bump.outputs.new_version }}" + VERSION="${{ steps.bump.outputs.new_version }}" + BRANCH="chore/chart-${VERSION}" git config user.name "openclaw-helm-bot[bot]" git config user.email "3185992+openclaw-helm-bot[bot]@users.noreply.github.com" git checkout -b "$BRANCH" git add charts/agent-broker/Chart.yaml charts/agent-broker/values.yaml - git commit -m "chore: bump chart to ${{ steps.bump.outputs.new_version }}" + git commit -m "chore: bump chart to ${VERSION}" git push origin "$BRANCH" + if [[ "$VERSION" == *-beta* ]]; then + LABEL="beta" + else + LABEL="stable" + fi gh pr create \ - --title "chore: bump chart to ${{ steps.bump.outputs.new_version }}" \ - --body "Auto-generated chart version bump for image \`${{ github.sha }}\`." \ + --title "chore: bump chart to ${VERSION}" \ + --body "Auto-generated chart version bump (${LABEL}) for image \`${{ github.sha }}\`." \ --base main --head "$BRANCH" diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 00000000..a62976f8 --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,56 @@ +# Releasing + +## Version Scheme + +Chart versions follow SemVer with beta pre-releases: + +- **Beta**: `0.2.1-beta.12345` — auto-generated on every push to main +- **Stable**: `0.2.1` — manually triggered, visible to `helm install` + +Users running `helm install` only see stable versions. Beta versions require `--devel` or explicit `--version`. + +## Development Flow + +``` + PR merged to main + │ + ▼ + ┌─────────────┐ ┌──────────────────┐ ┌─────────────────────┐ + │ CI: Build │────>│ CI: Bump PR │────>│ Merge bump PR │ + │ 3 images │ │ 0.2.1-beta.12345 │ │ → publishes beta │ + └─────────────┘ └──────────────────┘ └─────────────────────┘ + │ + ┌───────────────────────────────────────────────┘ + ▼ + helm install ... --version 0.2.1-beta.12345 (explicit only) + helm install ... (still gets 0.2.0 stable) +``` + +## Stable Release + +``` + Actions → Build & Release → Run workflow + [bump: patch] [✅ Stable release] + │ + ▼ + ┌─────────────┐ ┌──────────────────┐ ┌─────────────────────┐ + │ CI: Build │────>│ CI: Bump PR │────>│ Merge bump PR │ + │ 3 images │ │ 0.2.1 │ │ → publishes stable │ + └─────────────┘ └──────────────────┘ └─────────────────────┘ + │ + ┌───────────────────────────────────────────────┘ + ▼ + helm install ... (gets 0.2.1 🎉) +``` + +## Image Tags + +Each build produces three multi-arch images tagged with the git short SHA: + +``` +ghcr.io/thepagent/agent-broker: # kiro-cli +ghcr.io/thepagent/agent-broker-codex: # codex +ghcr.io/thepagent/agent-broker-claude: # claude +``` + +The `latest` tag always points to the most recent build. From 26b1cda6e9574e0d329de1f1db4cfe5a77eff6fc Mon Sep 17 00:00:00 2001 From: thepagent Date: Sat, 4 Apr 2026 22:17:18 +0800 Subject: [PATCH 19/80] feat: add Gemini CLI support (native ACP) (#34) * feat: add Gemini CLI support (native ACP) - Add Dockerfile.gemini (node:22 + @google/gemini-cli) - Add gemini preset to _helpers.tpl (command: gemini, args: [--acp]) - Add gemini to CI build matrix - Add gemini auth hint to NOTES.txt Closes #15 * docs: add Gemini to README presets, install examples, and auth instructions --------- Co-authored-by: thepagent --- .github/workflows/build.yml | 2 ++ Dockerfile.gemini | 23 ++++++++++++++++++++++ README.md | 14 +++++++++++++ charts/agent-broker/templates/NOTES.txt | 12 +++++++++++ charts/agent-broker/templates/_helpers.tpl | 3 +++ 5 files changed, 54 insertions(+) create mode 100644 Dockerfile.gemini diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f8a934d8..f82f713d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -44,6 +44,7 @@ jobs: - { suffix: "", dockerfile: "Dockerfile", artifact: "default" } - { suffix: "-codex", dockerfile: "Dockerfile.codex", artifact: "codex" } - { suffix: "-claude", dockerfile: "Dockerfile.claude", artifact: "claude" } + - { suffix: "-gemini", dockerfile: "Dockerfile.gemini", artifact: "gemini" } platform: - { os: linux/amd64, runner: ubuntu-latest } - { os: linux/arm64, runner: ubuntu-24.04-arm } @@ -103,6 +104,7 @@ jobs: - { suffix: "", artifact: "default" } - { suffix: "-codex", artifact: "codex" } - { suffix: "-claude", artifact: "claude" } + - { suffix: "-gemini", artifact: "gemini" } runs-on: ubuntu-latest permissions: contents: read diff --git a/Dockerfile.gemini b/Dockerfile.gemini new file mode 100644 index 00000000..ae4e1e1b --- /dev/null +++ b/Dockerfile.gemini @@ -0,0 +1,23 @@ +# --- 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 Gemini CLI (native ACP support via --acp) +RUN npm install -g @google/gemini-cli + +RUN mkdir -p /home/agent && chown node:node /home/agent +ENV HOME=/home/agent +WORKDIR /home/agent + +COPY --from=builder /build/target/release/agent-broker /usr/local/bin/agent-broker + +ENTRYPOINT ["agent-broker"] +CMD ["/etc/agent-broker/config.toml"] diff --git a/README.md b/README.md index 9ca3ce80..4c05fdb2 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,7 @@ Swap backends using the `agent.preset` Helm value or manual config. Tested backe | (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` | ### Helm Install (recommended) @@ -111,6 +112,12 @@ helm install agent-broker agent-broker/agent-broker \ --set discord.botToken="$DISCORD_BOT_TOKEN" \ --set discord.allowedChannels[0]="YOUR_CHANNEL_ID" \ --set agent.preset=claude + +# Gemini +helm install agent-broker agent-broker/agent-broker \ + --set discord.botToken="$DISCORD_BOT_TOKEN" \ + --set discord.allowedChannels[0]="YOUR_CHANNEL_ID" \ + --set agent.preset=gemini ``` Then authenticate inside the pod (first time only): @@ -125,6 +132,10 @@ kubectl exec -it deployment/agent-broker -- codex login --device-auth # Claude Code kubectl exec -it deployment/agent-broker -- claude setup-token # Then: helm upgrade agent-broker agent-broker/agent-broker --set env.CLAUDE_CODE_OAUTH_TOKEN="" + +# Gemini (Google OAuth — open URL in browser, curl callback from pod) +kubectl exec -it deployment/agent-broker -- gemini +# Or use API key: helm upgrade agent-broker agent-broker/agent-broker --set env.GEMINI_API_KEY="" ``` Restart after auth: `kubectl rollout restart deployment agent-broker` @@ -262,6 +273,9 @@ Use one of these prompts with any coding CLI (Kiro CLI, Claude Code, Codex, Gemi **Claude Code:** > Install agent-broker on my local k8s cluster using the Helm chart from https://thepagent.github.io/agent-broker with `--set agent.preset=claude`. My Discord bot token is in the environment variable DISCORD_BOT_TOKEN and my channel ID is . After install, follow the NOTES output to authenticate, then restart the deployment. +**Gemini:** +> Install agent-broker on my local k8s cluster using the Helm chart from https://thepagent.github.io/agent-broker with `--set agent.preset=gemini`. My Discord bot token is in the environment variable DISCORD_BOT_TOKEN and my channel ID is . After install, follow the NOTES output to authenticate, then restart the deployment. + ### Build & Push ```bash diff --git a/charts/agent-broker/templates/NOTES.txt b/charts/agent-broker/templates/NOTES.txt index 6f012e65..93ce32df 100644 --- a/charts/agent-broker/templates/NOTES.txt +++ b/charts/agent-broker/templates/NOTES.txt @@ -28,6 +28,18 @@ Authenticate Claude Code (first time only): Then set the token as an env var (token is valid for 1 year): helm upgrade {{ .Release.Name }} agent-broker/agent-broker --set env.CLAUDE_CODE_OAUTH_TOKEN="" +{{- else if eq $cmd "gemini" }} + +Authenticate Gemini CLI (first time only): + + kubectl exec -it deployment/{{ include "agent-broker.fullname" . }} -- gemini + + Select "Sign in with Google", open the URL in your browser, then + curl the localhost callback URL from within the pod. + + Alternatively, set GEMINI_API_KEY: + + helm upgrade {{ .Release.Name }} agent-broker/agent-broker --set env.GEMINI_API_KEY="" {{- else }} Authenticate your agent CLI (first time only) — refer to your CLI's documentation. diff --git a/charts/agent-broker/templates/_helpers.tpl b/charts/agent-broker/templates/_helpers.tpl index 708df752..852c50f3 100644 --- a/charts/agent-broker/templates/_helpers.tpl +++ b/charts/agent-broker/templates/_helpers.tpl @@ -40,6 +40,7 @@ Resolve agent preset → image repository {{- if .Values.agent.preset }} {{- if eq .Values.agent.preset "codex" }}ghcr.io/thepagent/agent-broker-codex {{- else if eq .Values.agent.preset "claude" }}ghcr.io/thepagent/agent-broker-claude + {{- else if eq .Values.agent.preset "gemini" }}ghcr.io/thepagent/agent-broker-gemini {{- else }}{{ .Values.image.repository }} {{- end }} {{- else }}{{ .Values.image.repository }} @@ -53,6 +54,7 @@ Resolve agent preset → command {{- if .Values.agent.preset }} {{- if eq .Values.agent.preset "codex" }}codex-acp {{- else if eq .Values.agent.preset "claude" }}claude-agent-acp + {{- else if eq .Values.agent.preset "gemini" }}gemini {{- else }}{{ .Values.agent.command }} {{- end }} {{- else }}{{ .Values.agent.command }} @@ -65,6 +67,7 @@ Resolve agent preset → args {{- define "agent-broker.agent.args" -}} {{- if .Values.agent.preset }} {{- if or (eq .Values.agent.preset "codex") (eq .Values.agent.preset "claude") }}[] + {{- else if eq .Values.agent.preset "gemini" }}["--acp"] {{- else }}{{ .Values.agent.args | toJson }} {{- end }} {{- else }}{{ .Values.agent.args | toJson }} From f4af0c0bc38603cf07d9a0534ee2d0dd8d22f004 Mon Sep 17 00:00:00 2001 From: "openclaw-helm-bot[bot]" <271055092+openclaw-helm-bot[bot]@users.noreply.github.com> Date: Sat, 4 Apr 2026 22:29:33 +0800 Subject: [PATCH 20/80] chore: bump chart to 0.2.1-beta.22 (#35) Co-authored-by: openclaw-helm-bot[bot] <3185992+openclaw-helm-bot[bot]@users.noreply.github.com> --- charts/agent-broker/Chart.yaml | 4 ++-- charts/agent-broker/values.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/charts/agent-broker/Chart.yaml b/charts/agent-broker/Chart.yaml index 5fb575e0..df67b719 100644 --- a/charts/agent-broker/Chart.yaml +++ b/charts/agent-broker/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: agent-broker description: Discord ↔ ACP coding CLI bridge (Kiro CLI, Claude Code, Codex, Gemini) type: application -version: 0.2.0 -appVersion: "7a6fe69" +version: 0.2.1-beta.22 +appVersion: "afad7f9" diff --git a/charts/agent-broker/values.yaml b/charts/agent-broker/values.yaml index 9f21c096..f5cb314d 100644 --- a/charts/agent-broker/values.yaml +++ b/charts/agent-broker/values.yaml @@ -1,6 +1,6 @@ image: repository: ghcr.io/thepagent/agent-broker - tag: "7a6fe69" + tag: "afad7f9" pullPolicy: IfNotPresent replicas: 1 From 391dd9e802867fc05f7d18c394c8ee5477b150e9 Mon Sep 17 00:00:00 2001 From: thepagent Date: Sat, 4 Apr 2026 22:41:35 +0800 Subject: [PATCH 21/80] chore: bump chart to 0.3.0 (#37) Co-authored-by: thepagent --- charts/agent-broker/Chart.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/agent-broker/Chart.yaml b/charts/agent-broker/Chart.yaml index df67b719..482e6c81 100644 --- a/charts/agent-broker/Chart.yaml +++ b/charts/agent-broker/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: agent-broker description: Discord ↔ ACP coding CLI bridge (Kiro CLI, Claude Code, Codex, Gemini) type: application -version: 0.2.1-beta.22 +version: 0.3.0 appVersion: "afad7f9" From 40bafe7e42fee013e34bdd0935a0cf66efbda74a Mon Sep 17 00:00:00 2001 From: thepagent Date: Mon, 6 Apr 2026 12:38:59 +0800 Subject: [PATCH 22/80] feat: structured sender identity injection (#62) Inject Discord sender identity as a structured JSON block into the prompt, enabling downstream CLIs to identify message senders. Implements Phase 1+2 of #61. Fields: schema, sender_id, sender_name, display_name, channel, channel_id, is_bot. Thread names use the original prompt text (no sender prefix pollution). Closes #61 --- src/discord.rs | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/discord.rs b/src/discord.rs index 32c495ab..8cb60e15 100644 --- a/src/discord.rs +++ b/src/discord.rs @@ -73,7 +73,26 @@ impl EventHandler for Handler { return; } - tracing::debug!(prompt = %prompt, in_thread, "processing"); + // Inject structured sender context so the downstream CLI can identify who sent the message + let display_name = msg.member.as_ref() + .and_then(|m| m.nick.as_ref()) + .unwrap_or(&msg.author.name); + let sender_ctx = serde_json::json!({ + "schema": "agent-broker.sender.v1", + "sender_id": msg.author.id.to_string(), + "sender_name": msg.author.name, + "display_name": display_name, + "channel": "discord", + "channel_id": msg.channel_id.to_string(), + "is_bot": msg.author.bot, + }); + let prompt_with_sender = format!( + "\n{}\n\n\n{}", + serde_json::to_string(&sender_ctx).unwrap(), + prompt + ); + + tracing::debug!(prompt = %prompt_with_sender, in_thread, "processing"); let thread_id = if in_thread { msg.channel_id.get() @@ -119,7 +138,7 @@ impl EventHandler for Handler { let result = stream_prompt( &self.pool, &thread_key, - &prompt, + &prompt_with_sender, &ctx, thread_channel, thinking_msg.id, From 62640ac6e762be67b414cc81c8f521a3d4192f7c Mon Sep 17 00:00:00 2001 From: thepagent Date: Mon, 6 Apr 2026 12:47:56 +0800 Subject: [PATCH 23/80] fix: detect and prevent Helm float64 mangling of Discord channel IDs (#64) Add fail-fast validation in configmap template to detect scientific notation in channel IDs, plus --set-string warnings in NOTES.txt and values.yaml. Closes #43 --- charts/agent-broker/templates/NOTES.txt | 5 +++++ charts/agent-broker/templates/configmap.yaml | 5 +++++ charts/agent-broker/values.yaml | 2 ++ 3 files changed, 12 insertions(+) diff --git a/charts/agent-broker/templates/NOTES.txt b/charts/agent-broker/templates/NOTES.txt index 93ce32df..c332fe1e 100644 --- a/charts/agent-broker/templates/NOTES.txt +++ b/charts/agent-broker/templates/NOTES.txt @@ -1,5 +1,10 @@ agent-broker {{ .Chart.AppVersion }} has been installed! +⚠️ Discord channel IDs must be set with --set-string (not --set) to avoid float64 precision loss: + + helm upgrade {{ .Release.Name }} agent-broker/agent-broker \ + --set-string discord.allowedChannels[0]="" + {{- if not .Values.discord.botToken }} ⚠️ No bot token was provided. Create the secret manually: diff --git a/charts/agent-broker/templates/configmap.yaml b/charts/agent-broker/templates/configmap.yaml index 77a16525..3cbd3244 100644 --- a/charts/agent-broker/templates/configmap.yaml +++ b/charts/agent-broker/templates/configmap.yaml @@ -8,6 +8,11 @@ data: config.toml: | [discord] bot_token = "${DISCORD_BOT_TOKEN}" + {{- range .Values.discord.allowedChannels }} + {{- if regexMatch "e\\+|E\\+" (toString .) }} + {{- fail (printf "discord.allowedChannels contains a mangled ID: %s — use --set-string instead of --set for channel IDs" (toString .)) }} + {{- end }} + {{- end }} allowed_channels = [{{ range $i, $ch := .Values.discord.allowedChannels }}{{ if $i }}, {{ end }}"{{ $ch }}"{{ end }}] [agent] diff --git a/charts/agent-broker/values.yaml b/charts/agent-broker/values.yaml index f5cb314d..d33ad5b4 100644 --- a/charts/agent-broker/values.yaml +++ b/charts/agent-broker/values.yaml @@ -17,6 +17,8 @@ persistence: discord: botToken: "" # set via --set or external secret + # ⚠️ Use --set-string for channel IDs to avoid float64 precision loss: + # helm install ... --set-string discord.allowedChannels[0]="" allowedChannels: - "YOUR_CHANNEL_ID" From 52228cc6e330f983555abbb0c1985acf6a672aa8 Mon Sep 17 00:00:00 2001 From: thepagent Date: Mon, 6 Apr 2026 12:50:23 +0800 Subject: [PATCH 24/80] chore: bump chart to 0.3.1-beta.1 (#65) Includes #62 (sender identity injection) and #64 (Helm float64 validation). --- charts/agent-broker/Chart.yaml | 4 ++-- charts/agent-broker/values.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/charts/agent-broker/Chart.yaml b/charts/agent-broker/Chart.yaml index 482e6c81..55d99a45 100644 --- a/charts/agent-broker/Chart.yaml +++ b/charts/agent-broker/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: agent-broker description: Discord ↔ ACP coding CLI bridge (Kiro CLI, Claude Code, Codex, Gemini) type: application -version: 0.3.0 -appVersion: "afad7f9" +version: 0.3.1-beta.1 +appVersion: "899c3d7" diff --git a/charts/agent-broker/values.yaml b/charts/agent-broker/values.yaml index d33ad5b4..4dd08d66 100644 --- a/charts/agent-broker/values.yaml +++ b/charts/agent-broker/values.yaml @@ -1,6 +1,6 @@ image: repository: ghcr.io/thepagent/agent-broker - tag: "afad7f9" + tag: "899c3d7" pullPolicy: IfNotPresent replicas: 1 From ee2aa99e16aa2757c26fa20fed5320e9b1e19261 Mon Sep 17 00:00:00 2001 From: thepagent Date: Mon, 6 Apr 2026 13:25:47 +0800 Subject: [PATCH 25/80] fix: CI appVersion mismatch + Helm OCI namespace collision (#67) 1. bump-chart: push directly to main with [skip ci] instead of creating a PR. This ensures appVersion in Chart.yaml always matches the actual image SHA from the build that triggered it. Previously the bump PR merge created a new commit SHA that had no corresponding image. 2. release.yml: push Helm charts to oci://ghcr.io//charts instead of oci://ghcr.io/ to avoid namespace collision with Docker images in the same GHCR repo. Fixes #66 Co-authored-by: thepagent --- .github/workflows/build.yml | 37 ++++++++++++++++++----------------- .github/workflows/release.yml | 4 ++-- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f82f713d..c97b2a4b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -189,33 +189,34 @@ jobs: fi echo "new_version=$new_version" >> "$GITHUB_OUTPUT" + - name: Resolve image SHA + id: image-sha + run: | + # Use the commit SHA that triggered this build — this is the SHA + # that merge-manifests tagged the Docker image with (type=sha,prefix=). + # We capture it here explicitly so it survives the bump commit. + IMAGE_SHA="${{ github.sha }}" + IMAGE_SHA="${IMAGE_SHA:0:7}" + echo "sha=${IMAGE_SHA}" >> "$GITHUB_OUTPUT" + - name: Update Chart.yaml and values.yaml run: | - SHORT_SHA="${{ github.sha }}" - SHORT_SHA="${SHORT_SHA:0:7}" + IMAGE_SHA="${{ steps.image-sha.outputs.sha }}" sed -i "s/^version: .*/version: ${{ steps.bump.outputs.new_version }}/" charts/agent-broker/Chart.yaml - sed -i "s/^appVersion: .*/appVersion: \"${SHORT_SHA}\"/" charts/agent-broker/Chart.yaml + sed -i "s/^appVersion: .*/appVersion: \"${IMAGE_SHA}\"/" charts/agent-broker/Chart.yaml sed -i "s|repository: .*|repository: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}|" charts/agent-broker/values.yaml - sed -i "s/tag: .*/tag: \"${SHORT_SHA}\"/" charts/agent-broker/values.yaml + sed -i "s/tag: .*/tag: \"${IMAGE_SHA}\"/" charts/agent-broker/values.yaml - - name: Create bump PR + - name: Push chart bump directly to main env: GH_TOKEN: ${{ steps.app-token.outputs.token }} run: | VERSION="${{ steps.bump.outputs.new_version }}" - BRANCH="chore/chart-${VERSION}" + IMAGE_SHA="${{ steps.image-sha.outputs.sha }}" git config user.name "openclaw-helm-bot[bot]" git config user.email "3185992+openclaw-helm-bot[bot]@users.noreply.github.com" - git checkout -b "$BRANCH" git add charts/agent-broker/Chart.yaml charts/agent-broker/values.yaml - git commit -m "chore: bump chart to ${VERSION}" - git push origin "$BRANCH" - if [[ "$VERSION" == *-beta* ]]; then - LABEL="beta" - else - LABEL="stable" - fi - gh pr create \ - --title "chore: bump chart to ${VERSION}" \ - --body "Auto-generated chart version bump (${LABEL}) for image \`${{ github.sha }}\`." \ - --base main --head "$BRANCH" + git commit -m "chore: bump chart to ${VERSION} [skip ci] + + image: ${IMAGE_SHA}" + git push origin main diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index abe26f17..1c74c53a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -49,7 +49,7 @@ jobs: NAME=$(grep '^name:' ${CHART}/Chart.yaml | awk '{print $2}') VERSION=$(grep '^version:' ${CHART}/Chart.yaml | awk '{print $2}') helm package ${CHART} - helm push ${NAME}-${VERSION}.tgz oci://ghcr.io/${{ github.repository_owner }} + helm push ${NAME}-${VERSION}.tgz oci://ghcr.io/${{ github.repository_owner }}/charts - name: Append OCI install instructions to release notes env: @@ -85,7 +85,7 @@ jobs: ### OCI Registry \`\`\`bash - helm install agent-broker oci://ghcr.io/${OWNER}/agent-broker --version ${VERSION} + helm install agent-broker oci://ghcr.io/${OWNER}/charts/agent-broker --version ${VERSION} \`\`\` EOF From f0a2d0e0d38b9197ff96212c6a79e2bb6e17a2d0 Mon Sep 17 00:00:00 2001 From: thepagent Date: Mon, 6 Apr 2026 13:33:26 +0800 Subject: [PATCH 26/80] fix: use PR + auto-merge for chart bump (branch protection) (#68) Direct push to main is blocked by branch protection rules. Revert to PR approach but with auto-merge enabled, so the appVersion still matches the correct image SHA. Follows up on #66 Co-authored-by: thepagent --- .github/workflows/build.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c97b2a4b..353f5eb5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -207,16 +207,23 @@ jobs: sed -i "s|repository: .*|repository: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}|" charts/agent-broker/values.yaml sed -i "s/tag: .*/tag: \"${IMAGE_SHA}\"/" charts/agent-broker/values.yaml - - name: Push chart bump directly to main + - name: Create and auto-merge bump PR env: GH_TOKEN: ${{ steps.app-token.outputs.token }} run: | VERSION="${{ steps.bump.outputs.new_version }}" IMAGE_SHA="${{ steps.image-sha.outputs.sha }}" + BRANCH="chore/chart-${VERSION}" git config user.name "openclaw-helm-bot[bot]" git config user.email "3185992+openclaw-helm-bot[bot]@users.noreply.github.com" + git checkout -b "$BRANCH" git add charts/agent-broker/Chart.yaml charts/agent-broker/values.yaml - git commit -m "chore: bump chart to ${VERSION} [skip ci] + git commit -m "chore: bump chart to ${VERSION} image: ${IMAGE_SHA}" - git push origin main + git push origin "$BRANCH" + PR_URL=$(gh pr create \ + --title "chore: bump chart to ${VERSION}" \ + --body "Auto-generated chart version bump for image \`${IMAGE_SHA}\`." \ + --base main --head "$BRANCH") + gh pr merge "$PR_URL" --squash --auto --delete-branch From 1abb0faecbfd57573cd8164b0853cf6d3a8c8861 Mon Sep 17 00:00:00 2001 From: "openclaw-helm-bot[bot]" <271055092+openclaw-helm-bot[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 13:35:47 +0800 Subject: [PATCH 27/80] chore: bump chart to 0.3.2-beta.25 (#69) image: f0a2d0e Co-authored-by: openclaw-helm-bot[bot] <3185992+openclaw-helm-bot[bot]@users.noreply.github.com> --- charts/agent-broker/Chart.yaml | 4 ++-- charts/agent-broker/values.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/charts/agent-broker/Chart.yaml b/charts/agent-broker/Chart.yaml index 55d99a45..6c6c775d 100644 --- a/charts/agent-broker/Chart.yaml +++ b/charts/agent-broker/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: agent-broker description: Discord ↔ ACP coding CLI bridge (Kiro CLI, Claude Code, Codex, Gemini) type: application -version: 0.3.1-beta.1 -appVersion: "899c3d7" +version: 0.3.2-beta.25 +appVersion: "b36ff36" diff --git a/charts/agent-broker/values.yaml b/charts/agent-broker/values.yaml index 4dd08d66..0ab4d5a4 100644 --- a/charts/agent-broker/values.yaml +++ b/charts/agent-broker/values.yaml @@ -1,6 +1,6 @@ image: repository: ghcr.io/thepagent/agent-broker - tag: "899c3d7" + tag: "b36ff36" pullPolicy: IfNotPresent replicas: 1 From 3865d9817989ee0eb813bfbadcbae78f2fa756b2 Mon Sep 17 00:00:00 2001 From: thepagent Date: Mon, 6 Apr 2026 13:40:39 +0800 Subject: [PATCH 28/80] docs: use --set-string for Discord channel IDs in README (#70) Discord Snowflake IDs exceed float64 precision. Using --set causes silent message ignoring due to scientific notation mangling. Ref: #43 Co-authored-by: thepagent --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4c05fdb2..385f4be1 100644 --- a/README.md +++ b/README.md @@ -99,24 +99,24 @@ helm repo update # Kiro CLI (default) helm install agent-broker agent-broker/agent-broker \ --set discord.botToken="$DISCORD_BOT_TOKEN" \ - --set discord.allowedChannels[0]="YOUR_CHANNEL_ID" + --set-string discord.allowedChannels[0]="YOUR_CHANNEL_ID" # Codex helm install agent-broker agent-broker/agent-broker \ --set discord.botToken="$DISCORD_BOT_TOKEN" \ - --set discord.allowedChannels[0]="YOUR_CHANNEL_ID" \ + --set-string discord.allowedChannels[0]="YOUR_CHANNEL_ID" \ --set agent.preset=codex # Claude Code helm install agent-broker agent-broker/agent-broker \ --set discord.botToken="$DISCORD_BOT_TOKEN" \ - --set discord.allowedChannels[0]="YOUR_CHANNEL_ID" \ + --set-string discord.allowedChannels[0]="YOUR_CHANNEL_ID" \ --set agent.preset=claude # Gemini helm install agent-broker agent-broker/agent-broker \ --set discord.botToken="$DISCORD_BOT_TOKEN" \ - --set discord.allowedChannels[0]="YOUR_CHANNEL_ID" \ + --set-string discord.allowedChannels[0]="YOUR_CHANNEL_ID" \ --set agent.preset=gemini ``` From d379446926d8158f0895066afd98ab67a069881b Mon Sep 17 00:00:00 2001 From: "openclaw-helm-bot[bot]" <271055092+openclaw-helm-bot[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 14:39:08 +0800 Subject: [PATCH 29/80] chore: bump chart to 0.3.3 (#71) image: 3865d98 Co-authored-by: openclaw-helm-bot[bot] <3185992+openclaw-helm-bot[bot]@users.noreply.github.com> --- charts/agent-broker/Chart.yaml | 4 ++-- charts/agent-broker/values.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/charts/agent-broker/Chart.yaml b/charts/agent-broker/Chart.yaml index 6c6c775d..cfcaa872 100644 --- a/charts/agent-broker/Chart.yaml +++ b/charts/agent-broker/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: agent-broker description: Discord ↔ ACP coding CLI bridge (Kiro CLI, Claude Code, Codex, Gemini) type: application -version: 0.3.2-beta.25 -appVersion: "b36ff36" +version: 0.3.3 +appVersion: "dd7e1ca" diff --git a/charts/agent-broker/values.yaml b/charts/agent-broker/values.yaml index 0ab4d5a4..7bcfafd8 100644 --- a/charts/agent-broker/values.yaml +++ b/charts/agent-broker/values.yaml @@ -1,6 +1,6 @@ image: repository: ghcr.io/thepagent/agent-broker - tag: "b36ff36" + tag: "dd7e1ca" pullPolicy: IfNotPresent replicas: 1 From 5b98a40077c216e2bf17e6d57b0f4a86166c61c9 Mon Sep 17 00:00:00 2001 From: thepagent Date: Tue, 7 Apr 2026 00:37:20 +0800 Subject: [PATCH 30/80] chore: add CODEOWNERS file (#89) Define @thepagent as code owner for all files on main branch. Co-authored-by: thepagent --- .github/CODEOWNERS | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..1ad83f66 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,7 @@ +# Default code owner for all files +* @thepagent + +# Explicitly protect the CODEOWNERS file itself. While the wildcard above +# already covers it, this ensures ownership is preserved even if more +# specific patterns are added later (CODEOWNERS uses last-match-wins). +/.github/CODEOWNERS @thepagent From cd65c3bfa454c9f1ec996f9126b0c12a93ffe2fe Mon Sep 17 00:00:00 2001 From: chaodufashi Date: Tue, 7 Apr 2026 11:25:07 +0800 Subject: [PATCH 31/80] =?UTF-8?q?chore:=20rename=20agent-broker=20?= =?UTF-8?q?=E2=86=92=20OpenAB=20(Open=20Agent=20Broker)=20(#96)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename the project from agent-broker to OpenAB. Changes: - README.md: new project title and description - Cargo.toml/Cargo.lock: package name agent-broker → openab - RELEASING.md: image refs → ghcr.io/openabdev/openab* - charts/agent-broker/ → charts/openab/ (directory rename) - All Helm templates: helper names, image repos, config paths - All 4 Dockerfiles: binary name + config mount path - k8s/ manifests: resource names - src/: client info, schema, log messages - docs/discord-bot-howto.md: all references Note: .github/workflows/ changes require workflow scope. Co-authored-by: openab-bot --- Cargo.lock | 2 +- Cargo.toml | 2 +- Dockerfile | 6 +- Dockerfile.claude | 6 +- Dockerfile.codex | 6 +- Dockerfile.gemini | 6 +- README.md | 62 +++++++++---------- RELEASING.md | 6 +- charts/{agent-broker => openab}/Chart.yaml | 2 +- .../templates/NOTES.txt | 22 +++---- .../templates/_helpers.tpl | 28 ++++----- .../templates/configmap.yaml | 8 +-- .../templates/deployment.yaml | 20 +++--- .../templates/pvc.yaml | 4 +- .../templates/secret.yaml | 4 +- charts/{agent-broker => openab}/values.yaml | 2 +- docs/discord-bot-howto.md | 6 +- k8s/configmap.yaml | 2 +- k8s/deployment.yaml | 20 +++--- k8s/pvc.yaml | 2 +- k8s/secret.yaml | 2 +- src/acp/connection.rs | 2 +- src/discord.rs | 2 +- src/main.rs | 2 +- 24 files changed, 112 insertions(+), 112 deletions(-) rename charts/{agent-broker => openab}/Chart.yaml (89%) rename charts/{agent-broker => openab}/templates/NOTES.txt (52%) rename charts/{agent-broker => openab}/templates/_helpers.tpl (69%) rename charts/{agent-broker => openab}/templates/configmap.yaml (82%) rename charts/{agent-broker => openab}/templates/deployment.yaml (76%) rename charts/{agent-broker => openab}/templates/pvc.yaml (76%) rename charts/{agent-broker => openab}/templates/secret.yaml (69%) rename charts/{agent-broker => openab}/values.yaml (95%) diff --git a/Cargo.lock b/Cargo.lock index ea342114..53acb5ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9,7 +9,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] -name = "agent-broker" +name = "openab" version = "0.1.0" dependencies = [ "anyhow", diff --git a/Cargo.toml b/Cargo.toml index c32b9e63..edfdf870 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "agent-broker" +name = "openab" version = "0.1.0" edition = "2021" diff --git a/Dockerfile b/Dockerfile index 4a8679d9..740d7b4a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,7 +24,7 @@ RUN mkdir -p /home/agent/.local/share/kiro-cli /home/agent/.kiro ENV HOME=/home/agent WORKDIR /home/agent -COPY --from=builder /build/target/release/agent-broker /usr/local/bin/agent-broker +COPY --from=builder /build/target/release/openab /usr/local/bin/openab -ENTRYPOINT ["agent-broker"] -CMD ["/etc/agent-broker/config.toml"] +ENTRYPOINT ["openab"] +CMD ["/etc/openab/config.toml"] diff --git a/Dockerfile.claude b/Dockerfile.claude index b88ac38d..61eea700 100644 --- a/Dockerfile.claude +++ b/Dockerfile.claude @@ -17,7 +17,7 @@ RUN mkdir -p /home/agent && chown node:node /home/agent ENV HOME=/home/agent WORKDIR /home/agent -COPY --from=builder /build/target/release/agent-broker /usr/local/bin/agent-broker +COPY --from=builder /build/target/release/openab /usr/local/bin/openab -ENTRYPOINT ["agent-broker"] -CMD ["/etc/agent-broker/config.toml"] +ENTRYPOINT ["openab"] +CMD ["/etc/openab/config.toml"] diff --git a/Dockerfile.codex b/Dockerfile.codex index cf10ecae..4d4f0186 100644 --- a/Dockerfile.codex +++ b/Dockerfile.codex @@ -17,7 +17,7 @@ RUN mkdir -p /home/agent && chown node:node /home/agent ENV HOME=/home/agent WORKDIR /home/agent -COPY --from=builder /build/target/release/agent-broker /usr/local/bin/agent-broker +COPY --from=builder /build/target/release/openab /usr/local/bin/openab -ENTRYPOINT ["agent-broker"] -CMD ["/etc/agent-broker/config.toml"] +ENTRYPOINT ["openab"] +CMD ["/etc/openab/config.toml"] diff --git a/Dockerfile.gemini b/Dockerfile.gemini index ae4e1e1b..1ad797f8 100644 --- a/Dockerfile.gemini +++ b/Dockerfile.gemini @@ -17,7 +17,7 @@ RUN mkdir -p /home/agent && chown node:node /home/agent ENV HOME=/home/agent WORKDIR /home/agent -COPY --from=builder /build/target/release/agent-broker /usr/local/bin/agent-broker +COPY --from=builder /build/target/release/openab /usr/local/bin/openab -ENTRYPOINT ["agent-broker"] -CMD ["/etc/agent-broker/config.toml"] +ENTRYPOINT ["openab"] +CMD ["/etc/openab/config.toml"] diff --git a/README.md b/README.md index 385f4be1..84bc5ed2 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,17 @@ -# agent-broker +# OpenAB — Open Agent Broker -A Rust bridge service between Discord and any ACP-compatible coding CLI (Kiro CLI, Claude Code, Codex, Gemini, etc.) using the [Agent Client Protocol](https://github.com/anthropics/agent-protocol) over stdio JSON-RPC. +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. ``` ┌──────────────┐ Gateway WS ┌──────────────┐ ACP stdio ┌──────────────┐ -│ Discord │◄─────────────►│ agent-broker │──────────────►│ coding CLI │ +│ Discord │◄─────────────►│ openab │──────────────►│ coding CLI │ │ User │ │ (Rust) │◄── JSON-RPC ──│ (acp mode) │ └──────────────┘ └──────────────┘ └──────────────┘ ``` ## Demo -![agent-broker demo](images/demo.png) +![openab demo](images/demo.png) ## Features @@ -65,7 +65,7 @@ cargo run # Production cargo build --release -./target/release/agent-broker config.toml +./target/release/openab config.toml ``` If no config path is given, it defaults to `config.toml` in the current directory. @@ -93,28 +93,28 @@ Swap backends using the `agent.preset` Helm value or manual config. Tested backe ### Helm Install (recommended) ```bash -helm repo add agent-broker https://thepagent.github.io/agent-broker +helm repo add openab https://openabdev.github.io/openab helm repo update # Kiro CLI (default) -helm install agent-broker agent-broker/agent-broker \ +helm install openab openab/openab \ --set discord.botToken="$DISCORD_BOT_TOKEN" \ --set-string discord.allowedChannels[0]="YOUR_CHANNEL_ID" # Codex -helm install agent-broker agent-broker/agent-broker \ +helm install openab openab/openab \ --set discord.botToken="$DISCORD_BOT_TOKEN" \ --set-string discord.allowedChannels[0]="YOUR_CHANNEL_ID" \ --set agent.preset=codex # Claude Code -helm install agent-broker agent-broker/agent-broker \ +helm install openab openab/openab \ --set discord.botToken="$DISCORD_BOT_TOKEN" \ --set-string discord.allowedChannels[0]="YOUR_CHANNEL_ID" \ --set agent.preset=claude # Gemini -helm install agent-broker agent-broker/agent-broker \ +helm install openab openab/openab \ --set discord.botToken="$DISCORD_BOT_TOKEN" \ --set-string discord.allowedChannels[0]="YOUR_CHANNEL_ID" \ --set agent.preset=gemini @@ -124,21 +124,21 @@ Then authenticate inside the pod (first time only): ```bash # Kiro CLI -kubectl exec -it deployment/agent-broker -- kiro-cli login --use-device-flow +kubectl exec -it deployment/openab -- kiro-cli login --use-device-flow # Codex -kubectl exec -it deployment/agent-broker -- codex login --device-auth +kubectl exec -it deployment/openab -- codex login --device-auth # Claude Code -kubectl exec -it deployment/agent-broker -- claude setup-token -# Then: helm upgrade agent-broker agent-broker/agent-broker --set env.CLAUDE_CODE_OAUTH_TOKEN="" +kubectl exec -it deployment/openab -- claude setup-token +# Then: helm upgrade openab openab/openab --set env.CLAUDE_CODE_OAUTH_TOKEN="" # Gemini (Google OAuth — open URL in browser, curl callback from pod) -kubectl exec -it deployment/agent-broker -- gemini -# Or use API key: helm upgrade agent-broker agent-broker/agent-broker --set env.GEMINI_API_KEY="" +kubectl exec -it deployment/openab -- gemini +# Or use API key: helm upgrade openab openab/openab --set env.GEMINI_API_KEY="" ``` -Restart after auth: `kubectl rollout restart deployment agent-broker` +Restart after auth: `kubectl rollout restart deployment openab` ### Manual config.toml @@ -211,7 +211,7 @@ error_hold_ms = 2500 # keep error emoji for 2.5s ## Kubernetes Deployment -The Docker image bundles both `agent-broker` and `kiro-cli` in a single container (agent-broker spawns kiro-cli as a child process). +The Docker image bundles both `openab` and `kiro-cli` in a single container (openab spawns kiro-cli as a child process). ### Pod Architecture @@ -219,7 +219,7 @@ The Docker image bundles both `agent-broker` and `kiro-cli` in a single containe ┌─ Kubernetes Pod ─────────────────────────────────────────────────┐ │ │ │ ┌─────────────────────────────────────────────────────────┐ │ -│ │ agent-broker (main process, PID 1) │ │ +│ │ openab (main process, PID 1) │ │ │ │ │ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌───────────┐ │ │ │ │ │ Discord │ │ Session Pool │ │ Reaction │ │ │ @@ -255,7 +255,7 @@ The Docker image bundles both `agent-broker` and `kiro-cli` in a single containe └──────────────────┘ └──────────────┘ ``` -- **Single container** — agent-broker is PID 1, spawns kiro-cli as a child process +- **Single container** — openab is PID 1, spawns kiro-cli as a child process - **stdio JSON-RPC** — ACP communication over stdin/stdout, no network ports needed - **Session pool** — one kiro-cli process per Discord thread, up to `max_sessions` - **PVC** — persists OAuth tokens and settings across pod restarts @@ -265,30 +265,30 @@ The Docker image bundles both `agent-broker` and `kiro-cli` in a single containe Use one of these prompts with any coding CLI (Kiro CLI, Claude Code, Codex, Gemini, etc.) on the host that has `helm` and `kubectl` access to your cluster: **Kiro CLI (default):** -> Install agent-broker on my local k8s cluster using the Helm chart from https://thepagent.github.io/agent-broker. My Discord bot token is in the environment variable DISCORD_BOT_TOKEN and my channel ID is . After install, follow the NOTES output to authenticate, then restart the deployment. +> Install openab on my local k8s cluster using the Helm chart from https://openabdev.github.io/openab. My Discord bot token is in the environment variable DISCORD_BOT_TOKEN and my channel ID is . After install, follow the NOTES output to authenticate, then restart the deployment. **Codex:** -> Install agent-broker on my local k8s cluster using the Helm chart from https://thepagent.github.io/agent-broker with `--set agent.preset=codex`. My Discord bot token is in the environment variable DISCORD_BOT_TOKEN and my channel ID is . After install, follow the NOTES output to authenticate, then restart the deployment. +> Install openab on my local k8s cluster using the Helm chart from https://openabdev.github.io/openab with `--set agent.preset=codex`. My Discord bot token is in the environment variable DISCORD_BOT_TOKEN and my channel ID is . After install, follow the NOTES output to authenticate, then restart the deployment. **Claude Code:** -> Install agent-broker on my local k8s cluster using the Helm chart from https://thepagent.github.io/agent-broker with `--set agent.preset=claude`. My Discord bot token is in the environment variable DISCORD_BOT_TOKEN and my channel ID is . After install, follow the NOTES output to authenticate, then restart the deployment. +> Install openab on my local k8s cluster using the Helm chart from https://openabdev.github.io/openab with `--set agent.preset=claude`. My Discord bot token is in the environment variable DISCORD_BOT_TOKEN and my channel ID is . After install, follow the NOTES output to authenticate, then restart the deployment. **Gemini:** -> Install agent-broker on my local k8s cluster using the Helm chart from https://thepagent.github.io/agent-broker with `--set agent.preset=gemini`. My Discord bot token is in the environment variable DISCORD_BOT_TOKEN and my channel ID is . After install, follow the NOTES output to authenticate, then restart the deployment. +> Install openab on my local k8s cluster using the Helm chart from https://openabdev.github.io/openab with `--set agent.preset=gemini`. My Discord bot token is in the environment variable DISCORD_BOT_TOKEN and my channel ID is . After install, follow the NOTES output to authenticate, then restart the deployment. ### Build & Push ```bash -docker build -t agent-broker:latest . -docker tag agent-broker:latest /agent-broker:latest -docker push /agent-broker:latest +docker build -t openab:latest . +docker tag openab:latest /openab:latest +docker push /openab:latest ``` ### Deploy ```bash # Create the secret with your bot token -kubectl create secret generic agent-broker-secret \ +kubectl create secret generic openab-secret \ --from-literal=discord-bot-token="your-token" # Edit k8s/configmap.yaml with your channel IDs @@ -302,13 +302,13 @@ kubectl apply -f k8s/deployment.yaml kiro-cli requires a one-time OAuth login. The PVC persists the tokens across pod restarts. ```bash -kubectl exec -it deployment/agent-broker -- kiro-cli login --use-device-flow +kubectl exec -it deployment/openab -- kiro-cli login --use-device-flow ``` Follow the device code flow in your browser, then restart the pod: ```bash -kubectl rollout restart deployment agent-broker +kubectl rollout restart deployment openab ``` ### Manifests @@ -316,7 +316,7 @@ kubectl rollout restart deployment agent-broker | File | Purpose | |------|---------| | `k8s/deployment.yaml` | Single-container pod with config + data volume mounts | -| `k8s/configmap.yaml` | `config.toml` mounted at `/etc/agent-broker/` | +| `k8s/configmap.yaml` | `config.toml` mounted at `/etc/openab/` | | `k8s/secret.yaml` | `DISCORD_BOT_TOKEN` injected as env var | | `k8s/pvc.yaml` | Persistent storage for auth + settings | diff --git a/RELEASING.md b/RELEASING.md index a62976f8..7061d0ec 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -48,9 +48,9 @@ Users running `helm install` only see stable versions. Beta versions require `-- Each build produces three multi-arch images tagged with the git short SHA: ``` -ghcr.io/thepagent/agent-broker: # kiro-cli -ghcr.io/thepagent/agent-broker-codex: # codex -ghcr.io/thepagent/agent-broker-claude: # claude +ghcr.io/openabdev/openab: # kiro-cli +ghcr.io/openabdev/openab-codex: # codex +ghcr.io/openabdev/openab-claude: # claude ``` The `latest` tag always points to the most recent build. diff --git a/charts/agent-broker/Chart.yaml b/charts/openab/Chart.yaml similarity index 89% rename from charts/agent-broker/Chart.yaml rename to charts/openab/Chart.yaml index cfcaa872..ff62efa4 100644 --- a/charts/agent-broker/Chart.yaml +++ b/charts/openab/Chart.yaml @@ -1,5 +1,5 @@ apiVersion: v2 -name: agent-broker +name: openab description: Discord ↔ ACP coding CLI bridge (Kiro CLI, Claude Code, Codex, Gemini) type: application version: 0.3.3 diff --git a/charts/agent-broker/templates/NOTES.txt b/charts/openab/templates/NOTES.txt similarity index 52% rename from charts/agent-broker/templates/NOTES.txt rename to charts/openab/templates/NOTES.txt index c332fe1e..21567d81 100644 --- a/charts/agent-broker/templates/NOTES.txt +++ b/charts/openab/templates/NOTES.txt @@ -1,50 +1,50 @@ -agent-broker {{ .Chart.AppVersion }} has been installed! +openab {{ .Chart.AppVersion }} has been installed! ⚠️ Discord channel IDs must be set with --set-string (not --set) to avoid float64 precision loss: - helm upgrade {{ .Release.Name }} agent-broker/agent-broker \ + helm upgrade {{ .Release.Name }} openab/openab \ --set-string discord.allowedChannels[0]="" {{- if not .Values.discord.botToken }} ⚠️ No bot token was provided. Create the secret manually: - kubectl create secret generic {{ include "agent-broker.fullname" . }} \ + kubectl create secret generic {{ include "openab.fullname" . }} \ --from-literal=discord-bot-token="YOUR_TOKEN" {{- end }} -{{- $cmd := include "agent-broker.agent.command" . | trim }} +{{- $cmd := include "openab.agent.command" . | trim }} {{- if eq $cmd "kiro-cli" }} Authenticate kiro-cli (first time only): - kubectl exec -it deployment/{{ include "agent-broker.fullname" . }} -- kiro-cli login --use-device-flow + kubectl exec -it deployment/{{ include "openab.fullname" . }} -- kiro-cli login --use-device-flow {{- else if eq $cmd "codex-acp" }} Authenticate Codex (first time only): - kubectl exec -it deployment/{{ include "agent-broker.fullname" . }} -- codex login --device-auth + kubectl exec -it deployment/{{ include "openab.fullname" . }} -- codex login --device-auth {{- else if eq $cmd "claude-agent-acp" }} Authenticate Claude Code (first time only): - kubectl exec -it deployment/{{ include "agent-broker.fullname" . }} -- claude setup-token + kubectl exec -it deployment/{{ include "openab.fullname" . }} -- claude setup-token Then set the token as an env var (token is valid for 1 year): - helm upgrade {{ .Release.Name }} agent-broker/agent-broker --set env.CLAUDE_CODE_OAUTH_TOKEN="" + helm upgrade {{ .Release.Name }} openab/openab --set env.CLAUDE_CODE_OAUTH_TOKEN="" {{- else if eq $cmd "gemini" }} Authenticate Gemini CLI (first time only): - kubectl exec -it deployment/{{ include "agent-broker.fullname" . }} -- gemini + kubectl exec -it deployment/{{ include "openab.fullname" . }} -- gemini Select "Sign in with Google", open the URL in your browser, then curl the localhost callback URL from within the pod. Alternatively, set GEMINI_API_KEY: - helm upgrade {{ .Release.Name }} agent-broker/agent-broker --set env.GEMINI_API_KEY="" + helm upgrade {{ .Release.Name }} openab/openab --set env.GEMINI_API_KEY="" {{- else }} Authenticate your agent CLI (first time only) — refer to your CLI's documentation. @@ -52,4 +52,4 @@ Authenticate your agent CLI (first time only) — refer to your CLI's documentat Then restart the pod: - kubectl rollout restart deployment/{{ include "agent-broker.fullname" . }} + kubectl rollout restart deployment/{{ include "openab.fullname" . }} diff --git a/charts/agent-broker/templates/_helpers.tpl b/charts/openab/templates/_helpers.tpl similarity index 69% rename from charts/agent-broker/templates/_helpers.tpl rename to charts/openab/templates/_helpers.tpl index 852c50f3..9a0e6bea 100644 --- a/charts/agent-broker/templates/_helpers.tpl +++ b/charts/openab/templates/_helpers.tpl @@ -1,8 +1,8 @@ -{{- define "agent-broker.name" -}} +{{- define "openab.name" -}} {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} {{- end }} -{{- define "agent-broker.fullname" -}} +{{- define "openab.fullname" -}} {{- if .Values.fullnameOverride }} {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} {{- else }} @@ -15,32 +15,32 @@ {{- end }} {{- end }} -{{- define "agent-broker.chart" -}} +{{- define "openab.chart" -}} {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} {{- end }} -{{- define "agent-broker.labels" -}} -helm.sh/chart: {{ include "agent-broker.chart" . }} -{{ include "agent-broker.selectorLabels" . }} +{{- define "openab.labels" -}} +helm.sh/chart: {{ include "openab.chart" . }} +{{ include "openab.selectorLabels" . }} {{- if .Chart.AppVersion }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} {{- end }} app.kubernetes.io/managed-by: {{ .Release.Service }} {{- end }} -{{- define "agent-broker.selectorLabels" -}} -app.kubernetes.io/name: {{ include "agent-broker.name" . }} +{{- define "openab.selectorLabels" -}} +app.kubernetes.io/name: {{ include "openab.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} {{/* Resolve agent preset → image repository */}} -{{- define "agent-broker.image.repository" -}} +{{- define "openab.image.repository" -}} {{- if .Values.agent.preset }} - {{- if eq .Values.agent.preset "codex" }}ghcr.io/thepagent/agent-broker-codex - {{- else if eq .Values.agent.preset "claude" }}ghcr.io/thepagent/agent-broker-claude - {{- else if eq .Values.agent.preset "gemini" }}ghcr.io/thepagent/agent-broker-gemini + {{- if eq .Values.agent.preset "codex" }}ghcr.io/openabdev/openab-codex + {{- else if eq .Values.agent.preset "claude" }}ghcr.io/openabdev/openab-claude + {{- else if eq .Values.agent.preset "gemini" }}ghcr.io/openabdev/openab-gemini {{- else }}{{ .Values.image.repository }} {{- end }} {{- else }}{{ .Values.image.repository }} @@ -50,7 +50,7 @@ Resolve agent preset → image repository {{/* Resolve agent preset → command */}} -{{- define "agent-broker.agent.command" -}} +{{- define "openab.agent.command" -}} {{- if .Values.agent.preset }} {{- if eq .Values.agent.preset "codex" }}codex-acp {{- else if eq .Values.agent.preset "claude" }}claude-agent-acp @@ -64,7 +64,7 @@ Resolve agent preset → command {{/* Resolve agent preset → args */}} -{{- define "agent-broker.agent.args" -}} +{{- define "openab.agent.args" -}} {{- if .Values.agent.preset }} {{- if or (eq .Values.agent.preset "codex") (eq .Values.agent.preset "claude") }}[] {{- else if eq .Values.agent.preset "gemini" }}["--acp"] diff --git a/charts/agent-broker/templates/configmap.yaml b/charts/openab/templates/configmap.yaml similarity index 82% rename from charts/agent-broker/templates/configmap.yaml rename to charts/openab/templates/configmap.yaml index 3cbd3244..e719c92c 100644 --- a/charts/agent-broker/templates/configmap.yaml +++ b/charts/openab/templates/configmap.yaml @@ -1,9 +1,9 @@ apiVersion: v1 kind: ConfigMap metadata: - name: {{ include "agent-broker.fullname" . }} + name: {{ include "openab.fullname" . }} labels: - {{- include "agent-broker.labels" . | nindent 4 }} + {{- include "openab.labels" . | nindent 4 }} data: config.toml: | [discord] @@ -16,8 +16,8 @@ data: allowed_channels = [{{ range $i, $ch := .Values.discord.allowedChannels }}{{ if $i }}, {{ end }}"{{ $ch }}"{{ end }}] [agent] - command = "{{ include "agent-broker.agent.command" . | trim }}" - args = {{ include "agent-broker.agent.args" . | trim }} + command = "{{ include "openab.agent.command" . | trim }}" + args = {{ include "openab.agent.args" . | trim }} working_dir = "{{ .Values.agent.workingDir }}" {{- if .Values.agent.env }} env = { {{ range $k, $v := .Values.agent.env }}{{ $k }} = "{{ $v }}", {{ end }} } diff --git a/charts/agent-broker/templates/deployment.yaml b/charts/openab/templates/deployment.yaml similarity index 76% rename from charts/agent-broker/templates/deployment.yaml rename to charts/openab/templates/deployment.yaml index 8335d3b1..023ec727 100644 --- a/charts/agent-broker/templates/deployment.yaml +++ b/charts/openab/templates/deployment.yaml @@ -1,9 +1,9 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: {{ include "agent-broker.fullname" . }} + name: {{ include "openab.fullname" . }} labels: - {{- include "agent-broker.labels" . | nindent 4 }} + {{- include "openab.labels" . | nindent 4 }} spec: replicas: {{ .Values.replicas }} {{- with .Values.strategy }} @@ -12,23 +12,23 @@ spec: {{- end }} selector: matchLabels: - {{- include "agent-broker.selectorLabels" . | nindent 6 }} + {{- include "openab.selectorLabels" . | nindent 6 }} template: metadata: annotations: checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} labels: - {{- include "agent-broker.selectorLabels" . | nindent 8 }} + {{- include "openab.selectorLabels" . | nindent 8 }} spec: containers: - - name: agent-broker - image: "{{ include "agent-broker.image.repository" . | trim }}:{{ .Values.image.tag }}" + - name: openab + image: "{{ include "openab.image.repository" . | trim }}:{{ .Values.image.tag }}" imagePullPolicy: {{ .Values.image.pullPolicy }} env: - name: DISCORD_BOT_TOKEN valueFrom: secretKeyRef: - name: {{ include "agent-broker.fullname" . }} + name: {{ include "openab.fullname" . }} key: discord-bot-token - name: HOME value: /home/agent @@ -46,7 +46,7 @@ spec: {{- end }} volumeMounts: - name: config - mountPath: /etc/agent-broker + mountPath: /etc/openab readOnly: true {{- if .Values.persistence.enabled }} - name: data @@ -72,9 +72,9 @@ spec: volumes: - name: config configMap: - name: {{ include "agent-broker.fullname" . }} + name: {{ include "openab.fullname" . }} {{- if .Values.persistence.enabled }} - name: data persistentVolumeClaim: - claimName: {{ include "agent-broker.fullname" . }} + claimName: {{ include "openab.fullname" . }} {{- end }} diff --git a/charts/agent-broker/templates/pvc.yaml b/charts/openab/templates/pvc.yaml similarity index 76% rename from charts/agent-broker/templates/pvc.yaml rename to charts/openab/templates/pvc.yaml index cf28384b..6e720698 100644 --- a/charts/agent-broker/templates/pvc.yaml +++ b/charts/openab/templates/pvc.yaml @@ -2,9 +2,9 @@ apiVersion: v1 kind: PersistentVolumeClaim metadata: - name: {{ include "agent-broker.fullname" . }} + name: {{ include "openab.fullname" . }} labels: - {{- include "agent-broker.labels" . | nindent 4 }} + {{- include "openab.labels" . | nindent 4 }} spec: accessModes: - ReadWriteOnce diff --git a/charts/agent-broker/templates/secret.yaml b/charts/openab/templates/secret.yaml similarity index 69% rename from charts/agent-broker/templates/secret.yaml rename to charts/openab/templates/secret.yaml index 13f43be2..2096730a 100644 --- a/charts/agent-broker/templates/secret.yaml +++ b/charts/openab/templates/secret.yaml @@ -2,9 +2,9 @@ apiVersion: v1 kind: Secret metadata: - name: {{ include "agent-broker.fullname" . }} + name: {{ include "openab.fullname" . }} labels: - {{- include "agent-broker.labels" . | nindent 4 }} + {{- include "openab.labels" . | nindent 4 }} annotations: "helm.sh/resource-policy": keep type: Opaque diff --git a/charts/agent-broker/values.yaml b/charts/openab/values.yaml similarity index 95% rename from charts/agent-broker/values.yaml rename to charts/openab/values.yaml index 7bcfafd8..59ed4d1d 100644 --- a/charts/agent-broker/values.yaml +++ b/charts/openab/values.yaml @@ -1,5 +1,5 @@ image: - repository: ghcr.io/thepagent/agent-broker + repository: ghcr.io/openabdev/openab tag: "dd7e1ca" pullPolicy: IfNotPresent diff --git a/docs/discord-bot-howto.md b/docs/discord-bot-howto.md index c2e189cf..672ac8c9 100644 --- a/docs/discord-bot-howto.md +++ b/docs/discord-bot-howto.md @@ -1,6 +1,6 @@ # Discord Bot Setup Guide -Step-by-step guide to create and configure a Discord bot for agent-broker. +Step-by-step guide to create and configure a Discord bot for openab. ## 1. Create a Discord Application @@ -47,7 +47,7 @@ Step-by-step guide to create and configure a Discord bot for agent-broker. 3. Click **Copy Channel ID** 4. Use this ID in `allowed_channels` in your config -## 7. Configure agent-broker +## 7. Configure openab Set the bot token and channel ID: @@ -65,7 +65,7 @@ allowed_channels = ["your-channel-id-from-step-6"] For Kubernetes: ```bash -kubectl create secret generic agent-broker-secret \ +kubectl create secret generic openab-secret \ --from-literal=discord-bot-token="your-token-from-step-3" ``` diff --git a/k8s/configmap.yaml b/k8s/configmap.yaml index f180017e..79f9f791 100644 --- a/k8s/configmap.yaml +++ b/k8s/configmap.yaml @@ -1,7 +1,7 @@ apiVersion: v1 kind: ConfigMap metadata: - name: agent-broker-config + name: openab-config data: config.toml: | [discord] diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml index a3574ea7..50f3fd5d 100644 --- a/k8s/deployment.yaml +++ b/k8s/deployment.yaml @@ -1,36 +1,36 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: agent-broker + name: openab labels: - app: agent-broker + app: openab spec: replicas: 1 strategy: type: Recreate # PVC is ReadWriteOnce selector: matchLabels: - app: agent-broker + app: openab template: metadata: labels: - app: agent-broker + app: openab spec: containers: - - name: agent-broker - image: agent-broker:latest + - name: openab + image: openab:latest imagePullPolicy: Never env: - name: DISCORD_BOT_TOKEN valueFrom: secretKeyRef: - name: agent-broker-secret + name: openab-secret key: discord-bot-token - name: HOME value: /home/agent volumeMounts: - name: config - mountPath: /etc/agent-broker + mountPath: /etc/openab readOnly: true - name: data mountPath: /home/agent/.kiro @@ -41,7 +41,7 @@ spec: volumes: - name: config configMap: - name: agent-broker-config + name: openab-config - name: data persistentVolumeClaim: - claimName: agent-broker-data + claimName: openab-data diff --git a/k8s/pvc.yaml b/k8s/pvc.yaml index 1df2a630..0f25a3c0 100644 --- a/k8s/pvc.yaml +++ b/k8s/pvc.yaml @@ -1,7 +1,7 @@ apiVersion: v1 kind: PersistentVolumeClaim metadata: - name: agent-broker-data + name: openab-data spec: accessModes: - ReadWriteOnce diff --git a/k8s/secret.yaml b/k8s/secret.yaml index 2fd3e91e..914116b8 100644 --- a/k8s/secret.yaml +++ b/k8s/secret.yaml @@ -1,7 +1,7 @@ apiVersion: v1 kind: Secret metadata: - name: agent-broker-secret + name: openab-secret type: Opaque stringData: discord-bot-token: "REPLACE_ME" diff --git a/src/acp/connection.rs b/src/acp/connection.rs index 991ed9f2..9fc5a1f1 100644 --- a/src/acp/connection.rs +++ b/src/acp/connection.rs @@ -209,7 +209,7 @@ impl AcpConnection { Some(json!({ "protocolVersion": 1, "clientCapabilities": {}, - "clientInfo": {"name": "agent-broker", "version": "0.1.0"}, + "clientInfo": {"name": "openab", "version": "0.1.0"}, })), ) .await?; diff --git a/src/discord.rs b/src/discord.rs index 8cb60e15..da52c691 100644 --- a/src/discord.rs +++ b/src/discord.rs @@ -78,7 +78,7 @@ impl EventHandler for Handler { .and_then(|m| m.nick.as_ref()) .unwrap_or(&msg.author.name); let sender_ctx = serde_json::json!({ - "schema": "agent-broker.sender.v1", + "schema": "openab.sender.v1", "sender_id": msg.author.id.to_string(), "sender_name": msg.author.name, "display_name": display_name, diff --git a/src/main.rs b/src/main.rs index 32588966..4d6e6c30 100644 --- a/src/main.rs +++ b/src/main.rs @@ -81,6 +81,6 @@ async fn main() -> anyhow::Result<()> { // Cleanup cleanup_handle.abort(); shutdown_pool.shutdown().await; - info!("agent-broker shut down"); + info!("openab shut down"); Ok(()) } From 1a193485ce5e67bd8f4f7d5adce23cb6e64fe21a Mon Sep 17 00:00:00 2001 From: thepagent Date: Tue, 7 Apr 2026 11:29:47 +0800 Subject: [PATCH 32/80] =?UTF-8?q?chore:=20update=20workflow=20files=20for?= =?UTF-8?q?=20agent-broker=20=E2=86=92=20openab=20rename=20(#98)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #97 Co-authored-by: thepagent --- .github/workflows/build.yml | 12 ++++++------ .github/workflows/release.yml | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 353f5eb5..a6ea0bb9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -164,7 +164,7 @@ jobs: - name: Get current chart version id: current run: | - chart_version=$(grep '^version:' charts/agent-broker/Chart.yaml | awk '{print $2}') + chart_version=$(grep '^version:' charts/openab/Chart.yaml | awk '{print $2}') echo "chart_version=$chart_version" >> "$GITHUB_OUTPUT" - name: Bump chart version @@ -202,10 +202,10 @@ jobs: - name: Update Chart.yaml and values.yaml run: | IMAGE_SHA="${{ steps.image-sha.outputs.sha }}" - sed -i "s/^version: .*/version: ${{ steps.bump.outputs.new_version }}/" charts/agent-broker/Chart.yaml - sed -i "s/^appVersion: .*/appVersion: \"${IMAGE_SHA}\"/" charts/agent-broker/Chart.yaml - sed -i "s|repository: .*|repository: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}|" charts/agent-broker/values.yaml - sed -i "s/tag: .*/tag: \"${IMAGE_SHA}\"/" charts/agent-broker/values.yaml + sed -i "s/^version: .*/version: ${{ steps.bump.outputs.new_version }}/" charts/openab/Chart.yaml + sed -i "s/^appVersion: .*/appVersion: \"${IMAGE_SHA}\"/" charts/openab/Chart.yaml + sed -i "s|repository: .*|repository: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}|" charts/openab/values.yaml + sed -i "s/tag: .*/tag: \"${IMAGE_SHA}\"/" charts/openab/values.yaml - name: Create and auto-merge bump PR env: @@ -217,7 +217,7 @@ jobs: git config user.name "openclaw-helm-bot[bot]" git config user.email "3185992+openclaw-helm-bot[bot]@users.noreply.github.com" git checkout -b "$BRANCH" - git add charts/agent-broker/Chart.yaml charts/agent-broker/values.yaml + git add charts/openab/Chart.yaml charts/openab/values.yaml git commit -m "chore: bump chart to ${VERSION} image: ${IMAGE_SHA}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1c74c53a..534bb7ab 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,7 +6,7 @@ on: branches: - main paths: - - "charts/agent-broker/Chart.yaml" + - "charts/openab/Chart.yaml" jobs: release: @@ -45,7 +45,7 @@ jobs: - name: Push chart to OCI registry run: | - CHART=charts/agent-broker + CHART=charts/openab NAME=$(grep '^name:' ${CHART}/Chart.yaml | awk '{print $2}') VERSION=$(grep '^version:' ${CHART}/Chart.yaml | awk '{print $2}') helm package ${CHART} @@ -57,7 +57,7 @@ jobs: run: | OWNER="${{ github.repository_owner }}" REPO="${{ github.event.repository.name }}" - CHART=charts/agent-broker + CHART=charts/openab NAME=$(grep '^name:' ${CHART}/Chart.yaml | awk '{print $2}') VERSION=$(grep '^version:' ${CHART}/Chart.yaml | awk '{print $2}') APP_VERSION=$(grep '^appVersion:' ${CHART}/Chart.yaml | awk '{print $2}' | tr -d '"') @@ -78,14 +78,14 @@ jobs: ### Helm Repository (GitHub Pages) \`\`\`bash - helm repo add agent-broker https://${OWNER}.github.io/${REPO} + helm repo add openab https://${OWNER}.github.io/${REPO} helm repo update - helm install agent-broker agent-broker/agent-broker --version ${VERSION} + helm install openab openab/openab --version ${VERSION} \`\`\` ### OCI Registry \`\`\`bash - helm install agent-broker oci://ghcr.io/${OWNER}/charts/agent-broker --version ${VERSION} + helm install openab oci://ghcr.io/${OWNER}/charts/openab --version ${VERSION} \`\`\` EOF From b672373a713a04661c07b1e262ff8842c2b5181c Mon Sep 17 00:00:00 2001 From: "openab-app[bot]" <274185012+openab-app[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 13:58:07 +0800 Subject: [PATCH 33/80] chore: bump chart to 0.3.4-beta.28 (#101) image: 1a19348 Co-authored-by: openclaw-helm-bot[bot] <3185992+openclaw-helm-bot[bot]@users.noreply.github.com> --- charts/openab/Chart.yaml | 4 ++-- charts/openab/values.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/charts/openab/Chart.yaml b/charts/openab/Chart.yaml index ff62efa4..ec61b68b 100644 --- a/charts/openab/Chart.yaml +++ b/charts/openab/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: openab description: Discord ↔ ACP coding CLI bridge (Kiro CLI, Claude Code, Codex, Gemini) type: application -version: 0.3.3 -appVersion: "dd7e1ca" +version: 0.3.4-beta.28 +appVersion: "699f7d9" diff --git a/charts/openab/values.yaml b/charts/openab/values.yaml index 59ed4d1d..42fac26a 100644 --- a/charts/openab/values.yaml +++ b/charts/openab/values.yaml @@ -1,6 +1,6 @@ image: repository: ghcr.io/openabdev/openab - tag: "dd7e1ca" + tag: "699f7d9" pullPolicy: IfNotPresent replicas: 1 From 9ef6d9e499ca05cac12b8cc9339bd79f33f62175 Mon Sep 17 00:00:00 2001 From: thepagent Date: Tue, 7 Apr 2026 14:01:18 +0800 Subject: [PATCH 34/80] chore: update bot identity to openab-app[bot] (#102) Co-authored-by: thepagent --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a6ea0bb9..8b1c4c70 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -214,8 +214,8 @@ jobs: VERSION="${{ steps.bump.outputs.new_version }}" IMAGE_SHA="${{ steps.image-sha.outputs.sha }}" BRANCH="chore/chart-${VERSION}" - git config user.name "openclaw-helm-bot[bot]" - git config user.email "3185992+openclaw-helm-bot[bot]@users.noreply.github.com" + git config user.name "openab-app[bot]" + git config user.email "274185012+openab-app[bot]@users.noreply.github.com" git checkout -b "$BRANCH" git add charts/openab/Chart.yaml charts/openab/values.yaml git commit -m "chore: bump chart to ${VERSION} From 18fe4cb4b16044b3a3b22c72d78d0dd45dfce1e2 Mon Sep 17 00:00:00 2001 From: thepagent Date: Tue, 7 Apr 2026 19:13:18 +0800 Subject: [PATCH 35/80] fix: harden Dockerfiles with non-root user, HEALTHCHECK, and securityContext (#73) - Dockerfile: useradd -u 1000, --chown=agent:agent, curl --retry, HEALTHCHECK - Dockerfile.claude/codex/gemini: use built-in node user /home/node, --chown=node:node, npm --retry, HEALTHCHECK - Helm chart: podSecurityContext + containerSecurityContext, preset-aware home helper - k8s manifests: pod + container securityContext Co-authored-by: thepagent --- Dockerfile | 19 ++++++++++++++++--- Dockerfile.claude | 20 +++++++++++++++----- Dockerfile.codex | 20 +++++++++++++++----- Dockerfile.gemini | 20 +++++++++++++++----- charts/openab/templates/_helpers.tpl | 9 +++++++++ charts/openab/templates/configmap.yaml | 2 +- charts/openab/templates/deployment.yaml | 14 +++++++++++--- charts/openab/values.yaml | 12 ++++++++++++ k8s/deployment.yaml | 10 ++++++++++ 9 files changed, 104 insertions(+), 22 deletions(-) diff --git a/Dockerfile b/Dockerfile index 740d7b4a..fdb14e2a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,17 +14,30 @@ RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates RUN ARCH=$(dpkg --print-architecture) && \ if [ "$ARCH" = "arm64" ]; then URL="https://desktop-release.q.us-east-1.amazonaws.com/latest/kirocli-aarch64-linux.zip"; \ else URL="https://desktop-release.q.us-east-1.amazonaws.com/latest/kirocli-x86_64-linux.zip"; fi && \ - curl --proto '=https' --tlsv1.2 -sSf "$URL" -o /tmp/kirocli.zip && \ + curl --proto '=https' --tlsv1.2 -sSf --retry 3 --retry-delay 5 "$URL" -o /tmp/kirocli.zip && \ unzip /tmp/kirocli.zip -d /tmp && \ cp /tmp/kirocli/bin/* /usr/local/bin/ && \ chmod +x /usr/local/bin/kiro-cli* && \ rm -rf /tmp/kirocli /tmp/kirocli.zip -RUN mkdir -p /home/agent/.local/share/kiro-cli /home/agent/.kiro +# 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 useradd -m -s /bin/bash -u 1000 agent +RUN mkdir -p /home/agent/.local/share/kiro-cli /home/agent/.kiro && \ + chown -R agent:agent /home/agent ENV HOME=/home/agent WORKDIR /home/agent -COPY --from=builder /build/target/release/openab /usr/local/bin/openab +COPY --from=builder --chown=agent:agent /build/target/release/openab /usr/local/bin/openab +USER agent +HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ + CMD pgrep -x openab || exit 1 ENTRYPOINT ["openab"] CMD ["/etc/openab/config.toml"] diff --git a/Dockerfile.claude b/Dockerfile.claude index 61eea700..2c8b90ab 100644 --- a/Dockerfile.claude +++ b/Dockerfile.claude @@ -11,13 +11,23 @@ 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 claude-agent-acp adapter and Claude Code CLI -RUN npm install -g @agentclientprotocol/claude-agent-acp@0.25.0 @anthropic-ai/claude-code +RUN npm install -g @agentclientprotocol/claude-agent-acp@0.25.0 @anthropic-ai/claude-code --retry 3 -RUN mkdir -p /home/agent && chown node:node /home/agent -ENV HOME=/home/agent -WORKDIR /home/agent +# 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/* -COPY --from=builder /build/target/release/openab /usr/local/bin/openab +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"] diff --git a/Dockerfile.codex b/Dockerfile.codex index 4d4f0186..b7ab4921 100644 --- a/Dockerfile.codex +++ b/Dockerfile.codex @@ -11,13 +11,23 @@ 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/* # Pre-install codex-acp and codex CLI globally -RUN npm install -g @zed-industries/codex-acp@0.9.5 @openai/codex +RUN npm install -g @zed-industries/codex-acp@0.9.5 @openai/codex --retry 3 -RUN mkdir -p /home/agent && chown node:node /home/agent -ENV HOME=/home/agent -WORKDIR /home/agent +# 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/* -COPY --from=builder /build/target/release/openab /usr/local/bin/openab +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"] diff --git a/Dockerfile.gemini b/Dockerfile.gemini index 1ad797f8..a5ce9201 100644 --- a/Dockerfile.gemini +++ b/Dockerfile.gemini @@ -11,13 +11,23 @@ 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 Gemini CLI (native ACP support via --acp) -RUN npm install -g @google/gemini-cli +RUN npm install -g @google/gemini-cli --retry 3 -RUN mkdir -p /home/agent && chown node:node /home/agent -ENV HOME=/home/agent -WORKDIR /home/agent +# 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/* -COPY --from=builder /build/target/release/openab /usr/local/bin/openab +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"] diff --git a/charts/openab/templates/_helpers.tpl b/charts/openab/templates/_helpers.tpl index 9a0e6bea..9dbea5fd 100644 --- a/charts/openab/templates/_helpers.tpl +++ b/charts/openab/templates/_helpers.tpl @@ -73,3 +73,12 @@ Resolve agent preset → args {{- else }}{{ .Values.agent.args | toJson }} {{- end }} {{- end }} + +{{/* +Resolve agent preset → home directory +*/}} +{{- define "openab.agent.home" -}} +{{- if and .Values.agent.preset (or (eq .Values.agent.preset "codex") (eq .Values.agent.preset "claude") (eq .Values.agent.preset "gemini")) }}/home/node +{{- else }}/home/agent +{{- end }} +{{- end }} diff --git a/charts/openab/templates/configmap.yaml b/charts/openab/templates/configmap.yaml index e719c92c..44b99a01 100644 --- a/charts/openab/templates/configmap.yaml +++ b/charts/openab/templates/configmap.yaml @@ -18,7 +18,7 @@ data: [agent] command = "{{ include "openab.agent.command" . | trim }}" args = {{ include "openab.agent.args" . | trim }} - working_dir = "{{ .Values.agent.workingDir }}" + working_dir = "{{ include "openab.agent.home" . | trim }}" {{- if .Values.agent.env }} env = { {{ range $k, $v := .Values.agent.env }}{{ $k }} = "{{ $v }}", {{ end }} } {{- end }} diff --git a/charts/openab/templates/deployment.yaml b/charts/openab/templates/deployment.yaml index 023ec727..483bb775 100644 --- a/charts/openab/templates/deployment.yaml +++ b/charts/openab/templates/deployment.yaml @@ -20,10 +20,18 @@ spec: labels: {{- include "openab.selectorLabels" . | nindent 8 }} spec: + {{- with .Values.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} containers: - name: openab image: "{{ include "openab.image.repository" . | trim }}:{{ .Values.image.tag }}" imagePullPolicy: {{ .Values.image.pullPolicy }} + {{- with .Values.containerSecurityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} env: - name: DISCORD_BOT_TOKEN valueFrom: @@ -31,7 +39,7 @@ spec: name: {{ include "openab.fullname" . }} key: discord-bot-token - name: HOME - value: /home/agent + value: {{ include "openab.agent.home" . | trim }} {{- range $key, $value := .Values.env }} - name: {{ $key }} value: {{ $value | quote }} @@ -50,11 +58,11 @@ spec: readOnly: true {{- if .Values.persistence.enabled }} - name: data - mountPath: /home/agent + mountPath: {{ include "openab.agent.home" . | trim }} {{- end }} {{- if .Values.agentsMd }} - name: config - mountPath: /home/agent/AGENTS.md + mountPath: {{ include "openab.agent.home" . | trim }}/AGENTS.md subPath: AGENTS.md {{- end }} {{- with .Values.nodeSelector }} diff --git a/charts/openab/values.yaml b/charts/openab/values.yaml index 42fac26a..7295cbe6 100644 --- a/charts/openab/values.yaml +++ b/charts/openab/values.yaml @@ -10,6 +10,18 @@ strategy: resources: {} +podSecurityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + +containerSecurityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + persistence: enabled: true storageClass: "" diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml index 50f3fd5d..cb12c2ba 100644 --- a/k8s/deployment.yaml +++ b/k8s/deployment.yaml @@ -16,10 +16,20 @@ spec: labels: app: openab spec: + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 containers: - name: openab image: openab:latest imagePullPolicy: Never + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL env: - name: DISCORD_BOT_TOKEN valueFrom: From 114e41b740c49a0d24e07cfcfd87bdfe3de36272 Mon Sep 17 00:00:00 2001 From: "openab-app[bot]" <274185012+openab-app[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 11:18:20 +0000 Subject: [PATCH 36/80] chore: bump chart to 0.3.5-beta.29 (#110) image: 18fe4cb Co-authored-by: openab-app[bot] <274185012+openab-app[bot]@users.noreply.github.com> --- charts/openab/Chart.yaml | 4 ++-- charts/openab/values.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/charts/openab/Chart.yaml b/charts/openab/Chart.yaml index ec61b68b..5065e03b 100644 --- a/charts/openab/Chart.yaml +++ b/charts/openab/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: openab description: Discord ↔ ACP coding CLI bridge (Kiro CLI, Claude Code, Codex, Gemini) type: application -version: 0.3.4-beta.28 -appVersion: "699f7d9" +version: 0.3.5-beta.29 +appVersion: "5fdc891" diff --git a/charts/openab/values.yaml b/charts/openab/values.yaml index 7295cbe6..8c3de5cc 100644 --- a/charts/openab/values.yaml +++ b/charts/openab/values.yaml @@ -1,6 +1,6 @@ image: repository: ghcr.io/openabdev/openab - tag: "699f7d9" + tag: "5fdc891" pullPolicy: IfNotPresent replicas: 1 From cdf0c45659634f3639fd759e30733584db877e02 Mon Sep 17 00:00:00 2001 From: "openab-app[bot]" <274185012+openab-app[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 11:49:41 +0000 Subject: [PATCH 37/80] chore: bump chart to 0.4.0-beta.30 (#112) image: 114e41b Co-authored-by: openab-app[bot] <274185012+openab-app[bot]@users.noreply.github.com> --- charts/openab/Chart.yaml | 4 ++-- charts/openab/values.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/charts/openab/Chart.yaml b/charts/openab/Chart.yaml index 5065e03b..00048fec 100644 --- a/charts/openab/Chart.yaml +++ b/charts/openab/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: openab description: Discord ↔ ACP coding CLI bridge (Kiro CLI, Claude Code, Codex, Gemini) type: application -version: 0.3.5-beta.29 -appVersion: "5fdc891" +version: 0.4.0-beta.30 +appVersion: "59d052f" diff --git a/charts/openab/values.yaml b/charts/openab/values.yaml index 8c3de5cc..496fe791 100644 --- a/charts/openab/values.yaml +++ b/charts/openab/values.yaml @@ -1,6 +1,6 @@ image: repository: ghcr.io/openabdev/openab - tag: "5fdc891" + tag: "59d052f" pullPolicy: IfNotPresent replicas: 1 From e9729e72be094d161051068873adfcf476aacb92 Mon Sep 17 00:00:00 2001 From: "openab-app[bot]" <274185012+openab-app[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 19:52:44 +0800 Subject: [PATCH 38/80] chore: bump chart to 0.5.0 (#113) image: cdf0c45 Co-authored-by: openab-app[bot] <274185012+openab-app[bot]@users.noreply.github.com> --- charts/openab/Chart.yaml | 4 ++-- charts/openab/values.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/charts/openab/Chart.yaml b/charts/openab/Chart.yaml index 00048fec..bbfb5dc8 100644 --- a/charts/openab/Chart.yaml +++ b/charts/openab/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: openab description: Discord ↔ ACP coding CLI bridge (Kiro CLI, Claude Code, Codex, Gemini) type: application -version: 0.4.0-beta.30 -appVersion: "59d052f" +version: 0.5.0 +appVersion: "9c70cdd" diff --git a/charts/openab/values.yaml b/charts/openab/values.yaml index 496fe791..b6f857e7 100644 --- a/charts/openab/values.yaml +++ b/charts/openab/values.yaml @@ -1,6 +1,6 @@ image: repository: ghcr.io/openabdev/openab - tag: "59d052f" + tag: "9c70cdd" pullPolicy: IfNotPresent replicas: 1 From b500bf603a9fd8d65501bb95f0f08783b5c618dd Mon Sep 17 00:00:00 2001 From: Neil Kuan <46012524+neilkuan@users.noreply.github.com> Date: Tue, 7 Apr 2026 22:47:12 +0800 Subject: [PATCH 39/80] fix: correct default tracing filter to match crate name (#115) * fix: correct default tracing filter to match crate name The default env filter used `agent_broker=info` but the crate is named `openab`, so no logs were emitted without explicitly setting RUST_LOG. * fix: restore openab package definition in Cargo.lock --- Cargo.lock | 34 +++++++++++++++++----------------- src/main.rs | 2 +- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 53acb5ab..7fe18255 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,23 +8,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" -[[package]] -name = "openab" -version = "0.1.0" -dependencies = [ - "anyhow", - "rand 0.8.5", - "regex", - "serde", - "serde_json", - "serenity", - "tokio", - "toml", - "tracing", - "tracing-subscriber", - "uuid", -] - [[package]] name = "aho-corasick" version = "1.1.4" @@ -776,6 +759,23 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "openab" +version = "0.1.0" +dependencies = [ + "anyhow", + "rand 0.8.5", + "regex", + "serde", + "serde_json", + "serenity", + "tokio", + "toml", + "tracing", + "tracing-subscriber", + "uuid", +] + [[package]] name = "parking_lot" version = "0.12.5" diff --git a/src/main.rs b/src/main.rs index 4d6e6c30..a216b668 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,7 +15,7 @@ async fn main() -> anyhow::Result<()> { tracing_subscriber::fmt() .with_env_filter( tracing_subscriber::EnvFilter::try_from_default_env() - .unwrap_or_else(|_| "agent_broker=info".into()), + .unwrap_or_else(|_| "openab=info".into()), ) .init(); From 8446e028730d1f0a02342e31f628b5a877e51147 Mon Sep 17 00:00:00 2001 From: "openab-app[bot]" <274185012+openab-app[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 22:57:04 +0800 Subject: [PATCH 40/80] chore: bump chart to 0.5.1 (#119) image: b500bf6 Co-authored-by: openab-app[bot] <274185012+openab-app[bot]@users.noreply.github.com> --- charts/openab/Chart.yaml | 4 ++-- charts/openab/values.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/charts/openab/Chart.yaml b/charts/openab/Chart.yaml index bbfb5dc8..5e41536b 100644 --- a/charts/openab/Chart.yaml +++ b/charts/openab/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: openab description: Discord ↔ ACP coding CLI bridge (Kiro CLI, Claude Code, Codex, Gemini) type: application -version: 0.5.0 -appVersion: "9c70cdd" +version: 0.5.1 +appVersion: "78f8d2c" diff --git a/charts/openab/values.yaml b/charts/openab/values.yaml index b6f857e7..bf6c0860 100644 --- a/charts/openab/values.yaml +++ b/charts/openab/values.yaml @@ -1,6 +1,6 @@ image: repository: ghcr.io/openabdev/openab - tag: "9c70cdd" + tag: "78f8d2c" pullPolicy: IfNotPresent replicas: 1 From e9db24628a70c1e81fde7d4d78c766d123b1b3c4 Mon Sep 17 00:00:00 2001 From: thepagent Date: Tue, 7 Apr 2026 23:25:42 +0800 Subject: [PATCH 41/80] fix: drop --auto from chart bump PR merge (#122) --auto enables auto-merge which waits for reviews that never come. Direct merge works because openab-app is a bypass actor on the ruleset. Closes #121 Co-authored-by: thepagent --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8b1c4c70..c73084ae 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -226,4 +226,4 @@ jobs: --title "chore: bump chart to ${VERSION}" \ --body "Auto-generated chart version bump for image \`${IMAGE_SHA}\`." \ --base main --head "$BRANCH") - gh pr merge "$PR_URL" --squash --auto --delete-branch + gh pr merge "$PR_URL" --squash --delete-branch From bb658ad1cd54267e61cf6e656d171de8f9711f34 Mon Sep 17 00:00:00 2001 From: thepagent Date: Tue, 7 Apr 2026 23:37:34 +0800 Subject: [PATCH 42/80] docs: add Helm chart publishing guide (#124) Co-authored-by: thepagent --- docs/helm-publishing.md | 73 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 docs/helm-publishing.md diff --git a/docs/helm-publishing.md b/docs/helm-publishing.md new file mode 100644 index 00000000..b4976360 --- /dev/null +++ b/docs/helm-publishing.md @@ -0,0 +1,73 @@ +# Helm Chart Publishing + +OpenAB publishes the Helm chart to two channels automatically via the `Release Charts` workflow (`.github/workflows/release.yml`). + +## Channels + +| Channel | URL | Install command | +|---------|-----|-----------------| +| GitHub Pages | `https://openabdev.github.io/openab` | `helm repo add openab https://openabdev.github.io/openab && helm install openab openab/openab` | +| OCI (GHCR) | `oci://ghcr.io/openabdev/charts/openab` | `helm install openab oci://ghcr.io/openabdev/charts/openab` | + +## How it works + +``` +charts/openab/Chart.yaml changed on main + │ + ▼ +┌─────────────────────────────┐ +│ Release Charts workflow │ +│ .github/workflows/ │ +│ release.yml │ +│ │ +│ 1. helm package │ +│ 2. helm push → OCI (GHCR) │ +│ 3. cr upload → GH Release │ +│ 4. cr index → gh-pages │ +│ 5. Update release notes │ +└─────────────────────────────┘ + │ + ▼ + Both channels updated +``` + +### Trigger + +The workflow runs when `charts/openab/Chart.yaml` is pushed to `main`. This happens automatically when the `Build & Release` workflow merges a chart bump PR. + +### OCI Registry + +`helm push` publishes the packaged chart to `oci://ghcr.io/openabdev/charts`. The GHCR packages must be **public** (configured at org level) for unauthenticated pulls. + +### GitHub Pages + +The [`chart-releaser`](https://github.com/helm/chart-releaser) (`cr`) tool uploads the `.tgz` as a GitHub Release asset, then updates `index.yaml` on the `gh-pages` branch. GitHub Pages serves this as a standard Helm repository. + +## Version flow + +``` +PR merged to main (src/ or Dockerfile changes) + → Build & Release workflow + → Builds Docker images (all 4 variants) + → Creates chart bump PR (patch/minor/major) + → App token merges the PR + → Chart.yaml change triggers Release Charts + → Publishes to OCI + GitHub Pages +``` + +## Stable vs beta + +The `Build & Release` workflow accepts two inputs via `workflow_dispatch`: + +| Input | Description | +|-------|-------------| +| `chart_bump` | `patch`, `minor`, or `major` | +| `release` | `true` for stable (e.g. `0.5.1`), omit for beta (e.g. `0.5.1-beta.34`) | + +Push-triggered builds always produce beta versions. Use `workflow_dispatch` with `release=true` for stable releases. + +Note: Helm hides beta versions by default. Use `--devel` to see them: + +```bash +helm search repo openab/openab --devel --versions +``` From 9b22fa2c8bf5794ae528b4475dac141af2fda779 Mon Sep 17 00:00:00 2001 From: chaodufashi Date: Wed, 8 Apr 2026 07:14:02 +0800 Subject: [PATCH 43/80] feat: add workflow to label new issues with needs-triage (#131) * feat: add workflow to label new issues with needs-triage * feat: add issue templates for bug, feature, and guidance * feat: add workflow_dispatch support for manual issue triage --------- Co-authored-by: Kiro --- .github/ISSUE_TEMPLATE/bug.yml | 22 ++++++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 1 + .github/ISSUE_TEMPLATE/feature.yml | 16 ++++++++++++++++ .github/ISSUE_TEMPLATE/guidance.yml | 10 ++++++++++ .github/workflows/issue-triage.yml | 20 ++++++++++++++++++++ 5 files changed, 69 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature.yml create mode 100644 .github/ISSUE_TEMPLATE/guidance.yml create mode 100644 .github/workflows/issue-triage.yml diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 00000000..bc8fc2e1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,22 @@ +name: Bug Report +description: Report a bug +labels: [bug, needs-triage] +body: + - type: textarea + attributes: + label: Description + description: What happened? + validations: + required: true + - type: textarea + attributes: + label: Steps to Reproduce + description: How can we reproduce this? + validations: + required: true + - type: textarea + attributes: + label: Expected Behavior + description: What did you expect to happen? + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..3ba13e0c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml new file mode 100644 index 00000000..2b873e1a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -0,0 +1,16 @@ +name: Feature Request +description: Suggest a new feature +labels: [feature, needs-triage] +body: + - type: textarea + attributes: + label: Description + description: What feature would you like? + validations: + required: true + - type: textarea + attributes: + label: Use Case + description: Why do you need this? + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/guidance.yml b/.github/ISSUE_TEMPLATE/guidance.yml new file mode 100644 index 00000000..4ba921fd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/guidance.yml @@ -0,0 +1,10 @@ +name: Guidance +description: Ask a question or request guidance +labels: [guidance, needs-triage] +body: + - type: textarea + attributes: + label: Question + description: What do you need help with? + validations: + required: true diff --git a/.github/workflows/issue-triage.yml b/.github/workflows/issue-triage.yml new file mode 100644 index 00000000..bfcd524f --- /dev/null +++ b/.github/workflows/issue-triage.yml @@ -0,0 +1,20 @@ +name: Issue Triage +on: + issues: + types: [opened] + workflow_dispatch: + inputs: + issue_number: + description: "Issue number to add needs-triage label" + required: true + type: number +jobs: + add-label: + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - run: gh issue edit "$ISSUE_NUMBER" --add-label needs-triage --repo ${{ github.repository }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ISSUE_NUMBER: ${{ github.event.issue.number || github.event.inputs.issue_number }} From d2414203fa7b732a90c4ab282d128b6153c72016 Mon Sep 17 00:00:00 2001 From: chaodufashi Date: Wed, 8 Apr 2026 07:47:59 +0800 Subject: [PATCH 44/80] docs: add issue triage guide for community transparency (#132) Co-authored-by: Kiro --- docs/steering/triage.md | 50 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 docs/steering/triage.md diff --git a/docs/steering/triage.md b/docs/steering/triage.md new file mode 100644 index 00000000..2f3594ee --- /dev/null +++ b/docs/steering/triage.md @@ -0,0 +1,50 @@ +# Issue Triage Guide for openabdev/openab + +## Steps + +1. **Confirm type** — ensure one of: `bug`, `feature`, `guidance` +2. **Verify claims** — be skeptical; find source code or official docs to confirm before accepting a bug report as valid +3. **Set priority** — add exactly one: + - `p0` 🔴 Critical — drop everything + - `p1` 🟠 High — address this sprint + - `p2` 🟡 Medium — planned work + - `p3` 🟤 Low — nice to have +4. **Remove `needs-triage`** — triage complete + +## Priority Guidelines + +| Priority | Criteria | +|----------|----------| +| p0 | Security vulnerability, data loss, entire system down | +| p1 | Major feature broken for a class of users (e.g. all Claude Code / Cursor users) | +| p2 | Bug with workaround, or planned feature work | +| p3 | Minor improvement, cosmetic, nice to have | + +## Response Template + +- **Issue at a Glance** — always include an ASCII diagram showing the flow and where things break +- Acknowledge the issue by investigating the relevant source code or official docs +- Confirm root cause or ask clarifying questions +- Link relevant spec/doc references when available +- Invite PR or state next steps +- **Draft response for human approval before posting to the issue comment** + +## Issue at a Glance Example + +``` +Discord User ──► openab ──► Claude Code / Cursor agent + │ + ▼ + session/request_permission + (agent asks: "can I run this tool?") + │ + ▼ + openab auto-reply (WRONG shape): + ┌─────────────────────────────────┐ + │ { "optionId": "allow_always" } │ ← flat, no wrapper + └─────────────────────────────────┘ + │ + ▼ + SDK cannot find `outcome` field + → treats as REFUSAL ❌ +``` From 944de4c07d3efbd58d5f82da92aa9e006759eef7 Mon Sep 17 00:00:00 2001 From: thepagent Date: Wed, 8 Apr 2026 21:38:00 +0900 Subject: [PATCH 45/80] feat: agents map multi-agent Helm chart (#54) * feat: agents map multi-agent Helm chart Replaces single agent.preset with agents map supporting multiple agents per Helm release. Each agent gets its own Deployment, ConfigMap, Secret, and PVC. Rebased onto charts/openab/ rename (was charts/agent-broker/). Uses openab.* template helpers. Preserves security contexts and channel ID validation from main. Breaking change: see PR description for migration table. Closes #51 * fix: address PR #54 review comments - Fix checksum/config to hash only current agent's config () instead of the entire configmap template (prevents cross-agent rollouts) - Make DISCORD_BOT_TOKEN secretKeyRef conditional on botToken being set (prevents CreateContainerConfigError when Secret doesn't exist) - Add nil-safe defaults for pool/reactions in configmap.yaml (prevents nil pointer dereference when nested keys are omitted) - Add comment explaining hardcoded replicas: 1 / strategy: Recreate (RWO PVC constraint) - Move restart command inside range loop in NOTES.txt (renders real deployment name per agent) * fix: address PR #54 second-round review comments - Fix args rendering null in TOML when unset (fallback to []) - Add default /home/agent for workingDir in configmap and deployment (HOME env, PVC mountPath, AGENTS.md mountPath) - Add commented-out multi-agent example in values.yaml --------- Co-authored-by: thepagent Co-authored-by: Neil Kuan --- charts/openab/Chart.yaml | 2 +- charts/openab/templates/NOTES.txt | 73 ++++++---------- charts/openab/templates/_helpers.tpl | 73 +++++----------- charts/openab/templates/configmap.yaml | 34 ++++---- charts/openab/templates/deployment.yaml | 67 ++++++++------- charts/openab/templates/pvc.yaml | 16 ++-- charts/openab/templates/secret.yaml | 12 ++- charts/openab/values.yaml | 108 +++++++++++++----------- 8 files changed, 178 insertions(+), 207 deletions(-) diff --git a/charts/openab/Chart.yaml b/charts/openab/Chart.yaml index 5e41536b..74b1644b 100644 --- a/charts/openab/Chart.yaml +++ b/charts/openab/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: openab description: Discord ↔ ACP coding CLI bridge (Kiro CLI, Claude Code, Codex, Gemini) type: application -version: 0.5.1 +version: 0.6.0 appVersion: "78f8d2c" diff --git a/charts/openab/templates/NOTES.txt b/charts/openab/templates/NOTES.txt index 21567d81..00729bfa 100644 --- a/charts/openab/templates/NOTES.txt +++ b/charts/openab/templates/NOTES.txt @@ -1,55 +1,30 @@ openab {{ .Chart.AppVersion }} has been installed! -⚠️ Discord channel IDs must be set with --set-string (not --set) to avoid float64 precision loss: - - helm upgrade {{ .Release.Name }} openab/openab \ - --set-string discord.allowedChannels[0]="" - -{{- if not .Values.discord.botToken }} - -⚠️ No bot token was provided. Create the secret manually: - - kubectl create secret generic {{ include "openab.fullname" . }} \ - --from-literal=discord-bot-token="YOUR_TOKEN" +⚠️ Discord channel IDs must be set with --set-string (not --set) to avoid float64 precision loss. + +Agents deployed: +{{- range $name, $cfg := .Values.agents }} + • {{ $name }} ({{ $cfg.command }}) +{{- if not $cfg.discord.botToken }} + ⚠️ No bot token provided. Create the secret manually: + kubectl create secret generic {{ include "openab.agentFullname" (dict "ctx" $ "agent" $name) }} \ + --from-literal=discord-bot-token="YOUR_TOKEN" {{- end }} -{{- $cmd := include "openab.agent.command" . | trim }} -{{- if eq $cmd "kiro-cli" }} - -Authenticate kiro-cli (first time only): - - kubectl exec -it deployment/{{ include "openab.fullname" . }} -- kiro-cli login --use-device-flow -{{- else if eq $cmd "codex-acp" }} - -Authenticate Codex (first time only): - - kubectl exec -it deployment/{{ include "openab.fullname" . }} -- codex login --device-auth -{{- else if eq $cmd "claude-agent-acp" }} - -Authenticate Claude Code (first time only): - - kubectl exec -it deployment/{{ include "openab.fullname" . }} -- claude setup-token - -Then set the token as an env var (token is valid for 1 year): - - helm upgrade {{ .Release.Name }} openab/openab --set env.CLAUDE_CODE_OAUTH_TOKEN="" -{{- else if eq $cmd "gemini" }} - -Authenticate Gemini CLI (first time only): - - kubectl exec -it deployment/{{ include "openab.fullname" . }} -- gemini - - Select "Sign in with Google", open the URL in your browser, then - curl the localhost callback URL from within the pod. - - Alternatively, set GEMINI_API_KEY: - - helm upgrade {{ .Release.Name }} openab/openab --set env.GEMINI_API_KEY="" -{{- else }} - -Authenticate your agent CLI (first time only) — refer to your CLI's documentation. +{{- if eq $cfg.command "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" }} + Authenticate: + kubectl exec -it deployment/{{ include "openab.agentFullname" (dict "ctx" $ "agent" $name) }} -- codex login --device-auth +{{- else if eq $cfg.command "claude-agent-acp" }} + Authenticate: + kubectl exec -it deployment/{{ include "openab.agentFullname" (dict "ctx" $ "agent" $name) }} -- claude setup-token +{{- else if eq $cfg.command "gemini" }} + Authenticate: + kubectl exec -it deployment/{{ include "openab.agentFullname" (dict "ctx" $ "agent" $name) }} -- gemini {{- end }} -Then restart the pod: - - kubectl rollout restart deployment/{{ include "openab.fullname" . }} + Restart after auth: + kubectl rollout restart deployment/{{ include "openab.agentFullname" (dict "ctx" $ "agent" $name) }} +{{- end }} diff --git a/charts/openab/templates/_helpers.tpl b/charts/openab/templates/_helpers.tpl index 9dbea5fd..1bf43b43 100644 --- a/charts/openab/templates/_helpers.tpl +++ b/charts/openab/templates/_helpers.tpl @@ -20,65 +20,32 @@ {{- end }} {{- define "openab.labels" -}} -helm.sh/chart: {{ include "openab.chart" . }} -{{ include "openab.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +helm.sh/chart: {{ include "openab.chart" .ctx }} +app.kubernetes.io/name: {{ include "openab.name" .ctx }} +app.kubernetes.io/instance: {{ .ctx.Release.Name }} +app.kubernetes.io/component: {{ .agent }} +{{- if .ctx.Chart.AppVersion }} +app.kubernetes.io/version: {{ .ctx.Chart.AppVersion | quote }} {{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} +app.kubernetes.io/managed-by: {{ .ctx.Release.Service }} {{- end }} {{- define "openab.selectorLabels" -}} -app.kubernetes.io/name: {{ include "openab.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/name: {{ include "openab.name" .ctx }} +app.kubernetes.io/instance: {{ .ctx.Release.Name }} +app.kubernetes.io/component: {{ .agent }} {{- end }} -{{/* -Resolve agent preset → image repository -*/}} -{{- define "openab.image.repository" -}} -{{- if .Values.agent.preset }} - {{- if eq .Values.agent.preset "codex" }}ghcr.io/openabdev/openab-codex - {{- else if eq .Values.agent.preset "claude" }}ghcr.io/openabdev/openab-claude - {{- else if eq .Values.agent.preset "gemini" }}ghcr.io/openabdev/openab-gemini - {{- else }}{{ .Values.image.repository }} - {{- end }} -{{- else }}{{ .Values.image.repository }} -{{- end }} -{{- end }} - -{{/* -Resolve agent preset → command -*/}} -{{- define "openab.agent.command" -}} -{{- if .Values.agent.preset }} - {{- if eq .Values.agent.preset "codex" }}codex-acp - {{- else if eq .Values.agent.preset "claude" }}claude-agent-acp - {{- else if eq .Values.agent.preset "gemini" }}gemini - {{- else }}{{ .Values.agent.command }} - {{- end }} -{{- else }}{{ .Values.agent.command }} -{{- end }} +{{/* Per-agent resource name: - */}} +{{- define "openab.agentFullname" -}} +{{- printf "%s-%s" (include "openab.fullname" .ctx) .agent | trunc 63 | trimSuffix "-" }} {{- end }} -{{/* -Resolve agent preset → args -*/}} -{{- define "openab.agent.args" -}} -{{- if .Values.agent.preset }} - {{- if or (eq .Values.agent.preset "codex") (eq .Values.agent.preset "claude") }}[] - {{- else if eq .Values.agent.preset "gemini" }}["--acp"] - {{- else }}{{ .Values.agent.args | toJson }} - {{- end }} -{{- else }}{{ .Values.agent.args | toJson }} -{{- end }} -{{- end }} - -{{/* -Resolve agent preset → home directory -*/}} -{{- define "openab.agent.home" -}} -{{- if and .Values.agent.preset (or (eq .Values.agent.preset "codex") (eq .Values.agent.preset "claude") (eq .Values.agent.preset "gemini")) }}/home/node -{{- else }}/home/agent -{{- end }} +{{/* Resolve image: agent-level override → global default */}} +{{- define "openab.agentImage" -}} +{{- $repo := .ctx.Values.image.repository }} +{{- $tag := .ctx.Values.image.tag }} +{{- if and .cfg.image .cfg.image.repository (ne .cfg.image.repository "") }}{{ $repo = .cfg.image.repository }}{{ end }} +{{- if and .cfg.image .cfg.image.tag (ne .cfg.image.tag "") }}{{ $tag = .cfg.image.tag }}{{ end }} +{{- printf "%s:%s" $repo $tag }} {{- end }} diff --git a/charts/openab/templates/configmap.yaml b/charts/openab/templates/configmap.yaml index 44b99a01..dd68c0f5 100644 --- a/charts/openab/templates/configmap.yaml +++ b/charts/openab/templates/configmap.yaml @@ -1,36 +1,40 @@ +{{- range $name, $cfg := .Values.agents }} +{{- $d := dict "ctx" $ "agent" $name "cfg" $cfg }} +--- apiVersion: v1 kind: ConfigMap metadata: - name: {{ include "openab.fullname" . }} + name: {{ include "openab.agentFullname" $d }} labels: - {{- include "openab.labels" . | nindent 4 }} + {{- include "openab.labels" $d | nindent 4 }} data: config.toml: | [discord] bot_token = "${DISCORD_BOT_TOKEN}" - {{- range .Values.discord.allowedChannels }} + {{- range $cfg.discord.allowedChannels }} {{- if regexMatch "e\\+|E\\+" (toString .) }} {{- fail (printf "discord.allowedChannels contains a mangled ID: %s — use --set-string instead of --set for channel IDs" (toString .)) }} {{- end }} {{- end }} - allowed_channels = [{{ range $i, $ch := .Values.discord.allowedChannels }}{{ if $i }}, {{ end }}"{{ $ch }}"{{ end }}] + allowed_channels = [{{ range $i, $ch := $cfg.discord.allowedChannels }}{{ if $i }}, {{ end }}"{{ $ch }}"{{ end }}] [agent] - command = "{{ include "openab.agent.command" . | trim }}" - args = {{ include "openab.agent.args" . | trim }} - working_dir = "{{ include "openab.agent.home" . | trim }}" - {{- if .Values.agent.env }} - env = { {{ range $k, $v := .Values.agent.env }}{{ $k }} = "{{ $v }}", {{ end }} } + command = "{{ $cfg.command }}" + args = {{ if $cfg.args }}{{ $cfg.args | toJson }}{{ else }}[]{{ end }} + working_dir = "{{ $cfg.workingDir | default "/home/agent" }}" + {{- if $cfg.env }} + env = { {{ range $k, $v := $cfg.env }}{{ $k }} = "{{ $v }}", {{ end }} } {{- end }} [pool] - max_sessions = {{ .Values.pool.maxSessions }} - session_ttl_hours = {{ .Values.pool.sessionTtlHours }} + max_sessions = {{ ($cfg.pool).maxSessions | default 10 }} + session_ttl_hours = {{ ($cfg.pool).sessionTtlHours | default 24 }} [reactions] - enabled = {{ .Values.reactions.enabled }} - remove_after_reply = {{ .Values.reactions.removeAfterReply }} - {{- if .Values.agentsMd }} + enabled = {{ ($cfg.reactions).enabled | default true }} + remove_after_reply = {{ ($cfg.reactions).removeAfterReply | default false }} + {{- if $cfg.agentsMd }} AGENTS.md: | - {{- .Values.agentsMd | nindent 4 }} + {{- $cfg.agentsMd | nindent 4 }} {{- end }} +{{- end }} diff --git a/charts/openab/templates/deployment.yaml b/charts/openab/templates/deployment.yaml index 483bb775..d404fb28 100644 --- a/charts/openab/templates/deployment.yaml +++ b/charts/openab/templates/deployment.yaml @@ -1,54 +1,60 @@ +{{- range $name, $cfg := .Values.agents }} +{{- $d := dict "ctx" $ "agent" $name "cfg" $cfg }} +{{- $pvcEnabled := and $cfg.persistence $cfg.persistence.enabled }} +--- apiVersion: apps/v1 kind: Deployment metadata: - name: {{ include "openab.fullname" . }} + name: {{ include "openab.agentFullname" $d }} labels: - {{- include "openab.labels" . | nindent 4 }} + {{- include "openab.labels" $d | nindent 4 }} spec: - replicas: {{ .Values.replicas }} - {{- with .Values.strategy }} + # Hardcoded for PVC-backed agents: RWO volumes can't be shared across pods, + # so rolling updates and multiple replicas are not supported. + replicas: 1 strategy: - {{- toYaml . | nindent 4 }} - {{- end }} + type: Recreate selector: matchLabels: - {{- include "openab.selectorLabels" . | nindent 6 }} + {{- include "openab.selectorLabels" $d | nindent 6 }} template: metadata: annotations: - checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + checksum/config: {{ $cfg | toJson | sha256sum }} labels: - {{- include "openab.selectorLabels" . | nindent 8 }} + {{- include "openab.selectorLabels" $d | nindent 8 }} spec: - {{- with .Values.podSecurityContext }} + {{- with $.Values.podSecurityContext }} securityContext: {{- toYaml . | nindent 8 }} {{- end }} containers: - name: openab - image: "{{ include "openab.image.repository" . | trim }}:{{ .Values.image.tag }}" - imagePullPolicy: {{ .Values.image.pullPolicy }} - {{- with .Values.containerSecurityContext }} + image: {{ include "openab.agentImage" $d | quote }} + imagePullPolicy: {{ $.Values.image.pullPolicy }} + {{- with $.Values.containerSecurityContext }} securityContext: {{- toYaml . | nindent 12 }} {{- end }} env: + {{- if $cfg.discord.botToken }} - name: DISCORD_BOT_TOKEN valueFrom: secretKeyRef: - name: {{ include "openab.fullname" . }} + name: {{ include "openab.agentFullname" $d }} key: discord-bot-token + {{- end }} - name: HOME - value: {{ include "openab.agent.home" . | trim }} - {{- range $key, $value := .Values.env }} - - name: {{ $key }} - value: {{ $value | quote }} + value: {{ $cfg.workingDir | default "/home/agent" }} + {{- range $k, $v := $cfg.env }} + - name: {{ $k }} + value: {{ $v | quote }} {{- end }} - {{- with .Values.envFrom }} + {{- with $cfg.envFrom }} envFrom: {{- toYaml . | nindent 12 }} {{- end }} - {{- with .Values.resources }} + {{- with $cfg.resources }} resources: {{- toYaml . | nindent 12 }} {{- end }} @@ -56,33 +62,34 @@ spec: - name: config mountPath: /etc/openab readOnly: true - {{- if .Values.persistence.enabled }} + {{- if $pvcEnabled }} - name: data - mountPath: {{ include "openab.agent.home" . | trim }} + mountPath: {{ $cfg.workingDir | default "/home/agent" }} {{- end }} - {{- if .Values.agentsMd }} + {{- if $cfg.agentsMd }} - name: config - mountPath: {{ include "openab.agent.home" . | trim }}/AGENTS.md + mountPath: {{ $cfg.workingDir | default "/home/agent" }}/AGENTS.md subPath: AGENTS.md {{- end }} - {{- with .Values.nodeSelector }} + {{- with $cfg.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} {{- end }} - {{- with .Values.affinity }} + {{- with $cfg.affinity }} affinity: {{- toYaml . | nindent 8 }} {{- end }} - {{- with .Values.tolerations }} + {{- with $cfg.tolerations }} tolerations: {{- toYaml . | nindent 8 }} {{- end }} volumes: - name: config configMap: - name: {{ include "openab.fullname" . }} - {{- if .Values.persistence.enabled }} + name: {{ include "openab.agentFullname" $d }} + {{- if $pvcEnabled }} - name: data persistentVolumeClaim: - claimName: {{ include "openab.fullname" . }} + claimName: {{ include "openab.agentFullname" $d }} {{- end }} +{{- end }} diff --git a/charts/openab/templates/pvc.yaml b/charts/openab/templates/pvc.yaml index 6e720698..069c7996 100644 --- a/charts/openab/templates/pvc.yaml +++ b/charts/openab/templates/pvc.yaml @@ -1,17 +1,21 @@ -{{- if .Values.persistence.enabled }} +{{- range $name, $cfg := .Values.agents }} +{{- if and $cfg.persistence $cfg.persistence.enabled }} +{{- $d := dict "ctx" $ "agent" $name "cfg" $cfg }} +--- apiVersion: v1 kind: PersistentVolumeClaim metadata: - name: {{ include "openab.fullname" . }} + name: {{ include "openab.agentFullname" $d }} labels: - {{- include "openab.labels" . | nindent 4 }} + {{- include "openab.labels" $d | nindent 4 }} spec: accessModes: - ReadWriteOnce - {{- if .Values.persistence.storageClass }} - storageClassName: {{ .Values.persistence.storageClass }} + {{- if $cfg.persistence.storageClass }} + storageClassName: {{ $cfg.persistence.storageClass }} {{- end }} resources: requests: - storage: {{ .Values.persistence.size }} + storage: {{ $cfg.persistence.size }} +{{- end }} {{- end }} diff --git a/charts/openab/templates/secret.yaml b/charts/openab/templates/secret.yaml index 2096730a..126cf038 100644 --- a/charts/openab/templates/secret.yaml +++ b/charts/openab/templates/secret.yaml @@ -1,13 +1,17 @@ -{{- if .Values.discord.botToken }} +{{- range $name, $cfg := .Values.agents }} +{{- if $cfg.discord.botToken }} +{{- $d := dict "ctx" $ "agent" $name "cfg" $cfg }} +--- apiVersion: v1 kind: Secret metadata: - name: {{ include "openab.fullname" . }} + name: {{ include "openab.agentFullname" $d }} labels: - {{- include "openab.labels" . | nindent 4 }} + {{- include "openab.labels" $d | nindent 4 }} annotations: "helm.sh/resource-policy": keep type: Opaque data: - discord-bot-token: {{ .Values.discord.botToken | b64enc | quote }} + discord-bot-token: {{ $cfg.discord.botToken | b64enc | quote }} +{{- end }} {{- end }} diff --git a/charts/openab/values.yaml b/charts/openab/values.yaml index bf6c0860..a807c526 100644 --- a/charts/openab/values.yaml +++ b/charts/openab/values.yaml @@ -3,13 +3,6 @@ image: tag: "78f8d2c" pullPolicy: IfNotPresent -replicas: 1 - -strategy: - type: Recreate - -resources: {} - podSecurityContext: runAsNonRoot: true runAsUser: 1000 @@ -22,45 +15,62 @@ containerSecurityContext: drop: - ALL -persistence: - enabled: true - storageClass: "" - size: 1Gi - -discord: - botToken: "" # set via --set or external secret - # ⚠️ Use --set-string for channel IDs to avoid float64 precision loss: - # helm install ... --set-string discord.allowedChannels[0]="" - allowedChannels: - - "YOUR_CHANNEL_ID" - -agent: - preset: "" # kiro (default), codex, or claude — auto-configures image + command - command: kiro-cli - args: - - acp - - --trust-all-tools - workingDir: /home/agent - env: {} - # ANTHROPIC_API_KEY: "${ANTHROPIC_API_KEY}" - -pool: - maxSessions: 10 - sessionTtlHours: 24 - -reactions: - enabled: true - removeAfterReply: false - -agentsMd: "" - # agentsMd: | - # IDENTITY - your agent identity - # SOUL - your agent personality - # USER - how agent should address the user - -env: {} -envFrom: [] - -nodeSelector: {} -tolerations: [] -affinity: {} +agents: + kiro: + # To add a second agent, uncomment and fill in the block below: + # claude: + # command: claude-agent-acp + # args: [] + # discord: + # botToken: "" + # # ⚠️ Use --set-string for channel IDs to avoid float64 precision loss + # allowedChannels: + # - "YOUR_CHANNEL_ID" + # workingDir: /home/agent + # env: {} + # envFrom: [] + # pool: + # maxSessions: 10 + # sessionTtlHours: 24 + # reactions: + # enabled: true + # removeAfterReply: false + # persistence: + # enabled: true + # storageClass: "" + # size: 1Gi + # agentsMd: "" + # resources: {} + # nodeSelector: {} + # tolerations: [] + # affinity: {} + image: + repository: "" + tag: "" + command: kiro-cli + args: + - acp + - --trust-all-tools + discord: + botToken: "" + # ⚠️ Use --set-string for channel IDs to avoid float64 precision loss + allowedChannels: + - "YOUR_CHANNEL_ID" + workingDir: /home/agent + env: {} + envFrom: [] + pool: + maxSessions: 10 + sessionTtlHours: 24 + reactions: + enabled: true + removeAfterReply: false + persistence: + enabled: true + storageClass: "" + size: 1Gi + agentsMd: "" + resources: {} + nodeSelector: {} + tolerations: [] + affinity: {} From 7b896767bb9cb34d8fec5d1e1cbc456d463528fd Mon Sep 17 00:00:00 2001 From: Neil Kuan <46012524+neilkuan@users.noreply.github.com> Date: Wed, 8 Apr 2026 21:08:01 +0800 Subject: [PATCH 46/80] docs: update README for 0.6.0 agents map chart (#144) * docs: update README for 0.6.0 agents map chart - Replace agent.preset / discord.botToken flat values with agents. map syntax - Update Helm install commands for kiro/codex/claude/gemini - Add multi-agent values.yaml example - Update kubectl exec / rollout restart to openab- naming - Update Install with Your Coding CLI prompts * docs: use single quotes for --set-string channel ID args * docs: move Helm install details to gh-pages, keep main README concise * docs: fix formatting in README for better readability --- README.md | 86 ++++++++++++------------------------------------------- 1 file changed, 18 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index 84bc5ed2..41b620f4 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A lightweight, secure, cloud-native ACP harness that bridges Discord and any [Ag ``` ┌──────────────┐ Gateway WS ┌──────────────┐ ACP stdio ┌──────────────┐ -│ Discord │◄─────────────►│ openab │──────────────►│ coding CLI │ +│ Discord │◄─────────────►│ openab │──────────────►│ coding CLI │ │ User │ │ (Rust) │◄── JSON-RPC ──│ (acp mode) │ └──────────────┘ └──────────────┘ └──────────────┘ ``` @@ -81,93 +81,55 @@ The bot creates a thread. After that, just type in the thread — no @mention ne ## Pluggable Agent Backends -Swap backends using the `agent.preset` Helm value or manual config. Tested backends: +Supports Kiro CLI, Claude Code, Codex, Gemini, and any ACP-compatible CLI. -| Preset | CLI | ACP Adapter | Auth | -|--------|-----|-------------|------| -| (default) | Kiro CLI | Native `kiro-cli acp` | `kiro-cli login --use-device-flow` | +| 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` | ### Helm Install (recommended) +See the **[Helm chart docs](https://openabdev.github.io/openab)** for full installation instructions, values reference, and multi-agent examples. + ```bash helm repo add openab https://openabdev.github.io/openab helm repo update - -# Kiro CLI (default) -helm install openab openab/openab \ - --set discord.botToken="$DISCORD_BOT_TOKEN" \ - --set-string discord.allowedChannels[0]="YOUR_CHANNEL_ID" - -# Codex -helm install openab openab/openab \ - --set discord.botToken="$DISCORD_BOT_TOKEN" \ - --set-string discord.allowedChannels[0]="YOUR_CHANNEL_ID" \ - --set agent.preset=codex - -# Claude Code helm install openab openab/openab \ - --set discord.botToken="$DISCORD_BOT_TOKEN" \ - --set-string discord.allowedChannels[0]="YOUR_CHANNEL_ID" \ - --set agent.preset=claude - -# Gemini -helm install openab openab/openab \ - --set discord.botToken="$DISCORD_BOT_TOKEN" \ - --set-string discord.allowedChannels[0]="YOUR_CHANNEL_ID" \ - --set agent.preset=gemini -``` - -Then authenticate inside the pod (first time only): - -```bash -# Kiro CLI -kubectl exec -it deployment/openab -- kiro-cli login --use-device-flow - -# Codex -kubectl exec -it deployment/openab -- codex login --device-auth - -# Claude Code -kubectl exec -it deployment/openab -- claude setup-token -# Then: helm upgrade openab openab/openab --set env.CLAUDE_CODE_OAUTH_TOKEN="" - -# Gemini (Google OAuth — open URL in browser, curl callback from pod) -kubectl exec -it deployment/openab -- gemini -# Or use API key: helm upgrade openab openab/openab --set env.GEMINI_API_KEY="" + --set agents.kiro.discord.botToken="$DISCORD_BOT_TOKEN" \ + --set-string 'agents.kiro.discord.allowedChannels[0]=YOUR_CHANNEL_ID' ``` -Restart after auth: `kubectl rollout restart deployment openab` - ### Manual config.toml -For non-Helm deployments, swap the `[agent]` block: +For non-Helm deployments, configure the `[agent]` block per CLI: ```toml # Kiro CLI (default) [agent] command = "kiro-cli" args = ["acp", "--trust-all-tools"] -working_dir = "/tmp" +working_dir = "/home/agent" # Codex (requires codex-acp in PATH) [agent] command = "codex-acp" args = [] -working_dir = "/tmp" +working_dir = "/home/agent" # Claude Code (requires claude-agent-acp in PATH) [agent] command = "claude-agent-acp" args = [] -working_dir = "/tmp" +working_dir = "/home/agent" # Gemini [agent] command = "gemini" args = ["--acp"] -working_dir = "/tmp" +working_dir = "/home/agent" env = { GEMINI_API_KEY = "${GEMINI_API_KEY}" } ``` @@ -219,7 +181,7 @@ The Docker image bundles both `openab` and `kiro-cli` in a single container (ope ┌─ Kubernetes Pod ─────────────────────────────────────────────────┐ │ │ │ ┌─────────────────────────────────────────────────────────┐ │ -│ │ openab (main process, PID 1) │ │ +│ │ openab (main process, PID 1) │ │ │ │ │ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌───────────┐ │ │ │ │ │ Discord │ │ Session Pool │ │ Reaction │ │ │ @@ -262,19 +224,7 @@ The Docker image bundles both `openab` and `kiro-cli` in a single container (ope ### Install with Your Coding CLI -Use one of these prompts with any coding CLI (Kiro CLI, Claude Code, Codex, Gemini, 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. My Discord bot token is in the environment variable DISCORD_BOT_TOKEN and my channel ID is . After install, follow the NOTES output to authenticate, then restart the deployment. - -**Codex:** -> Install openab on my local k8s cluster using the Helm chart from https://openabdev.github.io/openab with `--set agent.preset=codex`. My Discord bot token is in the environment variable DISCORD_BOT_TOKEN and my channel ID is . After install, follow the NOTES output to authenticate, then restart the deployment. - -**Claude Code:** -> Install openab on my local k8s cluster using the Helm chart from https://openabdev.github.io/openab with `--set agent.preset=claude`. My Discord bot token is in the environment variable DISCORD_BOT_TOKEN and my channel ID is . After install, follow the NOTES output to authenticate, then restart the deployment. - -**Gemini:** -> Install openab on my local k8s cluster using the Helm chart from https://openabdev.github.io/openab with `--set agent.preset=gemini`. My Discord bot token is in the environment variable DISCORD_BOT_TOKEN and my channel ID is . After install, follow the NOTES output to authenticate, then restart the deployment. +See the **[Helm chart docs](https://openabdev.github.io/openab)** for per-agent install commands (Kiro CLI, Claude Code, Codex, Gemini) and values reference. ### Build & Push @@ -302,13 +252,13 @@ kubectl apply -f k8s/deployment.yaml kiro-cli requires a one-time OAuth login. The PVC persists the tokens across pod restarts. ```bash -kubectl exec -it deployment/openab -- kiro-cli login --use-device-flow +kubectl exec -it deployment/openab-kiro -- kiro-cli login --use-device-flow ``` Follow the device code flow in your browser, then restart the pod: ```bash -kubectl rollout restart deployment openab +kubectl rollout restart deployment/openab-kiro ``` ### Manifests From 1a944794ce921f52581d88f1dddb1d6a6f2e84c0 Mon Sep 17 00:00:00 2001 From: Neil Kuan <46012524+neilkuan@users.noreply.github.com> Date: Thu, 9 Apr 2026 05:45:45 +0800 Subject: [PATCH 47/80] fix(helm): per-agent image as string, fallback to appVersion, persistence.size default 1Gi (#145) * fix(helm): per-agent image as string, fallback to appVersion, persistence.size default 1Gi * fix(helm): guard nil persistence in pvc template * docs: fix claude working_dir to /home/node in manual config example * fix: readme * feat(helm): add per-agent enabled flag to skip resource creation Default kiro agent persists via Helm deep merge even when users only define a different agent. Add `agents..enabled` (default true) so users can set `agents.kiro.enabled: false` to avoid creating unwanted resources. Update README with all three deployment scenarios. --- README.md | 29 ++++++++++++++++++++++--- charts/openab/templates/NOTES.txt | 2 ++ charts/openab/templates/_helpers.tpl | 28 +++++++++++++++++++----- charts/openab/templates/configmap.yaml | 2 ++ charts/openab/templates/deployment.yaml | 6 +++-- charts/openab/templates/pvc.yaml | 8 ++++--- charts/openab/templates/secret.yaml | 2 ++ charts/openab/values.yaml | 11 +++++----- 8 files changed, 69 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 41b620f4..6c784477 100644 --- a/README.md +++ b/README.md @@ -97,11 +97,34 @@ See the **[Helm chart docs](https://openabdev.github.io/openab)** for full insta ```bash helm repo add openab https://openabdev.github.io/openab helm repo update + +# Kiro CLI only (default) helm install openab openab/openab \ --set agents.kiro.discord.botToken="$DISCORD_BOT_TOKEN" \ --set-string 'agents.kiro.discord.allowedChannels[0]=YOUR_CHANNEL_ID' + +# Claude Code only (disable default kiro) +helm install openab openab/openab \ + --set agents.kiro.enabled=false \ + --set agents.claude.discord.botToken="$DISCORD_BOT_TOKEN" \ + --set-string 'agents.claude.discord.allowedChannels[0]=YOUR_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 + +# Multi-agent (kiro + claude 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 ``` +Each agent key in `agents` map creates its own Deployment, ConfigMap, Secret, and PVC. Set `agents..enabled: false` to skip creating resources for an agent. + ### Manual config.toml For non-Helm deployments, configure the `[agent]` block per CLI: @@ -117,19 +140,19 @@ working_dir = "/home/agent" [agent] command = "codex-acp" args = [] -working_dir = "/home/agent" +working_dir = "/home/node" # Claude Code (requires claude-agent-acp in PATH) [agent] command = "claude-agent-acp" args = [] -working_dir = "/home/agent" +working_dir = "/home/node" # Gemini [agent] command = "gemini" args = ["--acp"] -working_dir = "/home/agent" +working_dir = "/home/node" env = { GEMINI_API_KEY = "${GEMINI_API_KEY}" } ``` diff --git a/charts/openab/templates/NOTES.txt b/charts/openab/templates/NOTES.txt index 00729bfa..37f1c709 100644 --- a/charts/openab/templates/NOTES.txt +++ b/charts/openab/templates/NOTES.txt @@ -4,6 +4,7 @@ openab {{ .Chart.AppVersion }} has been installed! Agents deployed: {{- range $name, $cfg := .Values.agents }} +{{- if ne (include "openab.agentEnabled" $cfg) "false" }} • {{ $name }} ({{ $cfg.command }}) {{- if not $cfg.discord.botToken }} ⚠️ No bot token provided. Create the secret manually: @@ -28,3 +29,4 @@ Agents deployed: Restart after auth: kubectl rollout restart deployment/{{ include "openab.agentFullname" (dict "ctx" $ "agent" $name) }} {{- end }} +{{- end }} diff --git a/charts/openab/templates/_helpers.tpl b/charts/openab/templates/_helpers.tpl index 1bf43b43..770d557a 100644 --- a/charts/openab/templates/_helpers.tpl +++ b/charts/openab/templates/_helpers.tpl @@ -41,11 +41,27 @@ app.kubernetes.io/component: {{ .agent }} {{- printf "%s-%s" (include "openab.fullname" .ctx) .agent | trunc 63 | trimSuffix "-" }} {{- end }} -{{/* Resolve image: agent-level override → global default */}} +{{/* Resolve image: agent-level string override → global default (repository:tag, tag defaults to appVersion) */}} {{- define "openab.agentImage" -}} -{{- $repo := .ctx.Values.image.repository }} -{{- $tag := .ctx.Values.image.tag }} -{{- if and .cfg.image .cfg.image.repository (ne .cfg.image.repository "") }}{{ $repo = .cfg.image.repository }}{{ end }} -{{- if and .cfg.image .cfg.image.tag (ne .cfg.image.tag "") }}{{ $tag = .cfg.image.tag }}{{ end }} -{{- printf "%s:%s" $repo $tag }} +{{- if and .cfg.image (kindIs "string" .cfg.image) (ne .cfg.image "") }} +{{- .cfg.image }} +{{- else }} +{{- $tag := default .ctx.Chart.AppVersion .ctx.Values.image.tag }} +{{- printf "%s:%s" .ctx.Values.image.repository $tag }} +{{- end }} +{{- end }} + +{{/* 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 */}} +{{- define "openab.agentEnabled" -}} +{{- if eq (.enabled | toString) "false" }}false{{ else }}true{{ end }} +{{- end }} + +{{/* 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 }} diff --git a/charts/openab/templates/configmap.yaml b/charts/openab/templates/configmap.yaml index dd68c0f5..273e924c 100644 --- a/charts/openab/templates/configmap.yaml +++ b/charts/openab/templates/configmap.yaml @@ -1,4 +1,5 @@ {{- range $name, $cfg := .Values.agents }} +{{- if ne (include "openab.agentEnabled" $cfg) "false" }} {{- $d := dict "ctx" $ "agent" $name "cfg" $cfg }} --- apiVersion: v1 @@ -38,3 +39,4 @@ data: {{- $cfg.agentsMd | nindent 4 }} {{- end }} {{- end }} +{{- end }} diff --git a/charts/openab/templates/deployment.yaml b/charts/openab/templates/deployment.yaml index d404fb28..f1ab9b0b 100644 --- a/charts/openab/templates/deployment.yaml +++ b/charts/openab/templates/deployment.yaml @@ -1,6 +1,7 @@ {{- range $name, $cfg := .Values.agents }} +{{- if ne (include "openab.agentEnabled" $cfg) "false" }} {{- $d := dict "ctx" $ "agent" $name "cfg" $cfg }} -{{- $pvcEnabled := and $cfg.persistence $cfg.persistence.enabled }} +{{- $pvcEnabled := not (eq (include "openab.persistenceEnabled" $cfg) "false") }} --- apiVersion: apps/v1 kind: Deployment @@ -31,7 +32,7 @@ spec: containers: - name: openab image: {{ include "openab.agentImage" $d | quote }} - imagePullPolicy: {{ $.Values.image.pullPolicy }} + imagePullPolicy: {{ include "openab.agentImagePullPolicy" $d }} {{- with $.Values.containerSecurityContext }} securityContext: {{- toYaml . | nindent 12 }} @@ -93,3 +94,4 @@ spec: claimName: {{ include "openab.agentFullname" $d }} {{- end }} {{- end }} +{{- end }} diff --git a/charts/openab/templates/pvc.yaml b/charts/openab/templates/pvc.yaml index 069c7996..e771e608 100644 --- a/charts/openab/templates/pvc.yaml +++ b/charts/openab/templates/pvc.yaml @@ -1,5 +1,6 @@ {{- range $name, $cfg := .Values.agents }} -{{- if and $cfg.persistence $cfg.persistence.enabled }} +{{- if ne (include "openab.agentEnabled" $cfg) "false" }} +{{- if not (eq (include "openab.persistenceEnabled" $cfg) "false") }} {{- $d := dict "ctx" $ "agent" $name "cfg" $cfg }} --- apiVersion: v1 @@ -11,11 +12,12 @@ metadata: spec: accessModes: - ReadWriteOnce - {{- if $cfg.persistence.storageClass }} + {{- if and $cfg.persistence $cfg.persistence.storageClass }} storageClassName: {{ $cfg.persistence.storageClass }} {{- end }} resources: requests: - storage: {{ $cfg.persistence.size }} + storage: {{ (and $cfg.persistence $cfg.persistence.size) | default "1Gi" }} +{{- end }} {{- end }} {{- end }} diff --git a/charts/openab/templates/secret.yaml b/charts/openab/templates/secret.yaml index 126cf038..fd090208 100644 --- a/charts/openab/templates/secret.yaml +++ b/charts/openab/templates/secret.yaml @@ -1,4 +1,5 @@ {{- range $name, $cfg := .Values.agents }} +{{- if ne (include "openab.agentEnabled" $cfg) "false" }} {{- if $cfg.discord.botToken }} {{- $d := dict "ctx" $ "agent" $name "cfg" $cfg }} --- @@ -15,3 +16,4 @@ data: discord-bot-token: {{ $cfg.discord.botToken | b64enc | quote }} {{- end }} {{- end }} +{{- end }} diff --git a/charts/openab/values.yaml b/charts/openab/values.yaml index a807c526..1f7c2134 100644 --- a/charts/openab/values.yaml +++ b/charts/openab/values.yaml @@ -1,6 +1,7 @@ image: repository: ghcr.io/openabdev/openab - tag: "78f8d2c" + # tag defaults to .Chart.AppVersion + tag: "" pullPolicy: IfNotPresent podSecurityContext: @@ -17,6 +18,7 @@ containerSecurityContext: agents: kiro: + enabled: true # set to false to skip creating resources for this agent # To add a second agent, uncomment and fill in the block below: # claude: # command: claude-agent-acp @@ -44,9 +46,8 @@ agents: # nodeSelector: {} # tolerations: [] # affinity: {} - image: - repository: "" - tag: "" + # image: "ghcr.io/openabdev/openab-claude:latest" + image: "" command: kiro-cli args: - acp @@ -68,7 +69,7 @@ agents: persistence: enabled: true storageClass: "" - size: 1Gi + size: 1Gi # defaults to 1Gi if not set agentsMd: "" resources: {} nodeSelector: {} From aa5245e491d49aa3174acd34edfbeb9a8aab81e7 Mon Sep 17 00:00:00 2001 From: thepagent Date: Thu, 9 Apr 2026 06:58:05 +0900 Subject: [PATCH 48/80] chore: bump chart to 0.6.1-beta.1 (#150) Co-authored-by: thepagent --- charts/openab/Chart.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/charts/openab/Chart.yaml b/charts/openab/Chart.yaml index 74b1644b..a7ca8b14 100644 --- a/charts/openab/Chart.yaml +++ b/charts/openab/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: openab description: Discord ↔ ACP coding CLI bridge (Kiro CLI, Claude Code, Codex, Gemini) type: application -version: 0.6.0 -appVersion: "78f8d2c" +version: 0.6.1-beta.1 +appVersion: "52ac30a" From 582a081e6658a0a18b15348f223578645a5b998f Mon Sep 17 00:00:00 2001 From: ShaunTsai Date: Thu, 9 Apr 2026 13:36:37 +0800 Subject: [PATCH 49/80] docs: add Discord community invite link to README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 6c784477..ab50c15f 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ 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. +🪼 **Join our community!** Come say hi on Discord — we'd love to have you: **[🪼 OpenAB — Official](https://discord.gg/YNksK9M6)** 🎉 + ``` ┌──────────────┐ Gateway WS ┌──────────────┐ ACP stdio ┌──────────────┐ │ Discord │◄─────────────►│ openab │──────────────►│ coding CLI │ From c5e03dccde5dff71fd62c2f5ca0a95253ea76825 Mon Sep 17 00:00:00 2001 From: Masami <266806885+masami-agent@users.noreply.github.com> Date: Fri, 10 Apr 2026 07:28:35 +0800 Subject: [PATCH 50/80] feat: add allowed_users config for per-user access control (#108) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add allowed_users config for per-user access control - Add allowed_users field to DiscordConfig (serde default = empty) - Add user ID check in discord message handler; react 🚫 if denied - Extract parse_id_set() helper to DRY up channel/user ID parsing - Fail closed when all configured IDs are invalid - Log tracing::warn on 🚫 reaction failure and invalid ID entries - Helm: use toJson for both allowedChannels and allowedUsers - Helm: add regexMatch validation for allowedUsers (--set mangling) - Consistent rendering: both lists always rendered (no if-condition) Closes #107 * refactor: improve logging and style for allowed_users - Upgrade denied user log from debug to info (security audit) - Add parsed allowlist count log after parse_id_set - Simplify ReactionType::Unicode path via import * docs: add allowed_users setup guide and config examples - discord-bot-howto.md: add User ID setup (step 7), allowed_users config example, access control behavior table - README.md: add allowed_users to Quick Start and config reference * fix: add trailing newline to main.rs --------- Co-authored-by: masami-agent --- README.md | 2 ++ charts/openab/templates/configmap.yaml | 8 +++++++- charts/openab/values.yaml | 3 +++ config.toml.example | 1 + docs/discord-bot-howto.md | 24 ++++++++++++++++++++-- src/config.rs | 2 ++ src/discord.rs | 11 +++++++++- src/main.rs | 28 ++++++++++++++++++++------ 8 files changed, 69 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index ab50c15f..4cb03ab2 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ Edit `config.toml`: [discord] bot_token = "${DISCORD_BOT_TOKEN}" allowed_channels = ["YOUR_CHANNEL_ID"] +# allowed_users = ["YOUR_USER_ID"] # optional: restrict who can use the bot [agent] command = "kiro-cli" @@ -164,6 +165,7 @@ env = { GEMINI_API_KEY = "${GEMINI_API_KEY}" } [discord] bot_token = "${DISCORD_BOT_TOKEN}" # supports env var expansion allowed_channels = ["123456789"] # channel ID allowlist +# allowed_users = ["987654321"] # user ID allowlist (empty = all users) [agent] command = "kiro-cli" # CLI command diff --git a/charts/openab/templates/configmap.yaml b/charts/openab/templates/configmap.yaml index 273e924c..9b97cfd8 100644 --- a/charts/openab/templates/configmap.yaml +++ b/charts/openab/templates/configmap.yaml @@ -17,7 +17,13 @@ data: {{- fail (printf "discord.allowedChannels contains a mangled ID: %s — use --set-string instead of --set for channel IDs" (toString .)) }} {{- end }} {{- end }} - allowed_channels = [{{ range $i, $ch := $cfg.discord.allowedChannels }}{{ if $i }}, {{ end }}"{{ $ch }}"{{ end }}] + allowed_channels = {{ $cfg.discord.allowedChannels | default list | toJson }} + {{- range $cfg.discord.allowedUsers }} + {{- if regexMatch "e\\+|E\\+" (toString .) }} + {{- fail (printf "discord.allowedUsers contains a mangled ID: %s — use --set-string instead of --set for user IDs" (toString .)) }} + {{- end }} + {{- end }} + allowed_users = {{ $cfg.discord.allowedUsers | default list | toJson }} [agent] command = "{{ $cfg.command }}" diff --git a/charts/openab/values.yaml b/charts/openab/values.yaml index 1f7c2134..d2c7a783 100644 --- a/charts/openab/values.yaml +++ b/charts/openab/values.yaml @@ -28,6 +28,7 @@ agents: # # ⚠️ Use --set-string for channel IDs to avoid float64 precision loss # allowedChannels: # - "YOUR_CHANNEL_ID" + # allowedUsers: [] # workingDir: /home/agent # env: {} # envFrom: [] @@ -57,6 +58,8 @@ agents: # ⚠️ Use --set-string for channel IDs to avoid float64 precision loss allowedChannels: - "YOUR_CHANNEL_ID" + # ⚠️ Use --set-string for user IDs to avoid float64 precision loss + allowedUsers: [] # empty = allow all users (default) workingDir: /home/agent env: {} envFrom: [] diff --git a/config.toml.example b/config.toml.example index c4227dc6..598c3017 100644 --- a/config.toml.example +++ b/config.toml.example @@ -1,6 +1,7 @@ [discord] bot_token = "${DISCORD_BOT_TOKEN}" allowed_channels = ["1234567890"] +# allowed_users = [""] # empty or omitted = allow all users [agent] command = "kiro-cli" diff --git a/docs/discord-bot-howto.md b/docs/discord-bot-howto.md index 672ac8c9..b80cd9e2 100644 --- a/docs/discord-bot-howto.md +++ b/docs/discord-bot-howto.md @@ -47,7 +47,14 @@ Step-by-step guide to create and configure a Discord bot for openab. 3. Click **Copy Channel ID** 4. Use this ID in `allowed_channels` in your config -## 7. Configure openab +## 7. Get Your User ID (optional) + +1. Make sure **Developer Mode** is enabled (see step 6) +2. Right-click your own username (in a message or the member list) +3. Click **Copy User ID** +4. Use this ID in `allowed_users` to restrict who can interact with the bot + +## 8. Configure openab Set the bot token and channel ID: @@ -61,15 +68,28 @@ In `config.toml`: [discord] bot_token = "${DISCORD_BOT_TOKEN}" allowed_channels = ["your-channel-id-from-step-6"] +# allowed_users = ["your-user-id-from-step-7"] # optional: restrict who can use the bot ``` +### Access control behavior + +| `allowed_channels` | `allowed_users` | Result | +|---|---|---| +| empty | empty | All users, all channels (default) | +| set | empty | Only these channels, all users | +| empty | set | All channels, only these users | +| set | set | **AND** — must be in allowed channel AND allowed user | + +- Empty `allowed_users` (default) = no user filtering, fully backward compatible +- Denied users get a 🚫 reaction and no reply + For Kubernetes: ```bash kubectl create secret generic openab-secret \ --from-literal=discord-bot-token="your-token-from-step-3" ``` -## 8. Test +## 9. Test In the allowed channel, mention the bot: diff --git a/src/config.rs b/src/config.rs index 719feafa..6d341e27 100644 --- a/src/config.rs +++ b/src/config.rs @@ -18,6 +18,8 @@ pub struct DiscordConfig { pub bot_token: String, #[serde(default)] pub allowed_channels: Vec, + #[serde(default)] + pub allowed_users: Vec, } #[derive(Debug, Deserialize)] diff --git a/src/discord.rs b/src/discord.rs index da52c691..5b4bb8b0 100644 --- a/src/discord.rs +++ b/src/discord.rs @@ -3,7 +3,7 @@ use crate::config::ReactionsConfig; use crate::format; use crate::reactions::StatusReactionController; use serenity::async_trait; -use serenity::model::channel::Message; +use serenity::model::channel::{Message, ReactionType}; use serenity::model::gateway::Ready; use serenity::model::id::{ChannelId, MessageId}; use serenity::prelude::*; @@ -15,6 +15,7 @@ use tracing::{error, info}; pub struct Handler { pub pool: Arc, pub allowed_channels: HashSet, + pub allowed_users: HashSet, pub reactions_config: ReactionsConfig, } @@ -64,6 +65,14 @@ impl EventHandler for Handler { return; } + if !self.allowed_users.is_empty() && !self.allowed_users.contains(&msg.author.id.get()) { + tracing::info!(user_id = %msg.author.id, "denied user, ignoring"); + if let Err(e) = msg.react(&ctx.http, ReactionType::Unicode("🚫".into())).await { + tracing::warn!(error = %e, "failed to react with 🚫"); + } + return; + } + let prompt = if is_mentioned { strip_mention(&msg.content) } else { diff --git a/src/main.rs b/src/main.rs index a216b668..05bbfd84 100644 --- a/src/main.rs +++ b/src/main.rs @@ -29,6 +29,7 @@ async fn main() -> anyhow::Result<()> { agent_cmd = %cfg.agent.command, pool_max = cfg.pool.max_sessions, channels = ?cfg.discord.allowed_channels, + users = ?cfg.discord.allowed_users, reactions = cfg.reactions.enabled, "config loaded" ); @@ -36,16 +37,14 @@ async fn main() -> anyhow::Result<()> { let pool = Arc::new(acp::SessionPool::new(cfg.agent, cfg.pool.max_sessions)); let ttl_secs = cfg.pool.session_ttl_hours * 3600; - let allowed_channels: HashSet = cfg - .discord - .allowed_channels - .iter() - .filter_map(|s| s.parse().ok()) - .collect(); + let allowed_channels = parse_id_set(&cfg.discord.allowed_channels, "allowed_channels")?; + let allowed_users = parse_id_set(&cfg.discord.allowed_users, "allowed_users")?; + info!(channels = allowed_channels.len(), users = allowed_users.len(), "parsed allowlists"); let handler = discord::Handler { pool: pool.clone(), allowed_channels, + allowed_users, reactions_config: cfg.reactions, }; @@ -84,3 +83,20 @@ async fn main() -> anyhow::Result<()> { info!("openab shut down"); Ok(()) } + +fn parse_id_set(raw: &[String], label: &str) -> anyhow::Result> { + let set: HashSet = raw + .iter() + .filter_map(|s| match s.parse() { + Ok(id) => Some(id), + Err(_) => { + tracing::warn!(value = %s, label = label, "ignoring invalid entry"); + None + } + }) + .collect(); + if !raw.is_empty() && set.is_empty() { + anyhow::bail!("all {label} entries failed to parse — refusing to start with an empty allowlist"); + } + Ok(set) +} From 034fc95e024d7623163f91fdeb3a65657996094d Mon Sep 17 00:00:00 2001 From: chaodufashi Date: Fri, 10 Apr 2026 08:11:17 +0800 Subject: [PATCH 51/80] docs: add gh auth device flow guide for agent environments (#174) * docs: add gh auth device flow guide for agent environments * docs: suggest ~/.kiro/steering/gh.md for persistent agent config * docs: clarify steering snippet is Kiro CLI only --------- Co-authored-by: chaodu-agent --- docs/discord-bot-howto.md | 1 + docs/gh-auth-device-flow.md | 92 +++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 docs/gh-auth-device-flow.md diff --git a/docs/discord-bot-howto.md b/docs/discord-bot-howto.md index b80cd9e2..db1a0a06 100644 --- a/docs/discord-bot-howto.md +++ b/docs/discord-bot-howto.md @@ -104,3 +104,4 @@ The bot should create a thread and respond. After that, just type in the thread - **Bot doesn't respond** — check that the channel ID is correct and the bot has permissions in that channel - **"Sent invalid authentication"** — the bot token is wrong or expired, reset it in the Developer Portal - **"Failed to start agent"** — kiro-cli isn't authenticated, run `kiro-cli login --use-device-flow` inside the container +- **`gh` commands fail with 401** — the agent needs GitHub CLI authentication. See [gh auth device flow guide](gh-auth-device-flow.md) for how to authenticate in a headless container diff --git a/docs/gh-auth-device-flow.md b/docs/gh-auth-device-flow.md new file mode 100644 index 00000000..83e3ad7b --- /dev/null +++ b/docs/gh-auth-device-flow.md @@ -0,0 +1,92 @@ +# GitHub CLI Authentication in Agent Environments + +How to authenticate `gh` (GitHub CLI) when the agent runs in a headless container and the user may be on mobile. + +## Why `gh` auth matters + +`gh` is one of the most common tools agents use to interact with GitHub — reviewing PRs, creating issues, commenting, approving, merging, etc. Before the agent can do any of this, `gh` must be authenticated. + +## Challenges + +This isn't a typical `gh login` scenario. Three things make it tricky: + +1. **The agent runs in a K8s pod with no browser** — `gh auth login --web` can't open a browser, so device flow (code + URL) is the only option +2. **The user might be on mobile, not at a desktop** — they're chatting via Discord on their phone, so the agent must send the URL and code as a clickable message +3. **The user authorizes on their phone** — they tap the link, enter the code in mobile Safari/Chrome, and the agent's background process picks up the token automatically + +``` +┌───────────┐ "review PR #108" ┌───────────┐ gh pr view ┌───────────┐ +│ Discord │──────────────────►│ OpenAB │────────────►│ GitHub │ +│ User │ │ + Agent │◄────────────│ API │ +└───────────┘ └─────┬─────┘ 401 🚫 └───────────┘ + │ + │ needs gh auth login first! + ▼ + ┌───────────┐ device flow ┌───────────┐ + │ Agent │─────────────►│ GitHub │ + │ (nohup) │ code+URL │ /login/ │ + └─────┬─────┘◄─────────────│ device │ + │ └─────┬─────┘ + │ sends code+URL │ + ▼ │ + ┌───────────┐ authorize ┌─────▼─────┐ + │ Discord │─────────────►│ Browser │ + │ User │ enters code │ (mobile) │ + └───────────┘ └───────────┘ +``` + +## The problem with naive approaches + +`gh auth login --web` uses device flow: it prints a one-time code + URL, then polls GitHub until the user authorizes. In an agent environment the shell is synchronous — it blocks until the command finishes: + +| Approach | What happens | +|---|---| +| Run directly | Blocks forever. User never sees the code. | +| `timeout N gh auth login -w` | Code appears only after timeout kills the process — token is never saved. | + +## Solution: `nohup` + background + read log + +```bash +nohup gh auth login --hostname github.com --git-protocol https -p https -w > /tmp/gh-login.log 2>&1 & +sleep 3 && cat /tmp/gh-login.log +``` + +How it works: +1. `nohup ... &` runs `gh` in the background so the shell returns immediately +2. `sleep 3 && cat` reads the log after `gh` has printed the code + URL +3. The agent sends the code + URL to the user (via Discord) +4. The user opens the link (even on mobile), enters the code +5. `gh` detects the authorization and saves the token +6. Done — `gh auth status` confirms login + +## Verify + +```bash +gh auth status +``` + +## Steering / prompt snippet (Kiro CLI only) + +> **Note:** This section applies only to [Kiro CLI](https://kiro.dev) agents. Other agent backends (Claude Code, Codex, Gemini) have their own prompt/config mechanisms. + +To make your Kiro agent always handle `gh login` correctly, create `~/.kiro/steering/gh.md`: + +```bash +mkdir -p ~/.kiro/steering +cat > ~/.kiro/steering/gh.md << 'EOF' +# GitHub CLI + +## Device Flow Login + +When asked to "gh login", always use nohup + background + read log: + +```bash +nohup gh auth login --hostname github.com --git-protocol https -p https -w > /tmp/gh-login.log 2>&1 & +sleep 3 && cat /tmp/gh-login.log +``` + +Never use `timeout`. The shell tool is synchronous — it blocks until the command finishes, so stdout won't be visible until then. `nohup` runs it in the background, `sleep 3 && cat` grabs the code immediately. +EOF +``` + +Kiro CLI automatically picks up `~/.kiro/steering/*.md` files as persistent context, so the agent will remember this across all sessions. From b212ec0a50d77fa0f2a304af1ebcb0904aa68866 Mon Sep 17 00:00:00 2001 From: 3mi Agent Date: Sat, 11 Apr 2026 03:26:12 +0800 Subject: [PATCH 52/80] feat: capture and display ACP error responses in Discord (#170) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: capture and display ACP error responses in Discord When the ACP agent returns an error, display a user-friendly message instead of '_(no response)_'. - Capture response_error from ACP notification before loop exits - Show error on empty response or append to existing content - format_error() uses protocol-level codes (JSON-RPC / HTTP) - no provider-specific strings - Provider-agnostic: message text passed through verbatim from upstream agent * feat: add format_user_error() for unified error display Two error formatters: - format_user_error(message: &str): handles startup/connection errors from pool.rs - timeout waiting for session/new → Request Timeout - connection/channel closed → Connection Lost - failed to spawn/no such file → Agent Not Found - pool exhausted → Service Busy - invalid api key/unauthorized → Unauthorized - format_coded_error(code, message): handles ACP response errors (JSON-RPC/HTTP codes) Both paths now use format_user_error() for startup errors and format_coded_error() for response errors. Link to #50. * fix: remove hardcoded 'claude' from format_user_error Address reviewer feedback: - Remove .contains("claude") from failed_to_spawn branch - was contradicting provider-agnostic goal - Make format_coded_error public for reuse by other adapters - Add comment explaining error + partial content display behavior - Add 17 unit tests for format_user_error and format_coded_error * refactor: extract error formatters to src/error_display.rs module Address reviewer feedback from #170: - Fix case sensitivity bug in timeout method extraction (use msg_lower.find) - Extract format_user_error + format_coded_error to separate module - JSON-RPC -32099..=-32000 range catch-all for server errors - Add mixed-case timeout test to verify fix - Tests moved to error_display module (19 total, all pass) The formatters are now reusable by other adapters (Slack, etc.) without depending on the discord module. * perf: cache regex in strip_mention using LazyLock Regex is now compiled once at startup via std::sync::LazyLock, instead of on every call. Covers the nice-to-have reviewer item. --------- Co-authored-by: OpenClaw Bot --- src/discord.rs | 30 +++++- src/error_display.rs | 212 +++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 1 + 3 files changed, 239 insertions(+), 4 deletions(-) create mode 100644 src/error_display.rs diff --git a/src/discord.rs b/src/discord.rs index 5b4bb8b0..f176a3d6 100644 --- a/src/discord.rs +++ b/src/discord.rs @@ -1,5 +1,6 @@ use crate::acp::{classify_notification, AcpEvent, SessionPool}; use crate::config::ReactionsConfig; +use crate::error_display::{format_coded_error, format_user_error}; use crate::format; use crate::reactions::StatusReactionController; use serenity::async_trait; @@ -8,6 +9,7 @@ use serenity::model::gateway::Ready; use serenity::model::id::{ChannelId, MessageId}; use serenity::prelude::*; use std::collections::HashSet; +use std::sync::LazyLock; use std::sync::Arc; use tokio::sync::watch; use tracing::{error, info}; @@ -127,7 +129,8 @@ impl EventHandler for Handler { let thread_key = thread_id.to_string(); if let Err(e) = self.pool.get_or_create(&thread_key).await { - let _ = edit(&ctx, thread_channel, thinking_msg.id, "⚠️ Failed to start agent.").await; + let msg = format_user_error(&e.to_string()); + let _ = edit(&ctx, thread_channel, thinking_msg.id, &format!("⚠️ {}", msg)).await; error!("pool error: {e}"); return; } @@ -263,8 +266,13 @@ async fn stream_prompt( // Process ACP notifications let mut got_first_text = false; + let mut response_error: Option = None; while let Some(notification) = rx.recv().await { if notification.id.is_some() { + // Capture error from ACP response to display in Discord + if let Some(ref err) = notification.error { + response_error = Some(format_coded_error(err.code, &err.message)); + } break; } @@ -305,8 +313,18 @@ async fn stream_prompt( // Final edit let final_content = compose_display(&tool_lines, &text_buf); + // If ACP returned both an error and partial text, show both. + // This can happen when the agent started producing content before hitting an error + // (e.g. context length limit, rate limit mid-stream). Showing both gives users + // full context rather than hiding the partial response. let final_content = if final_content.is_empty() { - "_(no response)_".to_string() + if let Some(err) = response_error { + format!("⚠️ {}", err) + } else { + "_(no response)_".to_string() + } + } else if let Some(err) = response_error { + format!("⚠️ {}\n\n{}", err, final_content) } else { final_content }; @@ -339,9 +357,12 @@ fn compose_display(tool_lines: &[String], text: &str) -> String { out } +static MENTION_RE: LazyLock = LazyLock::new(|| { + regex::Regex::new(r"<@[!&]?\d+>").unwrap() +}); + fn strip_mention(content: &str) -> String { - let re = regex::Regex::new(r"<@[!&]?\d+>").unwrap(); - re.replace_all(content, "").trim().to_string() + MENTION_RE.replace_all(content, "").trim().to_string() } fn shorten_thread_name(prompt: &str) -> String { @@ -378,3 +399,4 @@ async fn get_or_create_thread(ctx: &Context, msg: &Message, prompt: &str) -> any Ok(thread.id.get()) } + diff --git a/src/error_display.rs b/src/error_display.rs new file mode 100644 index 00000000..40f1479a --- /dev/null +++ b/src/error_display.rs @@ -0,0 +1,212 @@ +/// Format any error for user display in Discord. +/// +/// Handles two error categories: +/// - **Coded errors** (code != 0): JSON-RPC or HTTP status codes from upstream agent. +/// - **Startup/connection errors** (code == 0): Errors from pool.rs or connection.rs +/// where only the message string is available. +/// +/// Provider-agnostic: no provider-specific strings, message text passed through verbatim. +pub fn format_user_error(message: &str) -> String { + let msg_lower = message.to_lowercase(); + + // Startup / connection errors (code == 0 from anyhow) + if msg_lower.contains("timeout waiting for") { + // Use msg_lower for extraction to stay case-insistent with the match above. + // msg_lower and message are the same length, so byte offsets are valid. + if let Some(start) = msg_lower.find("timeout waiting for ") { + let rest = &message[start + "timeout waiting for ".len()..]; + let method = rest.split_whitespace().next().unwrap_or("request"); + return format!("**Request Timeout**\nTimeout waiting for {}, please try again.", method); + } + return "**Request Timeout**\nTimeout waiting for a response, please try again.".to_string(); + } + if msg_lower.contains("connection closed") || msg_lower.contains("channel closed") { + return "**Connection Lost**\nThe connection to the agent was lost, please try again.".to_string(); + } + if msg_lower.contains("failed to spawn") || msg_lower.contains("no such file") { + return "**Agent Not Found**\nCould not start the agent — please check your configuration.".to_string(); + } + if msg_lower.contains("pool exhausted") { + return "**Service Busy**\nAll agent sessions are in use, please try again shortly.".to_string(); + } + if msg_lower.contains("invalid api key") || msg_lower.contains("unauthorized") { + return "**Unauthorized**\nPlease check your API key configuration.".to_string(); + } + + // Unknown error — pass through as-is + if message.is_empty() { + "**Error**\nAn unknown error occurred.".to_string() + } else { + format!("**Error**\n{}", message) + } +} + +/// Format coded error from ACP agent for display in Discord. +/// Used for response errors that have a JSON-RPC or HTTP status code. +/// Public for reuse by other adapters (e.g. Slack). +pub fn format_coded_error(code: i64, message: &str) -> String { + let prefix = match code { + 400 => "**Bad Request**", + 401 => "**Unauthorized**", + 403 => "**Forbidden**", + 404 => "**Not Found**", + 408 => "**Request Timeout**", + 429 => "**Rate Limited**", + 500 => "**Internal Server Error**", + 502 => "**Bad Gateway**", + 503 => "**Service Unavailable**", + 504 => "**Gateway Timeout**", + -32600 => "**Invalid Request**", + -32601 => "**Method Not Found**", + -32602 => "**Invalid Params**", + -32603 => "**Internal Error**", + -32099..=-32000 => "**Server Error**", + _ => "**Error**", + }; + if message.is_empty() { + format!("{} (code: {})", prefix, code) + } else { + format!("{} (code: {})\n{}", prefix, code, message) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // ─── format_user_error tests ───────────────────────────────────────────── + + #[test] + fn test_format_user_error_timeout() { + let result = format_user_error("timeout waiting for session/new response"); + assert!(result.contains("Request Timeout")); + assert!(result.contains("session/new")); + } + + #[test] + fn test_format_user_error_connection_closed() { + let result = format_user_error("connection closed"); + assert!(result.contains("Connection Lost")); + } + + #[test] + fn test_format_user_error_channel_closed() { + let result = format_user_error("channel closed"); + assert!(result.contains("Connection Lost")); + } + + #[test] + fn test_format_user_error_failed_to_spawn() { + let result = format_user_error("failed to spawn /some/path: No such file"); + assert!(result.contains("Agent Not Found")); + assert!(result.contains("the agent")); // generic, no provider name + } + + #[test] + fn test_format_user_error_no_such_file() { + let result = format_user_error("binary /usr/bin/nonexistent: no such file"); + assert!(result.contains("Agent Not Found")); + } + + #[test] + fn test_format_user_error_pool_exhausted() { + let result = format_user_error("pool exhausted (5 sessions)"); + assert!(result.contains("Service Busy")); + } + + #[test] + fn test_format_user_error_invalid_api_key() { + let result = format_user_error("invalid api key"); + assert!(result.contains("Unauthorized")); + } + + #[test] + fn test_format_user_error_unauthorized() { + let result = format_user_error("unauthorized: token rejected"); + assert!(result.contains("Unauthorized")); + } + + #[test] + fn test_format_user_error_unknown() { + let result = format_user_error("something went wrong"); + assert!(result.contains("Error")); + assert!(result.contains("something went wrong")); + } + + #[test] + fn test_format_user_error_empty() { + let result = format_user_error(""); + assert!(result.contains("Error")); + assert!(result.contains("unknown")); + } + + #[test] + fn test_format_user_error_case_insensitive() { + assert!(format_user_error("TIMEOUT WAITING FOR foo").contains("Timeout")); + assert!(format_user_error("CONNECTION CLOSED").contains("Connection")); + assert!(format_user_error("POOL EXHAUSTED").contains("Busy")); + } + + #[test] + fn test_format_user_error_mixed_case_timeout() { + // Case-insensitive matching should still extract method correctly + let result = format_user_error("Timeout Waiting For custom/method"); + assert!(result.contains("Request Timeout")); + assert!(result.contains("custom/method")); + } + + // ─── format_coded_error tests ─────────────────────────────────────────── + + #[test] + fn test_format_coded_error_401() { + let result = format_coded_error(401, "invalid token"); + assert!(result.contains("Unauthorized")); + assert!(result.contains("401")); + assert!(result.contains("invalid token")); + } + + #[test] + fn test_format_coded_error_429() { + let result = format_coded_error(429, ""); + assert!(result.contains("Rate Limited")); + assert!(result.contains("429")); + assert!(!result.contains("\n")); // no message, no newline + } + + #[test] + fn test_format_coded_error_503() { + let result = format_coded_error(503, "service unavailable"); + assert!(result.contains("Service Unavailable")); + assert!(result.contains("503")); + assert!(result.contains("service unavailable")); + } + + #[test] + fn test_format_coded_error_json_rpc() { + let result = format_coded_error(-32602, "missing required parameter"); + assert!(result.contains("Invalid Params")); + assert!(result.contains("-32602")); + } + + #[test] + fn test_format_coded_error_server_error_range() { + let result = format_coded_error(-32050, "internal failure"); + assert!(result.contains("Server Error")); + assert!(result.contains("-32050")); + } + + #[test] + fn test_format_coded_error_connection_error() { + let result = format_coded_error(-32000, "connection refused"); + assert!(result.contains("Server Error")); // -32000 falls in -32099..=-32000 range + assert!(result.contains("-32000")); + } + + #[test] + fn test_format_coded_error_unknown_code() { + let result = format_coded_error(999, "something happened"); + assert!(result.contains("Error")); + assert!(result.contains("999")); + assert!(result.contains("something happened")); + } +} diff --git a/src/main.rs b/src/main.rs index 05bbfd84..39817342 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ mod acp; mod config; mod discord; +mod error_display; mod format; mod reactions; From c3c1607d3989ce0bd804eea8e26783191c801865 Mon Sep 17 00:00:00 2001 From: "openab-app[bot]" <274185012+openab-app[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 19:28:23 +0000 Subject: [PATCH 53/80] chore: bump chart to 0.6.2-beta.37 (#187) image: b212ec0 Co-authored-by: openab-app[bot] <274185012+openab-app[bot]@users.noreply.github.com> --- charts/openab/Chart.yaml | 4 ++-- charts/openab/values.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/charts/openab/Chart.yaml b/charts/openab/Chart.yaml index a7ca8b14..065371c8 100644 --- a/charts/openab/Chart.yaml +++ b/charts/openab/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: openab description: Discord ↔ ACP coding CLI bridge (Kiro CLI, Claude Code, Codex, Gemini) type: application -version: 0.6.1-beta.1 -appVersion: "52ac30a" +version: 0.6.2-beta.37 +appVersion: "3be10b0" diff --git a/charts/openab/values.yaml b/charts/openab/values.yaml index d2c7a783..6abe5bc2 100644 --- a/charts/openab/values.yaml +++ b/charts/openab/values.yaml @@ -1,7 +1,7 @@ image: repository: ghcr.io/openabdev/openab # tag defaults to .Chart.AppVersion - tag: "" + tag: "3be10b0" pullPolicy: IfNotPresent podSecurityContext: From 1f776213c09cdb37582efe3d9c96a9669cc87275 Mon Sep 17 00:00:00 2001 From: 3mi Agent Date: Sat, 11 Apr 2026 09:47:52 +0800 Subject: [PATCH 54/80] feat: support Discord image attachments via ACP ImageContent blocks (#158) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: support image attachments via ACP content blocks - Add ContentBlock enum with Text and Image variants - session_prompt() accepts Vec instead of &str - Add download_and_encode_image() using reqwest to fetch Discord attachments - Image format uses flat {data, mimeType} per claude-agent-acp API - Add reqwest and base64 dependencies - Max image size: 10MB * fix: address PR review feedback - security & quality fixes 🔴 Must Fix: - Add 10MB size limit check before downloading (defense in depth) - Filter non-image attachments by content-type or extension (skip if not image/*) - Remove unsafe fallback that treated unknown types as image/png 🟡 Should Fix: - Remove reqwest 'blocking' feature, use rustls-tls only (consistent with serenity) - Reuse static HTTP_CLIENT instead of creating new Client per attachment - Add once_cell for static Lazy client initialization 🟤 Cleanup: - Remove dead 'empty prompt' check (sender_context always present) - Add clarifying comment that image-only messages are intentional - Use as_deref() for cleaner Option to &str conversion * fix: address PR review feedback 1. Re-add empty message guard (prompt empty AND no attachments → skip) 2. Revert import style to top-level imports (classify_notification, AcpEvent) 3. Replace once_cell with std::sync::LazyLock (MSRV 1.80+) 4. Keep reqwest (static client has connection reuse advantage) * fix: re-export ContentBlock from acp mod for cleaner import paths Re-export ContentBlock from crate::acp to avoid inline crate::acp::connection:: path in discord.rs. This keeps imports consolidated in the acp module's public API and matches the existing pattern used for SessionPool, classify_notification, and AcpEvent. No functional changes — just moduleorganization. * Strip MIME type parameters to prevent LLM API rejection Discord's content_type may include parameters (e.g. 'image/jpeg; charset=utf-8'). Downstream LLM APIs (Claude, OpenAI, Gemini) reject MIME types with parameters. Fix: split on ';' and trim whitespace before passing to ContentBlock::Image. --------- Co-authored-by: OpenClaw Bot Co-authored-by: Openbot Co-authored-by: Hermes Agent Co-authored-by: Hermes Agent --- Cargo.lock | 176 ++++++++++++++++++++---------------------- Cargo.toml | 2 + src/acp/connection.rs | 39 +++++++++- src/acp/mod.rs | 1 + src/discord.rs | 163 +++++++++++++++++++++++++++++++++++--- 5 files changed, 276 insertions(+), 105 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7fe18255..ac944efe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -90,9 +90,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" -version = "1.2.57" +version = "1.2.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" +checksum = "b7a4d3ec6524d28a329fc53654bbadc9bdd7b0431f5d65f1a56ffb28a1ee5283" dependencies = [ "find-msvc-tools", "shlex", @@ -430,9 +430,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hyper" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" dependencies = [ "atomic-waker", "bytes", @@ -443,7 +443,6 @@ dependencies = [ "httparse", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", @@ -491,12 +490,13 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -504,9 +504,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -517,9 +517,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -531,15 +531,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", @@ -551,15 +551,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", @@ -599,9 +599,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.13.0" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "45a8a2b9cb3e0b0c1803dbb0758ffac5de2f425b23c28f518faabd9d805342ff" dependencies = [ "equivalent", "hashbrown 0.16.1", @@ -617,9 +617,9 @@ checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "iri-string" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8e7418f59cc01c88316161279a7f665217ae316b388e58a0d10e29f54f1e5eb" +checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" dependencies = [ "memchr", "serde", @@ -633,10 +633,12 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.91" +version = "0.3.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] @@ -655,15 +657,15 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.183" +version = "0.2.184" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" [[package]] name = "litemap" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "lock_api" @@ -729,9 +731,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "wasi", @@ -749,9 +751,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" [[package]] name = "once_cell" @@ -764,8 +766,10 @@ name = "openab" version = "0.1.0" dependencies = [ "anyhow", + "base64", "rand 0.8.5", "regex", + "reqwest", "serde", "serde_json", "serenity", @@ -811,17 +815,11 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - [[package]] name = "potential_utf" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] @@ -1091,9 +1089,9 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "rustls" @@ -1185,9 +1183,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" @@ -1331,9 +1329,9 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] name = "slab" @@ -1482,9 +1480,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", @@ -1507,9 +1505,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.50.0" +version = "1.51.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +checksum = "f66bf9585cda4b724d3e78ab34b73fb2bbaba9011b9bfdf69dc836382ea13b8c" dependencies = [ "bytes", "libc", @@ -1524,9 +1522,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.1" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", @@ -1834,9 +1832,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.22.0" +version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" dependencies = [ "getrandom 0.4.2", "js-sys", @@ -1890,9 +1888,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.114" +version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0" dependencies = [ "cfg-if", "once_cell", @@ -1903,23 +1901,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.64" +version = "0.4.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +checksum = "03623de6905b7206edd0a75f69f747f134b7f0a2323392d664448bf2d3c5d87e" dependencies = [ - "cfg-if", - "futures-util", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.114" +version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1927,9 +1921,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.114" +version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2" dependencies = [ "bumpalo", "proc-macro2", @@ -1940,9 +1934,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.114" +version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b" dependencies = [ "unicode-ident", ] @@ -1996,9 +1990,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.91" +version = "0.3.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a" dependencies = [ "js-sys", "wasm-bindgen", @@ -2293,15 +2287,15 @@ dependencies = [ [[package]] name = "writeable" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -2310,9 +2304,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", @@ -2322,18 +2316,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.47" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.47" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", @@ -2342,18 +2336,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", @@ -2369,9 +2363,9 @@ checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -2380,9 +2374,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -2391,9 +2385,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index edfdf870..11f7dadc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,3 +15,5 @@ uuid = { version = "1", features = ["v4"] } regex = "1" anyhow = "1" rand = "0.8" +reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] } +base64 = "0.22" diff --git a/src/acp/connection.rs b/src/acp/connection.rs index 9fc5a1f1..53770509 100644 --- a/src/acp/connection.rs +++ b/src/acp/connection.rs @@ -20,6 +20,29 @@ fn expand_env(val: &str) -> String { } use tokio::time::Instant; +/// A content block for the ACP prompt — either text or image. +#[derive(Debug, Clone)] +pub enum ContentBlock { + Text { text: String }, + Image { media_type: String, data: String }, +} + +impl ContentBlock { + pub fn to_json(&self) -> Value { + match self { + ContentBlock::Text { text } => json!({ + "type": "text", + "text": text + }), + ContentBlock::Image { media_type, data } => json!({ + "type": "image", + "data": data, + "mimeType": media_type + }), + } + } +} + pub struct AcpConnection { _proc: Child, stdin: Arc>, @@ -242,11 +265,12 @@ impl AcpConnection { Ok(session_id) } - /// Send a prompt and return a receiver for streaming notifications. - /// The final message on the channel will have id set (the prompt response). + /// Send a prompt with content blocks (text and/or images) and return a receiver + /// for streaming notifications. The final message on the channel will have id set + /// (the prompt response). pub async fn session_prompt( &mut self, - prompt: &str, + content_blocks: Vec, ) -> Result<(mpsc::UnboundedReceiver, u64)> { self.last_active = Instant::now(); @@ -259,12 +283,19 @@ impl AcpConnection { *self.notify_tx.lock().await = Some(tx); let id = self.next_id(); + + // Convert content blocks to JSON + let prompt_json: Vec = content_blocks + .iter() + .map(|b| b.to_json()) + .collect(); + let req = JsonRpcRequest::new( id, "session/prompt", Some(json!({ "sessionId": session_id, - "prompt": [{"type": "text", "text": prompt}], + "prompt": prompt_json, })), ); let data = serde_json::to_string(&req)?; diff --git a/src/acp/mod.rs b/src/acp/mod.rs index 1ae3b8be..c67cad82 100644 --- a/src/acp/mod.rs +++ b/src/acp/mod.rs @@ -4,3 +4,4 @@ pub mod protocol; pub use pool::SessionPool; pub use protocol::{classify_notification, AcpEvent}; +pub use connection::ContentBlock; diff --git a/src/discord.rs b/src/discord.rs index f176a3d6..901945e3 100644 --- a/src/discord.rs +++ b/src/discord.rs @@ -1,8 +1,11 @@ -use crate::acp::{classify_notification, AcpEvent, SessionPool}; +use crate::acp::{classify_notification, AcpEvent, ContentBlock, SessionPool}; use crate::config::ReactionsConfig; use crate::error_display::{format_coded_error, format_user_error}; use crate::format; use crate::reactions::StatusReactionController; +use base64::engine::general_purpose::STANDARD as BASE64; +use base64::Engine; +use std::sync::LazyLock; use serenity::async_trait; use serenity::model::channel::{Message, ReactionType}; use serenity::model::gateway::Ready; @@ -12,7 +15,16 @@ use std::collections::HashSet; use std::sync::LazyLock; use std::sync::Arc; use tokio::sync::watch; -use tracing::{error, info}; +use tracing::{debug, error, info}; + +/// Reusable HTTP client for downloading Discord attachments. +/// Built once with a 30s timeout and rustls TLS (no native-tls deps). +static HTTP_CLIENT: LazyLock = LazyLock::new(|| { + reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(30)) + .build() + .expect("static HTTP client must build") +}); pub struct Handler { pub pool: Arc, @@ -80,10 +92,15 @@ impl EventHandler for Handler { } else { msg.content.trim().to_string() }; - if prompt.is_empty() { + + // No text and no image attachments → skip to avoid wasting session slots + if prompt.is_empty() && msg.attachments.is_empty() { return; } + // Build content blocks: text + image attachments + let mut content_blocks = vec![]; + // Inject structured sender context so the downstream CLI can identify who sent the message let display_name = msg.member.as_ref() .and_then(|m| m.nick.as_ref()) @@ -103,7 +120,37 @@ impl EventHandler for Handler { prompt ); - tracing::debug!(prompt = %prompt_with_sender, in_thread, "processing"); + // Add text block (always, even if empty, we still send for sender context) + content_blocks.push(ContentBlock::Text { + text: prompt_with_sender.clone(), + }); + + // Add image attachments + if !msg.attachments.is_empty() { + for attachment in &msg.attachments { + if let Some(content_block) = download_and_encode_image(attachment).await { + debug!(url = %attachment.url, filename = %attachment.filename, "adding image attachment"); + content_blocks.push(content_block); + } else { + error!( + url = %attachment.url, + filename = %attachment.filename, + "failed to download image attachment" + ); + } + } + } + + tracing::debug!( + text_len = prompt_with_sender.len(), + num_attachments = msg.attachments.len(), + in_thread, + "processing" + ); + + // Note: image-only messages (no text) are intentionally allowed since + // prompt_with_sender always includes the non-empty sender_context XML. + // The guard above (prompt.is_empty() && no attachments) handles stickers/embeds. let thread_id = if in_thread { msg.channel_id.get() @@ -146,11 +193,11 @@ impl EventHandler for Handler { )); reactions.set_queued().await; - // Stream prompt with live edits + // Stream prompt with live edits (pass content blocks instead of just text) let result = stream_prompt( &self.pool, &thread_key, - &prompt_with_sender, + content_blocks, &ctx, thread_channel, thinking_msg.id, @@ -187,6 +234,103 @@ impl EventHandler for Handler { } } +/// Download a Discord image attachment and encode it as an ACP image content block. +/// +/// Discord attachment URLs are temporary and expire, so we must download +/// and encode the image data immediately. The ACP ImageContent schema +/// requires `{ data: base64_string, mimeType: "image/..." }`. +/// +/// Security: rejects non-image attachments (by content-type or extension) +/// and files larger than 10MB to prevent OOM/abuse. +async fn download_and_encode_image(attachment: &serenity::model::channel::Attachment) -> Option { + const MAX_SIZE: u64 = 10 * 1024 * 1024; // 10 MB + + let url = &attachment.url; + if url.is_empty() { + return None; + } + + // Determine media type — prefer content-type header, fallback to extension + let media_type = attachment + .content_type + .as_deref() + .or_else(|| { + attachment + .filename + .rsplit('.') + .next() + .and_then(|ext| match ext.to_lowercase().as_str() { + "png" => Some("image/png"), + "jpg" | "jpeg" => Some("image/jpeg"), + "gif" => Some("image/gif"), + "webp" => Some("image/webp"), + _ => None, + }) + }); + + // Validate that it's actually an image + let Some(mime) = media_type else { + debug!(filename = %attachment.filename, "skipping non-image attachment (no matching content-type or extension)"); + return None; + }; + // Strip MIME type parameters (e.g. "image/jpeg; charset=utf-8" → "image/jpeg") + // Downstream LLM APIs (Claude, OpenAI, Gemini) reject MIME types with parameters + let mime = mime.split(';').next().unwrap_or(mime).trim(); + if !mime.starts_with("image/") { + debug!(filename = %attachment.filename, mime = %mime, "skipping non-image attachment"); + return None; + } + + // Size check before downloading + if u64::from(attachment.size) > MAX_SIZE { + error!( + filename = %attachment.filename, + size = attachment.size, + max = MAX_SIZE, + "image attachment exceeds 10MB limit" + ); + return None; + } + + // Download using the static reusable client + let response = match HTTP_CLIENT.get(url).send().await { + Ok(resp) => resp, + Err(e) => { + error!("failed to download image {}: {}", url, e); + return None; + } + }; + + if !response.status().is_success() { + error!("HTTP error downloading image {}: {}", url, response.status()); + return None; + } + + let bytes = match response.bytes().await { + Ok(b) => b, + Err(e) => { + error!("failed to read image bytes from {}: {}", url, e); + return None; + } + }; + + // Final size check after download (defense in depth) + if bytes.len() as u64 > MAX_SIZE { + error!( + filename = %attachment.filename, + size = bytes.len(), + "downloaded image exceeds 10MB limit after decode" + ); + return None; + } + + let encoded = BASE64.encode(bytes.as_ref()); + Some(ContentBlock::Image { + media_type: mime.to_string(), + data: encoded, + }) +} + async fn edit(ctx: &Context, ch: ChannelId, msg_id: MessageId, content: &str) -> serenity::Result { ch.edit_message(&ctx.http, msg_id, serenity::builder::EditMessage::new().content(content)).await } @@ -194,24 +338,23 @@ async fn edit(ctx: &Context, ch: ChannelId, msg_id: MessageId, content: &str) -> async fn stream_prompt( pool: &SessionPool, thread_key: &str, - prompt: &str, + content_blocks: Vec, ctx: &Context, channel: ChannelId, msg_id: MessageId, reactions: Arc, ) -> anyhow::Result<()> { - let prompt = prompt.to_string(); let reactions = reactions.clone(); pool.with_connection(thread_key, |conn| { - let prompt = prompt.clone(); + let content_blocks = content_blocks.clone(); let ctx = ctx.clone(); let reactions = reactions.clone(); Box::pin(async move { let reset = conn.session_reset; conn.session_reset = false; - let (mut rx, _) = conn.session_prompt(&prompt).await?; + let (mut rx, _): (_, _) = conn.session_prompt(content_blocks).await?; reactions.set_thinking().await; let initial = if reset { From f8634e1048f536ffdd22478703391affe4ee214e Mon Sep 17 00:00:00 2001 From: JARVIS-Agent <55979927+JARVIS-coding-Agent@users.noreply.github.com> Date: Sat, 11 Apr 2026 13:41:33 +0800 Subject: [PATCH 55/80] fix: remove trailing comma in TOML inline table (#196) Co-authored-by: JARVIS-coding-Agent --- charts/openab/templates/configmap.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/openab/templates/configmap.yaml b/charts/openab/templates/configmap.yaml index 9b97cfd8..95bd68ef 100644 --- a/charts/openab/templates/configmap.yaml +++ b/charts/openab/templates/configmap.yaml @@ -30,7 +30,7 @@ data: args = {{ if $cfg.args }}{{ $cfg.args | toJson }}{{ else }}[]{{ end }} working_dir = "{{ $cfg.workingDir | default "/home/agent" }}" {{- if $cfg.env }} - env = { {{ range $k, $v := $cfg.env }}{{ $k }} = "{{ $v }}", {{ end }} } + env = { {{ $first := true }}{{ range $k, $v := $cfg.env }}{{ if not $first }}, {{ end }}{{ $k }} = "{{ $v }}"{{ $first = false }}{{ end }} } {{- end }} [pool] From 79473151cadc402670ffe51b203676cbc1ad1696 Mon Sep 17 00:00:00 2001 From: Cheng-Lung Sung Date: Sat, 11 Apr 2026 13:46:00 +0800 Subject: [PATCH 56/80] refactor: remove duplicate LazyLock import in src/discord.rs (#198) - The `std::sync::LazyLock` was imported twice in the same file. - This change removes the redundant import while keeping the necessary one. --- src/discord.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/discord.rs b/src/discord.rs index 901945e3..b8d7e53e 100644 --- a/src/discord.rs +++ b/src/discord.rs @@ -12,7 +12,6 @@ use serenity::model::gateway::Ready; use serenity::model::id::{ChannelId, MessageId}; use serenity::prelude::*; use std::collections::HashSet; -use std::sync::LazyLock; use std::sync::Arc; use tokio::sync::watch; use tracing::{debug, error, info}; From 16c49f3e6f339dfb0f064afafd4c17113d7a0ba6 Mon Sep 17 00:00:00 2001 From: "openab-app[bot]" <274185012+openab-app[bot]@users.noreply.github.com> Date: Sat, 11 Apr 2026 05:50:37 +0000 Subject: [PATCH 57/80] chore: bump chart to 0.6.3-beta.39 (#199) image: 7947315 Co-authored-by: openab-app[bot] <274185012+openab-app[bot]@users.noreply.github.com> --- charts/openab/Chart.yaml | 4 ++-- charts/openab/values.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/charts/openab/Chart.yaml b/charts/openab/Chart.yaml index 065371c8..af5733cb 100644 --- a/charts/openab/Chart.yaml +++ b/charts/openab/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: openab description: Discord ↔ ACP coding CLI bridge (Kiro CLI, Claude Code, Codex, Gemini) type: application -version: 0.6.2-beta.37 -appVersion: "3be10b0" +version: 0.6.3-beta.39 +appVersion: "94253a5" diff --git a/charts/openab/values.yaml b/charts/openab/values.yaml index 6abe5bc2..22b7a255 100644 --- a/charts/openab/values.yaml +++ b/charts/openab/values.yaml @@ -1,7 +1,7 @@ image: repository: ghcr.io/openabdev/openab # tag defaults to .Chart.AppVersion - tag: "3be10b0" + tag: "94253a5" pullPolicy: IfNotPresent podSecurityContext: From 6bde354c311922dc7cc34c562b8706c1537109fa Mon Sep 17 00:00:00 2001 From: Neil Kuan <46012524+neilkuan@users.noreply.github.com> Date: Sat, 11 Apr 2026 15:03:34 +0800 Subject: [PATCH 58/80] ci: tag-driven release with native GitHub workflows (#154) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ci: switch Build & Release to git tag driven flow - trigger: push tags v* (instead of push to main with paths filter) - version: parsed from tag (v0.7.0-beta.1 → 0.7.0-beta.1) - docker tags: sha + semver + major.minor + latest (stable only) - bump-chart: version comes directly from tag, no more GITHUB_RUN_NUMBER - workflow_dispatch: kept for manual trigger with explicit tag input * ci: mark beta chart releases as pre-release * ci: split chart release — beta pushes OCI only, stable opens PR Beta (tag contains '-'): → release-chart-beta: helm package + push to OCI registry → no PR, main branch untouched Stable (tag without '-'): → bump-chart-stable: update Chart.yaml + values.yaml → PR → auto merge → release.yml picks up Chart.yaml change → publish to GitHub Pages + OCI * ci: add resolve-tag job, fix workflow_dispatch bug, deduplicate - Add resolve-tag job: validates tag format, parses chart_version, image_sha, is_beta — single source of truth for all downstream jobs - Fix: workflow_dispatch now uses inputs.tag instead of github.ref_name for beta/stable branching (github.ref_name is branch name, not tag) - Remove 3× duplicated 'Resolve version tag' steps - IMAGE_SHA computed once in resolve-tag, not repeated per job * ci: stable release promotes beta image instead of rebuilding - build-image + merge-manifests: only run for beta tags - promote-stable: retag existing beta image with stable tags (version, major.minor, latest) using imagetools create - verify beta image exists before promoting — fail fast if not - bump-chart-stable now depends on promote-stable This ensures "what you tested is what you ship" — the stable release uses the exact same image artifact validated during beta. * ci: add tagpr workflow and config for automated release PR * ci: pin tagpr action to commit hash and bump checkout to v6 * ci: add tagpr workflow and config for automated release PR - Add tagpr.yml with GitHub App token (so tags trigger build.yml) - Simplify build.yml: remove beta/stable two-stage, all tags do full build - Add pre-release support: manual tags like v0.7.0-rc.1 won't overwrite latest - Configure .tagpr to sync Cargo.toml + Chart.yaml version/appVersion - Simplify release.yml: keep chart-releaser + install instructions - Align Cargo.toml and Chart.yaml versions to 0.6.0 - Rewrite RELEASING.md with new tag-driven flow * ci: add promote-stable to guarantee pre-release = stable image - Pre-release tag (v0.7.0-rc.1): full build, image tags = sha + version - Stable tag (v0.7.0): promote (re-tag) pre-release image, no rebuild - Verify pre-release image exists before promote, fail if not found - Update RELEASING.md with two-path flow diagram * docs: rewrite RELEASING.md with complete release flow and constraints * ci: promote-stable finds pre-release image by version tag, not commit SHA - promote-stable uses git tag -l to find latest pre-release tag (e.g. v0.7.0-rc.*) - re-tags pre-release image to stable tags (no rebuild, same artifact) - removes commit SHA dependency — pre-release and stable can be on different commits - natural flow: pre-release first → test → merge Release PR → auto promote * chore: update openab version to 0.6.0 in Cargo.lock * ci: upgrade tagpr to v1.18.1 and use vPrefix config * ci: replace deprecated app-id with client-id in create-github-app-token * ci: add CI workflow for PR validation on source and Dockerfile changes * docs: add GitHub App permissions to RELEASING.md * ci: replace tagpr with native GitHub workflows - Remove tagpr.yml and .tagpr (third-party dependency) - Add release-pr.yml: workflow_dispatch with auto/manual version bump - Add tag-on-merge.yml: auto-tag on release PR merge - Update RELEASING.md: document new flow, simplify App permissions * ci: address review feedback - release-chart: add resolve-tag.result == 'success' to if-condition - release-pr: add rust-toolchain + cargo generate-lockfile for Cargo.lock sync - tag-on-merge: validate version format before pushing tag - build: add default placeholder for workflow_dispatch tag input * fix: replace map_or with is_some_and to satisfy clippy * chore: use beta instead of rc for pre-release naming --------- Co-authored-by: thepagent --- .github/workflows/build.yml | 239 ++++++++++++++++------------- .github/workflows/ci.yml | 33 ++++ .github/workflows/release-pr.yml | 83 ++++++++++ .github/workflows/release.yml | 20 +-- .github/workflows/tag-on-merge.yml | 38 +++++ Cargo.lock | 2 +- Cargo.toml | 2 +- RELEASING.md | 214 ++++++++++++++++++++++---- charts/openab/Chart.yaml | 4 +- src/discord.rs | 2 +- 10 files changed, 476 insertions(+), 161 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release-pr.yml create mode 100644 .github/workflows/tag-on-merge.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c73084ae..51301bfd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,32 +2,17 @@ name: Build & Release on: push: - branches: - - main - paths: - - "src/**" - - "Cargo.toml" - - "Cargo.lock" - - "Dockerfile" - - "Dockerfile.*" + tags: + - "v*" workflow_dispatch: inputs: - chart_bump: - description: 'Chart version bump type' + tag: + description: 'Version tag (e.g. v0.7.0-beta.1 or v0.7.0)' required: true - type: choice - options: - - patch - - minor - - major - default: patch - release: - description: 'Stable release (no beta suffix)' - required: false - type: boolean - default: false + type: string + default: 'v' dry_run: - description: 'Dry run (show changes without committing)' + description: 'Dry run (build only, no push)' required: false type: boolean default: false @@ -37,7 +22,46 @@ env: IMAGE_NAME: ${{ github.repository }} jobs: + resolve-tag: + runs-on: ubuntu-latest + outputs: + tag: ${{ steps.resolve.outputs.tag }} + chart_version: ${{ steps.resolve.outputs.chart_version }} + is_prerelease: ${{ steps.resolve.outputs.is_prerelease }} + steps: + - name: Resolve and validate tag + id: resolve + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + TAG="${{ inputs.tag }}" + else + TAG="${GITHUB_REF_NAME}" + fi + + # Validate tag format + if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+ ]]; then + echo "::error::Invalid tag format '${TAG}'. Expected v{major}.{minor}.{patch}[-prerelease]" + exit 1 + fi + + CHART_VERSION="${TAG#v}" + + # Pre-release if version contains '-' (e.g. 0.7.0-beta.1) + if [[ "$CHART_VERSION" == *-* ]]; then + IS_PRERELEASE="true" + else + IS_PRERELEASE="false" + fi + + echo "tag=${TAG}" >> "$GITHUB_OUTPUT" + echo "chart_version=${CHART_VERSION}" >> "$GITHUB_OUTPUT" + echo "is_prerelease=${IS_PRERELEASE}" >> "$GITHUB_OUTPUT" + + # ── Pre-release path: full build ────────────────────────────── + build-image: + needs: resolve-tag + if: ${{ needs.resolve-tag.outputs.is_prerelease == 'true' }} strategy: matrix: variant: @@ -53,11 +77,11 @@ jobs: contents: read packages: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: docker/setup-buildx-action@v3 - - uses: docker/login-action@v3 + - uses: docker/login-action@v4 with: registry: ${{ env.REGISTRY }} username: ${{ github.repository_owner }} @@ -65,7 +89,7 @@ jobs: - name: Docker metadata id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}${{ matrix.variant.suffix }} @@ -96,8 +120,8 @@ jobs: retention-days: 1 merge-manifests: - needs: build-image - if: inputs.dry_run != true + needs: [resolve-tag, build-image] + if: ${{ inputs.dry_run != true && needs.resolve-tag.outputs.is_prerelease == 'true' }} strategy: matrix: variant: @@ -109,8 +133,6 @@ jobs: permissions: contents: read packages: write - outputs: - version: ${{ steps.meta.outputs.version }} steps: - name: Download digests uses: actions/download-artifact@v4 @@ -121,7 +143,7 @@ jobs: - uses: docker/setup-buildx-action@v3 - - uses: docker/login-action@v3 + - uses: docker/login-action@v4 with: registry: ${{ env.REGISTRY }} username: ${{ github.repository_owner }} @@ -129,12 +151,12 @@ jobs: - name: Docker metadata id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}${{ matrix.variant.suffix }} tags: | type=sha,prefix= - type=raw,value=latest + type=semver,pattern={{version}},value=${{ needs.resolve-tag.outputs.tag }} - name: Create manifest list working-directory: /tmp/digests @@ -142,88 +164,97 @@ jobs: docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ $(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}${{ matrix.variant.suffix }}@sha256:%s ' *) - bump-chart: - needs: merge-manifests - if: inputs.dry_run != true + # ── Stable path: promote pre-release image (no rebuild) ────── + + promote-stable: + needs: resolve-tag + if: ${{ inputs.dry_run != true && needs.resolve-tag.outputs.is_prerelease == 'false' }} + strategy: + matrix: + variant: + - { suffix: "" } + - { suffix: "-codex" } + - { suffix: "-claude" } + - { suffix: "-gemini" } runs-on: ubuntu-latest permissions: - contents: write - pull-requests: write + contents: read + packages: write steps: - - name: Generate App token - id: app-token - uses: actions/create-github-app-token@v1 - with: - app-id: ${{ secrets.APP_ID }} - private-key: ${{ secrets.APP_PRIVATE_KEY }} - - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 - - name: Get current chart version - id: current - run: | - chart_version=$(grep '^version:' charts/openab/Chart.yaml | awk '{print $2}') - echo "chart_version=$chart_version" >> "$GITHUB_OUTPUT" + - uses: docker/setup-buildx-action@v3 - - name: Bump chart version - id: bump + - uses: docker/login-action@v4 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Find pre-release image + id: find-prerelease run: | - current="${{ steps.current.outputs.chart_version }}" - # Strip any existing pre-release suffix for base version - base="${current%%-*}" - IFS='.' read -r major minor patch <<< "$base" - bump_type="${{ inputs.chart_bump }}" - bump_type="${bump_type:-patch}" - case "$bump_type" in - major) major=$((major + 1)); minor=0; patch=0 ;; - minor) minor=$((minor + 1)); patch=0 ;; - patch) patch=$((patch + 1)) ;; - esac - # Stable release: clean version. Otherwise: beta with run number. - if [ "${{ inputs.release }}" = "true" ]; then - new_version="${major}.${minor}.${patch}" - else - new_version="${major}.${minor}.${patch}-beta.${GITHUB_RUN_NUMBER}" + CHART_VERSION="${{ needs.resolve-tag.outputs.chart_version }}" + # Find latest pre-release tag matching this version (e.g. v0.7.0-beta.1) + PRERELEASE_TAG=$(git tag -l "v${CHART_VERSION}-*" --sort=-v:refname | head -1) + if [ -z "$PRERELEASE_TAG" ]; then + echo "::error::No pre-release tag found for v${CHART_VERSION}-*. Run a pre-release build first." + exit 1 fi - echo "new_version=$new_version" >> "$GITHUB_OUTPUT" + PRERELEASE_VERSION="${PRERELEASE_TAG#v}" + echo "Found pre-release: ${PRERELEASE_TAG} (${PRERELEASE_VERSION})" + echo "prerelease_version=${PRERELEASE_VERSION}" >> "$GITHUB_OUTPUT" - - name: Resolve image SHA - id: image-sha + - name: Verify pre-release image exists run: | - # Use the commit SHA that triggered this build — this is the SHA - # that merge-manifests tagged the Docker image with (type=sha,prefix=). - # We capture it here explicitly so it survives the bump commit. - IMAGE_SHA="${{ github.sha }}" - IMAGE_SHA="${IMAGE_SHA:0:7}" - echo "sha=${IMAGE_SHA}" >> "$GITHUB_OUTPUT" - - - name: Update Chart.yaml and values.yaml + IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}${{ matrix.variant.suffix }}" + PRERELEASE_VERSION="${{ steps.find-prerelease.outputs.prerelease_version }}" + echo "Checking ${IMAGE}:${PRERELEASE_VERSION} ..." + docker buildx imagetools inspect "${IMAGE}:${PRERELEASE_VERSION}" || \ + { echo "::error::Image ${IMAGE}:${PRERELEASE_VERSION} not found — build the pre-release first"; exit 1; } + + - name: Promote to stable tags run: | - IMAGE_SHA="${{ steps.image-sha.outputs.sha }}" - sed -i "s/^version: .*/version: ${{ steps.bump.outputs.new_version }}/" charts/openab/Chart.yaml - sed -i "s/^appVersion: .*/appVersion: \"${IMAGE_SHA}\"/" charts/openab/Chart.yaml - sed -i "s|repository: .*|repository: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}|" charts/openab/values.yaml - sed -i "s/tag: .*/tag: \"${IMAGE_SHA}\"/" charts/openab/values.yaml - - - name: Create and auto-merge bump PR - env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} + IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}${{ matrix.variant.suffix }}" + PRERELEASE_VERSION="${{ steps.find-prerelease.outputs.prerelease_version }}" + CHART_VERSION="${{ needs.resolve-tag.outputs.chart_version }}" + MAJOR_MINOR="${CHART_VERSION%.*}" + + echo "Promoting ${IMAGE}:${PRERELEASE_VERSION} → ${CHART_VERSION}, ${MAJOR_MINOR}, latest" + docker buildx imagetools create \ + -t "${IMAGE}:${CHART_VERSION}" \ + -t "${IMAGE}:${MAJOR_MINOR}" \ + -t "${IMAGE}:latest" \ + "${IMAGE}:${PRERELEASE_VERSION}" + + # ── Chart release (runs after either path) ─────────────────── + + release-chart: + needs: [resolve-tag, merge-manifests, promote-stable] + if: >- + ${{ always() && inputs.dry_run != true && + needs.resolve-tag.result == 'success' && + (needs.merge-manifests.result == 'success' || needs.promote-stable.result == 'success') }} + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v6 + + - name: Install Helm + uses: azure/setup-helm@v4 + + - uses: docker/login-action@v4 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push chart to OCI run: | - VERSION="${{ steps.bump.outputs.new_version }}" - IMAGE_SHA="${{ steps.image-sha.outputs.sha }}" - BRANCH="chore/chart-${VERSION}" - git config user.name "openab-app[bot]" - git config user.email "274185012+openab-app[bot]@users.noreply.github.com" - git checkout -b "$BRANCH" - git add charts/openab/Chart.yaml charts/openab/values.yaml - git commit -m "chore: bump chart to ${VERSION} - - image: ${IMAGE_SHA}" - git push origin "$BRANCH" - PR_URL=$(gh pr create \ - --title "chore: bump chart to ${VERSION}" \ - --body "Auto-generated chart version bump for image \`${IMAGE_SHA}\`." \ - --base main --head "$BRANCH") - gh pr merge "$PR_URL" --squash --delete-branch + CHART_VERSION="${{ needs.resolve-tag.outputs.chart_version }}" + helm package charts/openab + helm push openab-${CHART_VERSION}.tgz oci://ghcr.io/${{ github.repository_owner }}/charts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..4239edd9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,33 @@ +name: CI + +on: + pull_request: + paths: + - "src/**" + - "Cargo.toml" + - "Cargo.lock" + - "Dockerfile*" + +env: + CARGO_TERM_COLOR: always + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy + + - uses: Swatinem/rust-cache@v2 + + - name: cargo check + run: cargo check + + - name: cargo clippy + run: cargo clippy -- -D warnings + + - name: cargo test + run: cargo test diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml new file mode 100644 index 00000000..b6c3658f --- /dev/null +++ b/.github/workflows/release-pr.yml @@ -0,0 +1,83 @@ +name: Release PR + +on: + workflow_dispatch: + inputs: + version: + description: "Version (leave empty for auto bump, or specify e.g. 0.8.0-beta.1)" + required: false + type: string + bump: + description: "Auto bump type (ignored when version is specified)" + required: false + type: choice + options: + - patch + - minor + - major + default: patch + +jobs: + create-release-pr: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - name: Generate App token + id: app-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - uses: actions/checkout@v6 + with: + token: ${{ steps.app-token.outputs.token }} + fetch-depth: 0 + + - name: Resolve version + id: version + run: | + if [ -n "${{ inputs.version }}" ]; then + VERSION="${{ inputs.version }}" + else + CURRENT=$(grep '^version = ' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/') + BASE="${CURRENT%%-*}" + IFS='.' read -r major minor patch <<< "$BASE" + case "${{ inputs.bump }}" in + major) major=$((major + 1)); minor=0; patch=0 ;; + minor) minor=$((minor + 1)); patch=0 ;; + patch) patch=$((patch + 1)) ;; + esac + VERSION="${major}.${minor}.${patch}-beta.1" + fi + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "::notice::Release version: ${VERSION}" + + - uses: dtolnay/rust-toolchain@stable + + - name: Update version files + run: | + VERSION="${{ steps.version.outputs.version }}" + sed -i "s/^version = .*/version = \"${VERSION}\"/" Cargo.toml + sed -i "s/^version: .*/version: ${VERSION}/" charts/openab/Chart.yaml + sed -i "s/^appVersion: .*/appVersion: \"${VERSION}\"/" charts/openab/Chart.yaml + cargo generate-lockfile + + - name: Create release PR + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + run: | + VERSION="${{ steps.version.outputs.version }}" + BRANCH="release/v${VERSION}" + git config user.name "openab-app[bot]" + git config user.email "274185012+openab-app[bot]@users.noreply.github.com" + git checkout -b "$BRANCH" + git add -A + git commit -m "release: v${VERSION}" + git push origin "$BRANCH" + gh pr create \ + --title "release: v${VERSION}" \ + --body "Merge this PR to tag \`v${VERSION}\` and trigger the build pipeline." \ + --base main --head "$BRANCH" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 534bb7ab..c2be4c13 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,10 +14,9 @@ jobs: permissions: contents: write pages: write - packages: write steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 @@ -29,13 +28,6 @@ jobs: - name: Install Helm uses: azure/setup-helm@v4 - - name: Login to GHCR - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Run chart-releaser uses: helm/chart-releaser-action@v1.6.0 with: @@ -43,15 +35,7 @@ jobs: env: CR_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Push chart to OCI registry - run: | - CHART=charts/openab - NAME=$(grep '^name:' ${CHART}/Chart.yaml | awk '{print $2}') - VERSION=$(grep '^version:' ${CHART}/Chart.yaml | awk '{print $2}') - helm package ${CHART} - helm push ${NAME}-${VERSION}.tgz oci://ghcr.io/${{ github.repository_owner }}/charts - - - name: Append OCI install instructions to release notes + - name: Append install instructions to release notes env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | diff --git a/.github/workflows/tag-on-merge.yml b/.github/workflows/tag-on-merge.yml new file mode 100644 index 00000000..e414d933 --- /dev/null +++ b/.github/workflows/tag-on-merge.yml @@ -0,0 +1,38 @@ +name: Tag on Release PR merge + +on: + pull_request: + types: [closed] + branches: [main] + +jobs: + tag: + if: github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'release/') + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Generate App token + id: app-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - uses: actions/checkout@v6 + with: + token: ${{ steps.app-token.outputs.token }} + + - name: Create and push tag + run: | + # release/v0.8.0-beta.1 → v0.8.0-beta.1 + VERSION="${GITHUB_HEAD_REF#release/}" + if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+ ]]; then + echo "::error::Invalid version format '${VERSION}'. Expected v{major}.{minor}.{patch}[-prerelease]" + exit 1 + fi + git config user.name "openab-app[bot]" + git config user.email "274185012+openab-app[bot]@users.noreply.github.com" + git tag "$VERSION" + git push origin "$VERSION" + echo "::notice::Tagged ${VERSION}" diff --git a/Cargo.lock b/Cargo.lock index ac944efe..5fad25ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -763,7 +763,7 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "openab" -version = "0.1.0" +version = "0.6.0" dependencies = [ "anyhow", "base64", diff --git a/Cargo.toml b/Cargo.toml index 11f7dadc..27f30527 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openab" -version = "0.1.0" +version = "0.6.0" edition = "2021" [dependencies] diff --git a/RELEASING.md b/RELEASING.md index 7061d0ec..8e2f35d6 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -2,55 +2,201 @@ ## Version Scheme -Chart versions follow SemVer with beta pre-releases: +Versions follow SemVer (e.g. `0.7.0`). Version bumps are controlled via `workflow_dispatch`: -- **Beta**: `0.2.1-beta.12345` — auto-generated on every push to main -- **Stable**: `0.2.1` — manually triggered, visible to `helm install` +| Method | 效果 | 範例 | +|---|---|---| +| Auto patch (default) | patch bump + beta | `0.6.0 → 0.6.1-beta.1` | +| Auto minor | minor bump + beta | `0.6.0 → 0.7.0-beta.1` | +| Auto major | major bump + beta | `0.6.0 → 1.0.0-beta.1` | +| Manual | 自行指定 | `0.8.0-beta.1` or `0.8.0` | -Users running `helm install` only see stable versions. Beta versions require `--devel` or explicit `--version`. +## Release Flow (Tag-Driven) -## Development Flow +> **核心原則:測過什麼就發什麼 (what you tested is what you ship)** +> stable release 不重新 build,直接 promote pre-release 驗證過的 image。 + +##### Step 1 — 建立 Release PR ``` - PR merged to main - │ - ▼ - ┌─────────────┐ ┌──────────────────┐ ┌─────────────────────┐ - │ CI: Build │────>│ CI: Bump PR │────>│ Merge bump PR │ - │ 3 images │ │ 0.2.1-beta.12345 │ │ → publishes beta │ - └─────────────┘ └──────────────────┘ └─────────────────────┘ - │ - ┌───────────────────────────────────────────────┘ - ▼ - helm install ... --version 0.2.1-beta.12345 (explicit only) - helm install ... (still gets 0.2.0 stable) + ┌─────────────────────────────────────────────────────────────────┐ + │ Maintainer 到 Actions → Release PR → Run workflow │ + │ │ + │ 選項 A: 留空 version,選 bump type → 自動算 (e.g. 0.7.0-beta.1) │ + │ 選項 B: 手動填 version (e.g. 0.8.0-beta.1 or 0.8.0) │ + │ │ + │ → release-pr.yml 觸發 │ + │ → 更新 Cargo.toml + Chart.yaml version/appVersion │ + │ → 建立 Release PR (branch: release/v0.7.0-beta.1) │ + └─────────────────────────────────────────────────────────────────┘ ``` -## Stable Release +##### Step 2 — Merge Release PR → 自動打 Tag → Build ``` - Actions → Build & Release → Run workflow - [bump: patch] [✅ Stable release] + ┌─────────────────────────────────────────────────────────────────┐ + │ Maintainer review & merge Release PR │ + │ │ + │ → tag-on-merge.yml 偵測 release/ branch merge │ + │ → 自動打 tag (e.g. v0.7.0-beta.1) │ + │ → build.yml 觸發 (is_prerelease=true) │ + │ → build-image: 4 variants × 2 platforms (amd64 + arm64) │ + │ → merge-manifests: image tags = + 0.7.0-beta.1 │ + │ → release-chart: helm chart → OCI registry │ + └─────────────────────────────────────────────────────────────────┘ │ ▼ - ┌─────────────┐ ┌──────────────────┐ ┌─────────────────────┐ - │ CI: Build │────>│ CI: Bump PR │────>│ Merge bump PR │ - │ 3 images │ │ 0.2.1 │ │ → publishes stable │ - └─────────────┘ └──────────────────┘ └─────────────────────┘ - │ - ┌───────────────────────────────────────────────┘ - ▼ - helm install ... (gets 0.2.1 🎉) + ┌─────────────────────────────────────────────────────────────────┐ + │ 部署 pre-release 進行測試: │ + │ │ + │ helm install openab \ │ + │ oci://ghcr.io/openabdev/charts/openab \ │ + │ --version 0.7.0-beta.1 │ + │ │ + │ 發現 bug?→ 修復 PR merge → 再跑一次 Release PR workflow │ + │ → 手動指定 v0.7.0-beta.2 → merge → 重新測試 │ + └─────────────────────────────────────────────────────────────────┘ ``` -## Image Tags +##### Step 3 — Stable Release(Promote) -Each build produces three multi-arch images tagged with the git short SHA: +``` + ┌─────────────────────────────────────────────────────────────────┐ + │ 測試通過後,再跑一次 Release PR workflow │ + │ → 手動指定 version: 0.7.0 (不帶 rc) │ + │ → merge Release PR │ + │ → tag-on-merge.yml 打 tag v0.7.0 │ + │ │ + │ → build.yml 觸發 (is_prerelease=false) │ + │ → promote-stable: │ + │ 1. 找到最新的 pre-release tag (v0.7.0-beta.2) │ + │ 2. 驗證 pre-release image 存在 │ + │ 3. re-tag 0.7.0-beta.2 → 0.7.0 / 0.7 / latest │ + │ ⚠️ 不 rebuild,跟 pre-release 是同一個 artifact │ + │ → release-chart: helm chart → OCI registry │ + └─────────────────────────────────────────────────────────────────┘ +``` + +##### Step 4 — Chart Release(自動) ``` -ghcr.io/openabdev/openab: # kiro-cli -ghcr.io/openabdev/openab-codex: # codex -ghcr.io/openabdev/openab-claude: # claude + ┌─────────────────────────────────────────────────────────────────┐ + │ release.yml 偵測到 Chart.yaml 變更 push to main │ + │ → chart-releaser 更新 GitHub Pages helm repo index │ + │ → 附加 install instructions 到 chart release notes │ + └─────────────────────────────────────────────────────────────────┘ +``` + +## 快速指令參考 + +```bash +# ── Pre-release ─────────────────────────────────────── +# 到 Actions → Release PR → Run workflow +# 留空 version,選 patch → 自動算 0.7.0-beta.1 +# 或手動填 version: 0.7.0-beta.1 +# → merge 產生的 Release PR → 自動打 tag → build + +# ── 第二輪 pre-release(beta.1 有 bug 時)───────────── +# 修 bug → PR merge to main +# 再跑 Release PR workflow,手動填 version: 0.7.0-beta.2 +# → merge → 自動打 tag → build + +# ── Stable release ──────────────────────────────────── +# 跑 Release PR workflow,手動填 version: 0.7.0 +# → merge → 自動打 tag → promote beta image (不 rebuild) + +# ── 手動重跑(build 失敗時)────────────────────────── +gh workflow run build.yml -f tag=v0.7.0-beta.1 +gh workflow run build.yml -f tag=v0.7.0 ``` -The `latest` tag always points to the most recent build. +## GitHub Releases + +| Release | Tag 格式 | 內容 | +|---|---|---| +| chart-releaser | `openab-0.7.0` | Version Info + Installation instructions | + +## Workflow 對應表 + +| Workflow | 觸發條件 | 用途 | +|---|---|---| +| `ci.yml` | pull_request (src/Cargo/Dockerfile) | cargo check + clippy + test | +| `release-pr.yml` | workflow_dispatch | 建立 Release PR(更新版本檔案) | +| `tag-on-merge.yml` | release/ PR merge to main | 自動打 tag | +| `build.yml` | tag push `v*` | pre-release: 完整 build / stable: promote | +| `release.yml` | Chart.yaml 變更 push to main | chart-releaser 更新 GitHub Pages index | + +## Version 同步 + +release-pr.yml 在 Release PR 中自動更新以下檔案的版本: + +| 檔案 | 欄位 | +|---|---| +| `Cargo.toml` | `version` | +| `charts/openab/Chart.yaml` | `version` | +| `charts/openab/Chart.yaml` | `appVersion` | + +三者統一為同一個 semver(e.g. `0.7.0`)。 + +## Image Variants + +每次 build 產出 4 個 multi-arch image (linux/amd64 + linux/arm64): + +``` +ghcr.io/openabdev/openab # default (kiro-cli) +ghcr.io/openabdev/openab-codex # codex +ghcr.io/openabdev/openab-claude # claude +ghcr.io/openabdev/openab-gemini # gemini +``` + +Image tags 依 release 類型不同: + +| Tag | Stable (`v0.7.0`) | Pre-release (`v0.7.0-beta.1`) | +|---|---|---| +| `` | v (from pre-release) | v | +| `0.7.0` / `0.7.0-beta.1` | v | v | +| `0.7` | v | x | +| `latest` | v | x | + +## Installation + +##### Helm Repository (GitHub Pages) + +```bash +helm repo add openab https://openabdev.github.io/openab +helm repo update +helm install openab openab/openab --version 0.7.0 +``` + +##### OCI Registry + +```bash +helm install openab oci://ghcr.io/openabdev/charts/openab --version 0.7.0 +``` + +## 手動操作 + +| 時機 | 做什麼 | +|---|---| +| 準備 release | Actions → Release PR → Run workflow | +| 需要 beta 測試 | 指定 version 如 `0.7.0-beta.1` | +| 測試通過 | 指定 stable version 如 `0.7.0` → promote | +| build 失敗或需重跑 | `gh workflow run build.yml -f tag=` | + +## GitHub App 權限 + +release-pr.yml 和 tag-on-merge.yml 使用 GitHub App token 來建立 PR 和推送 tag。App 需要以下 Repository permissions: + +| Permission | Access | +|---|---| +| Contents | Read and write | +| Metadata | Read-only (mandatory) | +| Pull requests | Read and write | + +對應的 secrets:`APP_ID`(Client ID)、`APP_PRIVATE_KEY`。 + +## 限制與注意事項 + +- **Stable release 必須先有 pre-release**:promote-stable 會查找 `v{version}-*` 的 pre-release tag,找不到就失敗 +- **promote 用 version tag 找 image**:不依賴 commit SHA,pre-release 和 stable 可以在不同 commit 上 +- **外部用戶不會裝到 pre-release**:`helm install` 預設只拿 stable 版本,pre-release 需明確指定 `--version` diff --git a/charts/openab/Chart.yaml b/charts/openab/Chart.yaml index af5733cb..bf2f1b38 100644 --- a/charts/openab/Chart.yaml +++ b/charts/openab/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: openab description: Discord ↔ ACP coding CLI bridge (Kiro CLI, Claude Code, Codex, Gemini) type: application -version: 0.6.3-beta.39 -appVersion: "94253a5" +version: 0.6.3 +appVersion: "0.6.3" diff --git a/src/discord.rs b/src/discord.rs index b8d7e53e..77539173 100644 --- a/src/discord.rs +++ b/src/discord.rs @@ -54,7 +54,7 @@ impl EventHandler for Handler { Ok(serenity::model::channel::Channel::Guild(gc)) => { let result = gc .parent_id - .map_or(false, |pid| self.allowed_channels.contains(&pid.get())); + .is_some_and(|pid| self.allowed_channels.contains(&pid.get())); tracing::debug!(channel_id = %msg.channel_id, parent_id = ?gc.parent_id, result, "thread check"); result } From cf500bd55edde8f6f279d5cfdbd36a963d37e3be Mon Sep 17 00:00:00 2001 From: thepagent Date: Sat, 11 Apr 2026 16:11:31 +0900 Subject: [PATCH 59/80] fix: align Cargo.toml version to 0.6.3 (#204) Co-authored-by: thepagent --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5fad25ed..a99ef59f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -763,7 +763,7 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "openab" -version = "0.6.0" +version = "0.6.3" dependencies = [ "anyhow", "base64", diff --git a/Cargo.toml b/Cargo.toml index 27f30527..0d6275e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openab" -version = "0.6.0" +version = "0.6.3" edition = "2021" [dependencies] From 156422d558985040f3e1f63a303c7d375cd34550 Mon Sep 17 00:00:00 2001 From: "openab-app[bot]" <274185012+openab-app[bot]@users.noreply.github.com> Date: Sat, 11 Apr 2026 16:16:14 +0900 Subject: [PATCH 60/80] release: v0.6.4-beta.1 (#205) Co-authored-by: openab-app[bot] <274185012+openab-app[bot]@users.noreply.github.com> --- Cargo.lock | 50 ++++++++++++++++++++-------------------- Cargo.toml | 2 +- charts/openab/Chart.yaml | 4 ++-- 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a99ef59f..11d25169 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -90,9 +90,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" -version = "1.2.59" +version = "1.2.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7a4d3ec6524d28a329fc53654bbadc9bdd7b0431f5d65f1a56ffb28a1ee5283" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" dependencies = [ "find-msvc-tools", "shlex", @@ -379,9 +379,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" [[package]] name = "heck" @@ -599,12 +599,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.13.1" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45a8a2b9cb3e0b0c1803dbb0758ffac5de2f425b23c28f518faabd9d805342ff" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.0", "serde", "serde_core", ] @@ -633,9 +633,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.94" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" dependencies = [ "cfg-if", "futures-util", @@ -763,7 +763,7 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "openab" -version = "0.6.3" +version = "0.6.4-beta.1" dependencies = [ "anyhow", "base64", @@ -1116,7 +1116,7 @@ dependencies = [ "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.10", + "rustls-webpki 0.103.11", "subtle", "zeroize", ] @@ -1144,9 +1144,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.10" +version = "0.103.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +checksum = "20a6af516fea4b20eccceaf166e8aa666ac996208e8a644ce3ef5aa783bc7cd4" dependencies = [ "ring", "rustls-pki-types", @@ -1888,9 +1888,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.117" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" dependencies = [ "cfg-if", "once_cell", @@ -1901,9 +1901,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.67" +version = "0.4.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03623de6905b7206edd0a75f69f747f134b7f0a2323392d664448bf2d3c5d87e" +checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" dependencies = [ "js-sys", "wasm-bindgen", @@ -1911,9 +1911,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.117" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1921,9 +1921,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.117" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" dependencies = [ "bumpalo", "proc-macro2", @@ -1934,9 +1934,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.117" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" dependencies = [ "unicode-ident", ] @@ -1990,9 +1990,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.94" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a" +checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/Cargo.toml b/Cargo.toml index 0d6275e0..c3f285d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openab" -version = "0.6.3" +version = "0.6.4-beta.1" edition = "2021" [dependencies] diff --git a/charts/openab/Chart.yaml b/charts/openab/Chart.yaml index bf2f1b38..614def5f 100644 --- a/charts/openab/Chart.yaml +++ b/charts/openab/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: openab description: Discord ↔ ACP coding CLI bridge (Kiro CLI, Claude Code, Codex, Gemini) type: application -version: 0.6.3 -appVersion: "0.6.3" +version: 0.6.4-beta.1 +appVersion: "0.6.4-beta.1" From 0d86071d55e90d74f625a136c41e835d3e6a921a Mon Sep 17 00:00:00 2001 From: thepagent Date: Sat, 11 Apr 2026 17:39:21 +0900 Subject: [PATCH 61/80] fix: keep Cargo.toml at stable version, read version from Chart.yaml (#206) - Cargo.toml stays at stable version on main (e.g. 0.6.4) - Chart.yaml gets the full version (beta or stable) - Version source changed from Cargo.toml to Chart.yaml - Removed rust-toolchain/cargo generate-lockfile (not needed) Co-authored-by: thepagent --- .github/workflows/release-pr.yml | 12 ++++++++---- Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index b6c3658f..b87d63f4 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -42,7 +42,7 @@ jobs: if [ -n "${{ inputs.version }}" ]; then VERSION="${{ inputs.version }}" else - CURRENT=$(grep '^version = ' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/') + CURRENT=$(grep '^version:' charts/openab/Chart.yaml | awk '{print $2}') BASE="${CURRENT%%-*}" IFS='.' read -r major minor patch <<< "$BASE" case "${{ inputs.bump }}" in @@ -55,15 +55,19 @@ jobs: echo "version=${VERSION}" >> "$GITHUB_OUTPUT" echo "::notice::Release version: ${VERSION}" - - uses: dtolnay/rust-toolchain@stable + # Determine stable version (strip pre-release suffix) + STABLE="${VERSION%%-*}" + echo "stable=${STABLE}" >> "$GITHUB_OUTPUT" - name: Update version files run: | VERSION="${{ steps.version.outputs.version }}" - sed -i "s/^version = .*/version = \"${VERSION}\"/" Cargo.toml + STABLE="${{ steps.version.outputs.stable }}" + # Chart.yaml always gets the full version (beta or stable) sed -i "s/^version: .*/version: ${VERSION}/" charts/openab/Chart.yaml sed -i "s/^appVersion: .*/appVersion: \"${VERSION}\"/" charts/openab/Chart.yaml - cargo generate-lockfile + # Cargo.toml only gets stable version (main stays clean) + sed -i "s/^version = .*/version = \"${STABLE}\"/" Cargo.toml - name: Create release PR env: diff --git a/Cargo.lock b/Cargo.lock index 11d25169..d123d5e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -763,7 +763,7 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "openab" -version = "0.6.4-beta.1" +version = "0.6.4" dependencies = [ "anyhow", "base64", diff --git a/Cargo.toml b/Cargo.toml index c3f285d6..2b56c028 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openab" -version = "0.6.4-beta.1" +version = "0.6.4" edition = "2021" [dependencies] From 6cba78b1425df2c429406e7483a8e39e008b34b2 Mon Sep 17 00:00:00 2001 From: "openab-app[bot]" <274185012+openab-app[bot]@users.noreply.github.com> Date: Sat, 11 Apr 2026 16:41:33 +0800 Subject: [PATCH 62/80] release: v0.6.4 (#207) Co-authored-by: openab-app[bot] <274185012+openab-app[bot]@users.noreply.github.com> --- charts/openab/Chart.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/charts/openab/Chart.yaml b/charts/openab/Chart.yaml index 614def5f..b8405f68 100644 --- a/charts/openab/Chart.yaml +++ b/charts/openab/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: openab description: Discord ↔ ACP coding CLI bridge (Kiro CLI, Claude Code, Codex, Gemini) type: application -version: 0.6.4-beta.1 -appVersion: "0.6.4-beta.1" +version: 0.6.4 +appVersion: "0.6.4" From b15e7eb59f3d4268329eacddd8a71a72d80baed5 Mon Sep 17 00:00:00 2001 From: chaodufashi Date: Sat, 11 Apr 2026 18:15:23 +0800 Subject: [PATCH 63/80] docs: replace hardcoded image SHA with :latest in README (#208) The helm install examples used a stale commit SHA (b500bf6) from PR #145. Now that tag-driven releases produce :latest on stable promote, use that instead. Co-authored-by: thepagent --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4cb03ab2..3a3b2fc6 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ helm install openab openab/openab \ --set agents.kiro.enabled=false \ --set agents.claude.discord.botToken="$DISCORD_BOT_TOKEN" \ --set-string 'agents.claude.discord.allowedChannels[0]=YOUR_CHANNEL_ID' \ - --set agents.claude.image=ghcr.io/openabdev/openab-claude:78f8d2c \ + --set agents.claude.image=ghcr.io/openabdev/openab-claude:latest \ --set agents.claude.command=claude-agent-acp \ --set agents.claude.workingDir=/home/node @@ -121,7 +121,7 @@ helm install openab openab/openab \ --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.image=ghcr.io/openabdev/openab-claude:latest \ --set agents.claude.command=claude-agent-acp \ --set agents.claude.workingDir=/home/node ``` From e75276d28dfc108b3f6f4dd10469dc258f070202 Mon Sep 17 00:00:00 2001 From: chaodufashi Date: Sat, 11 Apr 2026 19:23:28 +0800 Subject: [PATCH 64/80] feat: resize and compress images before base64 encoding (#210) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: resize and compress images before base64 encoding Follow OpenClaw's approach to prevent large image payloads from exceeding JSON-RPC transport limits (Internal Error -32603). Changes: - Add image crate dependency (jpeg, png, gif, webp) - Resize images so longest side <= 1200px (Lanczos3) - Re-encode as JPEG at quality 75 (~200-400KB after base64) - GIFs pass through unchanged to preserve animation - Fallback to original bytes if resize fails Fixes #209 * test: add unit tests for image resize and compression Tests cover: - Large image resized to max 1200px - Small image keeps original dimensions - Landscape/portrait aspect ratio preserved - Compressed output smaller than original - GIF passes through unchanged - Invalid data returns error * fix: preserve aspect ratio on resize + add fallback size check Address review feedback from @the3mi: - 🔴 Fix resize() to calculate proportional dimensions instead of forcing 1200x1200 (was distorting images) - 🟡 Add 1MB size check on fallback path when resize fails - Fix portrait/landscape test assertions to match correct aspect ratios * fix: restore post-download size check + use structured logging Address minor review feedback: - Restore defense-in-depth bytes.len() check after download - Use tracing structured fields (url = %url, error = %e) for consistency with codebase style --------- Co-authored-by: chaodu-agent --- Cargo.toml | 1 + src/discord.rs | 203 ++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 166 insertions(+), 38 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2b56c028..c4d6351b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,3 +17,4 @@ anyhow = "1" rand = "0.8" reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] } base64 = "0.22" +image = { version = "0.25", default-features = false, features = ["jpeg", "png", "gif", "webp"] } diff --git a/src/discord.rs b/src/discord.rs index 77539173..e098acb3 100644 --- a/src/discord.rs +++ b/src/discord.rs @@ -5,6 +5,8 @@ use crate::format; use crate::reactions::StatusReactionController; use base64::engine::general_purpose::STANDARD as BASE64; use base64::Engine; +use image::ImageReader; +use std::io::Cursor; use std::sync::LazyLock; use serenity::async_trait; use serenity::model::channel::{Message, ReactionType}; @@ -233,14 +235,20 @@ impl EventHandler for Handler { } } -/// Download a Discord image attachment and encode it as an ACP image content block. -/// -/// Discord attachment URLs are temporary and expire, so we must download -/// and encode the image data immediately. The ACP ImageContent schema -/// requires `{ data: base64_string, mimeType: "image/..." }`. +/// Maximum dimension (width or height) for resized images. +/// Matches OpenClaw's DEFAULT_IMAGE_MAX_DIMENSION_PX. +const IMAGE_MAX_DIMENSION_PX: u32 = 1200; + +/// JPEG quality for compressed output (OpenClaw uses progressive 85→35; +/// we start at 75 which is a good balance of quality vs size). +const IMAGE_JPEG_QUALITY: u8 = 75; + +/// Download a Discord image attachment, resize/compress it, then base64-encode +/// as an ACP image content block. /// -/// Security: rejects non-image attachments (by content-type or extension) -/// and files larger than 10MB to prevent OOM/abuse. +/// Large images are resized so the longest side is at most 1200px and +/// re-encoded as JPEG at quality 75. This keeps the base64 payload well +/// under typical JSON-RPC transport limits (~200-400KB after encoding). async fn download_and_encode_image(attachment: &serenity::model::channel::Attachment) -> Option { const MAX_SIZE: u64 = 10 * 1024 * 1024; // 10 MB @@ -267,69 +275,104 @@ async fn download_and_encode_image(attachment: &serenity::model::channel::Attach }) }); - // Validate that it's actually an image let Some(mime) = media_type else { - debug!(filename = %attachment.filename, "skipping non-image attachment (no matching content-type or extension)"); + debug!(filename = %attachment.filename, "skipping non-image attachment"); return None; }; - // Strip MIME type parameters (e.g. "image/jpeg; charset=utf-8" → "image/jpeg") - // Downstream LLM APIs (Claude, OpenAI, Gemini) reject MIME types with parameters let mime = mime.split(';').next().unwrap_or(mime).trim(); if !mime.starts_with("image/") { debug!(filename = %attachment.filename, mime = %mime, "skipping non-image attachment"); return None; } - // Size check before downloading if u64::from(attachment.size) > MAX_SIZE { - error!( - filename = %attachment.filename, - size = attachment.size, - max = MAX_SIZE, - "image attachment exceeds 10MB limit" - ); + error!(filename = %attachment.filename, size = attachment.size, "image exceeds 10MB limit"); return None; } - // Download using the static reusable client let response = match HTTP_CLIENT.get(url).send().await { Ok(resp) => resp, - Err(e) => { - error!("failed to download image {}: {}", url, e); - return None; - } + Err(e) => { error!(url = %url, error = %e, "download failed"); return None; } }; - if !response.status().is_success() { - error!("HTTP error downloading image {}: {}", url, response.status()); + error!(url = %url, status = %response.status(), "HTTP error downloading image"); return None; } - let bytes = match response.bytes().await { Ok(b) => b, - Err(e) => { - error!("failed to read image bytes from {}: {}", url, e); - return None; - } + Err(e) => { error!(url = %url, error = %e, "read failed"); return None; } }; - // Final size check after download (defense in depth) + // Defense-in-depth: verify actual download size if bytes.len() as u64 > MAX_SIZE { - error!( - filename = %attachment.filename, - size = bytes.len(), - "downloaded image exceeds 10MB limit after decode" - ); + error!(filename = %attachment.filename, size = bytes.len(), "downloaded image exceeds limit"); return None; } - let encoded = BASE64.encode(bytes.as_ref()); + // Resize and compress + let (output_bytes, output_mime) = match resize_and_compress(&bytes) { + Ok(result) => result, + Err(e) => { + // Fallback: use original bytes but reject if too large for transport + if bytes.len() > 1024 * 1024 { + error!(filename = %attachment.filename, error = %e, size = bytes.len(), "resize failed and original too large, skipping"); + return None; + } + debug!(filename = %attachment.filename, error = %e, "resize failed, using original"); + (bytes.to_vec(), mime.to_string()) + } + }; + + debug!( + filename = %attachment.filename, + original_size = bytes.len(), + compressed_size = output_bytes.len(), + "image processed" + ); + + let encoded = BASE64.encode(&output_bytes); Some(ContentBlock::Image { - media_type: mime.to_string(), + media_type: output_mime, data: encoded, }) } +/// Resize image so longest side ≤ IMAGE_MAX_DIMENSION_PX, then encode as JPEG. +/// Returns (compressed_bytes, mime_type). GIFs are passed through unchanged +/// to preserve animation. +fn resize_and_compress(raw: &[u8]) -> Result<(Vec, String), image::ImageError> { + let reader = ImageReader::new(Cursor::new(raw)) + .with_guessed_format()?; + + let format = reader.format(); + + // Pass through GIFs unchanged to preserve animation + if format == Some(image::ImageFormat::Gif) { + return Ok((raw.to_vec(), "image/gif".to_string())); + } + + let img = reader.decode()?; + let (w, h) = (img.width(), img.height()); + + // Resize preserving aspect ratio: scale so longest side = 1200px + let img = if w > IMAGE_MAX_DIMENSION_PX || h > IMAGE_MAX_DIMENSION_PX { + let max_side = std::cmp::max(w, h); + let ratio = f64::from(IMAGE_MAX_DIMENSION_PX) / f64::from(max_side); + let new_w = (f64::from(w) * ratio) as u32; + let new_h = (f64::from(h) * ratio) as u32; + img.resize(new_w, new_h, image::imageops::FilterType::Lanczos3) + } else { + img + }; + + // Encode as JPEG + let mut buf = Cursor::new(Vec::new()); + let encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut buf, IMAGE_JPEG_QUALITY); + img.write_with_encoder(encoder)?; + + Ok((buf.into_inner(), "image/jpeg".to_string())) +} + async fn edit(ctx: &Context, ch: ChannelId, msg_id: MessageId, content: &str) -> serenity::Result { ch.edit_message(&ctx.http, msg_id, serenity::builder::EditMessage::new().content(content)).await } @@ -542,3 +585,87 @@ async fn get_or_create_thread(ctx: &Context, msg: &Message, prompt: &str) -> any Ok(thread.id.get()) } + +#[cfg(test)] +mod tests { + use super::*; + + fn make_png(width: u32, height: u32) -> Vec { + let img = image::RgbImage::new(width, height); + let mut buf = Cursor::new(Vec::new()); + img.write_to(&mut buf, image::ImageFormat::Png).unwrap(); + buf.into_inner() + } + + #[test] + fn large_image_resized_to_max_dimension() { + let png = make_png(3000, 2000); + let (compressed, mime) = resize_and_compress(&png).unwrap(); + + assert_eq!(mime, "image/jpeg"); + let result = image::load_from_memory(&compressed).unwrap(); + assert!(result.width() <= IMAGE_MAX_DIMENSION_PX); + assert!(result.height() <= IMAGE_MAX_DIMENSION_PX); + } + + #[test] + fn small_image_keeps_original_dimensions() { + let png = make_png(800, 600); + let (compressed, mime) = resize_and_compress(&png).unwrap(); + + assert_eq!(mime, "image/jpeg"); + let result = image::load_from_memory(&compressed).unwrap(); + assert_eq!(result.width(), 800); + assert_eq!(result.height(), 600); + } + + #[test] + fn landscape_image_respects_aspect_ratio() { + let png = make_png(4000, 2000); + let (compressed, _) = resize_and_compress(&png).unwrap(); + + let result = image::load_from_memory(&compressed).unwrap(); + assert_eq!(result.width(), 1200); + assert_eq!(result.height(), 600); + } + + #[test] + fn portrait_image_respects_aspect_ratio() { + let png = make_png(2000, 4000); + let (compressed, _) = resize_and_compress(&png).unwrap(); + + let result = image::load_from_memory(&compressed).unwrap(); + assert_eq!(result.width(), 600); + assert_eq!(result.height(), 1200); + } + + #[test] + fn compressed_output_is_smaller_than_original() { + let png = make_png(3000, 2000); + let (compressed, _) = resize_and_compress(&png).unwrap(); + + assert!(compressed.len() < png.len(), "compressed {} should be < original {}", compressed.len(), png.len()); + } + + #[test] + fn gif_passes_through_unchanged() { + // Minimal valid GIF89a (1x1 pixel) + let gif: Vec = vec![ + 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, // GIF89a + 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, // logical screen descriptor + 0x2C, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, // image descriptor + 0x02, 0x02, 0x44, 0x01, 0x00, // image data + 0x3B, // trailer + ]; + let (output, mime) = resize_and_compress(&gif).unwrap(); + + assert_eq!(mime, "image/gif"); + assert_eq!(output, gif); + } + + #[test] + fn invalid_data_returns_error() { + let garbage = vec![0x00, 0x01, 0x02, 0x03]; + assert!(resize_and_compress(&garbage).is_err()); + } +} From fb063b7154e830b34d4e26be17231cb11826901e Mon Sep 17 00:00:00 2001 From: "openab-app[bot]" <274185012+openab-app[bot]@users.noreply.github.com> Date: Sat, 11 Apr 2026 19:32:33 +0800 Subject: [PATCH 65/80] release: v0.6.5-beta.1 (#211) Co-authored-by: openab-app[bot] <274185012+openab-app[bot]@users.noreply.github.com> --- Cargo.toml | 2 +- charts/openab/Chart.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c4d6351b..f8efb7f9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openab" -version = "0.6.4" +version = "0.6.5" edition = "2021" [dependencies] diff --git a/charts/openab/Chart.yaml b/charts/openab/Chart.yaml index b8405f68..88807a3f 100644 --- a/charts/openab/Chart.yaml +++ b/charts/openab/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: openab description: Discord ↔ ACP coding CLI bridge (Kiro CLI, Claude Code, Codex, Gemini) type: application -version: 0.6.4 -appVersion: "0.6.4" +version: 0.6.5-beta.1 +appVersion: "0.6.5-beta.1" From a46c124f7071cf56fc67836381d16cef763e17f2 Mon Sep 17 00:00:00 2001 From: "openab-app[bot]" <274185012+openab-app[bot]@users.noreply.github.com> Date: Sat, 11 Apr 2026 22:01:36 +0800 Subject: [PATCH 66/80] release: v0.6.5 (#221) Co-authored-by: openab-app[bot] <274185012+openab-app[bot]@users.noreply.github.com> --- charts/openab/Chart.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/charts/openab/Chart.yaml b/charts/openab/Chart.yaml index 88807a3f..6babfde8 100644 --- a/charts/openab/Chart.yaml +++ b/charts/openab/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: openab description: Discord ↔ ACP coding CLI bridge (Kiro CLI, Claude Code, Codex, Gemini) type: application -version: 0.6.5-beta.1 -appVersion: "0.6.5-beta.1" +version: 0.6.5 +appVersion: "0.6.5" From ab469d8c23ecb97db742aea015b3471904f0e686 Mon Sep 17 00:00:00 2001 From: marvin-69-jpg Date: Sun, 12 Apr 2026 03:46:21 +0800 Subject: [PATCH 67/80] fix: dedupe tool call display by toolCallId and sanitize titles (#138) fix: dedupe tool call display by toolCallId and sanitize titles --- src/acp/protocol.rs | 23 +++++++--- src/discord.rs | 100 +++++++++++++++++++++++++++++++++++++++----- 2 files changed, 108 insertions(+), 15 deletions(-) diff --git a/src/acp/protocol.rs b/src/acp/protocol.rs index d3e96ed5..82f00eb8 100644 --- a/src/acp/protocol.rs +++ b/src/acp/protocol.rs @@ -60,8 +60,8 @@ impl std::fmt::Display for JsonRpcError { pub enum AcpEvent { Text(String), Thinking, - ToolStart { title: String }, - ToolDone { title: String, status: String }, + ToolStart { id: String, title: String }, + ToolDone { id: String, title: String, status: String }, Status, } @@ -70,6 +70,19 @@ pub fn classify_notification(msg: &JsonRpcMessage) -> Option { let update = params.get("update")?; let session_update = update.get("sessionUpdate")?.as_str()?; + // toolCallId is the stable identity across tool_call → tool_call_update + // events for the same tool invocation. claude-agent-acp emits the first + // event before the input fields are streamed in (so the title falls back + // to "Terminal" / "Edit" / etc.) and refines them in a later + // tool_call_update; without the id we can't tell those events belong to + // the same call and end up rendering placeholder + refined as two + // separate lines. + let tool_id = update + .get("toolCallId") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + match session_update { "agent_message_chunk" => { let text = update.get("content")?.get("text")?.as_str()?; @@ -80,15 +93,15 @@ pub fn classify_notification(msg: &JsonRpcMessage) -> Option { } "tool_call" => { let title = update.get("title").and_then(|v| v.as_str()).unwrap_or("").to_string(); - Some(AcpEvent::ToolStart { title }) + Some(AcpEvent::ToolStart { id: tool_id, title }) } "tool_call_update" => { let title = update.get("title").and_then(|v| v.as_str()).unwrap_or("").to_string(); let status = update.get("status").and_then(|v| v.as_str()).unwrap_or("").to_string(); if status == "completed" || status == "failed" { - Some(AcpEvent::ToolDone { title, status }) + Some(AcpEvent::ToolDone { id: tool_id, title, status }) } else { - Some(AcpEvent::ToolStart { title }) + Some(AcpEvent::ToolStart { id: tool_id, title }) } } "plan" => Some(AcpEvent::Status), diff --git a/src/discord.rs b/src/discord.rs index e098acb3..f515c711 100644 --- a/src/discord.rs +++ b/src/discord.rs @@ -407,7 +407,14 @@ async fn stream_prompt( let (buf_tx, buf_rx) = watch::channel(initial); let mut text_buf = String::new(); - let mut tool_lines: Vec = Vec::new(); + // Tool calls indexed by toolCallId. Vec preserves first-seen + // order. We store id + title + state separately so a ToolDone + // event that arrives without a refreshed title (claude-agent-acp's + // update events don't always re-send the title field) can still + // reuse the title we already learned from a prior + // tool_call_update — only the icon flips 🔧 → ✅ / ❌. Rendering + // happens on the fly in compose_display(). + let mut tool_lines: Vec = Vec::new(); let current_msg_id = msg_id; if reset { @@ -474,16 +481,53 @@ async fn stream_prompt( AcpEvent::Thinking => { reactions.set_thinking().await; } - AcpEvent::ToolStart { title, .. } if !title.is_empty() => { + AcpEvent::ToolStart { id, title } if !title.is_empty() => { reactions.set_tool(&title).await; - tool_lines.push(format!("🔧 `{title}`...")); + let title = sanitize_title(&title); + // Dedupe by toolCallId: replace if we've already + // seen this id, otherwise append a new entry. + // claude-agent-acp emits a placeholder title + // ("Terminal", "Edit", etc.) on the first event + // and refines it via tool_call_update; without + // dedup the placeholder and refined version + // appear as two separate orphaned lines. + if let Some(slot) = tool_lines.iter_mut().find(|e| e.id == id) { + slot.title = title; + slot.state = ToolState::Running; + } else { + tool_lines.push(ToolEntry { + id, + title, + state: ToolState::Running, + }); + } let _ = buf_tx.send(compose_display(&tool_lines, &text_buf)); } - AcpEvent::ToolDone { title, status, .. } => { + AcpEvent::ToolDone { id, title, status } => { reactions.set_thinking().await; - let icon = if status == "completed" { "✅" } else { "❌" }; - if let Some(line) = tool_lines.iter_mut().rev().find(|l| l.contains(&title)) { - *line = format!("{icon} `{title}`"); + let new_state = if status == "completed" { + ToolState::Completed + } else { + ToolState::Failed + }; + // Find by id (the title is unreliable — substring + // match against the placeholder "Terminal" would + // never find the refined entry). Preserve the + // existing title if the Done event omits it. + if let Some(slot) = tool_lines.iter_mut().find(|e| e.id == id) { + if !title.is_empty() { + slot.title = sanitize_title(&title); + } + slot.state = new_state; + } else if !title.is_empty() { + // Done arrived without a prior Start (rare + // race) — record it so we still show + // something. + tool_lines.push(ToolEntry { + id, + title: sanitize_title(&title), + state: new_state, + }); } let _ = buf_tx.send(compose_display(&tool_lines, &text_buf)); } @@ -529,11 +573,47 @@ async fn stream_prompt( .await } -fn compose_display(tool_lines: &[String], text: &str) -> String { +/// Flatten a tool-call title into a single line that's safe to render +/// inside Discord inline-code spans. Discord renders single-backtick +/// code on a single line only, so multi-line shell commands (heredocs, +/// `&&`-chained commands split across lines) appear truncated; we +/// collapse newlines to ` ; ` and rewrite embedded backticks so they +/// don't break the wrapping span. +fn sanitize_title(title: &str) -> String { + title.replace('\r', "").replace('\n', " ; ").replace('`', "'") +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ToolState { + Running, + Completed, + Failed, +} + +#[derive(Debug, Clone)] +struct ToolEntry { + id: String, + title: String, + state: ToolState, +} + +impl ToolEntry { + fn render(&self) -> String { + let icon = match self.state { + ToolState::Running => "🔧", + ToolState::Completed => "✅", + ToolState::Failed => "❌", + }; + let suffix = if self.state == ToolState::Running { "..." } else { "" }; + format!("{icon} `{}`{}", self.title, suffix) + } +} + +fn compose_display(tool_lines: &[ToolEntry], text: &str) -> String { let mut out = String::new(); if !tool_lines.is_empty() { - for line in tool_lines { - out.push_str(line); + for entry in tool_lines { + out.push_str(&entry.render()); out.push('\n'); } out.push('\n'); From 11ca8d61cdbcef419b6a3fa1e5667463dc6021d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E8=AA=9E=E5=AB=A3?= Date: Sun, 12 Apr 2026 03:53:01 +0800 Subject: [PATCH 68/80] fix: prevent Discord message fragmentation during streaming (fixes #81) (#135) fix: prevent Discord message fragmentation during streaming (fixes #81) --- src/discord.rs | 22 ++++++++-------------- src/format.rs | 34 ++++++++++++++++++++++++++-------- 2 files changed, 34 insertions(+), 22 deletions(-) diff --git a/src/discord.rs b/src/discord.rs index f515c711..d3a3f820 100644 --- a/src/discord.rs +++ b/src/discord.rs @@ -421,31 +421,25 @@ async fn stream_prompt( text_buf.push_str("⚠️ _Session expired, starting fresh..._\n\n"); } - // Spawn edit-streaming task + // Spawn edit-streaming task — only edits the single message, never sends new ones. + // Long content is truncated during streaming; final multi-message split happens after. let edit_handle = { let ctx = ctx.clone(); let mut buf_rx = buf_rx.clone(); tokio::spawn(async move { let mut last_content = String::new(); - let mut current_edit_msg = msg_id; loop { tokio::time::sleep(std::time::Duration::from_millis(1500)).await; if buf_rx.has_changed().unwrap_or(false) { let content = buf_rx.borrow_and_update().clone(); if content != last_content { - if content.len() > 1900 { - let chunks = format::split_message(&content, 1900); - if let Some(first) = chunks.first() { - let _ = edit(&ctx, channel, current_edit_msg, first).await; - } - for chunk in chunks.iter().skip(1) { - if let Ok(new_msg) = channel.say(&ctx.http, chunk).await { - current_edit_msg = new_msg.id; - } - } + let display = if content.chars().count() > 1900 { + let truncated = format::truncate_chars(&content, 1900); + format!("{truncated}…") } else { - let _ = edit(&ctx, channel, current_edit_msg, &content).await; - } + content.clone() + }; + let _ = edit(&ctx, channel, msg_id, &display).await; last_content = content; } } diff --git a/src/format.rs b/src/format.rs index a0026ebb..841cf559 100644 --- a/src/format.rs +++ b/src/format.rs @@ -1,31 +1,40 @@ -/// Split text into chunks at line boundaries, each <= limit chars. +/// Split text into chunks at line boundaries, each <= limit Unicode characters (UTF-8 safe). +/// Discord's message limit counts Unicode characters, not bytes. pub fn split_message(text: &str, limit: usize) -> Vec { - if text.len() <= limit { + if text.chars().count() <= limit { return vec![text.to_string()]; } let mut chunks = Vec::new(); let mut current = String::new(); + let mut current_len: usize = 0; for line in text.split('\n') { + let line_chars = line.chars().count(); // +1 for the newline - if !current.is_empty() && current.len() + line.len() + 1 > limit { + if !current.is_empty() && current_len + line_chars + 1 > limit { chunks.push(current); current = String::new(); + current_len = 0; } if !current.is_empty() { current.push('\n'); + current_len += 1; } - // If a single line exceeds limit, hard-split it - if line.len() > limit { - for chunk in line.as_bytes().chunks(limit) { - if !current.is_empty() { + // If a single line exceeds limit, hard-split on char boundaries + if line_chars > limit { + for ch in line.chars() { + if current_len + 1 > limit { chunks.push(current); + current = String::new(); + current_len = 0; } - current = String::from_utf8_lossy(chunk).to_string(); + current.push(ch); + current_len += 1; } } else { current.push_str(line); + current_len += line_chars; } } if !current.is_empty() { @@ -33,3 +42,12 @@ pub fn split_message(text: &str, limit: usize) -> Vec { } chunks } + +/// Truncate a string to at most `limit` Unicode characters. +/// Discord's message limit counts Unicode characters, not bytes. +pub fn truncate_chars(s: &str, limit: usize) -> &str { + match s.char_indices().nth(limit) { + Some((idx, _)) => &s[..idx], + None => s, + } +} From 4e7562f1115dc356b2f93dd235fa234d0c966561 Mon Sep 17 00:00:00 2001 From: "openab-app[bot]" <274185012+openab-app[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 03:59:56 +0800 Subject: [PATCH 69/80] release: v0.6.6-beta.1 (#222) Co-authored-by: openab-app[bot] <274185012+openab-app[bot]@users.noreply.github.com> --- Cargo.toml | 2 +- charts/openab/Chart.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f8efb7f9..cf504eb0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openab" -version = "0.6.5" +version = "0.6.6" edition = "2021" [dependencies] diff --git a/charts/openab/Chart.yaml b/charts/openab/Chart.yaml index 6babfde8..64d5c181 100644 --- a/charts/openab/Chart.yaml +++ b/charts/openab/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: openab description: Discord ↔ ACP coding CLI bridge (Kiro CLI, Claude Code, Codex, Gemini) type: application -version: 0.6.5 -appVersion: "0.6.5" +version: 0.6.6-beta.1 +appVersion: "0.6.6-beta.1" From ca6ad268a6acae5617f0a828523e0634f411f7f1 Mon Sep 17 00:00:00 2001 From: "openab-app[bot]" <274185012+openab-app[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 04:34:50 +0800 Subject: [PATCH 70/80] release: v0.6.6 (#223) Co-authored-by: openab-app[bot] <274185012+openab-app[bot]@users.noreply.github.com> --- charts/openab/Chart.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/charts/openab/Chart.yaml b/charts/openab/Chart.yaml index 64d5c181..c8a2c25c 100644 --- a/charts/openab/Chart.yaml +++ b/charts/openab/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: openab description: Discord ↔ ACP coding CLI bridge (Kiro CLI, Claude Code, Codex, Gemini) type: application -version: 0.6.6-beta.1 -appVersion: "0.6.6-beta.1" +version: 0.6.6 +appVersion: "0.6.6" From a0fe11ff6cf1f014b7a0e101e06b8e704db40411 Mon Sep 17 00:00:00 2001 From: chaodufashi Date: Sun, 12 Apr 2026 05:44:22 +0800 Subject: [PATCH 71/80] feat: support voice message STT (Speech-to-Text) for Discord (#225) * feat: support voice message STT (Speech-to-Text) for Discord Add optional STT support that transcribes Discord voice message attachments (audio/ogg) via any OpenAI-compatible /audio/transcriptions endpoint and injects the transcript into the ACP prompt as text. - New src/stt.rs: ~50-line module calling POST /audio/transcriptions - New SttConfig in config.rs: enabled, api_key, model, base_url - discord.rs: detect audio/* attachments, download, transcribe, inject - Defaults to Groq free tier (whisper-large-v3-turbo) - Supports any OpenAI-compatible endpoint via base_url (Groq, OpenAI, local whisper server, etc.) - Feature is opt-in: disabled by default, zero impact when unconfigured Closes #224 * fix: add json feature to reqwest for resp.json() in stt module * docs: add STT configuration and deployment guide * fix: address PR review feedback - Reuse shared HTTP_CLIENT in stt.rs instead of creating per-call client - Pass actual MIME type from attachment (not hardcoded audio/ogg) - Fix attachment routing: check audio first, avoid wasted image download - Add api_key validation at startup (fail fast on empty key) - Add response_format=json to multipart form (fixes local servers) - Update docs: clarify api_key requirement, add Technical Notes section * feat: auto-detect GROQ_API_KEY from env when stt.enabled=true If stt.enabled = true and api_key is not set in config, openab automatically checks for GROQ_API_KEY in the environment. This allows minimal config: [stt] enabled = true No api_key line needed if the env var exists. * fix: only auto-detect GROQ_API_KEY when base_url points to Groq Prevents leaking Groq API key to unrelated endpoints when user sets a custom base_url without explicitly setting api_key. * docs: clarify GROQ_API_KEY auto-detect scope in stt.md * fix: move STT auto-detect before handler construction The handler clones stt_config at construction time. Auto-detect was running after the clone, so the handler never received the detected api_key. Now auto-detect runs first. --------- Co-authored-by: openab-bot --- Cargo.lock | 139 ++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 2 +- docs/stt.md | 144 +++++++++++++++++++++++++++++++++++++++++++++++++ src/config.rs | 28 ++++++++++ src/discord.rs | 55 +++++++++++++++---- src/main.rs | 20 ++++++- src/stt.rs | 61 +++++++++++++++++++++ 7 files changed, 437 insertions(+), 12 deletions(-) create mode 100644 docs/stt.md create mode 100644 src/stt.rs diff --git a/Cargo.lock b/Cargo.lock index d123d5e6..7c98b754 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -49,6 +49,12 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + [[package]] name = "base64" version = "0.22.1" @@ -76,12 +82,24 @@ version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.11.1" @@ -110,6 +128,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -205,6 +229,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -362,6 +395,16 @@ dependencies = [ "wasip3", ] +[[package]] +name = "gif" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -597,6 +640,34 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "gif", + "image-webp", + "moxcms", + "num-traits", + "png", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + [[package]] name = "indexmap" version = "2.14.0" @@ -740,6 +811,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -755,6 +836,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -763,10 +853,11 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "openab" -version = "0.6.4" +version = "0.6.6" dependencies = [ "anyhow", "base64", + "image", "rand 0.8.5", "regex", "reqwest", @@ -815,6 +906,19 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "potential_utf" version = "0.1.5" @@ -858,6 +962,18 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pxfm" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quinn" version = "0.11.9" @@ -2026,6 +2142,12 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + [[package]] name = "windows-link" version = "0.2.1" @@ -2399,3 +2521,18 @@ name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] diff --git a/Cargo.toml b/Cargo.toml index cf504eb0..7ea46dbe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,6 @@ uuid = { version = "1", features = ["v4"] } regex = "1" anyhow = "1" rand = "0.8" -reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] } +reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "multipart", "json"] } base64 = "0.22" image = { version = "0.25", default-features = false, features = ["jpeg", "png", "gif", "webp"] } diff --git a/docs/stt.md b/docs/stt.md new file mode 100644 index 00000000..1eea3978 --- /dev/null +++ b/docs/stt.md @@ -0,0 +1,144 @@ +# Speech-to-Text (STT) for Voice Messages + +openab can automatically transcribe Discord voice message attachments and forward the transcript to your ACP agent as text. + +## Quick Start + +Add an `[stt]` section to your `config.toml`: + +```toml +[stt] +enabled = true +``` + +If `GROQ_API_KEY` is set in your environment, that's all you need — openab will auto-detect it and use Groq's free tier. You can also set the key explicitly: + +```toml +[stt] +enabled = true +api_key = "${GROQ_API_KEY}" +``` + +## How It Works + +``` +Discord voice message (.ogg) + │ + ▼ + openab downloads the audio file + │ + ▼ + POST /audio/transcriptions → STT provider + │ + ▼ + transcript injected as: + "[Voice message transcript]: " + │ + ▼ + ACP agent receives plain text +``` + +The transcript is prepended to the prompt as a `ContentBlock::Text`, so the downstream agent (Kiro CLI, Claude Code, etc.) sees it as regular text input. + +## Configuration Reference + +```toml +[stt] +enabled = true # default: false +api_key = "${GROQ_API_KEY}" # required for cloud providers +model = "whisper-large-v3-turbo" # default +base_url = "https://api.groq.com/openai/v1" # default +``` + +| Field | Required | Default | Description | +|---|---|---|---| +| `enabled` | no | `false` | Enable/disable STT. When disabled, audio attachments are silently skipped. | +| `api_key` | no* | — | API key for the STT provider. *Auto-detected from `GROQ_API_KEY` env var if not set. For local servers, use any non-empty string (e.g. `"not-needed"`). | +| `model` | no | `whisper-large-v3-turbo` | Whisper model name. Varies by provider. | +| `base_url` | no | `https://api.groq.com/openai/v1` | OpenAI-compatible API base URL. | + +## Deployment Options + +openab uses the standard OpenAI-compatible `/audio/transcriptions` endpoint. Any provider that implements this API works — just change `base_url`. + +### Option 1: Groq Cloud (recommended, free tier) + +```toml +[stt] +enabled = true +api_key = "${GROQ_API_KEY}" +``` + +- Free tier with rate limits +- Model: `whisper-large-v3-turbo` (default) +- Sign up at https://console.groq.com + +### Option 2: OpenAI + +```toml +[stt] +enabled = true +api_key = "${OPENAI_API_KEY}" +model = "whisper-1" +base_url = "https://api.openai.com/v1" +``` + +- ~$0.006 per minute of audio +- Model: `whisper-1` + +### Option 3: Local Whisper Server + +For users running openab on a Mac Mini, home lab, or any machine with a local whisper server: + +```toml +[stt] +enabled = true +api_key = "not-needed" +model = "large-v3-turbo" +base_url = "http://localhost:8080/v1" +``` + +- Audio stays local — never leaves your machine +- No API key or cloud account needed +- Apple Silicon users get hardware acceleration + +Compatible local whisper servers: + +| Server | Install | Apple Silicon | +|---|---|---| +| [faster-whisper-server](https://github.com/fedirz/faster-whisper-server) | `pip install faster-whisper-server` | ✅ CoreML | +| [whisper.cpp server](https://github.com/ggerganov/whisper.cpp) | `brew install whisper-cpp` | ✅ Metal | +| [LocalAI](https://github.com/mudler/LocalAI) | Docker or binary | ✅ | + +### Option 4: LAN / Sidecar Server + +Point to a whisper server running on another machine in your network: + +```toml +[stt] +enabled = true +api_key = "not-needed" +base_url = "http://192.168.1.100:8080/v1" +``` + +### Not Supported + +- **Ollama** — does not expose an `/audio/transcriptions` endpoint. + +## Disabling STT + +Omit the `[stt]` section entirely, or set: + +```toml +[stt] +enabled = false +``` + +When disabled, audio attachments are silently skipped with no impact on existing functionality. + +## Technical Notes + +- openab sends `response_format=json` in the transcription request to ensure the response is always parseable JSON. Some local whisper servers default to plain text output without this parameter. +- The actual MIME type from the Discord attachment is passed through to the STT API (e.g. `audio/ogg`, `audio/mp4`, `audio/wav`). +- Environment variables in config values are expanded via `${VAR}` syntax (e.g. `api_key = "${GROQ_API_KEY}"`). +- The `api_key` field is auto-detected from the `GROQ_API_KEY` environment variable when using the default Groq endpoint. If you set a custom `base_url` (e.g. local server), auto-detect is disabled to avoid leaking the Groq key to unrelated endpoints — you must set `api_key` explicitly. diff --git a/src/config.rs b/src/config.rs index 6d341e27..c4ed3d30 100644 --- a/src/config.rs +++ b/src/config.rs @@ -11,8 +11,36 @@ pub struct Config { pub pool: PoolConfig, #[serde(default)] pub reactions: ReactionsConfig, + #[serde(default)] + pub stt: SttConfig, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct SttConfig { + #[serde(default)] + pub enabled: bool, + #[serde(default)] + pub api_key: String, + #[serde(default = "default_stt_model")] + pub model: String, + #[serde(default = "default_stt_base_url")] + pub base_url: String, } +impl Default for SttConfig { + fn default() -> Self { + Self { + enabled: false, + api_key: String::new(), + model: default_stt_model(), + base_url: default_stt_base_url(), + } + } +} + +fn default_stt_model() -> String { "whisper-large-v3-turbo".into() } +fn default_stt_base_url() -> String { "https://api.groq.com/openai/v1".into() } + #[derive(Debug, Deserialize)] pub struct DiscordConfig { pub bot_token: String, diff --git a/src/discord.rs b/src/discord.rs index d3a3f820..e267064e 100644 --- a/src/discord.rs +++ b/src/discord.rs @@ -1,5 +1,5 @@ use crate::acp::{classify_notification, AcpEvent, ContentBlock, SessionPool}; -use crate::config::ReactionsConfig; +use crate::config::{ReactionsConfig, SttConfig}; use crate::error_display::{format_coded_error, format_user_error}; use crate::format; use crate::reactions::StatusReactionController; @@ -32,6 +32,7 @@ pub struct Handler { pub allowed_channels: HashSet, pub allowed_users: HashSet, pub reactions_config: ReactionsConfig, + pub stt_config: SttConfig, } #[async_trait] @@ -126,18 +127,23 @@ impl EventHandler for Handler { text: prompt_with_sender.clone(), }); - // Add image attachments + // Process attachments: route by content type (audio → STT, image → encode) if !msg.attachments.is_empty() { for attachment in &msg.attachments { - if let Some(content_block) = download_and_encode_image(attachment).await { + if is_audio_attachment(attachment) { + if self.stt_config.enabled { + if let Some(transcript) = download_and_transcribe(attachment, &self.stt_config).await { + debug!(filename = %attachment.filename, chars = transcript.len(), "voice transcript injected"); + content_blocks.insert(0, ContentBlock::Text { + text: format!("[Voice message transcript]: {transcript}"), + }); + } + } else { + debug!(filename = %attachment.filename, "skipping audio attachment (STT disabled)"); + } + } else if let Some(content_block) = download_and_encode_image(attachment).await { debug!(url = %attachment.url, filename = %attachment.filename, "adding image attachment"); content_blocks.push(content_block); - } else { - error!( - url = %attachment.url, - filename = %attachment.filename, - "failed to download image attachment" - ); } } } @@ -235,6 +241,37 @@ impl EventHandler for Handler { } } +/// Check if an attachment is an audio file (voice messages are typically audio/ogg). +fn is_audio_attachment(attachment: &serenity::model::channel::Attachment) -> bool { + let mime = attachment.content_type.as_deref().unwrap_or(""); + mime.starts_with("audio/") +} + +/// Download an audio attachment and transcribe it via the configured STT provider. +async fn download_and_transcribe( + attachment: &serenity::model::channel::Attachment, + stt_config: &SttConfig, +) -> Option { + const MAX_SIZE: u64 = 25 * 1024 * 1024; // 25 MB (Whisper API limit) + + if u64::from(attachment.size) > MAX_SIZE { + error!(filename = %attachment.filename, size = attachment.size, "audio exceeds 25MB limit"); + return None; + } + + let resp = HTTP_CLIENT.get(&attachment.url).send().await.ok()?; + if !resp.status().is_success() { + error!(url = %attachment.url, status = %resp.status(), "audio download failed"); + return None; + } + let bytes = resp.bytes().await.ok()?.to_vec(); + + let mime_type = attachment.content_type.as_deref().unwrap_or("audio/ogg"); + let mime_type = mime_type.split(';').next().unwrap_or(mime_type).trim(); + + crate::stt::transcribe(&HTTP_CLIENT, stt_config, bytes, attachment.filename.clone(), mime_type).await +} + /// Maximum dimension (width or height) for resized images. /// Matches OpenClaw's DEFAULT_IMAGE_MAX_DIMENSION_PX. const IMAGE_MAX_DIMENSION_PX: u32 = 1200; diff --git a/src/main.rs b/src/main.rs index 39817342..225bf236 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ mod discord; mod error_display; mod format; mod reactions; +mod stt; use serenity::prelude::*; use std::collections::HashSet; @@ -25,7 +26,7 @@ async fn main() -> anyhow::Result<()> { .map(PathBuf::from) .unwrap_or_else(|| PathBuf::from("config.toml")); - let cfg = config::load_config(&config_path)?; + let mut cfg = config::load_config(&config_path)?; info!( agent_cmd = %cfg.agent.command, pool_max = cfg.pool.max_sessions, @@ -42,11 +43,28 @@ async fn main() -> anyhow::Result<()> { let allowed_users = parse_id_set(&cfg.discord.allowed_users, "allowed_users")?; info!(channels = allowed_channels.len(), users = allowed_users.len(), "parsed allowlists"); + // Resolve STT config before constructing handler (auto-detect mutates cfg.stt) + if cfg.stt.enabled { + if cfg.stt.api_key.is_empty() && cfg.stt.base_url.contains("groq.com") { + if let Ok(key) = std::env::var("GROQ_API_KEY") { + if !key.is_empty() { + info!("stt.api_key not set, using GROQ_API_KEY from environment"); + cfg.stt.api_key = key; + } + } + } + if cfg.stt.api_key.is_empty() { + anyhow::bail!("stt.enabled = true but no API key found — set stt.api_key in config or export GROQ_API_KEY"); + } + info!(model = %cfg.stt.model, base_url = %cfg.stt.base_url, "STT enabled"); + } + let handler = discord::Handler { pool: pool.clone(), allowed_channels, allowed_users, reactions_config: cfg.reactions, + stt_config: cfg.stt.clone(), }; let intents = GatewayIntents::GUILD_MESSAGES diff --git a/src/stt.rs b/src/stt.rs new file mode 100644 index 00000000..122db9b6 --- /dev/null +++ b/src/stt.rs @@ -0,0 +1,61 @@ +use crate::config::SttConfig; +use reqwest::multipart; +use tracing::{debug, error}; + +/// Transcribe audio bytes via an OpenAI-compatible `/audio/transcriptions` endpoint. +pub async fn transcribe( + client: &reqwest::Client, + cfg: &SttConfig, + audio_bytes: Vec, + filename: String, + mime_type: &str, +) -> Option { + let url = format!("{}/audio/transcriptions", cfg.base_url.trim_end_matches('/')); + + let file_part = multipart::Part::bytes(audio_bytes) + .file_name(filename) + .mime_str(mime_type) + .ok()?; + + let form = multipart::Form::new() + .part("file", file_part) + .text("model", cfg.model.clone()) + .text("response_format", "json"); + + let resp = match client + .post(&url) + .bearer_auth(&cfg.api_key) + .multipart(form) + .send() + .await + { + Ok(r) => r, + Err(e) => { + error!(error = %e, "STT request failed"); + return None; + } + }; + + if !resp.status().is_success() { + let status = resp.status(); + let body = resp.text().await.unwrap_or_default(); + error!(status = %status, body = %body, "STT API error"); + return None; + } + + let json: serde_json::Value = match resp.json().await { + Ok(v) => v, + Err(e) => { + error!(error = %e, "STT response parse failed"); + return None; + } + }; + + let text = json.get("text")?.as_str()?.trim().to_string(); + if text.is_empty() { + return None; + } + + debug!(chars = text.len(), "STT transcription complete"); + Some(text) +} From cd5a21061fd443a13d7f8d8db02d86852c815a6f Mon Sep 17 00:00:00 2001 From: "openab-app[bot]" <274185012+openab-app[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 05:49:13 +0800 Subject: [PATCH 72/80] release: v0.6.7-beta.1 (#226) Co-authored-by: openab-app[bot] <274185012+openab-app[bot]@users.noreply.github.com> --- Cargo.toml | 2 +- charts/openab/Chart.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7ea46dbe..af321ae4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openab" -version = "0.6.6" +version = "0.6.7" edition = "2021" [dependencies] diff --git a/charts/openab/Chart.yaml b/charts/openab/Chart.yaml index c8a2c25c..ec86ddfe 100644 --- a/charts/openab/Chart.yaml +++ b/charts/openab/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: openab description: Discord ↔ ACP coding CLI bridge (Kiro CLI, Claude Code, Codex, Gemini) type: application -version: 0.6.6 -appVersion: "0.6.6" +version: 0.6.7-beta.1 +appVersion: "0.6.7-beta.1" From edd54f0d02e01a934cc057e3a493d3810ee378e9 Mon Sep 17 00:00:00 2001 From: chaodufashi Date: Sun, 12 Apr 2026 06:13:06 +0800 Subject: [PATCH 73/80] helm: add first-class STT config to chart (#228) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * helm: add first-class STT config to chart Add stt as a first-class config block in the Helm chart so users can enable STT with a single helm upgrade command: helm upgrade openab openab/openab \ --set agents.kiro.stt.enabled=true \ --set agents.kiro.stt.apiKey=gsk_xxx - values.yaml: add stt defaults (enabled, apiKey, model, baseUrl) - configmap.yaml: render [stt] section when enabled, using ${STT_API_KEY} - secret.yaml: store apiKey in K8s Secret (same pattern as botToken) - deployment.yaml: inject STT_API_KEY env var from Secret API key stays out of the configmap — follows the existing DISCORD_BOT_TOKEN pattern. Closes #227 * docs: add Helm chart deployment section to stt.md * docs: mention STT support in README with link to docs/stt.md * fix(helm): fail fast when stt.enabled=true but apiKey is empty --------- Co-authored-by: openab-bot --- README.md | 1 + charts/openab/templates/configmap.yaml | 11 +++++++++++ charts/openab/templates/deployment.yaml | 7 +++++++ charts/openab/templates/secret.yaml | 3 +++ charts/openab/values.yaml | 5 +++++ docs/stt.md | 20 ++++++++++++++++++++ 6 files changed, 47 insertions(+) diff --git a/README.md b/README.md index 3a3b2fc6..772f5f72 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ A lightweight, secure, cloud-native ACP harness that bridges Discord and any [Ag - **Session pool** — one CLI process per thread, auto-managed lifecycle - **ACP protocol** — JSON-RPC over stdio with tool call, thinking, and permission auto-reply support - **Kubernetes-ready** — Dockerfile + k8s manifests with PVC for auth persistence +- **Voice message STT** — auto-transcribes Discord voice messages via Groq, OpenAI, or local Whisper server ([docs/stt.md](docs/stt.md)) ## Quick Start diff --git a/charts/openab/templates/configmap.yaml b/charts/openab/templates/configmap.yaml index 95bd68ef..194d8c25 100644 --- a/charts/openab/templates/configmap.yaml +++ b/charts/openab/templates/configmap.yaml @@ -40,6 +40,17 @@ data: [reactions] enabled = {{ ($cfg.reactions).enabled | default true }} remove_after_reply = {{ ($cfg.reactions).removeAfterReply | default false }} + {{- if ($cfg.stt).enabled }} + {{- if not ($cfg.stt).apiKey }} + {{ fail (printf "agents.%s.stt.apiKey is required when stt.enabled=true" $name) }} + {{- end }} + + [stt] + enabled = true + api_key = "${STT_API_KEY}" + model = "{{ ($cfg.stt).model | default "whisper-large-v3-turbo" }}" + base_url = "{{ ($cfg.stt).baseUrl | default "https://api.groq.com/openai/v1" }}" + {{- end }} {{- if $cfg.agentsMd }} AGENTS.md: | {{- $cfg.agentsMd | nindent 4 }} diff --git a/charts/openab/templates/deployment.yaml b/charts/openab/templates/deployment.yaml index f1ab9b0b..0d45041d 100644 --- a/charts/openab/templates/deployment.yaml +++ b/charts/openab/templates/deployment.yaml @@ -45,6 +45,13 @@ spec: name: {{ include "openab.agentFullname" $d }} key: discord-bot-token {{- end }} + {{- if and ($cfg.stt).enabled ($cfg.stt).apiKey }} + - name: STT_API_KEY + valueFrom: + secretKeyRef: + name: {{ include "openab.agentFullname" $d }} + key: stt-api-key + {{- end }} - name: HOME value: {{ $cfg.workingDir | default "/home/agent" }} {{- range $k, $v := $cfg.env }} diff --git a/charts/openab/templates/secret.yaml b/charts/openab/templates/secret.yaml index fd090208..2cdd27c8 100644 --- a/charts/openab/templates/secret.yaml +++ b/charts/openab/templates/secret.yaml @@ -14,6 +14,9 @@ metadata: type: Opaque data: discord-bot-token: {{ $cfg.discord.botToken | b64enc | quote }} + {{- if and ($cfg.stt).enabled ($cfg.stt).apiKey }} + stt-api-key: {{ $cfg.stt.apiKey | b64enc | quote }} + {{- end }} {{- end }} {{- end }} {{- end }} diff --git a/charts/openab/values.yaml b/charts/openab/values.yaml index 22b7a255..01be5561 100644 --- a/charts/openab/values.yaml +++ b/charts/openab/values.yaml @@ -69,6 +69,11 @@ agents: reactions: enabled: true removeAfterReply: false + stt: + enabled: false + apiKey: "" + model: "whisper-large-v3-turbo" + baseUrl: "https://api.groq.com/openai/v1" persistence: enabled: true storageClass: "" diff --git a/docs/stt.md b/docs/stt.md index 1eea3978..157b6f66 100644 --- a/docs/stt.md +++ b/docs/stt.md @@ -125,6 +125,26 @@ base_url = "http://192.168.1.100:8080/v1" - **Ollama** — does not expose an `/audio/transcriptions` endpoint. +## Helm Chart (Kubernetes) + +When deploying via the openab Helm chart, STT is a first-class config block — no manual configmap patching needed: + +```bash +helm upgrade openab openab/openab \ + --set agents.kiro.stt.enabled=true \ + --set agents.kiro.stt.apiKey=gsk_xxx +``` + +The API key is stored in a K8s Secret and injected as an env var (never in plaintext in the configmap). You can also customize model and endpoint: + +```bash +helm upgrade openab openab/openab \ + --set agents.kiro.stt.enabled=true \ + --set agents.kiro.stt.apiKey=gsk_xxx \ + --set agents.kiro.stt.model=whisper-large-v3-turbo \ + --set agents.kiro.stt.baseUrl=https://api.groq.com/openai/v1 +``` + ## Disabling STT Omit the `[stt]` section entirely, or set: From c62bfdfb366959adf6634e44d8dbdb04ecde3cbe Mon Sep 17 00:00:00 2001 From: "openab-app[bot]" <274185012+openab-app[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 06:16:25 +0800 Subject: [PATCH 74/80] release: v0.6.8-beta.1 (#229) Co-authored-by: openab-app[bot] <274185012+openab-app[bot]@users.noreply.github.com> --- Cargo.toml | 2 +- charts/openab/Chart.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index af321ae4..9d5b9911 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openab" -version = "0.6.7" +version = "0.6.8" edition = "2021" [dependencies] diff --git a/charts/openab/Chart.yaml b/charts/openab/Chart.yaml index ec86ddfe..5a4e2b15 100644 --- a/charts/openab/Chart.yaml +++ b/charts/openab/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: openab description: Discord ↔ ACP coding CLI bridge (Kiro CLI, Claude Code, Codex, Gemini) type: application -version: 0.6.7-beta.1 -appVersion: "0.6.7-beta.1" +version: 0.6.8-beta.1 +appVersion: "0.6.8-beta.1" From d1755f186b59403353a4613366b16a95252ac319 Mon Sep 17 00:00:00 2001 From: "openab-app[bot]" <274185012+openab-app[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 07:08:44 +0800 Subject: [PATCH 75/80] release: v0.7.0 (#230) Co-authored-by: openab-app[bot] <274185012+openab-app[bot]@users.noreply.github.com> --- Cargo.toml | 2 +- charts/openab/Chart.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9d5b9911..7ab5a1d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openab" -version = "0.6.8" +version = "0.7.0" edition = "2021" [dependencies] diff --git a/charts/openab/Chart.yaml b/charts/openab/Chart.yaml index 5a4e2b15..4ab9d1e8 100644 --- a/charts/openab/Chart.yaml +++ b/charts/openab/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: openab description: Discord ↔ ACP coding CLI bridge (Kiro CLI, Claude Code, Codex, Gemini) type: application -version: 0.6.8-beta.1 -appVersion: "0.6.8-beta.1" +version: 0.7.0 +appVersion: "0.7.0" From 54a4324085efd56015ad13a1ad72c02c7f15d7b8 Mon Sep 17 00:00:00 2001 From: "openab-app[bot]" <274185012+openab-app[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 07:42:46 +0800 Subject: [PATCH 76/80] release: v0.7.0-beta.1 (#231) Co-authored-by: openab-app[bot] <274185012+openab-app[bot]@users.noreply.github.com> --- charts/openab/Chart.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/charts/openab/Chart.yaml b/charts/openab/Chart.yaml index 4ab9d1e8..0ec7cb22 100644 --- a/charts/openab/Chart.yaml +++ b/charts/openab/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: openab description: Discord ↔ ACP coding CLI bridge (Kiro CLI, Claude Code, Codex, Gemini) type: application -version: 0.7.0 -appVersion: "0.7.0" +version: 0.7.0-beta.1 +appVersion: "0.7.0-beta.1" From b0d9bdde6289ccc872e7bf4e0e4124a05cd163aa Mon Sep 17 00:00:00 2001 From: JARVIS-Agent <55979927+JARVIS-coding-Agent@users.noreply.github.com> Date: Sun, 12 Apr 2026 13:30:04 +0800 Subject: [PATCH 77/80] fix: remove hardcoded image.tag to use Chart.AppVersion (#239) Set image.tag to empty string so the Helm template falls back to .Chart.AppVersion. Closes #235 --- charts/openab/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/openab/values.yaml b/charts/openab/values.yaml index 01be5561..956374cb 100644 --- a/charts/openab/values.yaml +++ b/charts/openab/values.yaml @@ -1,7 +1,7 @@ image: repository: ghcr.io/openabdev/openab # tag defaults to .Chart.AppVersion - tag: "94253a5" + tag: "" pullPolicy: IfNotPresent podSecurityContext: From 70adc831d0485332bbfc8a832dbd7fda9a9d2d93 Mon Sep 17 00:00:00 2001 From: "openab-app[bot]" <274185012+openab-app[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 13:36:07 +0800 Subject: [PATCH 78/80] release: v0.7.1-beta.1 (#248) Co-authored-by: openab-app[bot] <274185012+openab-app[bot]@users.noreply.github.com> --- Cargo.toml | 2 +- charts/openab/Chart.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7ab5a1d3..77b8ebe2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openab" -version = "0.7.0" +version = "0.7.1" edition = "2021" [dependencies] diff --git a/charts/openab/Chart.yaml b/charts/openab/Chart.yaml index 0ec7cb22..e7cbc8eb 100644 --- a/charts/openab/Chart.yaml +++ b/charts/openab/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: openab description: Discord ↔ ACP coding CLI bridge (Kiro CLI, Claude Code, Codex, Gemini) type: application -version: 0.7.0-beta.1 -appVersion: "0.7.0-beta.1" +version: 0.7.1-beta.1 +appVersion: "0.7.1-beta.1" From 7b60b38286805543d9e5ec6f6f4e670d03a9c4b3 Mon Sep 17 00:00:00 2001 From: "openab-app[bot]" <274185012+openab-app[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 13:50:56 +0800 Subject: [PATCH 79/80] release: v0.7.1 (#249) Co-authored-by: openab-app[bot] <274185012+openab-app[bot]@users.noreply.github.com> --- charts/openab/Chart.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/charts/openab/Chart.yaml b/charts/openab/Chart.yaml index e7cbc8eb..7fb38702 100644 --- a/charts/openab/Chart.yaml +++ b/charts/openab/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: openab description: Discord ↔ ACP coding CLI bridge (Kiro CLI, Claude Code, Codex, Gemini) type: application -version: 0.7.1-beta.1 -appVersion: "0.7.1-beta.1" +version: 0.7.1 +appVersion: "0.7.1" From 090103382513185b3d6dca2009afde663fc9ce4e Mon Sep 17 00:00:00 2001 From: IRISX Date: Mon, 13 Apr 2026 04:26:58 +0800 Subject: [PATCH 80/80] chore: add Copilot code review instructions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `.github/copilot-instructions.md` to guide GitHub Copilot's automated PR reviews with project-specific context: - ACP protocol correctness (JSON-RPC routing, notification handling) - Concurrency safety (atomic fields, Mutex across await, pool locks) - Discord API constraints (2000 char limit, 3s autocomplete deadline) - Skip CI-covered checks (rustfmt, clippy, tests) - >80% confidence threshold to reduce noise Modeled after block/goose's production-grade instructions. 3440 chars — within the 4000 char limit. --- .github/copilot-instructions.md | 67 +++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 .github/copilot-instructions.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..d4ab1a57 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,67 @@ +# GitHub Copilot Code Review Instructions + +## Review Philosophy +- Only comment when you have HIGH CONFIDENCE (>80%) that an issue exists +- Be concise: one sentence per comment when possible +- Focus on actionable feedback, not observations +- Silence is preferred over noisy false positives + +## Project Context +- **OpenAB**: A lightweight ACP (Agent Client Protocol) harness bridging Discord ↔ any ACP-compatible coding CLI over stdio JSON-RPC +- **Language**: Rust 2021 edition, single binary +- **Async runtime**: tokio (full features) +- **Discord**: serenity 0.12 (gateway + cache) +- **Error handling**: `anyhow::Result` everywhere, no `unwrap()` in production paths +- **Serialization**: serde + serde_json for ACP JSON-RPC, toml for config +- **Key modules**: `acp/connection.rs` (ACP stdio bridge), `acp/pool.rs` (session pool), `discord.rs` (Discord event handler), `config.rs` (TOML config), `usage.rs` (pluggable quota runners), `reactions.rs` (emoji reactions), `stt.rs` (speech-to-text) + +## Priority Areas (Review These) + +### Correctness +- Logic errors that could cause panics or incorrect behavior +- ACP JSON-RPC protocol violations (wrong method names, missing fields, incorrect response routing) +- Race conditions in async code (especially in the reader loop and session pool) +- Resource leaks (child processes not killed, channels not closed) +- Off-by-one in timeout calculations +- Incorrect error propagation — `unwrap()` in non-test code is always a bug + +### Concurrency & Safety +- Multiple atomic fields updated independently — document if readers may see mixed snapshots +- `Mutex` held across `.await` points (potential deadlock) +- Session pool lock scope — `RwLock` held during I/O can stall all sessions +- Child process lifecycle — `kill_on_drop` must be set, zombie processes must not accumulate + +### ACP Protocol +- `session/request_permission` must always get a response (auto-allow or forwarded) +- `session/update` notifications must not be consumed — forward to subscriber after capture +- `usage_update`, `available_commands_update`, `tool_call`, `agent_message_chunk` must be classified correctly +- Timeout values: initialize=90s, session/new=120s, others=30s (Gemini cold-start is slow) + +### Discord API +- Messages >2000 chars will be rejected — truncate or split +- Slash command registration is per-guild, max 100 per bot +- Autocomplete responses must return within 3s (no heavy I/O) +- Ephemeral messages for errors, regular messages for results + +### Config & Deployment +- `config.toml` fields must have sensible defaults — missing `[usage]` section should not crash +- Environment variable expansion via `${VAR}` must handle missing vars gracefully +- Agent `env` map is passed to child processes — sensitive values should not be logged + +## CI Pipeline (Do Not Flag These) +- `cargo fmt --check` — formatting is enforced by CI +- `cargo clippy --all-targets -- -D warnings` — lint warnings are enforced by CI +- `cargo test` — test failures are caught by CI + +## Skip These (Low Value) +- Style/formatting — CI handles via rustfmt +- Clippy warnings — CI handles +- Minor naming suggestions unless truly confusing +- Suggestions to add comments for self-documenting code +- Logging level suggestions unless security-relevant +- Import ordering + +## Response Format +1. State the problem (1 sentence) +2. Why it matters (1 sentence, only if not obvious) +3. Suggested fix (code snippet or specific action)