Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
66db95f
ci: add sync-build workflow
Mar 5, 2026
51f731d
feat: custom Dockerfile for Lazycat NAS deployment
Mar 5, 2026
e075197
ci: use main branch instead of custom
Mar 5, 2026
4c47d9a
feat: add gogcli to image
Mar 5, 2026
13d4baf
chore: sync to upstream v0.3.22
actions-user Mar 5, 2026
a8fc98e
feat: install brew + gogcli as non-root, add PATH for npm-global
Mar 5, 2026
6d86f1f
chore: sync to upstream v0.3.23
actions-user Mar 5, 2026
e393cc4
chore: sync to upstream v0.3.24
actions-user Mar 6, 2026
eec2adb
docs: add Docker Hub README with auto-sync via GitHub Actions
Mar 6, 2026
0350d46
feat: add jq to image
Mar 6, 2026
f2622af
docs: fix gogcli description — Google Workspace CLI, not GOG.com
Mar 6, 2026
74e8df2
fix: logo visibility in light theme
Mar 6, 2026
f0ebfe3
fix: invert logo in message avatar for light theme
Mar 6, 2026
e64eed3
fix: detect Claude Code CLI install and auth status in provider card
Mar 6, 2026
c72e09e
fix: use PAT_TOKEN for workflow push permission
Mar 7, 2026
50d7154
feat: add Playwright + Chromium to Docker image
Mar 7, 2026
15e64be
fix: use force push in sync workflow to avoid stale info rejection
Mar 7, 2026
9fe7ffe
chore: sync to upstream v0.3.26
actions-user Mar 7, 2026
54f7205
fix: add python -> python3 symlink for Browser Hand detection
Mar 7, 2026
72b0f8e
fix: default hourly cost quota should be unlimited (0.0), not $1.00
Mar 7, 2026
11fd79a
ci: trigger Docker rebuild on every push to main
Mar 7, 2026
d5c1565
chore: sync to upstream v0.3.27
actions-user Mar 7, 2026
b4ae2c9
chore: sync to upstream v0.3.34
Mar 9, 2026
0119000
fix: auto-skip conflicting commits during upstream rebase
Mar 9, 2026
bdf9b05
fix: replace Playwright with chromium package in Docker image
Mar 9, 2026
a4e82f2
fix: re-add --dangerously-skip-permissions to Claude Code driver
Mar 11, 2026
12565fc
chore: sync to upstream v0.3.47
actions-user Mar 12, 2026
33c9ffa
fix(whatsapp-gateway): reconnect on all non-logout disconnections wit…
f-liva Mar 12, 2026
7310f53
Merge remote-tracking branch 'fork/fix/whatsapp-gateway-reconnect'
Mar 12, 2026
1b2cdea
feat: add Qwen Code CLI as LLM provider + refactor build_args for tes…
Mar 12, 2026
254170c
feat: add Qwen Code CLI to Docker image
Mar 12, 2026
3abf3f5
fix: correct Qwen Code credentials path from ~/.qwen-code to ~/.qwen
Mar 12, 2026
7592b83
fix(images): fix Telegram image pipeline - dual bug in vision model r…
f-liva Mar 12, 2026
909e42d
Merge remote-tracking branch 'fork/fix/telegram-image-pipeline'
Mar 12, 2026
56180ec
feat: add Telegram notifications to CI workflow
Mar 12, 2026
a3d4998
fix: skip vision model swap when current model already supports vision
f-liva Mar 12, 2026
df0e920
fix: vision model swap respects config priority over current model
f-liva Mar 12, 2026
5e46d19
Merge branch 'fix/telegram-image-pipeline'
f-liva Mar 12, 2026
2f9f711
fix: use --file instead of --files for Claude Code CLI
f-liva Mar 12, 2026
4e28cbd
fix: use @path syntax for local images instead of --file flag
f-liva Mar 12, 2026
d6ba3cb
feat: add Telegram notification on build start
Mar 12, 2026
18fe89e
feat: add lifecycle_reactions config flag to disable emoji reactions
f-liva Mar 12, 2026
0573e12
feat(telegram): add reply_to_message context for quoted messages
f-liva Mar 12, 2026
30aa7f5
feat(whatsapp): propagate sender identity metadata to agent context
f-liva Mar 14, 2026
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.47
104 changes: 104 additions & 0 deletions .github/workflows/sync-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
name: Sync upstream & build custom image

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:
runs-on: ubuntu-latest
steps:
- name: Checkout fork
uses: actions/checkout@v4
with:
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: |
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")
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: github.event_name != 'push' && steps.check.outputs.new_release == 'true'
run: |
git config user.name "github-actions"
git config user.email "actions@github.com"
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
git push --force

- name: Set up Docker Buildx
if: github.event_name == 'push' || steps.check.outputs.new_release == 'true'
uses: docker/setup-buildx-action@v3

- name: Login to Docker Hub
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: 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 || 'custom' }}
cache-from: type=gha
cache-to: type=gha,mode=max

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

- 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 }})"
28 changes: 14 additions & 14 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)
29 changes: 27 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,36 @@ 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 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"]
38 changes: 37 additions & 1 deletion crates/openfang-api/src/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<dyn KernelHandle> = state.kernel.clone() as Arc<dyn KernelHandle>;
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) => {
Expand Down Expand Up @@ -6852,6 +6887,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
Expand Down
9 changes: 9 additions & 0 deletions crates/openfang-api/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,15 @@ pub struct MessageRequest {
/// Optional file attachments (uploaded via /upload endpoint).
#[serde(default)]
pub attachments: Vec<AttachmentRef>,
/// 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<std::collections::HashMap<String, serde_json::Value>>,
}

/// Response from sending a message.
Expand Down
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.
Loading