diff --git a/.current-upstream-version b/.current-upstream-version new file mode 100644 index 000000000..c40f247ce --- /dev/null +++ b/.current-upstream-version @@ -0,0 +1 @@ +v0.3.26 diff --git a/.github/workflows/sync-build.yml b/.github/workflows/sync-build.yml new file mode 100644 index 000000000..7b83ff94b --- /dev/null +++ b/.github/workflows/sync-build.yml @@ -0,0 +1,77 @@ +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: + fetch-depth: 0 + token: ${{ secrets.PAT_TOKEN }} + + - 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 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 + + - 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 + + - 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/Cargo.lock b/Cargo.lock index 0e58a8267..ead0a670b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2506,15 +2506,6 @@ dependencies = [ "windows-link 0.2.1", ] -[[package]] -name = "html-escape" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" -dependencies = [ - "utf8-width", -] - [[package]] name = "html5ever" version = "0.29.1" @@ -3875,7 +3866,7 @@ dependencies = [ [[package]] name = "openfang-api" -version = "0.3.25" +version = "0.3.24" dependencies = [ "async-trait", "axum", @@ -3912,7 +3903,7 @@ dependencies = [ [[package]] name = "openfang-channels" -version = "0.3.25" +version = "0.3.24" dependencies = [ "async-trait", "axum", @@ -3922,7 +3913,6 @@ dependencies = [ "futures", "hex", "hmac", - "html-escape", "imap", "lettre", "mailparse", @@ -3944,7 +3934,7 @@ dependencies = [ [[package]] name = "openfang-cli" -version = "0.3.25" +version = "0.3.24" dependencies = [ "clap", "clap_complete", @@ -3971,7 +3961,7 @@ dependencies = [ [[package]] name = "openfang-desktop" -version = "0.3.25" +version = "0.3.24" dependencies = [ "axum", "open", @@ -3997,7 +3987,7 @@ dependencies = [ [[package]] name = "openfang-extensions" -version = "0.3.25" +version = "0.3.24" dependencies = [ "aes-gcm", "argon2", @@ -4025,7 +4015,7 @@ dependencies = [ [[package]] name = "openfang-hands" -version = "0.3.25" +version = "0.3.24" dependencies = [ "chrono", "dashmap", @@ -4042,7 +4032,7 @@ dependencies = [ [[package]] name = "openfang-kernel" -version = "0.3.25" +version = "0.3.24" dependencies = [ "async-trait", "chrono", @@ -4078,7 +4068,7 @@ dependencies = [ [[package]] name = "openfang-memory" -version = "0.3.25" +version = "0.3.24" dependencies = [ "async-trait", "chrono", @@ -4097,7 +4087,7 @@ dependencies = [ [[package]] name = "openfang-migrate" -version = "0.3.25" +version = "0.3.24" dependencies = [ "chrono", "dirs 6.0.0", @@ -4116,7 +4106,7 @@ dependencies = [ [[package]] name = "openfang-runtime" -version = "0.3.25" +version = "0.3.24" dependencies = [ "anyhow", "async-trait", @@ -4148,7 +4138,7 @@ dependencies = [ [[package]] name = "openfang-skills" -version = "0.3.25" +version = "0.3.24" dependencies = [ "chrono", "hex", @@ -4171,7 +4161,7 @@ dependencies = [ [[package]] name = "openfang-types" -version = "0.3.25" +version = "0.3.24" dependencies = [ "async-trait", "chrono", @@ -4190,7 +4180,7 @@ dependencies = [ [[package]] name = "openfang-wire" -version = "0.3.25" +version = "0.3.24" dependencies = [ "async-trait", "chrono", @@ -7387,12 +7377,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" -[[package]] -name = "utf8-width" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -8818,7 +8802,7 @@ checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" [[package]] name = "xtask" -version = "0.3.25" +version = "0.3.24" [[package]] name = "yoke" diff --git a/DOCKER_README.md b/DOCKER_README.md new file mode 100644 index 000000000..d3e09a054 --- /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 +- **gog** — [Google Workspace CLI](https://gogcli.sh/) (Gmail, Calendar, Drive, Sheets, etc.) +- **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) diff --git a/Dockerfile b/Dockerfile index d794943ed..83cb0a6ad 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,11 +10,37 @@ 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 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 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 +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 +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/crates/openfang-api/static/css/layout.css b/crates/openfang-api/static/css/layout.css index 880e6ca3f..33919dac4 100644 --- a/crates/openfang-api/static/css/layout.css +++ b/crates/openfang-api/static/css/layout.css @@ -50,6 +50,11 @@ transition: opacity 0.2s, transform 0.2s; } +[data-theme="light"] .sidebar-logo img, +[data-theme="light"] .message-avatar 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 cfe72c0b5..7f900d403 100644 Binary files a/crates/openfang-api/static/logo.png and b/crates/openfang-api/static/logo.png differ diff --git a/crates/openfang-hands/bundled/browser/HAND.toml b/crates/openfang-hands/bundled/browser/HAND.toml index a08e0a1a9..72fef2833 100644 --- a/crates/openfang-hands/bundled/browser/HAND.toml +++ b/crates/openfang-hands/bundled/browser/HAND.toml @@ -17,7 +17,7 @@ tools = [ key = "python3" label = "Python 3 must be installed" requirement_type = "binary" -check_value = "python" +check_value = "python3" description = "Python 3 is required to run Playwright, the browser automation library that powers this hand." [requires.install] diff --git a/crates/openfang-runtime/src/model_catalog.rs b/crates/openfang-runtime/src/model_catalog.rs index 3dfacb6ac..1ef0501b5 100644 --- a/crates/openfang-runtime/src/model_catalog.rs +++ b/crates/openfang-runtime/src/model_catalog.rs @@ -56,6 +56,19 @@ 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: detect CLI installation + authentication + if provider.id == "claude-code" { + 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.key_required { provider.auth_status = AuthStatus::NotRequired; continue; @@ -71,7 +84,6 @@ impl ModelCatalog { std::env::var("OPENAI_API_KEY").is_ok() || read_codex_credential().is_some() } - "claude-code" => crate::drivers::claude_code::claude_code_available(), _ => false, }; 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 "$@"