From 1f85bd419441638b32a9192700076b8944a35885 Mon Sep 17 00:00:00 2001 From: Federico Liva Date: Thu, 5 Mar 2026 16:57:51 +0100 Subject: [PATCH 01/42] ci: add sync-build workflow --- .github/workflows/sync-build.yml | 68 ++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 .github/workflows/sync-build.yml diff --git a/.github/workflows/sync-build.yml b/.github/workflows/sync-build.yml new file mode 100644 index 000000000..931557e79 --- /dev/null +++ b/.github/workflows/sync-build.yml @@ -0,0 +1,68 @@ +name: Sync upstream & build custom image + +on: + schedule: + - cron: '0 */6 * * *' # ogni 6 ore + workflow_dispatch: # trigger manuale + +jobs: + sync-and-build: + runs-on: ubuntu-latest + steps: + - name: Checkout fork + uses: actions/checkout@v4 + with: + ref: custom + fetch-depth: 0 + + - name: Fetch upstream tags + run: | + git remote add upstream https://github.com/RightNow-AI/openfang.git || true + git fetch upstream --tags + + - name: Check for new release + id: check + run: | + LATEST_TAG=$(git tag -l 'v*' --sort=-v:refname | head -1) + CURRENT=$(cat .current-upstream-version 2>/dev/null || echo "none") + echo "latest=$LATEST_TAG" >> "$GITHUB_OUTPUT" + echo "current=$CURRENT" >> "$GITHUB_OUTPUT" + if [ "$LATEST_TAG" != "$CURRENT" ]; then + echo "new_release=true" >> "$GITHUB_OUTPUT" + else + echo "new_release=false" >> "$GITHUB_OUTPUT" + fi + + - name: Rebase custom on latest tag + if: steps.check.outputs.new_release == 'true' + run: | + git config user.name "github-actions" + git config user.email "actions@github.com" + git rebase ${{ steps.check.outputs.latest }} + echo "${{ steps.check.outputs.latest }}" > .current-upstream-version + git add .current-upstream-version + git commit -m "chore: sync to upstream ${{ steps.check.outputs.latest }}" || true + git push --force-with-lease + + - name: Set up Docker Buildx + if: steps.check.outputs.new_release == 'true' + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + if: steps.check.outputs.new_release == 'true' + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push + if: steps.check.outputs.new_release == 'true' + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: | + fliva/openfang:latest + fliva/openfang:${{ steps.check.outputs.latest }} + cache-from: type=gha + cache-to: type=gha,mode=max From 035ae3b21586384f3b6a2f400ac63447dc50e626 Mon Sep 17 00:00:00 2001 From: Federico Liva Date: Thu, 5 Mar 2026 16:23:34 +0100 Subject: [PATCH 02/42] feat: custom Dockerfile for Lazycat NAS deployment - Add toolchain: Node.js 22, Claude Code CLI, Python 3, uv, Go, gh, ffmpeg - Add gosu + non-root user (openfang) with passwordless sudo - Entrypoint drops root privileges via gosu for Claude Code compatibility - Add GitHub Actions workflow to auto-sync upstream releases Co-Authored-By: Claude Opus 4.6 --- .current-upstream-version | 1 + Dockerfile | 20 ++++++++++++++++++-- entrypoint.sh | 5 +++++ 3 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 .current-upstream-version create mode 100644 entrypoint.sh diff --git a/.current-upstream-version b/.current-upstream-version new file mode 100644 index 000000000..dd08ecbad --- /dev/null +++ b/.current-upstream-version @@ -0,0 +1 @@ +v0.3.20 diff --git a/Dockerfile b/Dockerfile index d794943ed..b6fea4216 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,11 +10,27 @@ COPY packages ./packages RUN cargo build --release --bin openfang FROM debian:bookworm-slim -RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/* +RUN apt-get update && apt-get install -y ca-certificates curl git ffmpeg python3 golang gosu sudo && rm -rf /var/lib/apt/lists/* +RUN curl -LsSf https://astral.sh/uv/install.sh | sh +RUN (type -p wget >/dev/null || (apt-get update && apt-get install -y wget)) && \ + mkdir -p -m 755 /etc/apt/keyrings && \ + out=$(mktemp) && wget -qO "$out" https://cli.github.com/packages/githubcli-archive-keyring.gpg && \ + cat "$out" | tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null && \ + chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg && \ + echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null && \ + apt-get update && apt-get install -y gh && rm -rf /var/lib/apt/lists/* +RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \ + apt-get install -y nodejs && \ + npm install -g @anthropic-ai/claude-code && \ + rm -rf /var/lib/apt/lists/* +RUN useradd -m -s /bin/bash openfang && echo "openfang ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/openfang COPY --from=builder /build/target/release/openfang /usr/local/bin/ COPY --from=builder /build/agents /opt/openfang/agents +RUN mkdir -p /data && chown openfang:openfang /data +COPY entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh EXPOSE 4200 VOLUME /data ENV OPENFANG_HOME=/data -ENTRYPOINT ["openfang"] +ENTRYPOINT ["entrypoint.sh"] CMD ["start"] diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 000000000..621b91e31 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,5 @@ +#!/bin/bash +# Drop root privileges and run openfang as the openfang user +chown -R openfang:openfang /data 2>/dev/null +chown -R openfang:openfang /home/openfang 2>/dev/null +exec gosu openfang openfang "$@" From 4e274aa9f26b303e6dddc8878052ba9fe1838457 Mon Sep 17 00:00:00 2001 From: Federico Liva Date: Thu, 5 Mar 2026 17:07:53 +0100 Subject: [PATCH 03/42] ci: use main branch instead of custom --- .github/workflows/sync-build.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/sync-build.yml b/.github/workflows/sync-build.yml index 931557e79..b49f2f337 100644 --- a/.github/workflows/sync-build.yml +++ b/.github/workflows/sync-build.yml @@ -12,7 +12,6 @@ jobs: - name: Checkout fork uses: actions/checkout@v4 with: - ref: custom fetch-depth: 0 - name: Fetch upstream tags @@ -33,7 +32,7 @@ jobs: echo "new_release=false" >> "$GITHUB_OUTPUT" fi - - name: Rebase custom on latest tag + - name: Rebase on latest tag if: steps.check.outputs.new_release == 'true' run: | git config user.name "github-actions" From bd2ef99fb1f1845a3c7565f440a0c0d73e5484c5 Mon Sep 17 00:00:00 2001 From: Federico Liva Date: Thu, 5 Mar 2026 18:30:09 +0100 Subject: [PATCH 04/42] feat: add gogcli to image --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index b6fea4216..707230f18 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,6 +23,7 @@ RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \ apt-get install -y nodejs && \ npm install -g @anthropic-ai/claude-code && \ rm -rf /var/lib/apt/lists/* +RUN GOBIN=/usr/local/bin go install github.com/steipete/gogcli@latest RUN useradd -m -s /bin/bash openfang && echo "openfang ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/openfang COPY --from=builder /build/target/release/openfang /usr/local/bin/ COPY --from=builder /build/agents /opt/openfang/agents From 4a1214b2de29724322e250174456eaacc296870a Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 5 Mar 2026 18:32:03 +0000 Subject: [PATCH 05/42] chore: sync to upstream v0.3.22 --- .current-upstream-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.current-upstream-version b/.current-upstream-version index dd08ecbad..25309a8ce 100644 --- a/.current-upstream-version +++ b/.current-upstream-version @@ -1 +1 @@ -v0.3.20 +v0.3.22 From e550aad2fb2f4a1db2d9ec1648a454dd5c2cfee1 Mon Sep 17 00:00:00 2001 From: Federico Liva Date: Thu, 5 Mar 2026 19:42:50 +0100 Subject: [PATCH 06/42] feat: install brew + gogcli as non-root, add PATH for npm-global --- Dockerfile | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 707230f18..9d5c68622 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,7 @@ COPY packages ./packages RUN cargo build --release --bin openfang FROM debian:bookworm-slim -RUN apt-get update && apt-get install -y ca-certificates curl git ffmpeg python3 golang gosu sudo && rm -rf /var/lib/apt/lists/* +RUN apt-get update && apt-get install -y ca-certificates curl git ffmpeg python3 gosu sudo procps build-essential && rm -rf /var/lib/apt/lists/* RUN curl -LsSf https://astral.sh/uv/install.sh | sh RUN (type -p wget >/dev/null || (apt-get update && apt-get install -y wget)) && \ mkdir -p -m 755 /etc/apt/keyrings && \ @@ -23,8 +23,15 @@ RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \ apt-get install -y nodejs && \ npm install -g @anthropic-ai/claude-code && \ rm -rf /var/lib/apt/lists/* -RUN GOBIN=/usr/local/bin go install github.com/steipete/gogcli@latest RUN useradd -m -s /bin/bash openfang && echo "openfang ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/openfang +USER openfang +RUN NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" +RUN eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" && brew install steipete/tap/gogcli +USER root +RUN echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"' >> /home/openfang/.bashrc && \ + echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"' >> /root/.bashrc && \ + echo 'export PATH="/data/npm-global/bin:$PATH"' >> /home/openfang/.bashrc && \ + echo 'export PATH="/data/npm-global/bin:$PATH"' >> /root/.bashrc COPY --from=builder /build/target/release/openfang /usr/local/bin/ COPY --from=builder /build/agents /opt/openfang/agents RUN mkdir -p /data && chown openfang:openfang /data From 8189b99cb4a2fadef065e7eaa2acaec4bda723fa Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 5 Mar 2026 18:44:10 +0000 Subject: [PATCH 07/42] chore: sync to upstream v0.3.23 --- .current-upstream-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.current-upstream-version b/.current-upstream-version index 25309a8ce..5e82df8ff 100644 --- a/.current-upstream-version +++ b/.current-upstream-version @@ -1 +1 @@ -v0.3.22 +v0.3.23 From 9b64dbc8f50d6884de439fd242411d3bfac456b8 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 6 Mar 2026 00:11:33 +0000 Subject: [PATCH 08/42] chore: sync to upstream v0.3.24 --- .current-upstream-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.current-upstream-version b/.current-upstream-version index 5e82df8ff..c71f96780 100644 --- a/.current-upstream-version +++ b/.current-upstream-version @@ -1 +1 @@ -v0.3.23 +v0.3.24 From a4c0bbaa44772dcc9e28106d20c3ae623fdef71e Mon Sep 17 00:00:00 2001 From: Federico Liva Date: Fri, 6 Mar 2026 09:11:06 +0100 Subject: [PATCH 09/42] docs: add Docker Hub README with auto-sync via GitHub Actions --- .github/workflows/sync-build.yml | 9 +++++++ DOCKER_README.md | 42 ++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 DOCKER_README.md diff --git a/.github/workflows/sync-build.yml b/.github/workflows/sync-build.yml index b49f2f337..23022281a 100644 --- a/.github/workflows/sync-build.yml +++ b/.github/workflows/sync-build.yml @@ -65,3 +65,12 @@ jobs: fliva/openfang:${{ steps.check.outputs.latest }} cache-from: type=gha cache-to: type=gha,mode=max + + - name: Update Docker Hub description + if: steps.check.outputs.new_release == 'true' + uses: peter-evans/dockerhub-description@v4 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + repository: fliva/openfang + readme-filepath: ./DOCKER_README.md diff --git a/DOCKER_README.md b/DOCKER_README.md new file mode 100644 index 000000000..1f39cab6f --- /dev/null +++ b/DOCKER_README.md @@ -0,0 +1,42 @@ +# OpenFang for Lazycat NAS + +Custom [OpenFang](https://github.com/RightNow-AI/openfang) Docker image optimized for deployment on Lazycat LCMD Microserver. + +**Automatically rebuilt on every new upstream release via GitHub Actions.** + +## What's included + +- **OpenFang Agent OS** — Rust-based autonomous AI agent daemon +- **Claude Code CLI** — Anthropic's CLI for Claude, as LLM provider +- **Node.js 22** — JavaScript runtime +- **Python 3** — Python runtime +- **Go** — via Homebrew +- **Homebrew** — package manager for additional tools +- **uv** — fast Python package manager +- **gh** — GitHub CLI +- **gogcli** — GOG.com CLI client +- **ffmpeg** — multimedia processing +- **jq** — JSON processor +- **git, curl, wget** — standard utilities + +## Non-root execution + +The image uses `gosu` to drop root privileges to the `openfang` user at runtime. This is required because Claude Code's `--dangerously-skip-permissions` flag refuses to run as root. + +The `openfang` user has passwordless `sudo` access, so it can still install system packages when needed. + +## Usage + +```bash +docker run -d \ + -p 4200:4200 \ + -v openfang-data:/data \ + -v openfang-home:/home/openfang \ + -e OPENFANG_HOME=/data \ + fliva/openfang:latest +``` + +## Source + +- **This fork**: [github.com/f-liva/openfang](https://github.com/f-liva/openfang) +- **Upstream**: [github.com/RightNow-AI/openfang](https://github.com/RightNow-AI/openfang) From b5d2bcce9e3bcb5316203577a3b4ed42dae1c746 Mon Sep 17 00:00:00 2001 From: Federico Liva Date: Fri, 6 Mar 2026 09:18:50 +0100 Subject: [PATCH 10/42] feat: add jq to image --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 9d5c68622..e443cda52 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,7 @@ COPY packages ./packages RUN cargo build --release --bin openfang FROM debian:bookworm-slim -RUN apt-get update && apt-get install -y ca-certificates curl git ffmpeg python3 gosu sudo procps build-essential && rm -rf /var/lib/apt/lists/* +RUN apt-get update && apt-get install -y ca-certificates curl git ffmpeg python3 gosu sudo procps build-essential jq && rm -rf /var/lib/apt/lists/* RUN curl -LsSf https://astral.sh/uv/install.sh | sh RUN (type -p wget >/dev/null || (apt-get update && apt-get install -y wget)) && \ mkdir -p -m 755 /etc/apt/keyrings && \ From bf31aa51392a8b04bfda23fd2be8eddeb364186e Mon Sep 17 00:00:00 2001 From: Federico Liva Date: Fri, 6 Mar 2026 10:27:25 +0100 Subject: [PATCH 11/42] =?UTF-8?q?docs:=20fix=20gogcli=20description=20?= =?UTF-8?q?=E2=80=94=20Google=20Workspace=20CLI,=20not=20GOG.com?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DOCKER_README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DOCKER_README.md b/DOCKER_README.md index 1f39cab6f..d3e09a054 100644 --- a/DOCKER_README.md +++ b/DOCKER_README.md @@ -14,7 +14,7 @@ Custom [OpenFang](https://github.com/RightNow-AI/openfang) Docker image optimize - **Homebrew** — package manager for additional tools - **uv** — fast Python package manager - **gh** — GitHub CLI -- **gogcli** — GOG.com CLI client +- **gog** — [Google Workspace CLI](https://gogcli.sh/) (Gmail, Calendar, Drive, Sheets, etc.) - **ffmpeg** — multimedia processing - **jq** — JSON processor - **git, curl, wget** — standard utilities From 3a5003bbd5f188e1a4b8d131f1a0dac46f0b68c3 Mon Sep 17 00:00:00 2001 From: Federico Liva Date: Fri, 6 Mar 2026 12:42:52 +0100 Subject: [PATCH 12/42] fix: logo visibility in light theme Replace opaque dark-background logo with transparent PNG and add CSS invert filter for light theme so the snake is visible in both dark and light modes. Co-Authored-By: Claude Opus 4.6 --- crates/openfang-api/static/css/layout.css | 4 ++++ crates/openfang-api/static/logo.png | Bin 10005 -> 174757 bytes 2 files changed, 4 insertions(+) diff --git a/crates/openfang-api/static/css/layout.css b/crates/openfang-api/static/css/layout.css index 880e6ca3f..c4b012113 100644 --- a/crates/openfang-api/static/css/layout.css +++ b/crates/openfang-api/static/css/layout.css @@ -50,6 +50,10 @@ transition: opacity 0.2s, transform 0.2s; } +[data-theme="light"] .sidebar-logo img { + filter: invert(1); +} + .sidebar-logo img:hover { opacity: 1; transform: scale(1.05); diff --git a/crates/openfang-api/static/logo.png b/crates/openfang-api/static/logo.png index cfe72c0b5cbbe95751eca341b353c86cef1aeae6..7f900d403a9c5ced9aea4f688aa3453273d6e458 100644 GIT binary patch literal 174757 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 nUI5!hQ4^M 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 St+#AY@J~xaM?D{B75qQSr$5yI literal 10005 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 From 0b73375cbc9f1d253966ef818d16c3dc9a876f2a Mon Sep 17 00:00:00 2001 From: Federico Liva Date: Fri, 6 Mar 2026 13:31:36 +0100 Subject: [PATCH 13/42] fix: invert logo in message avatar for light theme Extend the CSS invert filter to also cover .message-avatar img, so the agent logo in chat messages is visible in light mode. Co-Authored-By: Claude Opus 4.6 --- crates/openfang-api/static/css/layout.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/openfang-api/static/css/layout.css b/crates/openfang-api/static/css/layout.css index c4b012113..33919dac4 100644 --- a/crates/openfang-api/static/css/layout.css +++ b/crates/openfang-api/static/css/layout.css @@ -50,7 +50,8 @@ transition: opacity 0.2s, transform 0.2s; } -[data-theme="light"] .sidebar-logo img { +[data-theme="light"] .sidebar-logo img, +[data-theme="light"] .message-avatar img { filter: invert(1); } From 879eae8d24c8a342be7999f9f29616af1091bbc3 Mon Sep 17 00:00:00 2001 From: Federico Liva Date: Fri, 6 Mar 2026 15:46:22 +0100 Subject: [PATCH 14/42] fix: detect Claude Code CLI install and auth status in provider card MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, Claude Code had key_required=false so detect_auth() always set NotRequired ("No Key Needed"), regardless of whether the CLI was installed or authenticated. Now it checks: - CLI installed + authenticated → Configured - CLI installed, not authenticated → Missing (Not Set) - CLI not installed → NotRequired (No Key Needed) This also fixes the Runtime/Overview page showing Claude Code as "not configured" when it is actually ready. Fixes #376 Co-Authored-By: Claude Opus 4.6 --- crates/openfang-runtime/src/model_catalog.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/openfang-runtime/src/model_catalog.rs b/crates/openfang-runtime/src/model_catalog.rs index 5846c4d57..672b7b286 100644 --- a/crates/openfang-runtime/src/model_catalog.rs +++ b/crates/openfang-runtime/src/model_catalog.rs @@ -56,15 +56,16 @@ impl ModelCatalog { /// Only checks presence — never reads or stores the actual secret. pub fn detect_auth(&mut self) { for provider in &mut self.providers { - // Claude Code is special: no API key needed, but we probe for CLI - // installation so the dashboard shows "Configured" vs "Not Installed". + // Claude Code: detect CLI installation + authentication if provider.id == "claude-code" { - provider.auth_status = - if crate::drivers::claude_code::claude_code_available() { - AuthStatus::Configured - } else { - AuthStatus::Missing - }; + let cli_installed = crate::drivers::claude_code::ClaudeCodeDriver::detect().is_some(); + if cli_installed && crate::drivers::claude_code::claude_code_available() { + provider.auth_status = AuthStatus::Configured; + } else if cli_installed { + provider.auth_status = AuthStatus::Missing; + } else { + provider.auth_status = AuthStatus::NotRequired; + } continue; } if provider.id == "qwen-code" { @@ -92,7 +93,6 @@ impl ModelCatalog { std::env::var("OPENAI_API_KEY").is_ok() || read_codex_credential().is_some() } - // claude-code is handled above (before key_required check) _ => false, }; From c7c8458fd14c825940cd5ceee9167dbd7eddf5bd Mon Sep 17 00:00:00 2001 From: Federico Liva Date: Sat, 7 Mar 2026 09:37:05 +0100 Subject: [PATCH 15/42] fix: use PAT_TOKEN for workflow push permission GITHUB_TOKEN cannot push changes to .github/workflows/ files. Use a PAT with workflows permission instead. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/sync-build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/sync-build.yml b/.github/workflows/sync-build.yml index 23022281a..f1c681b1c 100644 --- a/.github/workflows/sync-build.yml +++ b/.github/workflows/sync-build.yml @@ -13,6 +13,7 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 + token: ${{ secrets.PAT_TOKEN }} - name: Fetch upstream tags run: | From 6460e0d159947c7b0320c14a9e4a8e4137cd8e3e Mon Sep 17 00:00:00 2001 From: Federico Liva Date: Sat, 7 Mar 2026 09:42:48 +0100 Subject: [PATCH 16/42] feat: add Playwright + Chromium to Docker image Install pip3, playwright, and Chromium with system dependencies for browser automation and PDF generation. Co-Authored-By: Claude Opus 4.6 --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index e443cda52..33050604c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,8 @@ COPY packages ./packages RUN cargo build --release --bin openfang FROM debian:bookworm-slim -RUN apt-get update && apt-get install -y ca-certificates curl git ffmpeg python3 gosu sudo procps build-essential jq && rm -rf /var/lib/apt/lists/* +RUN apt-get update && apt-get install -y ca-certificates curl git ffmpeg python3 python3-pip gosu sudo procps build-essential jq && rm -rf /var/lib/apt/lists/* +RUN pip3 install --break-system-packages playwright && playwright install --with-deps chromium RUN curl -LsSf https://astral.sh/uv/install.sh | sh RUN (type -p wget >/dev/null || (apt-get update && apt-get install -y wget)) && \ mkdir -p -m 755 /etc/apt/keyrings && \ From 07b2e0b15244554158daeebd2a418b3693112f6e Mon Sep 17 00:00:00 2001 From: Federico Liva Date: Sat, 7 Mar 2026 09:45:06 +0100 Subject: [PATCH 17/42] fix: use force push in sync workflow to avoid stale info rejection The rebase creates new commit hashes, and --force-with-lease fails when the remote was updated between fetches. Since this is an intentional rebase sync, --force is appropriate. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/sync-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sync-build.yml b/.github/workflows/sync-build.yml index f1c681b1c..7b83ff94b 100644 --- a/.github/workflows/sync-build.yml +++ b/.github/workflows/sync-build.yml @@ -42,7 +42,7 @@ jobs: echo "${{ steps.check.outputs.latest }}" > .current-upstream-version git add .current-upstream-version git commit -m "chore: sync to upstream ${{ steps.check.outputs.latest }}" || true - git push --force-with-lease + git push --force - name: Set up Docker Buildx if: steps.check.outputs.new_release == 'true' From 4eaa5a4dd61fb1845023417a5b5ec0dc41771658 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 7 Mar 2026 08:50:26 +0000 Subject: [PATCH 18/42] chore: sync to upstream v0.3.26 --- .current-upstream-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.current-upstream-version b/.current-upstream-version index c71f96780..c40f247ce 100644 --- a/.current-upstream-version +++ b/.current-upstream-version @@ -1 +1 @@ -v0.3.24 +v0.3.26 From 0510605e7e20dc354f4dfe4390f9b2b35a4a312e Mon Sep 17 00:00:00 2001 From: Federico Liva Date: Sat, 7 Mar 2026 13:29:42 +0100 Subject: [PATCH 19/42] fix: add python -> python3 symlink for Browser Hand detection OpenFang's Browser Hand checks for 'python' not 'python3'. Debian only installs the python3 binary, so add a symlink. Co-Authored-By: Claude Opus 4.6 --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 33050604c..83cb0a6ad 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,8 @@ RUN cargo build --release --bin openfang FROM debian:bookworm-slim RUN apt-get update && apt-get install -y ca-certificates curl git ffmpeg python3 python3-pip gosu sudo procps build-essential jq && rm -rf /var/lib/apt/lists/* -RUN pip3 install --break-system-packages playwright && playwright install --with-deps chromium +RUN ln -s /usr/bin/python3 /usr/bin/python && \ + pip3 install --break-system-packages playwright && playwright install --with-deps chromium RUN curl -LsSf https://astral.sh/uv/install.sh | sh RUN (type -p wget >/dev/null || (apt-get update && apt-get install -y wget)) && \ mkdir -p -m 755 /etc/apt/keyrings && \ From 26ec0ea9a311c3e20ead062ad2fc6aca3f37f4e3 Mon Sep 17 00:00:00 2001 From: Ambrogio Date: Sat, 7 Mar 2026 19:13:23 +0100 Subject: [PATCH 20/42] fix: default hourly cost quota should be unlimited (0.0), not $1.00 The ResourceQuota default set max_cost_per_hour_usd to 1.0 while daily and monthly were 0.0 (unlimited). This caused agents without explicit quota configuration to hit a hidden $1/hour cap. Also fixes apply_budget_defaults() which compared against the old hardcoded default value of 1.0. Fixes #416 Co-Authored-By: Claude Opus 4.6 --- crates/openfang-kernel/src/kernel.rs | 2 +- crates/openfang-types/src/agent.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/openfang-kernel/src/kernel.rs b/crates/openfang-kernel/src/kernel.rs index 99f5de91f..550eae9b1 100644 --- a/crates/openfang-kernel/src/kernel.rs +++ b/crates/openfang-kernel/src/kernel.rs @@ -5133,7 +5133,7 @@ fn apply_budget_defaults( budget: &openfang_types::config::BudgetConfig, resources: &mut ResourceQuota, ) { - // Only override hourly if agent has unlimited (0.0) and global is set + // Only override hourly if agent has unlimited default (0.0) and global is set if budget.max_hourly_usd > 0.0 && resources.max_cost_per_hour_usd == 0.0 { resources.max_cost_per_hour_usd = budget.max_hourly_usd; } diff --git a/crates/openfang-types/src/agent.rs b/crates/openfang-types/src/agent.rs index 5f002b778..f8d59fcfd 100644 --- a/crates/openfang-types/src/agent.rs +++ b/crates/openfang-types/src/agent.rs @@ -267,7 +267,7 @@ impl Default for ResourceQuota { max_tool_calls_per_minute: 60, max_llm_tokens_per_hour: 0, // unlimited by default max_network_bytes_per_hour: 100 * 1024 * 1024, // 100 MB - max_cost_per_hour_usd: 0.0, // unlimited by default + max_cost_per_hour_usd: 0.0, // unlimited max_cost_per_day_usd: 0.0, // unlimited max_cost_per_month_usd: 0.0, // unlimited } From 7c3ce066278dfc2225b57798697f441f738323cc Mon Sep 17 00:00:00 2001 From: Ambrogio Date: Sat, 7 Mar 2026 20:41:19 +0100 Subject: [PATCH 21/42] ci: trigger Docker rebuild on every push to main Previously the workflow only rebuilt when a new upstream tag was detected, so custom commits on main were silently ignored. Now pushes to main trigger a full build+push to Docker Hub. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/sync-build.yml | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/sync-build.yml b/.github/workflows/sync-build.yml index 7b83ff94b..2db83953c 100644 --- a/.github/workflows/sync-build.yml +++ b/.github/workflows/sync-build.yml @@ -4,6 +4,8 @@ on: schedule: - cron: '0 */6 * * *' # ogni 6 ore workflow_dispatch: # trigger manuale + push: + branches: [main] # rebuild ad ogni push su main jobs: sync-and-build: @@ -16,12 +18,14 @@ jobs: token: ${{ secrets.PAT_TOKEN }} - name: Fetch upstream tags + if: github.event_name != 'push' run: | git remote add upstream https://github.com/RightNow-AI/openfang.git || true git fetch upstream --tags - name: Check for new release id: check + if: github.event_name != 'push' run: | LATEST_TAG=$(git tag -l 'v*' --sort=-v:refname | head -1) CURRENT=$(cat .current-upstream-version 2>/dev/null || echo "none") @@ -34,7 +38,7 @@ jobs: fi - name: Rebase on latest tag - if: steps.check.outputs.new_release == 'true' + if: github.event_name != 'push' && steps.check.outputs.new_release == 'true' run: | git config user.name "github-actions" git config user.email "actions@github.com" @@ -45,30 +49,30 @@ jobs: git push --force - name: Set up Docker Buildx - if: steps.check.outputs.new_release == 'true' + if: github.event_name == 'push' || steps.check.outputs.new_release == 'true' uses: docker/setup-buildx-action@v3 - name: Login to Docker Hub - if: steps.check.outputs.new_release == 'true' + if: github.event_name == 'push' || steps.check.outputs.new_release == 'true' uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push - if: steps.check.outputs.new_release == 'true' + if: github.event_name == 'push' || steps.check.outputs.new_release == 'true' uses: docker/build-push-action@v6 with: context: . push: true tags: | fliva/openfang:latest - fliva/openfang:${{ steps.check.outputs.latest }} + fliva/openfang:${{ steps.check.outputs.latest || 'custom' }} cache-from: type=gha cache-to: type=gha,mode=max - name: Update Docker Hub description - if: steps.check.outputs.new_release == 'true' + if: github.event_name == 'push' || steps.check.outputs.new_release == 'true' uses: peter-evans/dockerhub-description@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} From 8570594603de4bc9cad8a02040489ed7df7720fa Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 7 Mar 2026 20:38:49 +0000 Subject: [PATCH 22/42] chore: sync to upstream v0.3.27 --- .current-upstream-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.current-upstream-version b/.current-upstream-version index c40f247ce..7eccf8697 100644 --- a/.current-upstream-version +++ b/.current-upstream-version @@ -1 +1 @@ -v0.3.26 +v0.3.27 From 189bd07051f6b51f3df2350f8c55a5d313cccf19 Mon Sep 17 00:00:00 2001 From: Federico Liva Date: Mon, 9 Mar 2026 09:57:59 +0100 Subject: [PATCH 23/42] chore: sync to upstream v0.3.34 --- .current-upstream-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.current-upstream-version b/.current-upstream-version index 7eccf8697..23ed88082 100644 --- a/.current-upstream-version +++ b/.current-upstream-version @@ -1 +1 @@ -v0.3.27 +v0.3.34 From 7e4383ccbb7d7ef6bccc633511a9de3cfa21dc4d Mon Sep 17 00:00:00 2001 From: Federico Liva Date: Mon, 9 Mar 2026 10:00:42 +0100 Subject: [PATCH 24/42] fix: auto-skip conflicting commits during upstream rebase When upstream changes conflict with custom fork commits, the rebase now automatically skips the conflicting commits instead of failing. Skipped commits are typically duplicates already applied upstream. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/sync-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sync-build.yml b/.github/workflows/sync-build.yml index 2db83953c..042beaaa7 100644 --- a/.github/workflows/sync-build.yml +++ b/.github/workflows/sync-build.yml @@ -42,7 +42,7 @@ jobs: run: | git config user.name "github-actions" git config user.email "actions@github.com" - git rebase ${{ steps.check.outputs.latest }} + GIT_SEQUENCE_EDITOR=true git rebase ${{ steps.check.outputs.latest }} || while git rebase --skip 2>/dev/null; do :; done echo "${{ steps.check.outputs.latest }}" > .current-upstream-version git add .current-upstream-version git commit -m "chore: sync to upstream ${{ steps.check.outputs.latest }}" || true From 2f7cc39574097dcdfff82a39282c49f519551198 Mon Sep 17 00:00:00 2001 From: Federico Liva Date: Mon, 9 Mar 2026 12:30:49 +0100 Subject: [PATCH 25/42] fix: replace Playwright with chromium package in Docker image MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Browser Hand no longer requires Playwright — it now needs Chromium directly. Replace pip3 playwright install with apt chromium package. Co-Authored-By: Claude Opus 4.6 --- Dockerfile | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 83cb0a6ad..23b52ee14 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,9 +10,8 @@ COPY packages ./packages RUN cargo build --release --bin openfang FROM debian:bookworm-slim -RUN apt-get update && apt-get install -y ca-certificates curl git ffmpeg python3 python3-pip gosu sudo procps build-essential jq && rm -rf /var/lib/apt/lists/* -RUN ln -s /usr/bin/python3 /usr/bin/python && \ - pip3 install --break-system-packages playwright && playwright install --with-deps chromium +RUN apt-get update && apt-get install -y ca-certificates curl git ffmpeg python3 python3-pip chromium gosu sudo procps build-essential jq && rm -rf /var/lib/apt/lists/* +RUN ln -s /usr/bin/python3 /usr/bin/python RUN curl -LsSf https://astral.sh/uv/install.sh | sh RUN (type -p wget >/dev/null || (apt-get update && apt-get install -y wget)) && \ mkdir -p -m 755 /etc/apt/keyrings && \ From 734f9df67a1937931d3297c639f252afe9a008e6 Mon Sep 17 00:00:00 2001 From: Federico Liva Date: Wed, 11 Mar 2026 08:59:58 +0100 Subject: [PATCH 26/42] fix: re-add --dangerously-skip-permissions to Claude Code driver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upstream v0.3.46 removed the flag, causing all agents using Claude Code to be completely paralyzed — every command requires interactive terminal approval that cannot be given via dashboard or Telegram. Without this flag, Claude Code as a provider is unusable in any non-interactive context (web UI, Telegram, API, scheduled tasks). Refs: #515, #325 Co-Authored-By: Claude Opus 4.6 --- .current-upstream-version | 2 +- crates/openfang-runtime/src/drivers/claude_code.rs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.current-upstream-version b/.current-upstream-version index 23ed88082..ab27bf490 100644 --- a/.current-upstream-version +++ b/.current-upstream-version @@ -1 +1 @@ -v0.3.34 +v0.3.46 diff --git a/crates/openfang-runtime/src/drivers/claude_code.rs b/crates/openfang-runtime/src/drivers/claude_code.rs index 1cdfe3b44..7e721ae67 100644 --- a/crates/openfang-runtime/src/drivers/claude_code.rs +++ b/crates/openfang-runtime/src/drivers/claude_code.rs @@ -202,6 +202,7 @@ impl LlmDriver for ClaudeCodeDriver { let mut cmd = tokio::process::Command::new(&self.cli_path); cmd.arg("-p") .arg(&prompt) + .arg("--dangerously-skip-permissions") .arg("--output-format") .arg("json"); @@ -305,6 +306,7 @@ impl LlmDriver for ClaudeCodeDriver { let mut cmd = tokio::process::Command::new(&self.cli_path); cmd.arg("-p") .arg(&prompt) + .arg("--dangerously-skip-permissions") .arg("--output-format") .arg("stream-json") .arg("--verbose"); From f5328e04816ba99a6ea610ccac6c43787a7e7b1e Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 12 Mar 2026 00:06:05 +0000 Subject: [PATCH 27/42] chore: sync to upstream v0.3.47 --- .current-upstream-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.current-upstream-version b/.current-upstream-version index ab27bf490..acf24e50e 100644 --- a/.current-upstream-version +++ b/.current-upstream-version @@ -1 +1 @@ -v0.3.46 +v0.3.47 From 0d62faa65df7d63b712188ecf419557701ab48c8 Mon Sep 17 00:00:00 2001 From: Federico Liva Date: Thu, 12 Mar 2026 10:36:25 +0100 Subject: [PATCH 28/42] feat: add Qwen Code CLI as LLM provider + refactor build_args for testability Add qwen-code as a new subprocess-based LLM provider, mirroring the claude-code driver pattern. Qwen Code CLI (qwen) uses --yolo for non-interactive mode and supports json/stream-json output formats. New files: - drivers/qwen_code.rs: full driver with complete/stream, env filtering Changes: - drivers/mod.rs: register qwen-code provider (defaults, create_driver) - model_catalog.rs: add provider info, 3 models (qwen3-coder, qwen-coder-plus, qwq-32b), aliases, auth detection - claude_code.rs: extract build_args() for testability, fix duplicate --dangerously-skip-permissions flag that was always added regardless of skip_permissions setting Tests: 31 new/updated (18 qwen-code + 13 claude-code) covering build_args with/without permission flags, streaming, model selection, prompt building, JSON parsing, and catalog integration. Co-Authored-By: Claude Opus 4.6 --- .../src/drivers/claude_code.rs | 97 ++++++--- crates/openfang-runtime/src/drivers/mod.rs | 7 +- .../openfang-runtime/src/drivers/qwen_code.rs | 192 ++++++++++++------ crates/openfang-runtime/src/model_catalog.rs | 34 +++- 4 files changed, 232 insertions(+), 98 deletions(-) diff --git a/crates/openfang-runtime/src/drivers/claude_code.rs b/crates/openfang-runtime/src/drivers/claude_code.rs index 7e721ae67..c73ef6f74 100644 --- a/crates/openfang-runtime/src/drivers/claude_code.rs +++ b/crates/openfang-runtime/src/drivers/claude_code.rs @@ -125,6 +125,42 @@ impl ClaudeCodeDriver { } } + /// Build the CLI argument list for a completion request. + /// + /// Exposed as a testable method so unit tests can verify that + /// `--dangerously-skip-permissions`, `--model`, and output format flags + /// are set correctly. + fn build_args( + &self, + prompt: &str, + model_flag: Option<&str>, + streaming: bool, + ) -> Vec { + let mut args = vec![ + "-p".to_string(), + prompt.to_string(), + ]; + + if self.skip_permissions { + args.push("--dangerously-skip-permissions".to_string()); + } + + args.push("--output-format".to_string()); + if streaming { + args.push("stream-json".to_string()); + args.push("--verbose".to_string()); + } else { + args.push("json".to_string()); + } + + if let Some(model) = model_flag { + args.push("--model".to_string()); + args.push(model.to_string()); + } + + args + } + /// Apply security env filtering to a command. /// /// Instead of `env_clear()` (which breaks Node.js, NVM, SSL, proxies), @@ -199,20 +235,10 @@ impl LlmDriver for ClaudeCodeDriver { let prompt = Self::build_prompt(&request); let model_flag = Self::model_flag(&request.model); - let mut cmd = tokio::process::Command::new(&self.cli_path); - cmd.arg("-p") - .arg(&prompt) - .arg("--dangerously-skip-permissions") - .arg("--output-format") - .arg("json"); + let args = self.build_args(&prompt, model_flag.as_deref(), false); - if self.skip_permissions { - cmd.arg("--dangerously-skip-permissions"); - } - - if let Some(ref model) = model_flag { - cmd.arg("--model").arg(model); - } + let mut cmd = tokio::process::Command::new(&self.cli_path); + cmd.args(&args); Self::apply_env_filter(&mut cmd); @@ -303,21 +329,10 @@ impl LlmDriver for ClaudeCodeDriver { let prompt = Self::build_prompt(&request); let model_flag = Self::model_flag(&request.model); - let mut cmd = tokio::process::Command::new(&self.cli_path); - cmd.arg("-p") - .arg(&prompt) - .arg("--dangerously-skip-permissions") - .arg("--output-format") - .arg("stream-json") - .arg("--verbose"); + let args = self.build_args(&prompt, model_flag.as_deref(), true); - if self.skip_permissions { - cmd.arg("--dangerously-skip-permissions"); - } - - if let Some(ref model) = model_flag { - cmd.arg("--model").arg(model); - } + let mut cmd = tokio::process::Command::new(&self.cli_path); + cmd.args(&args); Self::apply_env_filter(&mut cmd); @@ -540,6 +555,34 @@ mod tests { assert!(!driver.skip_permissions); } + #[test] + fn test_build_args_with_skip_permissions() { + let driver = ClaudeCodeDriver::new(None, true); + let args = driver.build_args("hello", Some("opus"), false); + assert!(args.contains(&"--dangerously-skip-permissions".to_string()), + "should contain --dangerously-skip-permissions when skip_permissions=true"); + assert!(args.contains(&"json".to_string())); + assert!(args.contains(&"--model".to_string())); + assert!(args.contains(&"opus".to_string())); + } + + #[test] + fn test_build_args_without_skip_permissions() { + let driver = ClaudeCodeDriver::new(None, false); + let args = driver.build_args("hello", Some("sonnet"), false); + assert!(!args.contains(&"--dangerously-skip-permissions".to_string()), + "should NOT contain --dangerously-skip-permissions when skip_permissions=false"); + } + + #[test] + fn test_build_args_streaming() { + let driver = ClaudeCodeDriver::new(None, true); + let args = driver.build_args("hello", None, true); + assert!(args.contains(&"stream-json".to_string())); + assert!(args.contains(&"--verbose".to_string())); + assert!(!args.contains(&"--model".to_string()), "no model flag when model_flag is None"); + } + #[test] fn test_sensitive_env_list_coverage() { // Ensure all major provider keys are in the strip list diff --git a/crates/openfang-runtime/src/drivers/mod.rs b/crates/openfang-runtime/src/drivers/mod.rs index 4ecb7c0c0..5ef67e858 100644 --- a/crates/openfang-runtime/src/drivers/mod.rs +++ b/crates/openfang-runtime/src/drivers/mod.rs @@ -151,6 +151,11 @@ fn provider_defaults(provider: &str) -> Option { api_key_env: "", key_required: false, }), + "qwen-code" => Some(ProviderDefaults { + base_url: "", + api_key_env: "", + key_required: false, + }), "moonshot" | "kimi" | "kimi2" => Some(ProviderDefaults { base_url: MOONSHOT_BASE_URL, api_key_env: "MOONSHOT_API_KEY", @@ -421,7 +426,7 @@ pub fn create_driver(config: &DriverConfig) -> Result, LlmErr "Unknown provider '{}'. Supported: anthropic, gemini, openai, groq, openrouter, \ deepseek, together, mistral, fireworks, ollama, vllm, lmstudio, perplexity, \ cohere, ai21, cerebras, sambanova, huggingface, xai, replicate, github-copilot, \ - chutes, venice, codex, claude-code. Or set base_url for a custom OpenAI-compatible endpoint.", + chutes, venice, codex, claude-code, qwen-code. Or set base_url for a custom OpenAI-compatible endpoint.", provider ), }) diff --git a/crates/openfang-runtime/src/drivers/qwen_code.rs b/crates/openfang-runtime/src/drivers/qwen_code.rs index a5ee8e06c..67429ee73 100644 --- a/crates/openfang-runtime/src/drivers/qwen_code.rs +++ b/crates/openfang-runtime/src/drivers/qwen_code.rs @@ -12,8 +12,8 @@ use serde::Deserialize; use tokio::io::AsyncBufReadExt; use tracing::{debug, warn}; -/// Environment variable names to strip from the subprocess to prevent -/// leaking API keys from other providers. +/// Environment variable names (and suffixes) to strip from the subprocess +/// to prevent leaking API keys from other providers. const SENSITIVE_ENV_EXACT: &[&str] = &[ "OPENAI_API_KEY", "ANTHROPIC_API_KEY", @@ -87,23 +87,37 @@ impl QwenCodeDriver { } } - /// Build the CLI arguments for a given request. - pub fn build_args(&self, prompt: &str, model: &str, streaming: bool) -> Vec { - let mut args = vec!["-p".to_string(), prompt.to_string()]; + /// Build the CLI argument list for a completion request. + /// + /// Exposed as a testable method so unit tests can verify that `--yolo`, + /// `--model`, and output format flags are set correctly. + pub fn build_args( + &self, + prompt: &str, + model: &str, + streaming: bool, + ) -> Vec { + let model_flag = Self::model_flag(model); + + let mut args = vec![ + "-p".to_string(), + prompt.to_string(), + "--output-format".to_string(), + if streaming { + "stream-json".to_string() + } else { + "json".to_string() + }, + ]; - args.push("--output-format".to_string()); if streaming { - args.push("stream-json".to_string()); args.push("--verbose".to_string()); - } else { - args.push("json".to_string()); } if self.skip_permissions { args.push("--yolo".to_string()); } - let model_flag = Self::model_flag(model); if let Some(ref m) = model_flag { args.push("--model".to_string()); args.push(m.clone()); @@ -147,10 +161,15 @@ impl QwenCodeDriver { } /// Apply security env filtering to a command. + /// + /// Instead of `env_clear()` (which breaks Node.js, NVM, SSL, proxies), + /// we keep the full environment and only remove known sensitive API keys + /// from other LLM providers. fn apply_env_filter(cmd: &mut tokio::process::Command) { for key in SENSITIVE_ENV_EXACT { cmd.env_remove(key); } + // Remove any env var with a sensitive suffix, unless it's QWEN_* for (key, _) in std::env::vars() { if key.starts_with("QWEN_") { continue; @@ -167,6 +186,9 @@ impl QwenCodeDriver { } /// JSON output from `qwen -p --output-format json`. +/// +/// The CLI may return the response text in different fields depending on +/// version: `result`, `content`, or `text`. We try all three. #[derive(Debug, Deserialize)] struct QwenJsonOutput { result: Option, @@ -213,9 +235,7 @@ impl LlmDriver for QwenCodeDriver { let args = self.build_args(&prompt, &request.model, false); let mut cmd = tokio::process::Command::new(&self.cli_path); - for arg in &args { - cmd.arg(arg); - } + cmd.args(&args); Self::apply_env_filter(&mut cmd); @@ -239,6 +259,7 @@ impl LlmDriver for QwenCodeDriver { let detail = if !stderr.is_empty() { &stderr } else { &stdout }; let code = output.status.code().unwrap_or(1); + // Provide actionable error messages let message = if detail.contains("not authenticated") || detail.contains("auth") || detail.contains("login") @@ -247,6 +268,13 @@ impl LlmDriver for QwenCodeDriver { format!( "Qwen Code CLI is not authenticated. Run: qwen auth\nDetail: {detail}" ) + } else if detail.contains("permission") + || detail.contains("--yolo") + { + format!( + "Qwen Code CLI requires permissions acceptance. \ + Run: qwen --yolo (once to accept)\nDetail: {detail}" + ) } else { format!("Qwen Code CLI exited with code {code}: {detail}") }; @@ -259,9 +287,9 @@ impl LlmDriver for QwenCodeDriver { let stdout = String::from_utf8_lossy(&output.stdout); + // Try JSON parse first if let Ok(parsed) = serde_json::from_str::(&stdout) { - let text = parsed - .result + let text = parsed.result .or(parsed.content) .or(parsed.text) .unwrap_or_default(); @@ -280,6 +308,7 @@ impl LlmDriver for QwenCodeDriver { }); } + // Fallback: treat entire stdout as plain text let text = stdout.trim().to_string(); Ok(CompletionResponse { content: vec![ContentBlock::Text { @@ -304,9 +333,7 @@ impl LlmDriver for QwenCodeDriver { let args = self.build_args(&prompt, &request.model, true); let mut cmd = tokio::process::Command::new(&self.cli_path); - for arg in &args { - cmd.arg(arg); - } + cmd.args(&args); Self::apply_env_filter(&mut cmd); @@ -343,47 +370,51 @@ impl LlmDriver for QwenCodeDriver { } match serde_json::from_str::(&line) { - Ok(event) => match event.r#type.as_str() { - "content" | "text" | "assistant" | "content_block_delta" => { - if let Some(ref content) = event.content { - full_text.push_str(content); - let _ = tx - .send(StreamEvent::TextDelta { - text: content.clone(), - }) - .await; - } - } - "result" | "done" | "complete" => { - if let Some(ref result) = event.result { - if full_text.is_empty() { - full_text = result.clone(); + Ok(event) => { + match event.r#type.as_str() { + "content" | "text" | "assistant" | "content_block_delta" => { + if let Some(ref content) = event.content { + full_text.push_str(content); let _ = tx .send(StreamEvent::TextDelta { - text: result.clone(), + text: content.clone(), }) .await; } } - if let Some(usage) = event.usage { - final_usage = TokenUsage { - input_tokens: usage.input_tokens, - output_tokens: usage.output_tokens, - }; + "result" | "done" | "complete" => { + if let Some(ref result) = event.result { + if full_text.is_empty() { + full_text = result.clone(); + let _ = tx + .send(StreamEvent::TextDelta { + text: result.clone(), + }) + .await; + } + } + if let Some(usage) = event.usage { + final_usage = TokenUsage { + input_tokens: usage.input_tokens, + output_tokens: usage.output_tokens, + }; + } } - } - _ => { - if let Some(ref content) = event.content { - full_text.push_str(content); - let _ = tx - .send(StreamEvent::TextDelta { - text: content.clone(), - }) - .await; + _ => { + // Unknown event type — try content field as fallback + if let Some(ref content) = event.content { + full_text.push_str(content); + let _ = tx + .send(StreamEvent::TextDelta { + text: content.clone(), + }) + .await; + } } } - }, + } Err(e) => { + // Not valid JSON — treat as raw text warn!(line = %line, error = %e, "Non-JSON line from Qwen CLI"); full_text.push_str(&line); let _ = tx @@ -393,6 +424,7 @@ impl LlmDriver for QwenCodeDriver { } } + // Wait for process to finish let status = child .wait() .await @@ -421,18 +453,22 @@ impl LlmDriver for QwenCodeDriver { } } -/// Check if the Qwen Code CLI is available. +/// Check if the Qwen Code CLI is available and authenticated. pub fn qwen_code_available() -> bool { QwenCodeDriver::detect().is_some() || qwen_credentials_exist() } /// Check if Qwen credentials exist. +/// +/// Qwen Code stores session/credentials in `~/.qwen` or `~/.qwen-code/` directory. fn qwen_credentials_exist() -> bool { if let Some(home) = home_dir() { let qwen_dir = home.join(".qwen"); + let qwen_code_dir = home.join(".qwen-code"); qwen_dir.join("credentials.json").exists() || qwen_dir.join(".credentials.json").exists() || qwen_dir.join("auth.json").exists() + || qwen_code_dir.exists() } else { false } @@ -482,6 +518,39 @@ mod tests { assert!(prompt.contains("Hello")); } + #[test] + fn test_build_prompt_multi_turn() { + use openfang_types::message::{Message, MessageContent}; + + let request = CompletionRequest { + model: "qwen-code/qwen3-coder".to_string(), + messages: vec![ + Message { + role: Role::User, + content: MessageContent::text("What is 2+2?"), + }, + Message { + role: Role::Assistant, + content: MessageContent::text("4"), + }, + Message { + role: Role::User, + content: MessageContent::text("And 3+3?"), + }, + ], + tools: vec![], + max_tokens: 1024, + temperature: 0.7, + system: None, + thinking: None, + }; + + let prompt = QwenCodeDriver::build_prompt(&request); + assert!(prompt.contains("[User]\nWhat is 2+2?")); + assert!(prompt.contains("[Assistant]\n4")); + assert!(prompt.contains("[User]\nAnd 3+3?")); + } + #[test] fn test_model_flag_mapping() { assert_eq!( @@ -531,29 +600,21 @@ mod tests { assert!(!driver.skip_permissions); } - #[test] - fn test_sensitive_env_list_coverage() { - assert!(SENSITIVE_ENV_EXACT.contains(&"OPENAI_API_KEY")); - assert!(SENSITIVE_ENV_EXACT.contains(&"ANTHROPIC_API_KEY")); - assert!(SENSITIVE_ENV_EXACT.contains(&"GEMINI_API_KEY")); - assert!(SENSITIVE_ENV_EXACT.contains(&"GROQ_API_KEY")); - assert!(SENSITIVE_ENV_EXACT.contains(&"DEEPSEEK_API_KEY")); - } - #[test] fn test_build_args_with_yolo() { let driver = QwenCodeDriver::new(None, true); let args = driver.build_args("test prompt", "qwen-code/qwen3-coder", false); - assert!(args.contains(&"--yolo".to_string())); + assert!(args.contains(&"--yolo".to_string()), "should contain --yolo when skip_permissions=true"); assert!(args.contains(&"json".to_string())); assert!(args.contains(&"--model".to_string())); + assert!(args.contains(&"qwen3-coder".to_string())); } #[test] fn test_build_args_without_yolo() { let driver = QwenCodeDriver::new(None, false); let args = driver.build_args("test prompt", "qwen-code/qwen3-coder", false); - assert!(!args.contains(&"--yolo".to_string())); + assert!(!args.contains(&"--yolo".to_string()), "should NOT contain --yolo when skip_permissions=false"); } #[test] @@ -564,6 +625,15 @@ mod tests { assert!(args.contains(&"--verbose".to_string())); } + #[test] + fn test_sensitive_env_list_coverage() { + assert!(SENSITIVE_ENV_EXACT.contains(&"OPENAI_API_KEY")); + assert!(SENSITIVE_ENV_EXACT.contains(&"ANTHROPIC_API_KEY")); + assert!(SENSITIVE_ENV_EXACT.contains(&"GEMINI_API_KEY")); + assert!(SENSITIVE_ENV_EXACT.contains(&"GROQ_API_KEY")); + assert!(SENSITIVE_ENV_EXACT.contains(&"DEEPSEEK_API_KEY")); + } + #[test] fn test_json_output_deserialization() { let json = r#"{"result":"Hello world","usage":{"input_tokens":10,"output_tokens":5}}"#; diff --git a/crates/openfang-runtime/src/model_catalog.rs b/crates/openfang-runtime/src/model_catalog.rs index 672b7b286..cc5ed0af4 100644 --- a/crates/openfang-runtime/src/model_catalog.rs +++ b/crates/openfang-runtime/src/model_catalog.rs @@ -78,6 +78,19 @@ impl ModelCatalog { continue; } + // Qwen Code: detect CLI installation + authentication + if provider.id == "qwen-code" { + let cli_installed = crate::drivers::qwen_code::QwenCodeDriver::detect().is_some(); + if cli_installed && crate::drivers::qwen_code::qwen_code_available() { + provider.auth_status = AuthStatus::Configured; + } else if cli_installed { + provider.auth_status = AuthStatus::Missing; + } else { + provider.auth_status = AuthStatus::NotRequired; + } + continue; + } + if !provider.key_required { provider.auth_status = AuthStatus::NotRequired; continue; @@ -850,8 +863,11 @@ fn builtin_aliases() -> HashMap { // Qwen Code aliases ("qwen-code", "qwen-code/qwen3-coder"), ("qwen-coder", "qwen-code/qwen3-coder"), + ("qwen-code-qwen3", "qwen-code/qwen3-coder"), ("qwen-coder-plus", "qwen-code/qwen-coder-plus"), + ("qwen-code-plus", "qwen-code/qwen-coder-plus"), ("qwq", "qwen-code/qwq-32b"), + ("qwen-code-qwq", "qwen-code/qwq-32b"), ]; pairs .into_iter() @@ -3457,10 +3473,10 @@ fn builtin_models() -> Vec { // Qwen Code CLI (3) — subprocess-based, free via Qwen OAuth // ══════════════════════════════════════════════════════════════ ModelCatalogEntry { - id: "qwen-code/qwen-coder-plus".into(), - display_name: "Qwen Coder Plus (CLI)".into(), + id: "qwen-code/qwen3-coder".into(), + display_name: "Qwen3 Coder (CLI)".into(), provider: "qwen-code".into(), - tier: ModelTier::Frontier, + tier: ModelTier::Smart, context_window: 131_072, max_output_tokens: 65_536, input_cost_per_m: 0.0, @@ -3468,13 +3484,13 @@ fn builtin_models() -> Vec { supports_tools: false, supports_vision: false, supports_streaming: true, - aliases: vec!["qwen-coder-plus".into()], + aliases: vec!["qwen-code".into(), "qwen-coder".into(), "qwen-code-qwen3".into()], }, ModelCatalogEntry { - id: "qwen-code/qwen3-coder".into(), - display_name: "Qwen3 Coder (CLI)".into(), + id: "qwen-code/qwen-coder-plus".into(), + display_name: "Qwen Coder Plus (CLI)".into(), provider: "qwen-code".into(), - tier: ModelTier::Smart, + tier: ModelTier::Frontier, context_window: 131_072, max_output_tokens: 65_536, input_cost_per_m: 0.0, @@ -3482,7 +3498,7 @@ fn builtin_models() -> Vec { supports_tools: false, supports_vision: false, supports_streaming: true, - aliases: vec!["qwen-code".into(), "qwen-coder".into()], + aliases: vec!["qwen-coder-plus".into(), "qwen-code-plus".into()], }, ModelCatalogEntry { id: "qwen-code/qwq-32b".into(), @@ -3496,7 +3512,7 @@ fn builtin_models() -> Vec { supports_tools: false, supports_vision: false, supports_streaming: true, - aliases: vec!["qwq".into()], + aliases: vec!["qwq".into(), "qwen-code-qwq".into()], }, // ══════════════════════════════════════════════════════════════ // Chutes.ai (5) From ddb4a730103d9d579b19d186a263e3536216f54c Mon Sep 17 00:00:00 2001 From: Federico Liva Date: Thu, 12 Mar 2026 11:09:51 +0100 Subject: [PATCH 29/42] feat: add Qwen Code CLI to Docker image Install @qwen-code/qwen-code alongside Claude Code so the qwen-code provider is available in the container. Co-Authored-By: Claude Opus 4.6 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 23b52ee14..b56fe096e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,7 +22,7 @@ RUN (type -p wget >/dev/null || (apt-get update && apt-get install -y wget)) && apt-get update && apt-get install -y gh && rm -rf /var/lib/apt/lists/* RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \ apt-get install -y nodejs && \ - npm install -g @anthropic-ai/claude-code && \ + npm install -g @anthropic-ai/claude-code @qwen-code/qwen-code && \ rm -rf /var/lib/apt/lists/* RUN useradd -m -s /bin/bash openfang && echo "openfang ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/openfang USER openfang From 2b3784b95dda3a57425dbf53d267511b258eb3de Mon Sep 17 00:00:00 2001 From: Federico Liva Date: Thu, 12 Mar 2026 11:13:01 +0100 Subject: [PATCH 30/42] fix: correct Qwen Code credentials path from ~/.qwen-code to ~/.qwen Co-Authored-By: Claude Opus 4.6 --- crates/openfang-runtime/src/drivers/qwen_code.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/openfang-runtime/src/drivers/qwen_code.rs b/crates/openfang-runtime/src/drivers/qwen_code.rs index 67429ee73..7c78350c1 100644 --- a/crates/openfang-runtime/src/drivers/qwen_code.rs +++ b/crates/openfang-runtime/src/drivers/qwen_code.rs @@ -460,15 +460,14 @@ pub fn qwen_code_available() -> bool { /// Check if Qwen credentials exist. /// -/// Qwen Code stores session/credentials in `~/.qwen` or `~/.qwen-code/` directory. +/// Qwen Code stores session/credentials in `~/.qwen/` directory. fn qwen_credentials_exist() -> bool { if let Some(home) = home_dir() { let qwen_dir = home.join(".qwen"); - let qwen_code_dir = home.join(".qwen-code"); - qwen_dir.join("credentials.json").exists() + qwen_dir.exists() + || qwen_dir.join("credentials.json").exists() || qwen_dir.join(".credentials.json").exists() || qwen_dir.join("auth.json").exists() - || qwen_code_dir.exists() } else { false } From 3e4ba74255cc4567408b6d6e58ff57412258fa63 Mon Sep 17 00:00:00 2001 From: Federico Liva Date: Thu, 12 Mar 2026 12:38:32 +0100 Subject: [PATCH 31/42] fix(images): fix Telegram image pipeline - dual bug in vision model routing and ClaudeCode driver Root cause 1: Vision model swap (kernel.rs) changed the model to qwen-vl-plus but left the provider as claude-code, routing images to the wrong driver. Root cause 2: ClaudeCodeDriver.build_prompt() called text_content() which silently dropped all ContentBlock::Image and ContentBlock::ImageUrl blocks. Fix: - kernel.rs: vision model swap now also updates the provider - claude_code.rs: full image support via temp files passed with --files flag (handles base64, data URIs, and HTTP URLs) - All other drivers: ensure ImageUrl content blocks are handled - compactor.rs: handle ImageUrl in conversation compaction - bridge.rs: improved image dispatch reliability Closes #528 Co-Authored-By: Claude Opus 4.6 --- crates/openfang-api/src/routes.rs | 1 + crates/openfang-channels/src/bridge.rs | 45 +++-- crates/openfang-kernel/src/kernel.rs | 28 ++++ crates/openfang-memory/src/session.rs | 3 + crates/openfang-runtime/src/compactor.rs | 3 + .../openfang-runtime/src/drivers/anthropic.rs | 6 + .../src/drivers/claude_code.rs | 157 +++++++++++++++++- crates/openfang-runtime/src/drivers/gemini.rs | 8 + crates/openfang-runtime/src/drivers/openai.rs | 7 + crates/openfang-types/src/config.rs | 5 + crates/openfang-types/src/message.rs | 7 + 11 files changed, 244 insertions(+), 26 deletions(-) diff --git a/crates/openfang-api/src/routes.rs b/crates/openfang-api/src/routes.rs index 68e728673..9cb40c11f 100644 --- a/crates/openfang-api/src/routes.rs +++ b/crates/openfang-api/src/routes.rs @@ -7023,6 +7023,7 @@ pub async fn set_provider_key( model: model_id, api_key_env: env_var.clone(), base_url: None, + vision_model: None, }; let mut guard = state .kernel diff --git a/crates/openfang-channels/src/bridge.rs b/crates/openfang-channels/src/bridge.rs index 60b295622..1db733868 100644 --- a/crates/openfang-channels/src/bridge.rs +++ b/crates/openfang-channels/src/bridge.rs @@ -529,26 +529,35 @@ async fn dispatch_message( return; } - // For images: download, base64 encode, and send as multimodal content blocks + // For images: build content blocks with the image URL for vision models. + // We pass the original URL rather than downloading + base64-encoding because + // many providers (DashScope/Qwen, OpenAI) prefer or require direct URLs. if let ChannelContent::Image { ref url, ref caption } = message.content { - let blocks = download_image_to_blocks(url, caption.as_deref()).await; - if blocks.iter().any(|b| matches!(b, ContentBlock::Image { .. })) { - // We have actual image data — send as structured blocks for vision - dispatch_with_blocks( - blocks, - message, - handle, - router, - adapter, - ct_str, - thread_id, - output_format, - lifecycle_reactions, - ) - .await; - return; + let mut blocks = Vec::new(); + if let Some(cap) = caption { + if !cap.is_empty() { + blocks.push(ContentBlock::Text { + text: cap.clone(), + provider_metadata: None, + }); + } } - // Image download failed — fall through to text description below + blocks.push(ContentBlock::ImageUrl { + url: url.clone(), + }); + dispatch_with_blocks( + blocks, + message, + handle, + router, + adapter, + ct_str, + thread_id, + output_format, + lifecycle_reactions, + ) + .await; + return; } let text = match &message.content { diff --git a/crates/openfang-kernel/src/kernel.rs b/crates/openfang-kernel/src/kernel.rs index 550eae9b1..de788471b 100644 --- a/crates/openfang-kernel/src/kernel.rs +++ b/crates/openfang-kernel/src/kernel.rs @@ -2274,6 +2274,34 @@ impl OpenFangKernel { } } + // If message contains images and a vision model is configured, swap to it. + // Many text models (e.g. qwen-plus) don't support image input — the vision + // model (e.g. qwen-vl-plus) handles multimodal content correctly. + if let Some(ref blocks) = content_blocks { + let has_images = blocks.iter().any(|b| { + matches!( + b, + openfang_types::message::ContentBlock::Image { .. } + | openfang_types::message::ContentBlock::ImageUrl { .. } + ) + }); + if has_images { + if let Some(ref vision_model) = self.config.default_model.vision_model { + info!( + agent = %manifest.name, + default_model = %manifest.model.model, + vision_model = %vision_model, + "Swapping to vision model for image content" + ); + manifest.model.model = vision_model.clone(); + // The vision model lives on the same provider as the default + // model. Without this swap, an agent using e.g. claude-code + // would try to send the image to the wrong driver. + manifest.model.provider = self.config.default_model.provider.clone(); + } + } + } + let driver = self.resolve_driver(&manifest)?; // Look up model's actual context window from the catalog diff --git a/crates/openfang-memory/src/session.rs b/crates/openfang-memory/src/session.rs index 74862c372..a7d1a83aa 100644 --- a/crates/openfang-memory/src/session.rs +++ b/crates/openfang-memory/src/session.rs @@ -584,6 +584,9 @@ impl SessionStore { ContentBlock::Image { media_type, .. } => { text_parts.push(format!("[image: {media_type}]")); } + ContentBlock::ImageUrl { ref url } => { + text_parts.push(format!("[image: {url}]")); + } ContentBlock::Thinking { thinking } => { text_parts.push(format!( "[thinking: {}]", diff --git a/crates/openfang-runtime/src/compactor.rs b/crates/openfang-runtime/src/compactor.rs index 855705469..e9e246a86 100644 --- a/crates/openfang-runtime/src/compactor.rs +++ b/crates/openfang-runtime/src/compactor.rs @@ -399,6 +399,9 @@ fn build_conversation_text(messages: &[Message], config: &CompactionConfig) -> S ContentBlock::Image { media_type, .. } => { conversation_text.push_str(&format!("[Image: {media_type}]\n\n")); } + ContentBlock::ImageUrl { url } => { + conversation_text.push_str(&format!("[Image: {url}]\n\n")); + } ContentBlock::Thinking { .. } => {} ContentBlock::Unknown => {} } diff --git a/crates/openfang-runtime/src/drivers/anthropic.rs b/crates/openfang-runtime/src/drivers/anthropic.rs index 857774e26..4d79c2a81 100644 --- a/crates/openfang-runtime/src/drivers/anthropic.rs +++ b/crates/openfang-runtime/src/drivers/anthropic.rs @@ -573,6 +573,12 @@ fn convert_message(msg: &Message) -> ApiMessage { data: data.clone(), }, }), + ContentBlock::ImageUrl { url } => { + // Anthropic requires base64; pass as text description for now. + Some(ApiContentBlock::Text { + text: format!("[Image: {url}]"), + }) + } ContentBlock::ToolUse { id, name, input, .. } => Some(ApiContentBlock::ToolUse { id: id.clone(), name: name.clone(), diff --git a/crates/openfang-runtime/src/drivers/claude_code.rs b/crates/openfang-runtime/src/drivers/claude_code.rs index c73ef6f74..01ed2c46b 100644 --- a/crates/openfang-runtime/src/drivers/claude_code.rs +++ b/crates/openfang-runtime/src/drivers/claude_code.rs @@ -7,8 +7,9 @@ use crate::llm_driver::{CompletionRequest, CompletionResponse, LlmDriver, LlmError, StreamEvent}; use async_trait::async_trait; -use openfang_types::message::{ContentBlock, Role, StopReason, TokenUsage}; +use openfang_types::message::{ContentBlock, MessageContent, Role, StopReason, TokenUsage}; use serde::Deserialize; +use std::path::PathBuf; use tokio::io::AsyncBufReadExt; use tracing::{debug, warn}; @@ -90,8 +91,12 @@ impl ClaudeCodeDriver { } /// Build a text prompt from the completion request messages. - fn build_prompt(request: &CompletionRequest) -> String { + /// + /// Image content blocks are represented as `[Attached image: ]` + /// placeholders — the actual image files are passed via `--files`. + fn build_prompt(request: &CompletionRequest, image_files: &[PathBuf]) -> String { let mut parts = Vec::new(); + let mut img_idx = 0; if let Some(ref sys) = request.system { parts.push(format!("[System]\n{sys}")); @@ -103,15 +108,135 @@ impl ClaudeCodeDriver { Role::Assistant => "Assistant", Role::System => "System", }; - let text = msg.content.text_content(); - if !text.is_empty() { - parts.push(format!("[{role_label}]\n{text}")); + + let mut msg_parts = Vec::new(); + + match &msg.content { + MessageContent::Text(s) => { + if !s.is_empty() { + msg_parts.push(s.clone()); + } + } + MessageContent::Blocks(blocks) => { + for block in blocks { + match block { + ContentBlock::Text { text, .. } => { + if !text.is_empty() { + msg_parts.push(text.clone()); + } + } + ContentBlock::Image { .. } | ContentBlock::ImageUrl { .. } => { + if img_idx < image_files.len() { + let fname = image_files[img_idx] + .file_name() + .map(|n| n.to_string_lossy().to_string()) + .unwrap_or_else(|| format!("image_{img_idx}")); + msg_parts.push(format!("[Attached image: {fname}]")); + img_idx += 1; + } + } + _ => {} + } + } + } + } + + if !msg_parts.is_empty() { + let combined = msg_parts.join("\n"); + parts.push(format!("[{role_label}]\n{combined}")); } } parts.join("\n\n") } + /// Extract image content blocks from messages and write them to temp files. + /// + /// Returns the list of temp file paths. The caller is responsible for + /// cleaning them up after the CLI finishes. + async fn extract_images_to_temp(request: &CompletionRequest) -> Vec { + use base64::Engine; + + let mut paths = Vec::new(); + let ts = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_millis()) + .unwrap_or(0); + + for msg in &request.messages { + if let MessageContent::Blocks(blocks) = &msg.content { + for (i, block) in blocks.iter().enumerate() { + match block { + ContentBlock::Image { media_type, data } => { + let ext = media_type + .strip_prefix("image/") + .unwrap_or("png"); + let path = PathBuf::from(format!( + "/tmp/openfang-img-{ts}-{i}.{ext}" + )); + if let Ok(bytes) = + base64::engine::general_purpose::STANDARD.decode(data) + { + if std::fs::write(&path, &bytes).is_ok() { + paths.push(path); + } + } + } + ContentBlock::ImageUrl { url } => { + // If it's a data URI, decode it; otherwise download + if let Some(rest) = url.strip_prefix("data:") { + // data:image/png;base64, + if let Some((meta, b64)) = rest.split_once(",") { + let ext = meta + .split(';') + .next() + .and_then(|m| m.strip_prefix("image/")) + .unwrap_or("png"); + let path = PathBuf::from(format!( + "/tmp/openfang-img-{ts}-{i}.{ext}" + )); + if let Ok(bytes) = + base64::engine::general_purpose::STANDARD.decode(b64) + { + if std::fs::write(&path, &bytes).is_ok() { + paths.push(path); + } + } + } + } else { + // HTTP(S) URL — try to download + let path = PathBuf::from(format!( + "/tmp/openfang-img-{ts}-{i}.jpg" + )); + match reqwest::get(url).await { + Ok(resp) => { + if let Ok(bytes) = resp.bytes().await { + if std::fs::write(&path, &bytes).is_ok() { + paths.push(path); + } + } + } + Err(e) => { + warn!(url = %url, error = %e, "Failed to download image for Claude CLI"); + } + } + } + } + _ => {} + } + } + } + } + paths + } + + /// Clean up temporary image files. + fn cleanup_temp_images(paths: &[PathBuf]) { + for p in paths { + let _ = std::fs::remove_file(p); + } + } + /// Map a model ID like "claude-code/opus" to CLI --model flag value. fn model_flag(model: &str) -> Option { let stripped = model @@ -232,7 +357,8 @@ impl LlmDriver for ClaudeCodeDriver { &self, request: CompletionRequest, ) -> Result { - let prompt = Self::build_prompt(&request); + let image_files = Self::extract_images_to_temp(&request).await; + let prompt = Self::build_prompt(&request, &image_files); let model_flag = Self::model_flag(&request.model); let args = self.build_args(&prompt, model_flag.as_deref(), false); @@ -240,6 +366,11 @@ impl LlmDriver for ClaudeCodeDriver { let mut cmd = tokio::process::Command::new(&self.cli_path); cmd.args(&args); + // Attach image files so the CLI can see them + for img_path in &image_files { + cmd.arg("--files").arg(img_path); + } + Self::apply_env_filter(&mut cmd); cmd.stdout(std::process::Stdio::piped()); @@ -257,6 +388,7 @@ impl LlmDriver for ClaudeCodeDriver { )))?; if !output.status.success() { + Self::cleanup_temp_images(&image_files); let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); let detail = if !stderr.is_empty() { &stderr } else { &stdout }; @@ -288,6 +420,7 @@ impl LlmDriver for ClaudeCodeDriver { }); } + Self::cleanup_temp_images(&image_files); let stdout = String::from_utf8_lossy(&output.stdout); // Try JSON parse first @@ -326,7 +459,8 @@ impl LlmDriver for ClaudeCodeDriver { request: CompletionRequest, tx: tokio::sync::mpsc::Sender, ) -> Result { - let prompt = Self::build_prompt(&request); + let image_files = Self::extract_images_to_temp(&request).await; + let prompt = Self::build_prompt(&request, &image_files); let model_flag = Self::model_flag(&request.model); let args = self.build_args(&prompt, model_flag.as_deref(), true); @@ -334,6 +468,11 @@ impl LlmDriver for ClaudeCodeDriver { let mut cmd = tokio::process::Command::new(&self.cli_path); cmd.args(&args); + // Attach image files so the CLI can see them + for img_path in &image_files { + cmd.arg("--files").arg(img_path); + } + Self::apply_env_filter(&mut cmd); cmd.stdout(std::process::Stdio::piped()); @@ -429,6 +568,8 @@ impl LlmDriver for ClaudeCodeDriver { .await .map_err(|e| LlmError::Http(format!("Claude CLI wait failed: {e}")))?; + Self::cleanup_temp_images(&image_files); + if !status.success() { warn!(code = ?status.code(), "Claude CLI exited with error"); } @@ -503,7 +644,7 @@ mod tests { thinking: None, }; - let prompt = ClaudeCodeDriver::build_prompt(&request); + let prompt = ClaudeCodeDriver::build_prompt(&request, &[]); assert!(prompt.contains("[System]")); assert!(prompt.contains("You are helpful.")); assert!(prompt.contains("[User]")); diff --git a/crates/openfang-runtime/src/drivers/gemini.rs b/crates/openfang-runtime/src/drivers/gemini.rs index 05144d30e..51369ee90 100644 --- a/crates/openfang-runtime/src/drivers/gemini.rs +++ b/crates/openfang-runtime/src/drivers/gemini.rs @@ -304,6 +304,14 @@ fn convert_messages( }, }); } + ContentBlock::ImageUrl { url } => { + // Gemini supports fileData for URL-based images; + // fall back to a text description if not supported. + parts.push(GeminiPart::Text { + text: format!("[Image: {url}]"), + thought_signature: None, + }); + } ContentBlock::ToolResult { content, tool_name, .. } => { diff --git a/crates/openfang-runtime/src/drivers/openai.rs b/crates/openfang-runtime/src/drivers/openai.rs index 15a5a6657..c52e772cf 100644 --- a/crates/openfang-runtime/src/drivers/openai.rs +++ b/crates/openfang-runtime/src/drivers/openai.rs @@ -278,6 +278,13 @@ impl LlmDriver for OpenAIDriver { }, }); } + ContentBlock::ImageUrl { url } => { + parts.push(OaiContentPart::ImageUrl { + image_url: OaiImageUrl { + url: url.clone(), + }, + }); + } ContentBlock::Thinking { .. } => {} _ => {} } diff --git a/crates/openfang-types/src/config.rs b/crates/openfang-types/src/config.rs index 421253b97..512a98626 100644 --- a/crates/openfang-types/src/config.rs +++ b/crates/openfang-types/src/config.rs @@ -1454,6 +1454,10 @@ pub struct DefaultModelConfig { pub api_key_env: String, /// Optional base URL override. pub base_url: Option, + /// Optional vision-capable model for image messages. + /// When set, agents receiving images will automatically use this model + /// instead of the default (which may not support vision). + pub vision_model: Option, } impl Default for DefaultModelConfig { @@ -1463,6 +1467,7 @@ impl Default for DefaultModelConfig { model: "claude-sonnet-4-20250514".to_string(), api_key_env: "ANTHROPIC_API_KEY".to_string(), base_url: None, + vision_model: None, } } } diff --git a/crates/openfang-types/src/message.rs b/crates/openfang-types/src/message.rs index 99be59571..72a608e88 100644 --- a/crates/openfang-types/src/message.rs +++ b/crates/openfang-types/src/message.rs @@ -56,6 +56,12 @@ pub enum ContentBlock { /// Base64-encoded image data. data: String, }, + /// A URL-referenced image (for providers like DashScope that prefer URLs over base64). + #[serde(rename = "image_url")] + ImageUrl { + /// The URL of the image. + url: String, + }, /// A tool use request from the assistant. #[serde(rename = "tool_use")] ToolUse { @@ -144,6 +150,7 @@ impl MessageContent { ContentBlock::Thinking { thinking } => thinking.len(), ContentBlock::ToolUse { .. } | ContentBlock::Image { .. } + | ContentBlock::ImageUrl { .. } | ContentBlock::Unknown => 0, }) .sum(), From eb351f12acbb417f890f8ab8cbfbba02f1e19b79 Mon Sep 17 00:00:00 2001 From: Federico Liva Date: Thu, 12 Mar 2026 12:47:11 +0100 Subject: [PATCH 32/42] feat: add Telegram notifications to CI workflow Notify on build success/failure via Telegram Bot API using TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID secrets. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/sync-build.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/sync-build.yml b/.github/workflows/sync-build.yml index 042beaaa7..4e09f9403 100644 --- a/.github/workflows/sync-build.yml +++ b/.github/workflows/sync-build.yml @@ -79,3 +79,19 @@ jobs: password: ${{ secrets.DOCKERHUB_TOKEN }} repository: fliva/openfang readme-filepath: ./DOCKER_README.md + + - name: Notify Telegram (success) + if: success() && (github.event_name == 'push' || steps.check.outputs.new_release == 'true') + run: | + curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \ + -d chat_id="${{ secrets.TELEGRAM_CHAT_ID }}" \ + -d parse_mode="Markdown" \ + -d text="✅ *OpenFang Build OK*%0A%0ATag: \`${{ steps.check.outputs.latest || 'custom' }}\`%0ACommit: \`${GITHUB_SHA::7}\`%0A[View run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})" + + - name: Notify Telegram (failure) + if: failure() + run: | + curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \ + -d chat_id="${{ secrets.TELEGRAM_CHAT_ID }}" \ + -d parse_mode="Markdown" \ + -d text="❌ *OpenFang Build FAILED*%0A%0ACommit: \`${GITHUB_SHA::7}\`%0A[View run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})" From 401b076743a549cef023d0e6fda131bb7ded0de0 Mon Sep 17 00:00:00 2001 From: Federico Liva Date: Thu, 12 Mar 2026 13:35:58 +0100 Subject: [PATCH 33/42] fix: skip vision model swap when current model already supports vision MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Check model catalog's supports_vision flag before swapping. Models like claude-opus-4-6 handle images natively — no need to swap to a separate vision model (which may use a different, unconfigured provider). Also warn when images arrive but no vision fallback is available. Co-Authored-By: Claude Opus 4.6 --- crates/openfang-kernel/src/kernel.rs | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/crates/openfang-kernel/src/kernel.rs b/crates/openfang-kernel/src/kernel.rs index de788471b..907c4676f 100644 --- a/crates/openfang-kernel/src/kernel.rs +++ b/crates/openfang-kernel/src/kernel.rs @@ -2274,9 +2274,9 @@ impl OpenFangKernel { } } - // If message contains images and a vision model is configured, swap to it. - // Many text models (e.g. qwen-plus) don't support image input — the vision - // model (e.g. qwen-vl-plus) handles multimodal content correctly. + // If message contains images and the current model doesn't support vision, + // swap to the configured vision model. Models that already support vision + // (e.g. claude-opus-4-6) keep running — no swap needed. if let Some(ref blocks) = content_blocks { let has_images = blocks.iter().any(|b| { matches!( @@ -2286,7 +2286,20 @@ impl OpenFangKernel { ) }); if has_images { - if let Some(ref vision_model) = self.config.default_model.vision_model { + let current_supports_vision = self + .model_catalog + .read() + .ok() + .and_then(|cat| cat.find_model(&manifest.model.model).map(|m| m.supports_vision)) + .unwrap_or(false); + + if current_supports_vision { + info!( + agent = %manifest.name, + model = %manifest.model.model, + "Current model supports vision — skipping swap" + ); + } else if let Some(ref vision_model) = self.config.default_model.vision_model { info!( agent = %manifest.name, default_model = %manifest.model.model, @@ -2298,6 +2311,12 @@ impl OpenFangKernel { // model. Without this swap, an agent using e.g. claude-code // would try to send the image to the wrong driver. manifest.model.provider = self.config.default_model.provider.clone(); + } else { + warn!( + agent = %manifest.name, + model = %manifest.model.model, + "Image received but model lacks vision and no vision_model configured" + ); } } } From 08f59a75833e9aba5ed761680157c3a5e58246b9 Mon Sep 17 00:00:00 2001 From: Federico Liva Date: Thu, 12 Mar 2026 13:39:39 +0100 Subject: [PATCH 34/42] fix: vision model swap respects config priority over current model When vision_model is explicitly set in config, always use it (forced override). Only fall back to the current agent model when no vision_model is configured and the model supports vision natively. Co-Authored-By: Claude Opus 4.6 --- crates/openfang-kernel/src/kernel.rs | 56 ++++++++++++++-------------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/crates/openfang-kernel/src/kernel.rs b/crates/openfang-kernel/src/kernel.rs index 907c4676f..32339cfd1 100644 --- a/crates/openfang-kernel/src/kernel.rs +++ b/crates/openfang-kernel/src/kernel.rs @@ -2274,9 +2274,10 @@ impl OpenFangKernel { } } - // If message contains images and the current model doesn't support vision, - // swap to the configured vision model. Models that already support vision - // (e.g. claude-opus-4-6) keep running — no swap needed. + // Vision model selection for image content. + // Priority: 1) explicit vision_model from config (forced override) + // 2) current agent model if it supports vision (no swap needed) + // 3) error — no vision capability available if let Some(ref blocks) = content_blocks { let has_images = blocks.iter().any(|b| { matches!( @@ -2286,37 +2287,38 @@ impl OpenFangKernel { ) }); if has_images { - let current_supports_vision = self - .model_catalog - .read() - .ok() - .and_then(|cat| cat.find_model(&manifest.model.model).map(|m| m.supports_vision)) - .unwrap_or(false); - - if current_supports_vision { + if let Some(ref vision_model) = self.config.default_model.vision_model { + // Explicit vision_model configured — always use it info!( agent = %manifest.name, - model = %manifest.model.model, - "Current model supports vision — skipping swap" - ); - } else if let Some(ref vision_model) = self.config.default_model.vision_model { - info!( - agent = %manifest.name, - default_model = %manifest.model.model, + current_model = %manifest.model.model, vision_model = %vision_model, - "Swapping to vision model for image content" + "Swapping to configured vision model for image content" ); manifest.model.model = vision_model.clone(); - // The vision model lives on the same provider as the default - // model. Without this swap, an agent using e.g. claude-code - // would try to send the image to the wrong driver. manifest.model.provider = self.config.default_model.provider.clone(); } else { - warn!( - agent = %manifest.name, - model = %manifest.model.model, - "Image received but model lacks vision and no vision_model configured" - ); + // No vision_model forced — check if current model handles vision + let current_supports_vision = self + .model_catalog + .read() + .ok() + .and_then(|cat| cat.find_model(&manifest.model.model).map(|m| m.supports_vision)) + .unwrap_or(false); + + if current_supports_vision { + info!( + agent = %manifest.name, + model = %manifest.model.model, + "Current model supports vision — no swap needed" + ); + } else { + warn!( + agent = %manifest.name, + model = %manifest.model.model, + "Image received but no vision_model configured and current model lacks vision support" + ); + } } } } From 6d67a0c83176c7e46b6fee4d38167c297c9343e0 Mon Sep 17 00:00:00 2001 From: Federico Liva Date: Thu, 12 Mar 2026 14:05:44 +0100 Subject: [PATCH 35/42] fix: use --file instead of --files for Claude Code CLI The CLI option is --file (singular), not --files. Co-Authored-By: Claude Opus 4.6 --- crates/openfang-runtime/src/drivers/claude_code.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/openfang-runtime/src/drivers/claude_code.rs b/crates/openfang-runtime/src/drivers/claude_code.rs index 01ed2c46b..6b3efa488 100644 --- a/crates/openfang-runtime/src/drivers/claude_code.rs +++ b/crates/openfang-runtime/src/drivers/claude_code.rs @@ -93,7 +93,7 @@ impl ClaudeCodeDriver { /// Build a text prompt from the completion request messages. /// /// Image content blocks are represented as `[Attached image: ]` - /// placeholders — the actual image files are passed via `--files`. + /// placeholders — the actual image files are passed via `--file`. fn build_prompt(request: &CompletionRequest, image_files: &[PathBuf]) -> String { let mut parts = Vec::new(); let mut img_idx = 0; @@ -368,7 +368,7 @@ impl LlmDriver for ClaudeCodeDriver { // Attach image files so the CLI can see them for img_path in &image_files { - cmd.arg("--files").arg(img_path); + cmd.arg("--file").arg(img_path); } Self::apply_env_filter(&mut cmd); @@ -470,7 +470,7 @@ impl LlmDriver for ClaudeCodeDriver { // Attach image files so the CLI can see them for img_path in &image_files { - cmd.arg("--files").arg(img_path); + cmd.arg("--file").arg(img_path); } Self::apply_env_filter(&mut cmd); From 7de627a5f3428a9d1f09ec644a6447bbc87960e3 Mon Sep 17 00:00:00 2001 From: Federico Liva Date: Thu, 12 Mar 2026 14:43:57 +0100 Subject: [PATCH 36/42] fix: use @path syntax for local images instead of --file flag The --file flag requires a session token for file downloads (Files API). Instead, embed @/tmp/image.jpg directly in the prompt text, which tells Claude Code CLI to read the local file natively. Co-Authored-By: Claude Opus 4.6 --- .../src/drivers/claude_code.rs | 21 ++++--------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/crates/openfang-runtime/src/drivers/claude_code.rs b/crates/openfang-runtime/src/drivers/claude_code.rs index 6b3efa488..cb47940d9 100644 --- a/crates/openfang-runtime/src/drivers/claude_code.rs +++ b/crates/openfang-runtime/src/drivers/claude_code.rs @@ -92,8 +92,8 @@ impl ClaudeCodeDriver { /// Build a text prompt from the completion request messages. /// - /// Image content blocks are represented as `[Attached image: ]` - /// placeholders — the actual image files are passed via `--file`. + /// Image content blocks are referenced using Claude Code's `@path` syntax, + /// which tells the CLI to read the local file directly. fn build_prompt(request: &CompletionRequest, image_files: &[PathBuf]) -> String { let mut parts = Vec::new(); let mut img_idx = 0; @@ -127,11 +127,8 @@ impl ClaudeCodeDriver { } ContentBlock::Image { .. } | ContentBlock::ImageUrl { .. } => { if img_idx < image_files.len() { - let fname = image_files[img_idx] - .file_name() - .map(|n| n.to_string_lossy().to_string()) - .unwrap_or_else(|| format!("image_{img_idx}")); - msg_parts.push(format!("[Attached image: {fname}]")); + let path = &image_files[img_idx]; + msg_parts.push(format!("@{}", path.display())); img_idx += 1; } } @@ -366,11 +363,6 @@ impl LlmDriver for ClaudeCodeDriver { let mut cmd = tokio::process::Command::new(&self.cli_path); cmd.args(&args); - // Attach image files so the CLI can see them - for img_path in &image_files { - cmd.arg("--file").arg(img_path); - } - Self::apply_env_filter(&mut cmd); cmd.stdout(std::process::Stdio::piped()); @@ -468,11 +460,6 @@ impl LlmDriver for ClaudeCodeDriver { let mut cmd = tokio::process::Command::new(&self.cli_path); cmd.args(&args); - // Attach image files so the CLI can see them - for img_path in &image_files { - cmd.arg("--file").arg(img_path); - } - Self::apply_env_filter(&mut cmd); cmd.stdout(std::process::Stdio::piped()); From 3c9ad5845fe0f5a0402e9373e9c0b421ea5f0780 Mon Sep 17 00:00:00 2001 From: Federico Liva Date: Thu, 12 Mar 2026 15:15:27 +0100 Subject: [PATCH 37/42] feat: add Telegram notification on build start Co-Authored-By: Claude Opus 4.6 --- .github/workflows/sync-build.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/sync-build.yml b/.github/workflows/sync-build.yml index 4e09f9403..66b59d72b 100644 --- a/.github/workflows/sync-build.yml +++ b/.github/workflows/sync-build.yml @@ -17,6 +17,13 @@ jobs: fetch-depth: 0 token: ${{ secrets.PAT_TOKEN }} + - name: Notify Telegram (start) + run: | + curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \ + -d chat_id="${{ secrets.TELEGRAM_CHAT_ID }}" \ + -d parse_mode="Markdown" \ + -d text="🚀 *OpenFang Build Started*%0A%0ATrigger: \`${{ github.event_name }}\`%0ACommit: \`${GITHUB_SHA::7}\`%0A[View run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})" + - name: Fetch upstream tags if: github.event_name != 'push' run: | From e860fafb21b3b3e2576396b1d738d0680b6a1762 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 13 Mar 2026 12:20:46 +0000 Subject: [PATCH 38/42] chore: sync to upstream v0.4.0 --- .current-upstream-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.current-upstream-version b/.current-upstream-version index acf24e50e..fb7a04cff 100644 --- a/.current-upstream-version +++ b/.current-upstream-version @@ -1 +1 @@ -v0.3.47 +v0.4.0 From f92c584ca55da80e12f893445c70620562621cb4 Mon Sep 17 00:00:00 2001 From: Federico Liva Date: Fri, 13 Mar 2026 16:12:10 +0100 Subject: [PATCH 39/42] feat: multi-profile token rotation for Claude Code driver When multiple Claude Code subscriptions are configured via the `profiles` field in DriverConfig, the new MultiProfileDriver automatically rotates between them on rate-limit errors. Cooldown timestamps are derived from the Anthropic OAuth usage API (`/api/oauth/usage`) so profiles are re-enabled at exactly the right time. Config example: profiles = ["~/.claude", "~/.claude-profiles/account-2"] Co-Authored-By: Claude Opus 4.6 --- crates/openfang-api/src/routes.rs | 1 + crates/openfang-kernel/src/kernel.rs | 5 + .../src/drivers/claude_code.rs | 12 +- crates/openfang-runtime/src/drivers/mod.rs | 18 +- .../src/drivers/multi_profile.rs | 1111 +++++++++++++++++ crates/openfang-runtime/src/llm_driver.rs | 7 + 6 files changed, 1147 insertions(+), 7 deletions(-) create mode 100644 crates/openfang-runtime/src/drivers/multi_profile.rs diff --git a/crates/openfang-api/src/routes.rs b/crates/openfang-api/src/routes.rs index 9cb40c11f..7b7343070 100644 --- a/crates/openfang-api/src/routes.rs +++ b/crates/openfang-api/src/routes.rs @@ -7187,6 +7187,7 @@ pub async fn test_provider( Some(base_url) }, skip_permissions: true, + profiles: vec![], }; match openfang_runtime::drivers::create_driver(&driver_config) { diff --git a/crates/openfang-kernel/src/kernel.rs b/crates/openfang-kernel/src/kernel.rs index 32339cfd1..42f168f76 100644 --- a/crates/openfang-kernel/src/kernel.rs +++ b/crates/openfang-kernel/src/kernel.rs @@ -582,6 +582,7 @@ impl OpenFangKernel { .clone() .or_else(|| config.provider_urls.get(&config.default_model.provider).cloned()), skip_permissions: true, + profiles: vec![], }; // Primary driver failure is non-fatal: the dashboard should remain accessible // even if the LLM provider is misconfigured. Users can fix config via dashboard. @@ -603,6 +604,7 @@ impl OpenFangKernel { api_key: std::env::var(env_var).ok(), base_url: config.provider_urls.get(provider).cloned(), skip_permissions: true, + profiles: vec![], }; match drivers::create_driver(&auto_config) { Ok(d) => { @@ -648,6 +650,7 @@ impl OpenFangKernel { .clone() .or_else(|| config.provider_urls.get(&fb.provider).cloned()), skip_permissions: true, + profiles: vec![], }; match drivers::create_driver(&fb_config) { Ok(d) => { @@ -4391,6 +4394,7 @@ impl OpenFangKernel { api_key, base_url, skip_permissions: true, + profiles: vec![], }; match drivers::create_driver(&driver_config) { @@ -4437,6 +4441,7 @@ impl OpenFangKernel { .clone() .or_else(|| self.lookup_provider_url(&fb.provider)), skip_permissions: true, + profiles: vec![], }; match drivers::create_driver(&config) { Ok(d) => chain.push((d, fb.model.clone())), diff --git a/crates/openfang-runtime/src/drivers/claude_code.rs b/crates/openfang-runtime/src/drivers/claude_code.rs index cb47940d9..262c1c2a8 100644 --- a/crates/openfang-runtime/src/drivers/claude_code.rs +++ b/crates/openfang-runtime/src/drivers/claude_code.rs @@ -94,7 +94,7 @@ impl ClaudeCodeDriver { /// /// Image content blocks are referenced using Claude Code's `@path` syntax, /// which tells the CLI to read the local file directly. - fn build_prompt(request: &CompletionRequest, image_files: &[PathBuf]) -> String { + pub fn build_prompt(request: &CompletionRequest, image_files: &[PathBuf]) -> String { let mut parts = Vec::new(); let mut img_idx = 0; @@ -151,7 +151,7 @@ impl ClaudeCodeDriver { /// /// Returns the list of temp file paths. The caller is responsible for /// cleaning them up after the CLI finishes. - async fn extract_images_to_temp(request: &CompletionRequest) -> Vec { + pub async fn extract_images_to_temp(request: &CompletionRequest) -> Vec { use base64::Engine; let mut paths = Vec::new(); @@ -228,14 +228,14 @@ impl ClaudeCodeDriver { } /// Clean up temporary image files. - fn cleanup_temp_images(paths: &[PathBuf]) { + pub fn cleanup_temp_images(paths: &[PathBuf]) { for p in paths { let _ = std::fs::remove_file(p); } } /// Map a model ID like "claude-code/opus" to CLI --model flag value. - fn model_flag(model: &str) -> Option { + pub fn model_flag(model: &str) -> Option { let stripped = model .strip_prefix("claude-code/") .unwrap_or(model); @@ -252,7 +252,7 @@ impl ClaudeCodeDriver { /// Exposed as a testable method so unit tests can verify that /// `--dangerously-skip-permissions`, `--model`, and output format flags /// are set correctly. - fn build_args( + pub fn build_args( &self, prompt: &str, model_flag: Option<&str>, @@ -288,7 +288,7 @@ impl ClaudeCodeDriver { /// Instead of `env_clear()` (which breaks Node.js, NVM, SSL, proxies), /// we keep the full environment and only remove known sensitive API keys /// from other LLM providers. - fn apply_env_filter(cmd: &mut tokio::process::Command) { + pub fn apply_env_filter(cmd: &mut tokio::process::Command) { for key in SENSITIVE_ENV_EXACT { cmd.env_remove(key); } diff --git a/crates/openfang-runtime/src/drivers/mod.rs b/crates/openfang-runtime/src/drivers/mod.rs index 5ef67e858..9dfebffd4 100644 --- a/crates/openfang-runtime/src/drivers/mod.rs +++ b/crates/openfang-runtime/src/drivers/mod.rs @@ -7,6 +7,7 @@ pub mod anthropic; pub mod claude_code; pub mod copilot; +pub mod multi_profile; pub mod fallback; pub mod gemini; pub mod openai; @@ -306,9 +307,18 @@ pub fn create_driver(config: &DriverConfig) -> Result, LlmErr return Ok(Arc::new(openai::OpenAIDriver::new(api_key, base_url))); } - // Claude Code CLI — subprocess-based, no API key needed + // Claude Code CLI — subprocess-based, no API key needed. + // When multiple profiles are configured, use the multi-profile driver + // for automatic rotation on rate-limit errors. if provider == "claude-code" { let cli_path = config.base_url.clone(); + if config.profiles.len() > 1 { + return Ok(Arc::new(multi_profile::MultiProfileDriver::new( + cli_path, + config.skip_permissions, + config.profiles.clone(), + ))); + } return Ok(Arc::new(claude_code::ClaudeCodeDriver::new( cli_path, config.skip_permissions, @@ -542,6 +552,7 @@ mod tests { api_key: Some("test".to_string()), base_url: Some("http://localhost:9999/v1".to_string()), skip_permissions: true, + profiles: vec![], }; let driver = create_driver(&config); assert!(driver.is_ok()); @@ -554,6 +565,7 @@ mod tests { api_key: None, base_url: None, skip_permissions: true, + profiles: vec![], }; let driver = create_driver(&config); assert!(driver.is_err()); @@ -656,6 +668,7 @@ mod tests { api_key: None, // not explicitly passed base_url: Some("https://integrate.api.nvidia.com/v1".to_string()), skip_permissions: true, + profiles: vec![], }; let driver = create_driver(&config); assert!(driver.is_ok(), "Custom provider with env var convention should succeed"); @@ -670,6 +683,7 @@ mod tests { api_key: None, base_url: None, skip_permissions: true, + profiles: vec![], }; let driver = create_driver(&config); assert!(driver.is_err()); @@ -685,6 +699,7 @@ mod tests { api_key: None, base_url: None, skip_permissions: true, + profiles: vec![], }; let result = create_driver(&config); assert!(result.is_err()); @@ -709,6 +724,7 @@ mod tests { api_key: Some("explicit-key".to_string()), base_url: Some("https://api.example.com/v1".to_string()), skip_permissions: true, + profiles: vec![], }; let driver = create_driver(&config); assert!(driver.is_ok()); diff --git a/crates/openfang-runtime/src/drivers/multi_profile.rs b/crates/openfang-runtime/src/drivers/multi_profile.rs new file mode 100644 index 000000000..b7b6a0a13 --- /dev/null +++ b/crates/openfang-runtime/src/drivers/multi_profile.rs @@ -0,0 +1,1111 @@ +//! Multi-profile wrapper for CLI-based LLM drivers (Claude Code, Qwen Code). +//! +//! When a user has multiple subscriptions, each with its own OAuth token stored +//! in a separate config directory, this wrapper automatically rotates between +//! them on rate-limit errors. Cooldown timestamps are derived from the +//! Anthropic usage API (`/api/oauth/usage`) so profiles are re-enabled at +//! exactly the right time. + +use crate::llm_driver::{CompletionRequest, CompletionResponse, LlmDriver, LlmError, StreamEvent}; +use async_trait::async_trait; +use serde::Deserialize; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use tokio::sync::Mutex; +use tracing::{debug, info, warn}; + +// ── Anthropic usage API ────────────────────────────────────────────────── + +const ANTHROPIC_USAGE_URL: &str = "https://api.anthropic.com/api/oauth/usage"; +const ANTHROPIC_BETA_HEADER: &str = "oauth-2025-04-20"; + +/// Utilisation data for a single rate-limit window. +#[derive(Debug, Deserialize)] +struct UsageWindow { + /// Percentage of the window consumed (0.0 – 100.0). + #[serde(default)] + utilization: f64, + /// ISO-8601 timestamp when the window resets. + #[serde(default)] + resets_at: Option, +} + +/// Response from `GET /api/oauth/usage`. +#[derive(Debug, Deserialize)] +struct UsageResponse { + #[serde(default)] + five_hour: Option, + #[serde(default)] + seven_day: Option, +} + +/// Parsed usage info we care about. +#[derive(Debug, Clone)] +pub struct ProfileUsage { + pub five_hour_utilization: f64, + pub five_hour_resets_at: Option>, + pub seven_day_utilization: f64, +} + +// ── Profile state ──────────────────────────────────────────────────────── + +/// Runtime state for a single credential profile. +struct ProfileState { + /// Display name (directory basename). + name: String, + /// Absolute path to the config directory containing `.credentials.json`. + config_dir: PathBuf, + /// When `Some`, the profile is in cooldown until this instant. + cooldown_until: Option, + /// Cached OAuth access token (read once from disk). + access_token: Option, +} + +impl ProfileState { + fn new(config_dir: PathBuf) -> Self { + let name = config_dir + .file_name() + .map(|n| n.to_string_lossy().to_string()) + .unwrap_or_else(|| "default".to_string()); + + Self { + name, + config_dir, + cooldown_until: None, + access_token: None, + } + } + + /// Read the OAuth access token from the credentials file on disk. + fn load_token(&mut self) -> Option<&str> { + if self.access_token.is_some() { + return self.access_token.as_deref(); + } + + let cred_paths = [ + self.config_dir.join(".credentials.json"), + self.config_dir.join("credentials.json"), + ]; + + for path in &cred_paths { + if let Ok(contents) = std::fs::read_to_string(path) { + if let Ok(json) = serde_json::from_str::(&contents) { + if let Some(token) = json + .get("claudeAiOauth") + .and_then(|o| o.get("accessToken")) + .and_then(|t| t.as_str()) + { + self.access_token = Some(token.to_string()); + debug!(profile = %self.name, path = %path.display(), "Loaded OAuth token"); + return self.access_token.as_deref(); + } + } + } + } + + warn!(profile = %self.name, "No valid credentials found"); + None + } + + /// Check if this profile is currently in cooldown. + fn is_available(&self) -> bool { + match self.cooldown_until { + Some(until) => std::time::Instant::now() >= until, + None => true, + } + } + + /// Put this profile in cooldown until the given instant. + fn set_cooldown(&mut self, until: std::time::Instant) { + info!( + profile = %self.name, + cooldown_secs = until.duration_since(std::time::Instant::now()).as_secs(), + "Profile entering cooldown" + ); + self.cooldown_until = Some(until); + } + + /// Clear cooldown (e.g. after a successful request). + fn clear_cooldown(&mut self) { + if self.cooldown_until.is_some() { + info!(profile = %self.name, "Profile cooldown cleared"); + self.cooldown_until = None; + } + } +} + +// ── Usage API client ───────────────────────────────────────────────────── + +/// Fetch usage from the Anthropic API for a given OAuth token. +async fn fetch_usage(access_token: &str) -> Result { + let client = reqwest::Client::new(); + let resp = client + .get(ANTHROPIC_USAGE_URL) + .header("Authorization", format!("Bearer {access_token}")) + .header("anthropic-beta", ANTHROPIC_BETA_HEADER) + .send() + .await + .map_err(|e| format!("HTTP request failed: {e}"))?; + + if resp.status() == 401 { + return Err("OAuth token expired — re-authenticate this profile".to_string()); + } + + let data: UsageResponse = resp + .json() + .await + .map_err(|e| format!("Failed to parse usage response: {e}"))?; + + let (five_util, five_resets) = match data.five_hour { + Some(w) => { + let resets = w.resets_at.and_then(|s| { + chrono::DateTime::parse_from_rfc3339(&s) + .ok() + .map(|dt| dt.with_timezone(&chrono::Utc)) + }); + (w.utilization, resets) + } + None => (0.0, None), + }; + + let seven_util = data.seven_day.map(|w| w.utilization).unwrap_or(0.0); + + Ok(ProfileUsage { + five_hour_utilization: five_util, + five_hour_resets_at: five_resets, + seven_day_utilization: seven_util, + }) +} + +/// Convert a future UTC reset time to a `std::time::Instant`. +fn utc_to_instant(dt: chrono::DateTime) -> std::time::Instant { + let now_utc = chrono::Utc::now(); + if dt <= now_utc { + return std::time::Instant::now(); + } + let delta = (dt - now_utc).to_std().unwrap_or(std::time::Duration::from_secs(0)); + // Add a small buffer (30s) to avoid racing the reset + std::time::Instant::now() + delta + std::time::Duration::from_secs(30) +} + +/// Default cooldown when we can't determine the reset time (30 minutes). +const FALLBACK_COOLDOWN_SECS: u64 = 30 * 60; + +// ── Multi-profile driver ───────────────────────────────────────────────── + +/// A wrapper driver that manages multiple credential profiles for a +/// CLI-based LLM provider, rotating on rate-limit errors. +pub struct MultiProfileDriver { + /// The underlying driver factory. For each request we pick a profile, + /// set `CLAUDE_CONFIG_DIR`, and delegate to a fresh driver instance. + cli_path: String, + skip_permissions: bool, + /// Mutable profile state, protected by a mutex. + profiles: Arc>>, + /// Index of the currently active profile. + current: Arc>, +} + +impl MultiProfileDriver { + /// Create a new multi-profile driver. + /// + /// `profile_dirs` must contain at least one path. Each path should be + /// a directory containing Claude OAuth credentials. If only one path + /// is given, the driver behaves identically to a plain `ClaudeCodeDriver` + /// but with rate-limit detection and reporting. + pub fn new( + cli_path: Option, + skip_permissions: bool, + profile_dirs: Vec, + ) -> Self { + let profiles: Vec = profile_dirs + .into_iter() + .map(|dir| { + let expanded = expand_tilde(&dir); + ProfileState::new(PathBuf::from(expanded)) + }) + .collect(); + + let names: Vec<&str> = profiles.iter().map(|p| p.name.as_str()).collect(); + info!(profiles = ?names, "Multi-profile Claude driver initialized"); + + Self { + cli_path: cli_path + .filter(|s| !s.is_empty()) + .unwrap_or_else(|| "claude".to_string()), + skip_permissions, + profiles: Arc::new(Mutex::new(profiles)), + current: Arc::new(Mutex::new(0)), + } + } + + /// Pick the next available (non-cooldown) profile index. + /// Returns `None` if all profiles are in cooldown. + async fn next_available(&self) -> Option<(usize, PathBuf)> { + let profiles = self.profiles.lock().await; + let count = profiles.len(); + let current = *self.current.lock().await; + + // Try from current, then wrap around + for offset in 0..count { + let idx = (current + offset) % count; + if profiles[idx].is_available() { + return Some((idx, profiles[idx].config_dir.clone())); + } + } + None + } + + /// Mark a profile as rate-limited, querying the API for the exact + /// cooldown duration. + async fn handle_rate_limit(&self, profile_idx: usize) { + let mut profiles = self.profiles.lock().await; + let profile = &mut profiles[profile_idx]; + + // Try to get exact reset time from API + let cooldown_until = if let Some(token) = profile.load_token() { + let token = token.to_string(); + // Drop lock before the async call + drop(profiles); + + match fetch_usage(&token).await { + Ok(usage) => { + info!( + profile_idx, + five_hour_pct = usage.five_hour_utilization, + seven_day_pct = usage.seven_day_utilization, + resets_at = ?usage.five_hour_resets_at, + "Usage API response" + ); + usage + .five_hour_resets_at + .map(utc_to_instant) + .unwrap_or_else(|| { + std::time::Instant::now() + + std::time::Duration::from_secs(FALLBACK_COOLDOWN_SECS) + }) + } + Err(e) => { + warn!(error = %e, "Failed to fetch usage — using fallback cooldown"); + std::time::Instant::now() + + std::time::Duration::from_secs(FALLBACK_COOLDOWN_SECS) + } + } + } else { + drop(profiles); + std::time::Instant::now() + + std::time::Duration::from_secs(FALLBACK_COOLDOWN_SECS) + }; + + // Re-acquire lock and set cooldown + let mut profiles = self.profiles.lock().await; + profiles[profile_idx].set_cooldown(cooldown_until); + + // Advance current to next available + let count = profiles.len(); + for offset in 1..count { + let next = (profile_idx + offset) % count; + if profiles[next].is_available() { + *self.current.lock().await = next; + info!( + from = profiles[profile_idx].name, + to = profiles[next].name, + "Rotated to next profile" + ); + return; + } + } + warn!("All profiles are in cooldown — requests will fail until a window resets"); + } + + /// Check if an error is a rate-limit error. + fn is_rate_limit_error(err: &LlmError) -> bool { + match err { + LlmError::Api { status, message } => { + *status == 429 + || message.to_lowercase().contains("rate limit") + || message.to_lowercase().contains("quota") + || message.to_lowercase().contains("too many requests") + || message.to_lowercase().contains("usage limit") + || message.to_lowercase().contains("overloaded") + } + LlmError::Http(msg) => { + let lower = msg.to_lowercase(); + lower.contains("rate limit") + || lower.contains("429") + || lower.contains("quota") + || lower.contains("usage limit") + } + _ => false, + } + } + +} + +#[async_trait] +impl LlmDriver for MultiProfileDriver { + async fn complete( + &self, + request: CompletionRequest, + ) -> Result { + let profiles_count = self.profiles.lock().await.len(); + + // Try each available profile + for _attempt in 0..profiles_count { + let (idx, config_dir) = match self.next_available().await { + Some(pair) => pair, + None => { + return Err(LlmError::Api { + status: 429, + message: "All Claude Code profiles are rate-limited. \ + Requests will resume when a rate-limit window resets." + .to_string(), + }); + } + }; + + let profile_name = { + let profiles = self.profiles.lock().await; + profiles[idx].name.clone() + }; + + debug!(profile = %profile_name, config_dir = %config_dir.display(), "Using profile for completion"); + + let result = complete_with_config_dir( + &self.cli_path, + self.skip_permissions, + &config_dir, + request.clone(), + ) + .await; + + match &result { + Ok(_) => { + // Success — clear any stale cooldown + let mut profiles = self.profiles.lock().await; + profiles[idx].clear_cooldown(); + *self.current.lock().await = idx; + return result; + } + Err(e) if Self::is_rate_limit_error(e) => { + warn!(profile = %profile_name, error = %e, "Rate limit hit — rotating"); + self.handle_rate_limit(idx).await; + continue; + } + Err(_) => { + // Non-rate-limit error — don't rotate, just return + return result; + } + } + } + + Err(LlmError::Api { + status: 429, + message: "All Claude Code profiles exhausted after rotation attempts".to_string(), + }) + } + + async fn stream( + &self, + request: CompletionRequest, + tx: tokio::sync::mpsc::Sender, + ) -> Result { + let profiles_count = self.profiles.lock().await.len(); + + for _attempt in 0..profiles_count { + let (idx, config_dir) = match self.next_available().await { + Some(pair) => pair, + None => { + return Err(LlmError::Api { + status: 429, + message: "All Claude Code profiles are rate-limited. \ + Requests will resume when a rate-limit window resets." + .to_string(), + }); + } + }; + + let profile_name = { + let profiles = self.profiles.lock().await; + profiles[idx].name.clone() + }; + + debug!(profile = %profile_name, "Using profile for streaming"); + + let result = stream_with_config_dir( + &self.cli_path, + self.skip_permissions, + &config_dir, + request.clone(), + tx.clone(), + ) + .await; + + match &result { + Ok(_) => { + let mut profiles = self.profiles.lock().await; + profiles[idx].clear_cooldown(); + *self.current.lock().await = idx; + return result; + } + Err(e) if Self::is_rate_limit_error(e) => { + warn!(profile = %profile_name, error = %e, "Rate limit hit — rotating"); + self.handle_rate_limit(idx).await; + continue; + } + Err(_) => return result, + } + } + + Err(LlmError::Api { + status: 429, + message: "All Claude Code profiles exhausted after rotation attempts".to_string(), + }) + } +} + +// ── Helpers: spawn claude CLI with CLAUDE_CONFIG_DIR ────────────────────── + +/// Run a non-streaming completion using the Claude CLI with a specific config dir. +async fn complete_with_config_dir( + cli_path: &str, + skip_permissions: bool, + config_dir: &Path, + request: CompletionRequest, +) -> Result { + use super::claude_code::ClaudeCodeDriver; + use openfang_types::message::{ContentBlock, StopReason, TokenUsage}; + + let driver = ClaudeCodeDriver::new(Some(cli_path.to_string()), skip_permissions); + let image_files = ClaudeCodeDriver::extract_images_to_temp(&request).await; + let prompt = ClaudeCodeDriver::build_prompt(&request, &image_files); + let model_flag = ClaudeCodeDriver::model_flag(&request.model); + let args = driver.build_args(&prompt, model_flag.as_deref(), false); + + let mut cmd = tokio::process::Command::new(cli_path); + cmd.args(&args); + cmd.env("CLAUDE_CONFIG_DIR", config_dir); + ClaudeCodeDriver::apply_env_filter(&mut cmd); + cmd.stdout(std::process::Stdio::piped()); + cmd.stderr(std::process::Stdio::piped()); + + debug!(config_dir = %config_dir.display(), "Spawning Claude CLI with profile"); + + let output = cmd.output().await.map_err(|e| { + LlmError::Http(format!("Claude Code CLI failed to start: {e}")) + })?; + + ClaudeCodeDriver::cleanup_temp_images(&image_files); + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); + let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); + let detail = if !stderr.is_empty() { &stderr } else { &stdout }; + let code = output.status.code().unwrap_or(1); + + return Err(LlmError::Api { + status: code as u16, + message: format!("Claude Code CLI exited with code {code}: {detail}"), + }); + } + + let stdout = String::from_utf8_lossy(&output.stdout); + + // Try JSON parse + if let Ok(parsed) = serde_json::from_str::(&stdout) { + let text = parsed + .get("result") + .or_else(|| parsed.get("content")) + .or_else(|| parsed.get("text")) + .and_then(|v| v.as_str()) + .unwrap_or_default() + .to_string(); + + let (input_tokens, output_tokens) = parsed + .get("usage") + .map(|u| { + ( + u.get("input_tokens").and_then(|v| v.as_u64()).unwrap_or(0), + u.get("output_tokens").and_then(|v| v.as_u64()).unwrap_or(0), + ) + }) + .unwrap_or((0, 0)); + + return Ok(CompletionResponse { + content: vec![ContentBlock::Text { + text, + provider_metadata: None, + }], + stop_reason: StopReason::EndTurn, + tool_calls: Vec::new(), + usage: TokenUsage { + input_tokens, + output_tokens, + }, + }); + } + + // Fallback: plain text + Ok(CompletionResponse { + content: vec![ContentBlock::Text { + text: stdout.trim().to_string(), + provider_metadata: None, + }], + stop_reason: StopReason::EndTurn, + tool_calls: Vec::new(), + usage: TokenUsage { + input_tokens: 0, + output_tokens: 0, + }, + }) +} + +/// Run a streaming completion using the Claude CLI with a specific config dir. +async fn stream_with_config_dir( + cli_path: &str, + skip_permissions: bool, + config_dir: &Path, + request: CompletionRequest, + tx: tokio::sync::mpsc::Sender, +) -> Result { + use super::claude_code::ClaudeCodeDriver; + use openfang_types::message::{ContentBlock, StopReason, TokenUsage}; + use tokio::io::AsyncBufReadExt; + + let driver = ClaudeCodeDriver::new(Some(cli_path.to_string()), skip_permissions); + let image_files = ClaudeCodeDriver::extract_images_to_temp(&request).await; + let prompt = ClaudeCodeDriver::build_prompt(&request, &image_files); + let model_flag = ClaudeCodeDriver::model_flag(&request.model); + let args = driver.build_args(&prompt, model_flag.as_deref(), true); + + let mut cmd = tokio::process::Command::new(cli_path); + cmd.args(&args); + cmd.env("CLAUDE_CONFIG_DIR", config_dir); + ClaudeCodeDriver::apply_env_filter(&mut cmd); + cmd.stdout(std::process::Stdio::piped()); + cmd.stderr(std::process::Stdio::piped()); + + let mut child = cmd.spawn().map_err(|e| { + LlmError::Http(format!("Claude Code CLI failed to start: {e}")) + })?; + + let stdout = child + .stdout + .take() + .ok_or_else(|| LlmError::Http("No stdout from claude CLI".to_string()))?; + + let reader = tokio::io::BufReader::new(stdout); + let mut lines = reader.lines(); + + let mut full_text = String::new(); + let mut final_usage = TokenUsage { + input_tokens: 0, + output_tokens: 0, + }; + + while let Ok(Some(line)) = lines.next_line().await { + if line.trim().is_empty() { + continue; + } + + if let Ok(event) = serde_json::from_str::(&line) { + let event_type = event.get("type").and_then(|v| v.as_str()).unwrap_or(""); + + match event_type { + "content" | "text" | "assistant" | "content_block_delta" => { + if let Some(content) = event.get("content").and_then(|v| v.as_str()) { + full_text.push_str(content); + let _ = tx.send(StreamEvent::TextDelta { text: content.to_string() }).await; + } + } + "result" | "done" | "complete" => { + if let Some(result) = event.get("result").and_then(|v| v.as_str()) { + if full_text.is_empty() { + full_text = result.to_string(); + let _ = tx.send(StreamEvent::TextDelta { text: result.to_string() }).await; + } + } + if let Some(usage) = event.get("usage") { + final_usage = TokenUsage { + input_tokens: usage.get("input_tokens").and_then(|v| v.as_u64()).unwrap_or(0), + output_tokens: usage.get("output_tokens").and_then(|v| v.as_u64()).unwrap_or(0), + }; + } + } + _ => { + if let Some(content) = event.get("content").and_then(|v| v.as_str()) { + full_text.push_str(content); + let _ = tx.send(StreamEvent::TextDelta { text: content.to_string() }).await; + } + } + } + } else { + full_text.push_str(&line); + let _ = tx.send(StreamEvent::TextDelta { text: line }).await; + } + } + + let status = child.wait().await.map_err(|e| { + LlmError::Http(format!("Claude CLI wait failed: {e}")) + })?; + + ClaudeCodeDriver::cleanup_temp_images(&image_files); + + if !status.success() { + let code = status.code().unwrap_or(1); + // Check if the text we got so far indicates rate limiting + let lower = full_text.to_lowercase(); + if lower.contains("rate limit") || lower.contains("quota") || lower.contains("usage limit") { + return Err(LlmError::Api { + status: 429, + message: full_text, + }); + } + if full_text.is_empty() { + return Err(LlmError::Api { + status: code as u16, + message: format!("Claude Code CLI exited with code {code}"), + }); + } + } + + let _ = tx + .send(StreamEvent::ContentComplete { + stop_reason: StopReason::EndTurn, + usage: final_usage, + }) + .await; + + Ok(CompletionResponse { + content: vec![ContentBlock::Text { + text: full_text, + provider_metadata: None, + }], + stop_reason: StopReason::EndTurn, + tool_calls: Vec::new(), + usage: final_usage, + }) +} + +/// Expand a leading `~` to the user's home directory. +fn expand_tilde(path: &str) -> String { + if let Some(rest) = path.strip_prefix("~/") { + if let Ok(home) = std::env::var("HOME") { + return format!("{home}/{rest}"); + } + } else if path == "~" { + if let Ok(home) = std::env::var("HOME") { + return home; + } + } + path.to_string() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_profile_state_new() { + let p = ProfileState::new(PathBuf::from("/home/user/.claude")); + assert_eq!(p.name, ".claude"); + assert!(p.is_available()); + assert!(p.access_token.is_none()); + } + + #[test] + fn test_profile_cooldown() { + let mut p = ProfileState::new(PathBuf::from("/tmp/test-profile")); + assert!(p.is_available()); + + // Set cooldown 1 hour from now + let future = std::time::Instant::now() + std::time::Duration::from_secs(3600); + p.set_cooldown(future); + assert!(!p.is_available()); + + // Set cooldown in the past + let past = std::time::Instant::now() - std::time::Duration::from_secs(1); + p.cooldown_until = Some(past); + assert!(p.is_available()); + + // Clear cooldown + p.set_cooldown(future); + p.clear_cooldown(); + assert!(p.is_available()); + } + + #[test] + fn test_utc_to_instant_future() { + let future = chrono::Utc::now() + chrono::Duration::hours(3); + let instant = utc_to_instant(future); + // Should be roughly 3 hours + 30s buffer from now + let expected_secs = 3 * 3600 + 30; + let actual_secs = instant.duration_since(std::time::Instant::now()).as_secs(); + assert!(actual_secs >= expected_secs - 5 && actual_secs <= expected_secs + 5); + } + + #[test] + fn test_utc_to_instant_past() { + let past = chrono::Utc::now() - chrono::Duration::hours(1); + let instant = utc_to_instant(past); + // Should be approximately now + let diff = instant + .saturating_duration_since(std::time::Instant::now()) + .as_secs(); + assert!(diff <= 1); + } + + #[test] + fn test_is_rate_limit_error() { + assert!(MultiProfileDriver::is_rate_limit_error(&LlmError::Api { + status: 429, + message: "Too many requests".to_string(), + })); + + assert!(MultiProfileDriver::is_rate_limit_error(&LlmError::Api { + status: 1, + message: "You have exceeded your rate limit".to_string(), + })); + + assert!(MultiProfileDriver::is_rate_limit_error( + &LlmError::Http("Error 429: quota exceeded".to_string()) + )); + + assert!(!MultiProfileDriver::is_rate_limit_error(&LlmError::Api { + status: 500, + message: "Internal server error".to_string(), + })); + + assert!(!MultiProfileDriver::is_rate_limit_error( + &LlmError::Http("Connection refused".to_string()) + )); + } + + #[test] + fn test_multi_profile_driver_new() { + let driver = MultiProfileDriver::new( + None, + true, + vec![ + "~/.claude".to_string(), + "~/.claude-profiles/account-2".to_string(), + ], + ); + assert_eq!(driver.cli_path, "claude"); + assert!(driver.skip_permissions); + } + + #[test] + fn test_load_token_from_credentials() { + // Create a temp credentials file + let dir = std::env::temp_dir().join("openfang-test-profile"); + std::fs::create_dir_all(&dir).unwrap(); + let cred_path = dir.join(".credentials.json"); + std::fs::write( + &cred_path, + r#"{"claudeAiOauth":{"accessToken":"test-token-123","refreshToken":"rt","expiresAt":9999999999}}"#, + ) + .unwrap(); + + let mut profile = ProfileState::new(dir.clone()); + let token = profile.load_token(); + assert_eq!(token, Some("test-token-123")); + + // Cleanup + std::fs::remove_dir_all(&dir).unwrap(); + } + + #[tokio::test] + async fn test_next_available_all_in_cooldown() { + let driver = MultiProfileDriver::new( + None, + true, + vec!["/tmp/p1".to_string(), "/tmp/p2".to_string()], + ); + + // Put all in cooldown + { + let mut profiles = driver.profiles.lock().await; + let future = std::time::Instant::now() + std::time::Duration::from_secs(3600); + profiles[0].set_cooldown(future); + profiles[1].set_cooldown(future); + } + + assert!(driver.next_available().await.is_none()); + } + + #[tokio::test] + async fn test_next_available_skips_cooldown() { + let driver = MultiProfileDriver::new( + None, + true, + vec![ + "/tmp/pa".to_string(), + "/tmp/pb".to_string(), + "/tmp/pc".to_string(), + ], + ); + + // Put first profile in cooldown + { + let mut profiles = driver.profiles.lock().await; + let future = std::time::Instant::now() + std::time::Duration::from_secs(3600); + profiles[0].set_cooldown(future); + } + + let (idx, _) = driver.next_available().await.unwrap(); + assert_eq!(idx, 1); // Should skip index 0 + } + + // ── expand_tilde tests ─────────────────────────────────────────── + + #[test] + fn test_expand_tilde_home() { + let home = std::env::var("HOME").unwrap(); + assert_eq!(expand_tilde("~/.claude"), format!("{home}/.claude")); + assert_eq!( + expand_tilde("~/.claude-profiles/acct2"), + format!("{home}/.claude-profiles/acct2") + ); + } + + #[test] + fn test_expand_tilde_bare() { + let home = std::env::var("HOME").unwrap(); + assert_eq!(expand_tilde("~"), home); + } + + #[test] + fn test_expand_tilde_absolute_passthrough() { + assert_eq!(expand_tilde("/etc/claude"), "/etc/claude"); + assert_eq!(expand_tilde("/tmp/profile"), "/tmp/profile"); + } + + #[test] + fn test_expand_tilde_no_prefix() { + assert_eq!(expand_tilde("relative/path"), "relative/path"); + assert_eq!(expand_tilde(""), ""); + } + + // ── fetch_usage parsing tests ──────────────────────────────────── + + #[test] + fn test_parse_usage_response() { + let json = r#"{ + "five_hour": { + "utilization": 66.0, + "resets_at": "2026-03-14T05:00:00Z" + }, + "seven_day": { + "utilization": 14.0, + "resets_at": "2026-03-20T05:00:00Z" + } + }"#; + + let resp: UsageResponse = serde_json::from_str(json).unwrap(); + let five = resp.five_hour.unwrap(); + assert!((five.utilization - 66.0).abs() < 0.1); + assert_eq!(five.resets_at.as_deref(), Some("2026-03-14T05:00:00Z")); + + let seven = resp.seven_day.unwrap(); + assert!((seven.utilization - 14.0).abs() < 0.1); + } + + #[test] + fn test_parse_usage_response_empty_windows() { + let json = r#"{}"#; + let resp: UsageResponse = serde_json::from_str(json).unwrap(); + assert!(resp.five_hour.is_none()); + assert!(resp.seven_day.is_none()); + } + + #[test] + fn test_parse_usage_response_partial() { + let json = r#"{"five_hour": {"utilization": 99.5}}"#; + let resp: UsageResponse = serde_json::from_str(json).unwrap(); + let five = resp.five_hour.unwrap(); + assert!((five.utilization - 99.5).abs() < 0.1); + assert!(five.resets_at.is_none()); // no resets_at field + assert!(resp.seven_day.is_none()); + } + + // ── Profile rotation logic tests ───────────────────────────────── + + #[tokio::test] + async fn test_next_available_wraps_around() { + // If current=2 and profile 2 is in cooldown, should wrap to 0 + let driver = MultiProfileDriver::new( + None, + true, + vec![ + "/tmp/x0".to_string(), + "/tmp/x1".to_string(), + "/tmp/x2".to_string(), + ], + ); + + // Set current to 2, put profile 2 in cooldown + { + *driver.current.lock().await = 2; + let mut profiles = driver.profiles.lock().await; + let future = std::time::Instant::now() + std::time::Duration::from_secs(3600); + profiles[2].set_cooldown(future); + } + + let (idx, _) = driver.next_available().await.unwrap(); + assert_eq!(idx, 0); // Wrapped around to 0 + } + + #[tokio::test] + async fn test_next_available_prefers_current() { + // If current profile is available, it should be returned + let driver = MultiProfileDriver::new( + None, + true, + vec!["/tmp/y0".to_string(), "/tmp/y1".to_string()], + ); + + *driver.current.lock().await = 1; + + let (idx, _) = driver.next_available().await.unwrap(); + assert_eq!(idx, 1); // Should prefer current + } + + #[tokio::test] + async fn test_expired_cooldown_becomes_available() { + let driver = MultiProfileDriver::new( + None, + true, + vec!["/tmp/z0".to_string(), "/tmp/z1".to_string()], + ); + + // Put profile 0 in cooldown that has already expired + { + let mut profiles = driver.profiles.lock().await; + profiles[0].cooldown_until = + Some(std::time::Instant::now() - std::time::Duration::from_secs(1)); + } + + let (idx, _) = driver.next_available().await.unwrap(); + assert_eq!(idx, 0); // Expired cooldown = available + } + + // ── Credentials file tests ─────────────────────────────────────── + + #[test] + fn test_load_token_alternate_filename() { + // Test credentials.json (without leading dot) + let dir = std::env::temp_dir().join("openfang-test-profile-alt"); + std::fs::create_dir_all(&dir).unwrap(); + let cred_path = dir.join("credentials.json"); + std::fs::write( + &cred_path, + r#"{"claudeAiOauth":{"accessToken":"alt-token-456"}}"#, + ) + .unwrap(); + + let mut profile = ProfileState::new(dir.clone()); + let token = profile.load_token(); + assert_eq!(token, Some("alt-token-456")); + + std::fs::remove_dir_all(&dir).unwrap(); + } + + #[test] + fn test_load_token_missing_dir() { + let mut profile = ProfileState::new(PathBuf::from("/nonexistent/path")); + assert!(profile.load_token().is_none()); + } + + #[test] + fn test_load_token_malformed_json() { + let dir = std::env::temp_dir().join("openfang-test-profile-bad"); + std::fs::create_dir_all(&dir).unwrap(); + std::fs::write(dir.join(".credentials.json"), "not json").unwrap(); + + let mut profile = ProfileState::new(dir.clone()); + assert!(profile.load_token().is_none()); + + std::fs::remove_dir_all(&dir).unwrap(); + } + + #[test] + fn test_load_token_missing_oauth_field() { + let dir = std::env::temp_dir().join("openfang-test-profile-nooauth"); + std::fs::create_dir_all(&dir).unwrap(); + std::fs::write( + dir.join(".credentials.json"), + r#"{"someOtherField": "value"}"#, + ) + .unwrap(); + + let mut profile = ProfileState::new(dir.clone()); + assert!(profile.load_token().is_none()); + + std::fs::remove_dir_all(&dir).unwrap(); + } + + #[test] + fn test_load_token_cached_on_second_call() { + let dir = std::env::temp_dir().join("openfang-test-profile-cache"); + std::fs::create_dir_all(&dir).unwrap(); + std::fs::write( + dir.join(".credentials.json"), + r#"{"claudeAiOauth":{"accessToken":"cached-tok"}}"#, + ) + .unwrap(); + + let mut profile = ProfileState::new(dir.clone()); + assert_eq!(profile.load_token(), Some("cached-tok")); + + // Remove the file — second call should still return cached token + std::fs::remove_dir_all(&dir).unwrap(); + assert_eq!(profile.load_token(), Some("cached-tok")); + } + + // ── Rate-limit error detection edge cases ──────────────────────── + + #[test] + fn test_is_rate_limit_overloaded() { + assert!(MultiProfileDriver::is_rate_limit_error(&LlmError::Api { + status: 529, + message: "API is overloaded".to_string(), + })); + } + + #[test] + fn test_is_rate_limit_usage_limit_in_http() { + assert!(MultiProfileDriver::is_rate_limit_error( + &LlmError::Http("Your usage limit has been reached".to_string()) + )); + } + + #[test] + fn test_is_not_rate_limit_auth_error() { + assert!(!MultiProfileDriver::is_rate_limit_error(&LlmError::Api { + status: 401, + message: "Unauthorized".to_string(), + })); + } + + // ── DriverConfig profiles field ────────────────────────────────── + + #[test] + fn test_driver_config_profiles_default_empty() { + let json = r#"{"provider":"claude-code"}"#; + let config: crate::llm_driver::DriverConfig = serde_json::from_str(json).unwrap(); + assert!(config.profiles.is_empty()); + } + + #[test] + fn test_driver_config_profiles_deserialized() { + let json = r#"{ + "provider": "claude-code", + "profiles": ["~/.claude", "~/.claude-profiles/acct2"] + }"#; + let config: crate::llm_driver::DriverConfig = serde_json::from_str(json).unwrap(); + assert_eq!(config.profiles.len(), 2); + assert_eq!(config.profiles[0], "~/.claude"); + assert_eq!(config.profiles[1], "~/.claude-profiles/acct2"); + } +} diff --git a/crates/openfang-runtime/src/llm_driver.rs b/crates/openfang-runtime/src/llm_driver.rs index 9178be9d4..07b6aeb5d 100644 --- a/crates/openfang-runtime/src/llm_driver.rs +++ b/crates/openfang-runtime/src/llm_driver.rs @@ -176,6 +176,12 @@ pub struct DriverConfig { /// restricts what agents can do, making this safe. #[serde(default = "default_skip_permissions")] pub skip_permissions: bool, + /// Optional list of config directory paths for CLI-based providers + /// (claude-code, qwen-code). Each directory contains its own OAuth + /// credentials. When multiple profiles are provided the driver + /// automatically rotates to the next profile on rate-limit errors. + #[serde(default)] + pub profiles: Vec, } fn default_skip_permissions() -> bool { @@ -190,6 +196,7 @@ impl std::fmt::Debug for DriverConfig { .field("api_key", &self.api_key.as_ref().map(|_| "")) .field("base_url", &self.base_url) .field("skip_permissions", &self.skip_permissions) + .field("profiles", &self.profiles) .finish() } } From 6db5dd682ef40a11c2190b2bfa0e33907ac65e47 Mon Sep 17 00:00:00 2001 From: Federico Liva Date: Sat, 14 Mar 2026 13:01:19 +0100 Subject: [PATCH 40/42] feat(whatsapp): propagate sender identity metadata to agent context WhatsApp gateway sends sender metadata (phone number, display name) but the API was silently discarding it because MessageRequest had no metadata field. This caused agents to treat all WhatsApp users as their owner. Changes: - Add optional metadata field to MessageRequest (types.rs) - Parse metadata into SenderContext and propagate through kernel (routes.rs, kernel.rs) - Inject sender identity into agent system prompt (prompt_builder.rs) - Add SenderContext struct to message types (message.rs) - Activate is_allowed() filter with open-by-default behavior (whatsapp.rs) - Add allowed_users check in API routes (open mode when list is empty) Co-Authored-By: Claude Opus 4.6 --- crates/openfang-api/src/routes.rs | 37 +++++++++++++++++- crates/openfang-api/src/types.rs | 9 +++++ crates/openfang-channels/src/whatsapp.rs | 4 +- crates/openfang-kernel/src/kernel.rs | 38 +++++++++++++++++-- crates/openfang-runtime/src/prompt_builder.rs | 20 +++++++++- crates/openfang-types/src/message.rs | 14 +++++++ 6 files changed, 114 insertions(+), 8 deletions(-) diff --git a/crates/openfang-api/src/routes.rs b/crates/openfang-api/src/routes.rs index 7b7343070..1a64f9c8c 100644 --- a/crates/openfang-api/src/routes.rs +++ b/crates/openfang-api/src/routes.rs @@ -358,10 +358,45 @@ pub async fn send_message( } } + // Convert metadata from the gateway into a SenderContext for the kernel + let sender_context = req.metadata.as_ref().map(|meta| { + openfang_types::message::SenderContext { + channel: meta.get("channel").and_then(|v| v.as_str().map(String::from)), + sender_id: meta.get("sender").and_then(|v| v.as_str().map(String::from)), + sender_name: meta.get("sender_name").and_then(|v| v.as_str().map(String::from)), + } + }); + + // SECURITY: Check allowed_users for channel-based messages (WhatsApp, etc.) + // If allowed_users is empty, all senders are permitted (open mode). + if let Some(ref ctx) = sender_context { + if let Some(ref sender_id) = ctx.sender_id { + if let Some(ref channel) = ctx.channel { + let channels_config = state.channels_config.read().await; + let blocked = match channel.as_str() { + "whatsapp" => channels_config.whatsapp.as_ref().map_or(false, |wa| { + !wa.allowed_users.is_empty() + && !wa.allowed_users.iter().any(|u| u == sender_id) + }), + _ => false, + }; + if blocked { + tracing::warn!( + "Rejected message from unlisted {channel} user {sender_id}" + ); + return ( + StatusCode::FORBIDDEN, + Json(serde_json::json!({"error": "Sender not in allowed_users list"})), + ); + } + } + } + } + let kernel_handle: Arc = state.kernel.clone() as Arc; match state .kernel - .send_message_with_handle(agent_id, &req.message, Some(kernel_handle)) + .send_message_with_handle_and_blocks(agent_id, &req.message, Some(kernel_handle), None, sender_context) .await { Ok(result) => { diff --git a/crates/openfang-api/src/types.rs b/crates/openfang-api/src/types.rs index 80b71140a..b16e19199 100644 --- a/crates/openfang-api/src/types.rs +++ b/crates/openfang-api/src/types.rs @@ -42,6 +42,15 @@ pub struct MessageRequest { /// Optional file attachments (uploaded via /upload endpoint). #[serde(default)] pub attachments: Vec, + /// Optional channel metadata (sender identity, channel type). + /// + /// Used by external gateways (e.g. WhatsApp) to forward sender information + /// so the agent knows who is writing. Expected keys: + /// - `channel`: channel name (e.g. "whatsapp", "telegram") + /// - `sender`: platform-specific sender ID (e.g. phone number) + /// - `sender_name`: human-readable sender name + #[serde(default)] + pub metadata: Option>, } /// Response from sending a message. diff --git a/crates/openfang-channels/src/whatsapp.rs b/crates/openfang-channels/src/whatsapp.rs index 82ad5840d..b8508c3c3 100644 --- a/crates/openfang-channels/src/whatsapp.rs +++ b/crates/openfang-channels/src/whatsapp.rs @@ -162,8 +162,8 @@ impl WhatsAppAdapter { } /// Check if a phone number is allowed. - #[allow(dead_code)] - fn is_allowed(&self, phone: &str) -> bool { + /// Returns true if allowed_users is empty (open mode) or phone is in the list. + pub fn is_allowed(&self, phone: &str) -> bool { self.allowed_users.is_empty() || self.allowed_users.iter().any(|u| u == phone) } diff --git a/crates/openfang-kernel/src/kernel.rs b/crates/openfang-kernel/src/kernel.rs index 42f168f76..1587fef20 100644 --- a/crates/openfang-kernel/src/kernel.rs +++ b/crates/openfang-kernel/src/kernel.rs @@ -1396,6 +1396,27 @@ impl OpenFangKernel { .await } + /// Send a message with channel sender context (from external gateways like WhatsApp). + /// + /// The sender context is injected into the agent's system prompt so the agent + /// can identify who is writing and apply appropriate privacy rules. + pub async fn send_message_with_sender_context( + &self, + agent_id: AgentId, + message: &str, + sender_context: openfang_types::message::SenderContext, + kernel_handle: Option>, + ) -> KernelResult { + self.send_message_with_handle_and_blocks( + agent_id, + message, + kernel_handle, + None, + Some(sender_context), + ) + .await + } + /// Send a multimodal message (text + images) to an agent and get a response. /// /// Used by channel bridges when a user sends a photo — the image is downloaded, @@ -1411,7 +1432,7 @@ impl OpenFangKernel { .get() .and_then(|w| w.upgrade()) .map(|arc| arc as Arc); - self.send_message_with_handle_and_blocks(agent_id, message, handle, Some(blocks)) + self.send_message_with_handle_and_blocks(agent_id, message, handle, Some(blocks), None) .await } @@ -1422,7 +1443,7 @@ impl OpenFangKernel { message: &str, kernel_handle: Option>, ) -> KernelResult { - self.send_message_with_handle_and_blocks(agent_id, message, kernel_handle, None) + self.send_message_with_handle_and_blocks(agent_id, message, kernel_handle, None, None) .await } @@ -1432,6 +1453,9 @@ impl OpenFangKernel { /// multimodal content (text + images) instead of just a text string. This /// enables vision models to process images sent from channels like Telegram. /// + /// When `sender_context` is `Some`, the sender identity is injected into the + /// agent's system prompt so it can distinguish between its owner and other users. + /// /// Per-agent locking ensures that concurrent messages for the same agent /// are serialized (preventing session corruption), while messages for /// different agents run in parallel. @@ -1441,6 +1465,7 @@ impl OpenFangKernel { message: &str, kernel_handle: Option>, content_blocks: Option>, + sender_context: Option, ) -> KernelResult { // Acquire per-agent lock to serialize concurrent messages for the same agent. // This prevents session corruption when multiple messages arrive in quick @@ -1470,7 +1495,7 @@ impl OpenFangKernel { self.execute_python_agent(&entry, agent_id, message).await } else { // Default: LLM agent loop (builtin:chat or any unrecognized module) - self.execute_llm_agent(&entry, agent_id, message, kernel_handle, content_blocks) + self.execute_llm_agent(&entry, agent_id, message, kernel_handle, content_blocks, sender_context) .await }; @@ -1713,6 +1738,8 @@ impl OpenFangKernel { .and_then(|(s, _)| s), user_name, channel_type: None, + sender_id: None, + sender_name: None, is_subagent: manifest .metadata .get("is_subagent") @@ -2079,6 +2106,7 @@ impl OpenFangKernel { message: &str, kernel_handle: Option>, content_blocks: Option>, + sender_context: Option, ) -> KernelResult { // Check metering quota before starting self.metering @@ -2183,7 +2211,9 @@ impl OpenFangKernel { .ok() .and_then(|(s, _)| s), user_name, - channel_type: None, + channel_type: sender_context.as_ref().and_then(|sc| sc.channel.clone()), + sender_id: sender_context.as_ref().and_then(|sc| sc.sender_id.clone()), + sender_name: sender_context.as_ref().and_then(|sc| sc.sender_name.clone()), is_subagent: manifest .metadata .get("is_subagent") diff --git a/crates/openfang-runtime/src/prompt_builder.rs b/crates/openfang-runtime/src/prompt_builder.rs index e0a8bd2a6..91b8a7b3b 100644 --- a/crates/openfang-runtime/src/prompt_builder.rs +++ b/crates/openfang-runtime/src/prompt_builder.rs @@ -37,6 +37,10 @@ pub struct PromptContext { pub user_name: Option, /// Channel type (telegram, discord, web, etc.). pub channel_type: Option, + /// Platform-specific sender ID (e.g. phone number) — from channel gateway metadata. + pub sender_id: Option, + /// Human-readable sender display name — from channel gateway metadata. + pub sender_name: Option, /// Whether this agent was spawned as a subagent. pub is_subagent: bool, /// Whether this agent has autonomous config. @@ -144,7 +148,21 @@ pub fn build_system_prompt(ctx: &PromptContext) -> String { // Section 9 — Channel Awareness (skip for subagents) if !ctx.is_subagent { if let Some(ref channel) = ctx.channel_type { - sections.push(build_channel_section(channel)); + let mut section = build_channel_section(channel); + // Append sender identity when available (from channel gateway metadata) + if ctx.sender_id.is_some() || ctx.sender_name.is_some() { + section.push_str("\n\n### Current Message Sender\n"); + if let Some(ref name) = ctx.sender_name { + section.push_str(&format!("- **Name**: {name}\n")); + } + if let Some(ref id) = ctx.sender_id { + section.push_str(&format!("- **Platform ID**: {id}\n")); + } + section.push_str("\nIMPORTANT: Check this sender identity against your USER.md to determine \ + if this is your owner/master or someone else. If it is NOT your owner, \ + read and follow PRIVACY-RULES.md before responding."); + } + sections.push(section); } } diff --git a/crates/openfang-types/src/message.rs b/crates/openfang-types/src/message.rs index 72a608e88..ac8fcd9bd 100644 --- a/crates/openfang-types/src/message.rs +++ b/crates/openfang-types/src/message.rs @@ -2,6 +2,20 @@ use serde::{Deserialize, Serialize}; +/// Sender context forwarded from channel gateways (e.g. WhatsApp, Telegram). +/// +/// Carries the identity of the person who sent the message so the agent +/// can distinguish between its owner and other users. +#[derive(Debug, Clone, Default)] +pub struct SenderContext { + /// Channel name (e.g. "whatsapp", "telegram"). + pub channel: Option, + /// Platform-specific sender ID (e.g. phone number, Telegram user ID). + pub sender_id: Option, + /// Human-readable sender display name. + pub sender_name: Option, +} + /// A message in an LLM conversation. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Message { From 7282cee4d5106edde02c47340e247ba6f3b02dd2 Mon Sep 17 00:00:00 2001 From: Federico Liva Date: Sat, 14 Mar 2026 20:32:27 +0100 Subject: [PATCH 41/42] fix(whatsapp-gateway): reply to group messages in group, not as private DM When a message arrives from a WhatsApp group (@g.us), the gateway now replies in the same group instead of sending a private DM to the sender. Changes: - Detect group messages via @g.us JID suffix - Extract real sender from msg.key.participant for groups - Reply to remoteJid (group) instead of always to sender JID - Add group metadata (group_jid, group_name, is_group) to forwarded context - Resolve agent name to UUID dynamically via /api/agents endpoint - sendMessage now accepts full JIDs (including @g.us groups) - Default agent changed from 'assistant' to 'ambrogio' Co-Authored-By: Claude Opus 4.6 --- packages/whatsapp-gateway/index.js | 93 +++++++++++++++++++++++++----- 1 file changed, 78 insertions(+), 15 deletions(-) diff --git a/packages/whatsapp-gateway/index.js b/packages/whatsapp-gateway/index.js index 18e845735..d3f426ae6 100644 --- a/packages/whatsapp-gateway/index.js +++ b/packages/whatsapp-gateway/index.js @@ -17,7 +17,8 @@ const __dirname = path.dirname(__filename); // --------------------------------------------------------------------------- const PORT = parseInt(process.env.WHATSAPP_GATEWAY_PORT || '3009', 10); const OPENFANG_URL = (process.env.OPENFANG_URL || 'http://127.0.0.1:4200').replace(/\/+$/, ''); -const DEFAULT_AGENT = process.env.OPENFANG_DEFAULT_AGENT || 'assistant'; +const DEFAULT_AGENT_NAME = process.env.OPENFANG_DEFAULT_AGENT || 'ambrogio'; +let resolvedAgentId = null; // will be resolved on first use // --------------------------------------------------------------------------- // State @@ -124,7 +125,9 @@ async function startConnection() { if (msg.key.fromMe) continue; if (msg.key.remoteJid === 'status@broadcast') continue; - const sender = msg.key.remoteJid || ''; + const remoteJid = msg.key.remoteJid || ''; + const isGroup = remoteJid.endsWith('@g.us'); + const text = msg.message?.conversation || msg.message?.extendedTextMessage?.text || msg.message?.imageMessage?.caption @@ -132,19 +135,35 @@ async function startConnection() { if (!text) continue; - // Extract phone number from JID (e.g. "1234567890@s.whatsapp.net" → "+1234567890") - const phone = '+' + sender.replace(/@.*$/, ''); + // For groups: real sender is in participant; for DMs: it's remoteJid + const senderJid = isGroup ? (msg.key.participant || '') : remoteJid; + const phone = '+' + senderJid.replace(/@.*$/, ''); const pushName = msg.pushName || phone; - console.log(`[gateway] Incoming from ${pushName} (${phone}): ${text.substring(0, 80)}`); + // Build metadata with group context + const metadata = { + channel: 'whatsapp', + sender: phone, + sender_name: pushName, + }; + + if (isGroup) { + metadata.group_jid = remoteJid; + metadata.group_name = msg.key.remoteJid; // basic group ID + metadata.is_group = true; + console.log(`[gateway] Group msg from ${pushName} (${phone}) in ${remoteJid}: ${text.substring(0, 80)}`); + } else { + console.log(`[gateway] Incoming from ${pushName} (${phone}): ${text.substring(0, 80)}`); + } // Forward to OpenFang agent try { - const response = await forwardToOpenFang(text, phone, pushName); + const response = await forwardToOpenFang(text, phone, pushName, metadata); if (response && sock) { - // Send agent response back to WhatsApp - await sock.sendMessage(sender, { text: response }); - console.log(`[gateway] Replied to ${pushName}`); + // Reply in the same context: group → group, DM → DM + const replyJid = isGroup ? remoteJid : senderJid.replace(/@.*$/, '') + '@s.whatsapp.net'; + await sock.sendMessage(replyJid, { text: response }); + console.log(`[gateway] Replied to ${pushName}${isGroup ? ' in group ' + remoteJid : ' privately'}`); } } catch (err) { console.error(`[gateway] Forward/reply failed:`, err.message); @@ -153,21 +172,60 @@ async function startConnection() { }); } +// --------------------------------------------------------------------------- +// Resolve agent name to UUID via OpenFang API +// --------------------------------------------------------------------------- +async function resolveAgentId(agentName) { + if (resolvedAgentId) return resolvedAgentId; + + return new Promise((resolve, reject) => { + const url = new URL(`${OPENFANG_URL}/api/agents`); + const req = http.request( + { hostname: url.hostname, port: url.port || 4200, path: url.pathname, method: 'GET', timeout: 10_000 }, + (res) => { + let body = ''; + res.on('data', (chunk) => (body += chunk)); + res.on('end', () => { + try { + const agents = JSON.parse(body); + const list = Array.isArray(agents) ? agents : agents.agents || []; + const match = list.find((a) => a.name === agentName || a.id === agentName); + if (match) { + resolvedAgentId = match.id; + console.log(`[gateway] Resolved agent "${agentName}" → ${match.id}`); + resolve(match.id); + } else { + reject(new Error(`Agent "${agentName}" not found`)); + } + } catch (e) { + reject(new Error('Failed to parse agents list')); + } + }); + }, + ); + req.on('error', reject); + req.on('timeout', () => { req.destroy(); reject(new Error('Agent resolve timeout')); }); + req.end(); + }); +} + // --------------------------------------------------------------------------- // Forward incoming message to OpenFang API, return agent response // --------------------------------------------------------------------------- -function forwardToOpenFang(text, phone, pushName) { +async function forwardToOpenFang(text, phone, pushName, metadata) { + const agentId = await resolveAgentId(DEFAULT_AGENT_NAME); + return new Promise((resolve, reject) => { const payload = JSON.stringify({ message: text, - metadata: { + metadata: metadata || { channel: 'whatsapp', sender: phone, sender_name: pushName, }, }); - const url = new URL(`${OPENFANG_URL}/api/agents/${encodeURIComponent(DEFAULT_AGENT)}/message`); + const url = new URL(`${OPENFANG_URL}/api/agents/${encodeURIComponent(agentId)}/message`); const req = http.request( { @@ -214,8 +272,13 @@ async function sendMessage(to, text) { throw new Error('WhatsApp not connected'); } - // Normalize phone → JID: "+1234567890" → "1234567890@s.whatsapp.net" - const jid = to.replace(/^\+/, '').replace(/@.*$/, '') + '@s.whatsapp.net'; + // If already a full JID (group or user), use as-is; otherwise normalize phone → JID + let jid; + if (to.includes('@')) { + jid = to; + } else { + jid = to.replace(/^\+/, '') + '@s.whatsapp.net'; + } await sock.sendMessage(jid, { text }); } @@ -335,7 +398,7 @@ const server = http.createServer(async (req, res) => { server.listen(PORT, '127.0.0.1', () => { console.log(`[gateway] WhatsApp Web gateway listening on http://127.0.0.1:${PORT}`); console.log(`[gateway] OpenFang URL: ${OPENFANG_URL}`); - console.log(`[gateway] Default agent: ${DEFAULT_AGENT}`); + console.log(`[gateway] Default agent: ${DEFAULT_AGENT_NAME}`); // Auto-connect if credentials already exist from a previous session const credsPath = path.join(__dirname, 'auth_store', 'creds.json'); From 88dd375a38390e8bc128e9d50c2b6ea6a1b37d72 Mon Sep 17 00:00:00 2001 From: Federico Liva Date: Sat, 14 Mar 2026 21:18:42 +0100 Subject: [PATCH 42/42] feat(whatsapp-gateway): add media message support (images, video, audio, documents) Previously the gateway silently dropped all media-only messages (images without captions, voice notes, documents, stickers). Now it downloads media via Baileys' downloadMediaMessage(), uploads to OpenFang's /upload endpoint, and forwards the file_id in the attachments array so the LLM can see the content. Co-Authored-By: Claude Opus 4.6 --- packages/whatsapp-gateway/index.js | 103 ++++++++++++++++++++++++++--- 1 file changed, 94 insertions(+), 9 deletions(-) diff --git a/packages/whatsapp-gateway/index.js b/packages/whatsapp-gateway/index.js index d3f426ae6..2d9e87e27 100644 --- a/packages/whatsapp-gateway/index.js +++ b/packages/whatsapp-gateway/index.js @@ -5,7 +5,7 @@ import { randomUUID } from 'node:crypto'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import makeWASocket, { useMultiFileAuthState, DisconnectReason, fetchLatestBaileysVersion } from '@whiskeysockets/baileys'; +import makeWASocket, { useMultiFileAuthState, DisconnectReason, fetchLatestBaileysVersion, downloadMediaMessage } from '@whiskeysockets/baileys'; import QRCode from 'qrcode'; import pino from 'pino'; @@ -128,12 +128,26 @@ async function startConnection() { const remoteJid = msg.key.remoteJid || ''; const isGroup = remoteJid.endsWith('@g.us'); + // Detect media messages + const mediaInfo = msg.message?.imageMessage + ? { type: 'image', mime: msg.message.imageMessage.mimetype || 'image/jpeg', caption: msg.message.imageMessage.caption || '' } + : msg.message?.videoMessage + ? { type: 'video', mime: msg.message.videoMessage.mimetype || 'video/mp4', caption: msg.message.videoMessage.caption || '' } + : msg.message?.audioMessage + ? { type: 'audio', mime: msg.message.audioMessage.mimetype || 'audio/ogg', caption: '' } + : msg.message?.documentMessage + ? { type: 'document', mime: msg.message.documentMessage.mimetype || 'application/octet-stream', caption: msg.message.documentMessage.caption || '', filename: msg.message.documentMessage.fileName || 'document' } + : msg.message?.stickerMessage + ? { type: 'sticker', mime: msg.message.stickerMessage.mimetype || 'image/webp', caption: '' } + : null; + const text = msg.message?.conversation || msg.message?.extendedTextMessage?.text - || msg.message?.imageMessage?.caption + || mediaInfo?.caption || ''; - if (!text) continue; + // Skip if no text AND no media + if (!text && !mediaInfo) continue; // For groups: real sender is in participant; for DMs: it's remoteJid const senderJid = isGroup ? (msg.key.participant || '') : remoteJid; @@ -151,14 +165,36 @@ async function startConnection() { metadata.group_jid = remoteJid; metadata.group_name = msg.key.remoteJid; // basic group ID metadata.is_group = true; - console.log(`[gateway] Group msg from ${pushName} (${phone}) in ${remoteJid}: ${text.substring(0, 80)}`); + } + + // Download and upload media if present + let attachments = []; + if (mediaInfo) { + try { + const buffer = await downloadMediaMessage(msg, 'buffer', {}); + const ext = mediaInfo.mime.split('/').pop()?.split(';')[0] || 'bin'; + const filename = mediaInfo.filename || `${mediaInfo.type}.${ext}`; + const agentId = await resolveAgentId(DEFAULT_AGENT_NAME); + const fileId = await uploadToOpenFang(agentId, buffer, mediaInfo.mime, filename); + attachments.push({ file_id: fileId, filename, content_type: mediaInfo.mime }); + console.log(`[gateway] Uploaded ${mediaInfo.type} (${(buffer.length / 1024).toFixed(1)}KB) → ${fileId}`); + } catch (err) { + console.error(`[gateway] Media download/upload failed:`, err.message); + // Still forward the text/caption if available + } + } + + const logText = text ? text.substring(0, 80) : `[${mediaInfo?.type || 'media'}]`; + if (isGroup) { + console.log(`[gateway] Group msg from ${pushName} (${phone}) in ${remoteJid}: ${logText}`); } else { - console.log(`[gateway] Incoming from ${pushName} (${phone}): ${text.substring(0, 80)}`); + console.log(`[gateway] Incoming from ${pushName} (${phone}): ${logText}`); } // Forward to OpenFang agent + const messageText = text || `[${mediaInfo?.type || 'media'} received]`; try { - const response = await forwardToOpenFang(text, phone, pushName, metadata); + const response = await forwardToOpenFang(messageText, phone, pushName, metadata, attachments); if (response && sock) { // Reply in the same context: group → group, DM → DM const replyJid = isGroup ? remoteJid : senderJid.replace(/@.*$/, '') + '@s.whatsapp.net'; @@ -209,21 +245,70 @@ async function resolveAgentId(agentName) { }); } +// --------------------------------------------------------------------------- +// Upload media to OpenFang API, return file_id +// --------------------------------------------------------------------------- +async function uploadToOpenFang(agentId, buffer, contentType, filename) { + return new Promise((resolve, reject) => { + const url = new URL(`${OPENFANG_URL}/api/agents/${encodeURIComponent(agentId)}/upload`); + + const req = http.request( + { + hostname: url.hostname, + port: url.port || 4200, + path: url.pathname, + method: 'POST', + headers: { + 'Content-Type': contentType, + 'Content-Length': buffer.length, + 'X-Filename': filename, + }, + timeout: 30_000, + }, + (res) => { + let body = ''; + res.on('data', (chunk) => (body += chunk)); + res.on('end', () => { + try { + const data = JSON.parse(body); + if (res.statusCode >= 400) { + reject(new Error(`Upload failed (${res.statusCode}): ${data.error || body}`)); + } else { + resolve(data.file_id || data.id || ''); + } + } catch { + reject(new Error(`Upload parse error: ${body}`)); + } + }); + }, + ); + + req.on('error', reject); + req.on('timeout', () => { req.destroy(); reject(new Error('Upload timeout')); }); + req.write(buffer); + req.end(); + }); +} + // --------------------------------------------------------------------------- // Forward incoming message to OpenFang API, return agent response // --------------------------------------------------------------------------- -async function forwardToOpenFang(text, phone, pushName, metadata) { +async function forwardToOpenFang(text, phone, pushName, metadata, attachments) { const agentId = await resolveAgentId(DEFAULT_AGENT_NAME); return new Promise((resolve, reject) => { - const payload = JSON.stringify({ + const body = { message: text, metadata: metadata || { channel: 'whatsapp', sender: phone, sender_name: pushName, }, - }); + }; + if (attachments && attachments.length > 0) { + body.attachments = attachments; + } + const payload = JSON.stringify(body); const url = new URL(`${OPENFANG_URL}/api/agents/${encodeURIComponent(agentId)}/message`);