Compare commits

..

16 Commits

Author SHA1 Message Date
c39174f0fe feat: add 7zz for CHM documentation extraction
Some checks failed
Build Hermes agent / build (pull_request) Has been cancelled
Download static 7-Zip binary at Docker build time for extracting Microsoft Compiled HTML Help (.chm) files. Follows the same pattern as the existing Himalaya CLI installation. 7zz is scraped from 7-zip.org/download.html at build time.
2026-05-13 16:27:32 -04:00
29ae32a1c5 Merge pull request 'fix: use ln -sf instead of update-alternatives --set for iptables-nft' (#28) from fix/vpn-iptables-nft-v3 into master
Reviewed-on: #28
2026-05-13 16:59:50 +00:00
8dff094768 fix: use ln -sf instead of update-alternatives --set
update-alternatives --set fails because the base image only registers
iptables-legacy as an alternative. The iptables-nft binary (/usr/sbin/iptables-nft)
exists but isn't in the alternatives database. Direct ln -sf bypasses this.
2026-05-13 12:58:43 -04:00
ec08f5eb5d Merge pull request 'fix: remove apk add iptables-nft — built-in on Alpine 3.18+' (#27) from fix/vpn-iptables-nft-v2 into master
Reviewed-on: #27
2026-05-13 16:49:23 +00:00
611e96b306 fix: remove apk add iptables-nft — built-in on Alpine 3.18+
In Alpine 3.18+, the 'iptables' package IS the nftables variant.
iptables-nft is not a separate package. The binary is already in
the base image — only need to flip update-alternatives.
2026-05-13 12:48:51 -04:00
f184ed957c Merge pull request 'fix: update wg-easy to official ghcr image with iptables-nft' (#26) from fix/vpn-iptables-nft-upstream into master
Reviewed-on: #26
2026-05-13 16:37:35 +00:00
2bf31c7ccc fix: update wg-easy to official ghcr image with iptables-nft
- Switch FROM weejewel/wg-easy:latest (4yr old, Alpine 3.11) to
  ghcr.io/wg-easy/wg-easy:latest (actively maintained, Alpine krypton)
- Use update-alternatives instead of raw ln -sf to flip iptables
  from legacy to nftables backend
- Fix compose build context: ./vpn -> . (Dockerfile was at same level)

The weejewel/wg-easy image lacked iptables-nft package in Alpine 3.11.
The new official image has it available, we just flip the alternatives.
The old ln -sf approach was fragile across Alpine versions.
2026-05-13 12:30:15 -04:00
f44f93e35a Merge pull request 'fix: add Himalaya email CLI to Hermes Docker image' (#25) from fix/himalaya-email-cli into master
Some checks failed
Build Hermes agent / build (push) Has been cancelled
Reviewed-on: #25
2026-05-13 15:03:40 +00:00
4cdd157e3f Merge pull request 'fix: add iptables-nft to wg-easy for nftables-only kernels' (#24) from fix/wg-easy-iptables-nft into master
Reviewed-on: #24
2026-05-13 15:03:25 +00:00
3ba0345887 Merge pull request 'feat: install custom Hermes tools at startup, remove deprecated fix-permissions.sh' (#23) from feat/hermes-custom-tools-startup into master
Some checks failed
Build Hermes agent / build (push) Failing after 2s
Build ollama (gfx906) / build (push) Failing after 2s
Reviewed-on: #23
2026-05-13 13:52:36 +00:00
27571ddb3f feat: add Himalaya email CLI to Hermes Docker image
Some checks failed
Build Hermes agent / build (pull_request) Failing after 2s
2026-05-12 18:09:40 -04:00
5e242eb946 fix: add iptables-nft to wg-easy for nftables-only kernels
wg-easy's Alpine wg-quick uses legacy iptables which requires the
iptable_nat kernel module. On NixOS kernels compiled without legacy
netfilter modules, the container crashes in a restart loop:

  iptables v1.8.3 (legacy): can't initialize iptables table 'nat'
  Table does not exist (do you need to insmod?)

Fix: build a custom image that installs Alpine's iptables-nft package
and symlinks iptables -> iptables-nft (nftables backend).
2026-05-12 14:52:33 -04:00
e607982b21 refactor: chown tools dir at build time instead of root at runtime
Some checks failed
Build Hermes agent / build (pull_request) Failing after 3s
Build ollama (gfx906) / build (pull_request) Failing after 2s
2026-05-12 14:47:34 -04:00
4627199217 feat: install custom tools at startup, remove deprecated fix-permissions.sh
Some checks failed
Build Hermes agent / build (pull_request) Failing after 41m55s
Build ollama (gfx906) / build (pull_request) Failing after 2s
2026-05-12 13:38:26 -04:00
3611d645db Merge pull request 'feat: add Gitea Actions CI workflows for Docker builds' (#21) from feat/gitea-actions into master
Reviewed-on: #21
2026-05-11 02:39:02 +00:00
34b1cb83a0 feat: add Gitea Actions workflows for ollama and hermes Docker builds 2026-05-10 21:33:10 -04:00
8 changed files with 214 additions and 42 deletions

View File

@@ -0,0 +1,31 @@
name: Build Hermes agent
on:
pull_request:
branches: [ master ]
paths:
- 'ai/hermes/**'
- 'ai/compose.yml'
push:
branches: [ master ]
paths:
- 'ai/hermes/**'
- 'ai/compose.yml'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
run: |
git clone -b "${{ github.head_ref || github.ref_name }}" \
https://gitea:${{ secrets.GITHUB_TOKEN }}@code.lazyworkhorse.net/gortium/compose.git .
git log --oneline -3
- name: Build hermes image
run: |
cd ai
docker compose build hermes 2>&1
- name: Verify image
run: |
docker run --rm ai-hermes /opt/hermes/.venv/bin/python --version 2>&1

View File

@@ -0,0 +1,31 @@
name: Build ollama (gfx906)
on:
pull_request:
branches: [ master ]
paths:
- 'ai/ollama/**'
- 'ai/compose.yml'
push:
branches: [ master ]
paths:
- 'ai/ollama/**'
- 'ai/compose.yml'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
run: |
git clone -b "${{ github.head_ref || github.ref_name }}" \
https://gitea:${{ secrets.GITHUB_TOKEN }}@code.lazyworkhorse.net/gortium/compose.git .
git log --oneline -3
- name: Build ollama image
run: |
cd ai
docker compose build ollama --no-cache 2>&1
- name: Verify version
run: |
docker run --rm ollama/ollama:rocm-gfx906 ollama --version 2>&1

View File

@@ -31,6 +31,9 @@ services:
ssh:
- default
container_name: hermes
entrypoint: ["/bin/bash", "-c",
"bash /opt/data/hermes-tools/install.sh && exec /usr/bin/tini -g -- /opt/hermes/docker/entrypoint.sh \"$@\"",
"hermes-entrypoint"]
restart: always
# Gateway run enables the internal API server on port 8642
command: gateway run

View File

@@ -61,6 +61,65 @@ urllib.request.urlretrieve(url, base + '/en_US-ryan-high.onnx')
urllib.request.urlretrieve(url + '.json', base + '/en_US-ryan-high.onnx.json')
PYEOF
# ---------- Install Himalaya email CLI ----------
RUN /opt/hermes/.venv/bin/python3 /dev/stdin << 'PYEOF'
import urllib.request, tarfile, os, shutil
url = 'https://github.com/pimalaya/himalaya/releases/download/v1.2.0/himalaya.x86_64-linux.tgz'
tgz = '/tmp/himalaya.tgz'
urllib.request.urlretrieve(url, tgz)
with tarfile.open(tgz) as t:
t.extractall('/tmp')
shutil.move('/tmp/himalaya', '/usr/local/bin/himalaya')
os.chmod('/usr/local/bin/himalaya', 0o755)
os.remove(tgz)
print('himalaya v1.2.0 installed')
PYEOF
# ---------- Install himalaya-ro wrapper ----------
COPY --chmod=0755 himalaya-ro.sh /usr/local/bin/himalaya-ro
# ---------- Install 7-Zip for CHM extraction ----------
RUN /opt/hermes/.venv/bin/python3 /dev/stdin << 'PYEOF'
import urllib.request, tarfile, os, shutil, re, subprocess
# Scrape 7-zip.org for latest Linux x64 binary
url = 'https://7-zip.org/download.html'
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
r = urllib.request.urlopen(req, timeout=15)
html = r.read().decode()
links = re.findall(r'href="(a/7z[\d]+-linux-x64\.tar\.xz)"', html)
if not links:
raise RuntimeError("Could not find 7z download link")
dl_url = f'https://7-zip.org/{links[0]}'
print(f'Downloading 7z from {dl_url}...')
req = urllib.request.Request(dl_url, headers={'User-Agent': 'Mozilla/5.0'})
r = urllib.request.urlopen(req, timeout=30)
data = r.read()
with open('/tmp/7z.tar.xz', 'wb') as f:
f.write(data)
subprocess.run(['tar', '-xJf', '/tmp/7z.tar.xz', '-C', '/tmp/'], check=True)
for root, dirs, files in os.walk('/tmp'):
for f in files:
if f == '7zz':
src = os.path.join(root, f)
shutil.move(src, '/usr/local/bin/7zz')
os.chmod('/usr/local/bin/7zz', 0o755)
print(f'7zz installed from {src}')
break
os.remove('/tmp/7z.tar.xz')
# Verify
r = subprocess.run(['/usr/local/bin/7zz'], capture_output=True, text=True)
print(f'7-Zip {r.stdout.strip()[:60]}')
PYEOF
# ---------- Runtime ----------
USER hermes
ENV HERMES_HOME=/opt/data
@@ -68,7 +127,8 @@ ENV PATH="/opt/data/.local/bin:${PATH}"
# Point browser tool to Playwright's Chromium (already in base image)
ENV CHROME_EXECUTABLE=/opt/hermes/.playwright/chromium/chrome-linux/chrome
VOLUME [ "/opt/data" ]
# Ensure tools directory and toolsets.py are writable by the hermes runtime user
# so custom tools can be injected from the persistent volume at startup.
RUN chown -R hermes:hermes /opt/hermes/tools /opt/hermes/toolsets.py
COPY --chmod=0755 fix-permissions.sh /opt/hermes/fix-permissions.sh
ENTRYPOINT [ "/usr/bin/tini", "-g", "--", "/opt/hermes/fix-permissions.sh" ]
VOLUME [ "/opt/data" ]

View File

@@ -1,38 +0,0 @@
#!/bin/bash
# Startup permission fix + TTS patch.
# Runs as root before the entrypoint drops to the hermes user.
set -e
HERMES_HOME="${HERMES_HOME:-/opt/data}"
# Fix ownership on critical writable directories
chown -R hermes:hermes \
"$HERMES_HOME/sessions" \
"$HERMES_HOME/checkpoints" \
"$HERMES_HOME/skills" \
"$HERMES_HOME/memories" \
"$HERMES_HOME/workspace" \
"$HERMES_HOME/pastes" \
"$HERMES_HOME/logs" \
"$HERMES_HOME/cron" \
"$HERMES_HOME/plans" \
"$HERMES_HOME/hooks" \
"$HERMES_HOME/cache" \
2>/dev/null || true
# Fix data volume root ownership
if [ "$(stat -c %u "$HERMES_HOME" 2>/dev/null)" != "$(id -u hermes)" ]; then
chown hermes:hermes "$HERMES_HOME" 2>/dev/null || true
fi
# ---------- Patch tts_tool.py: replace Edge TTS with Piper ----------
# Fallback runtime patch in case the volume's site-packages differ from the image.
# Idempotent: if already patched, the script does nothing.
PATCH_SCRIPT="/opt/hermes/patch_tts_tool.py"
if [ -f "$PATCH_SCRIPT" ]; then
echo "Applying TTS patch (Piper only, no Edge fallback)..."
/opt/hermes/.venv/bin/python3 "$PATCH_SCRIPT" 2>&1 || true
fi
# Chain to the official Hermes entrypoint
exec /opt/hermes/docker/entrypoint.sh "$@"

73
ai/hermes/himalaya-ro.sh Normal file
View File

@@ -0,0 +1,73 @@
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────
# himalaya-ro — Read-only wrapper for himalaya
#
# Blocks destructive commands and logs audit trail.
# Pass-through for read-only commands (list, read, search).
#
# Usage: himalaya-ro [options] <command> [args...]
#
# Install: place in PATH before the real himalaya, or use
# `ln -sf himalaya-ro /usr/local/bin/himalaya`
# ─────────────────────────────────────────────────────────────
set -o pipefail
# ── Configuration ───────────────────────────────────────────
HIMALAYA_BIN="${HIMALAYA_BIN:-/usr/local/bin/himalaya}"
AUDIT_LOG="${HIMALAYA_AUDIT_LOG:-/var/log/himalaya-audit.log}"
# ── Destructive commands we block ──────────────────────────
BLOCKED_CMDS=(
"message move"
"message delete"
"message copy"
"flag add"
"flag remove"
"folder create"
"folder delete"
"folder rename"
"template send"
"account configure"
"account delete"
)
# ── Determine the subcommand being invoked ─────────────────
# Strip leading options (--account, --output, etc.) to find the verb
ARGS=()
SKIP_NEXT=false
for arg in "$@"; do
if $SKIP_NEXT; then
SKIP_NEXT=false
continue
fi
if [[ "$arg" == --* ]]; then
case "$arg" in
--account|--output|--page|--page-size|--folder|--color|--format)
SKIP_NEXT=true ;;
esac
continue
fi
ARGS+=("$arg")
done
# Build subcommand string and check against blocklist
CMD_STR=""
for ((i=0; i<${#ARGS[@]}; i++)); do
if [ -z "$CMD_STR" ]; then
CMD_STR="${ARGS[$i]}"
else
CMD_STR="$CMD_STR ${ARGS[$i]}"
fi
for blocked in "${BLOCKED_CMDS[@]}"; do
if [[ "$CMD_STR" == "$blocked" ]]; then
TS=$(date '+%Y-%m-%d %H:%M:%S')
echo "[AUDIT] $TS BLOCKED: himalaya $*" >> "$AUDIT_LOG"
echo "ERROR: Command 'himalaya $CMD_STR ...' is blocked by read-only policy." >&2
echo " Audit log: $AUDIT_LOG" >&2
exit 100
fi
done
done
# ── Allow pass-through ─────────────────────────────────────
exec "$HIMALAYA_BIN" "$@"

9
vpn/Dockerfile Normal file
View File

@@ -0,0 +1,9 @@
# Custom wg-easy with iptables-nft (nftables-backed iptables)
# Fixes crash-loop when host kernel lacks legacy iptable_nat module.
FROM ghcr.io/wg-easy/wg-easy:latest
# The upstream image registers only iptables-legacy with update-alternatives.
# iptables-nft binary exists but isn't registered as an alternative key.
# Override the alternatives-managed symlinks directly.
RUN ln -sf /usr/sbin/iptables-nft /usr/sbin/iptables && \
ln -sf /usr/sbin/ip6tables-nft /usr/sbin/ip6tables

View File

@@ -2,7 +2,10 @@ version: "3.8"
services:
wireguard:
image: weejewel/wg-easy:latest
build:
context: .
dockerfile: Dockerfile
image: wg-easy-iptables-nft:latest
container_name: wireguard
cap_add:
- NET_ADMIN