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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions .github/workflows/sync-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
name: Sync upstream & build custom image

on:
schedule:
- cron: '0 */6 * * *' # ogni 6 ore
workflow_dispatch: # trigger manuale
push:
branches: [custom] # rebuild ad ogni push su custom

jobs:
sync-and-build:
runs-on: ubuntu-latest
steps:
- name: Checkout fork (custom branch)
uses: actions/checkout@v4
with:
ref: custom
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 }}\`%0ABranch: \`custom\`%0ACommit: \`${GITHUB_SHA::7}\`%0A[View run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})"

- name: Configure git
run: |
git config user.name "github-actions"
git config user.email "actions@github.com"

- name: Sync main with upstream
if: github.event_name != 'push'
run: |
git remote add upstream https://github.com/RightNow-AI/openfang.git || true
git fetch upstream --tags
git fetch origin main

# Fast-forward main to latest upstream tag
LATEST_TAG=$(git tag -l 'v*' --sort=-v:refname | head -1)
CURRENT=$(cat .current-upstream-version 2>/dev/null || echo "none")
echo "LATEST_TAG=$LATEST_TAG" >> "$GITHUB_ENV"
echo "CURRENT=$CURRENT" >> "$GITHUB_ENV"

if [ "$LATEST_TAG" != "$CURRENT" ]; then
echo "NEW_RELEASE=true" >> "$GITHUB_ENV"
# Reset main to latest tag, cherry-pick our generic fixes
git checkout -B main "$LATEST_TAG"
# Cherry-pick any commits on main that are ours (not upstream)
for commit in $(git log origin/main --oneline --format="%H" "$LATEST_TAG"..origin/main 2>/dev/null); do
git cherry-pick "$commit" --no-commit && git commit -C "$commit" || git cherry-pick --abort || true
done
git push origin main --force
# Now rebase custom on updated main
git checkout custom
git rebase main || git rebase --abort
echo "$LATEST_TAG" > .current-upstream-version
git add .current-upstream-version
git commit -m "chore: sync to upstream $LATEST_TAG" || true
git push origin custom --force
else
echo "NEW_RELEASE=false" >> "$GITHUB_ENV"
fi

- name: Set up Docker Buildx
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' || env.NEW_RELEASE == 'true'
uses: docker/setup-buildx-action@v3

- name: Login to Docker Hub
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' || env.NEW_RELEASE == 'true'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Build and push
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' || env.NEW_RELEASE == 'true'
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: |
fliva/openfang:latest
fliva/openfang:${{ env.LATEST_TAG || 'custom' }}
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Update Docker Hub description
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' || env.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

- name: Notify Telegram (success)
if: success() && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || env.NEW_RELEASE == 'true')
run: |
COMMIT_MSG=$(git log -1 --format=%s)
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: \`${{ env.LATEST_TAG || 'custom' }}\`%0A\`${COMMIT_MSG}\`"

- name: Notify Telegram (failure)
if: failure()
run: |
COMMIT_MSG=$(git log -1 --format=%s)
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%0A\`${COMMIT_MSG}\`"
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)
39 changes: 28 additions & 11 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,37 @@ ENV CARGO_PROFILE_RELEASE_LTO=${LTO} \
CARGO_PROFILE_RELEASE_CODEGEN_UNITS=${CODEGEN_UNITS}
RUN cargo build --release --bin openfang

FROM rust:1-slim-bookworm
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
python3 \
python3-pip \
python3-venv \
nodejs \
npm \
&& rm -rf /var/lib/apt/lists/*

FROM debian:bookworm-slim
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 && \
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 @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
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"]
64 changes: 63 additions & 1 deletion crates/openfang-api/src/channel_bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,30 @@ impl ChannelBridgeHandle for KernelBridgeAdapter {
Ok(result.response)
}

async fn send_message_with_context(
&self,
agent_id: AgentId,
message: &str,
ctx: openfang_channels::bridge::ChannelContext,
) -> Result<String, String> {
let result = self
.kernel
.send_message_with_handle(
agent_id,
message,
None,
ctx.sender_id,
ctx.sender_name,
ctx.channel_type,
)
.await
.map_err(|e| format!("{e}"))?;
if result.silent {
return Ok(String::new());
}
Ok(result.response)
}

async fn send_message_with_blocks(
&self,
agent_id: AgentId,
Expand All @@ -102,9 +126,47 @@ impl ChannelBridgeHandle for KernelBridgeAdapter {
};
let result = self
.kernel
.send_message_with_blocks(agent_id, &text, blocks)
.send_message_with_blocks(agent_id, &text, blocks, None)
.await
.map_err(|e| format!("{e}"))?;
Ok(result.response)
}

async fn send_message_with_blocks_and_context(
&self,
agent_id: AgentId,
blocks: Vec<openfang_types::message::ContentBlock>,
ctx: openfang_channels::bridge::ChannelContext,
) -> Result<String, String> {
let text: String = blocks
.iter()
.filter_map(|b| match b {
openfang_types::message::ContentBlock::Text { text, .. } => Some(text.as_str()),
_ => None,
})
.collect::<Vec<_>>()
.join("\n");
let text = if text.is_empty() {
"[Image]".to_string()
} else {
text
};
let result = self
.kernel
.send_message_with_handle_and_blocks(
agent_id,
&text,
None,
Some(blocks),
ctx.sender_id,
ctx.sender_name,
ctx.channel_type,
)
.await
.map_err(|e| format!("{e}"))?;
if result.silent {
return Ok(String::new());
}
Ok(result.response)
}

Expand Down
4 changes: 2 additions & 2 deletions crates/openfang-api/src/openai_compat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ pub async fn chat_completions(
let kernel_handle: Arc<dyn KernelHandle> = state.kernel.clone() as Arc<dyn KernelHandle>;
match state
.kernel
.send_message_with_handle(agent_id, &last_user_msg, Some(kernel_handle), None, None)
.send_message_with_handle(agent_id, &last_user_msg, Some(kernel_handle), None, None, None)
.await
{
Ok(result) => {
Expand Down Expand Up @@ -379,7 +379,7 @@ async fn stream_response(

let (mut rx, _handle) = state
.kernel
.send_message_streaming(agent_id, message, Some(kernel_handle), None, None)
.send_message_streaming(agent_id, message, Some(kernel_handle), None, None, None, None)
.map_err(|e| format!("Streaming setup failed: {e}"))?;

let (tx, stream_rx) = tokio::sync::mpsc::channel::<Result<SseEvent, Infallible>>(64);
Expand Down
Loading