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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .current-upstream-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v0.3.26
77 changes: 77 additions & 0 deletions .github/workflows/sync-build.yml
Original file line number Diff line number Diff line change
@@ -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
44 changes: 14 additions & 30 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

42 changes: 42 additions & 0 deletions DOCKER_README.md
Original file line number Diff line number Diff line change
@@ -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)
30 changes: 28 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
5 changes: 5 additions & 0 deletions crates/openfang-api/static/css/layout.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Binary file modified crates/openfang-api/static/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion crates/openfang-hands/bundled/browser/HAND.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
14 changes: 13 additions & 1 deletion crates/openfang-runtime/src/model_catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
};

Expand Down
5 changes: 5 additions & 0 deletions entrypoint.sh
Original file line number Diff line number Diff line change
@@ -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 "$@"