A `systemctl --user` unit runs under the per-user manager, which systemd starts at first login and stops when the user's last session ends — so closing the SSH session that started Skald killed it, and it never came up at boot. No crash and nothing in the journal: the whole cgroup is simply torn down. Both installers now enable lingering after installing the unit, and update.sh carries the same helper so an installation predating this fix is healed by an ordinary update. A failure to enable it only ever warns, with the manual command — it must not abort an install. Two things found on the way there: update.sh matched `case "$OS" in Linux) ... Darwin)`, but $OS had already been normalized to lowercase at the top of the file, so stop_service and start_service were both silent no-ops. None of the ordering the file documents at its head was executing: the tarball went over the running binary (ETXTBSY, aborting the update mid-way) and the safety-net restart in cleanup() was a no-op too, leaving the box down. Neither workflow published install.sh / install-nightly.sh to the web root, so the scripts served by builds.skaldagent.net were hand-copied and drifting from the repo — an installer fix would reach every existing box through update.sh but never a new one. Nightly publishes the nightly installer, release publishes the release one, both with the same atomic temp-and-rename the tarballs use. Also on the unit: dropped `After=docker.service`, which a user manager silently ignores rather than honouring advisorily, and moved `Restart=on-failure` to `always` — run.sh exits 0 on any graceful shutdown, including one nobody asked for, which on-failure reads as a clean stop. That is also what absorbs the boot race against Docker now that lingering makes us start at boot.
14 KiB
Skald Circle — SKALD
This file MUST be written in English. All project notes, decisions, and documentation here are in English.
Installation
Stable release
curl -fsSL https://builds.skaldagent.net/install.sh | bash
Nightly (latest automatic build)
curl -fsSL https://builds.skaldagent.net/install-nightly.sh | bash
Requirements
| Required | Notes |
|---|---|
| Docker | User container sandbox. The installer can install it |
| Linux (amd64/arm64) or macOS ARM64 (Apple Silicon) | Intel Mac not supported |
| systemd (Linux) or launchd (macOS) | For running as a service |
| Python 3 (optional) | For Python MCP servers (Gmail, GCal, GMaps, weather, SSH) and local TTS plugins |
| Node.js ≥ 18 (optional) | For WhatsApp MCP server |
The installer checks each requirement and offers to install Docker if missing. Python and Node.js are optional — the server starts regardless, but certain MCP servers won't work.
What it does
- Downloads the tarball from
builds.skaldagent.net - Extracts to
~/.local/share/skald-circle/(or$SKALD_DIR) - Configures the service (systemd user service / launchd agent)
- Runs
skald-setupto create the admin account - The server starts at
https://localhost:8443
Uninstallation
curl -fsSL https://builds.skaldagent.net/install.sh | bash
# The tarball contains uninstall.sh:
~/.local/share/skald-circle/uninstall.sh
Or, after installation: ~/.local/share/skald-circle/uninstall.sh
Bug fix: uninstall.sh fails on Docker-owned files in homes/ ✅
Problem: uninstall.sh runs rm -rf "$INSTALL_DIR" as the normal user, but homes/ contains files created by Docker containers running under different UIDs (often root). The removal fails with "Permission denied" on those files, leaving a broken install behind.
Fix: if rm -rf fails (non-zero exit), the script retries with sudo rm -rf. If even sudo fails, it prints an error message and exits non-zero so the user knows manual cleanup is needed.
From source
git clone https://github.com/.../skald-circle.git
cd skald-circle
cargo build --release
./run.sh
Current status
New application with agents and chatbots to help families and small groups collaborate, with supervised chat for children and vulnerable people.
Installer & startup architecture
install.sh
├── extracts tarball
├── creates .venv (inline — does NOT call run.sh)
└── runs skald-setup for interactive config
systemd service → ExecStart=run.sh
run.sh (supervisor)
├── creates .venv if it doesn't exist (for local dev)
├── runs skald-setup (first run only)
└── loop: executes skald binary, restart on exit 255
Rule: install.sh must NEVER call run.sh. The venv is created inline in the installer.
run.sh is only for the service supervisor or local development.
skald-setup is the only setup executable called by install.sh.
Bug fix: install.sh stuck on "Setting up Python virtual environment" ✅
Problem: the installer called "$INSTALL_DIR/run.sh" to create the venv. But run.sh after the venv runs skald-setup and then the server in a loop, hanging the installer forever.
Fix: the venv is now created inline in install.sh and install-nightly.sh, using the same logic as run.sh (uv > python3) but without starting the server.
Bug fix: "Unit docker.service not found" in user service ✅
Problem: the systemd user unit had Requires=docker.service, but docker.service is a system-level unit (not a user unit). systemctl --user couldn't find it and refused to start Skald.
Fix: removed Requires=docker.service from the user unit template in both install scripts. Kept After=docker.service (advisory, doesn't block if the unit isn't found).
Follow-up: After=docker.service was dropped too. It never did anything — a user manager has no view of system units, so the ordering was silently ignored rather than merely advisory, and keeping it suggested a guarantee that was not there. What actually handles the boot race is Restart (see below): the server fails fast when the Docker daemon is unreachable, and systemd brings it back a few seconds later.
Bug fix: the server dies when you log out ✅
Problem: systemctl --user start skald-circle worked, but closing the SSH session killed the server — and it never came up at boot. Not an application bug: a --user unit runs under the per-user manager (user@UID.service), which systemd starts at first login and stops when the user's last session ends, tearing down every user service in the cgroup. No crash, no error in the journal — the whole cgroup is simply killed.
Fix: both installers now run loginctl enable-linger $USER after installing the unit (helper enable_linger, tried unprivileged first, then sudo -n, then interactive sudo, and only warns if all three fail — a missing linger must never abort an install). update.sh carries the same helper so an installation predating this fix is healed by an ordinary update.
Also: Restart=on-failure → Restart=always. run.sh exits 0 on any graceful shutdown, including one nobody asked for (a stray SIGTERM to the server), which on-failure reads as a clean stop and leaves the box down. An explicit systemctl --user stop is unaffected — systemd never restarts after a requested stop. With lingering on, this is also what absorbs the boot race against Docker.
Bug fix: update.sh never stopped or restarted the service ✅
Problem: stop_service and start_service matched case "$OS" in Linux) … Darwin), but $OS had already been normalized to linux/darwin at the top of the script. Every branch fell through: both functions were no-ops. So the updater extracted the tarball over the running binary (ETXTBSY on Linux, aborting the update mid-way) and, when extraction did succeed, left the old build running in memory with the safety-net trap firing a restart that was itself a no-op. The careful stop → wait-for-exit → extract ordering the file documents at the top had not been executing at all.
Fix: matched the normalized lowercase values, with a comment at the seam saying why the capitalization is load-bearing.
Bug fix: skald-setup non interattivo con curl | bash ✅
Problem: skald-setup controlla isatty(0), ma con curl ... | bash stdin è un pipe, quindi saltava senza chiedere username/password. L'installer arrivava fino in fondo ma senza aver creato l'admin.
Fix: se IS_INTERACTIVE=false ma /dev/tty esiste, l'installer chiama skald-setup </dev/tty.
Agent icons — completed ✅
All agents now have Vector Paintings icons (painterly vector, warm and family-friendly), generated via ComfyUI:
Chat agents — warm animals:
| Agent | Animal | Status |
|---|---|---|
| Main Assistant | 🦊 Fox | ✅ |
| Project Coordinator | 🦡 Badger | ✅ |
| Researcher | 🐿️ Squirrel | ✅ |
| Generalist | 🦫 Beaver | ✅ |
| Code Explorer | 🕵️ Meerkat | ✅ |
| Software Architect | 🏗️ Heron | ✅ |
| Software Engineer | 🔧 Bear | ✅ |
| Spec Writer | 📝 Owl | ✅ |
| Tech Lead | 👑 Deer | ✅ |
| Business Analyst | 💼 Magpie | ✅ |
| Companion | 🦦 Otter | ✅ |
System agents — insect family:
| Agent | Animal | Status |
|---|---|---|
| Event triage | 🕷️ Spider | ✅ |
| Private Memory Lint | ✨ Firefly | ✅ |
| Shared Memory Lint | 🐝 Bee | ✅ |
Refactoring — completed ✅
- Removed Tauri/desktop dependency (
tauri.conf.json,src/desktop/,icons/,docs/desktop.md, gen schemas/) - Removed
build.rs(no longer needed) - New i18n system (core-api + plugin-mobile-connector + web)
- Configuration system refactoring
Auto-build CI/CD (NiPoGi)
Automatic build on NiPoGi with Gitea Actions (native runner v2.1.0):
| Component | File | Status |
|---|---|---|
ci/package.sh |
Creates distribution tarballs from compiled binaries | ✅ |
ci/verify-version.sh |
Verifies that a release hasn't been built yet | ✅ |
.gitea/workflows/nightly.yml |
Push to main → build amd64+arm64 → nightly/ |
✅ |
.gitea/workflows/release.yml |
PR check verify-version + merge → build → releases/v{ver}/ |
✅ |
| Native runner on NiPoGi | v2.1.0, host-mode systemd service, label linux-amd64 |
✅ |
| Cross toolchain (arm64) | gcc-aarch64-linux-gnu + rustup target add |
✅ |
Caddy builds.skaldagent.net |
file_server browse (directory listing) | ✅ |
Route53 builds.skaldagent.net |
A record → 145.40.169.107 | ✅ |
| CI cache | Persistent CARGO_TARGET_DIR at /home/dguiducci/.cache/skald-ci/target |
✅ |
install.sh |
One-liner script `curl ... | bash` — Linux (systemd) + macOS ARM64 (launchd) |
install-nightly.sh |
One-liner script for nightly builds — same OS support | ✅ |
uninstall.sh |
Bundled in tarball — stops service/agent, removes everything | ✅ |
releases/LATEST |
Auto-updated by release workflow to track latest version | ✅ |
Technical notes
scripts/removed — CI scripts live inci/(tracked by git); the legacy MCP servers it held are superseded by marketplace connectors- Build without
whisper-localon Linux (--no-default-features) aarch64-linux-gnu-stripfor ARM64 binariesactions/checkout@v4works (native runner has Node.js)- macOS ARM64 supported via
install.sh/install-nightly.sh(auto-detects OS, uses launchd)
macOS package script (ci/package-macos.sh)
Script to build and deploy the macOS ARM64 package directly from the MacBook.
| Detail | Value |
|---|---|
| File | ci/package-macos.sh |
Branch release |
Build + version check (curl) + upload to releases/v{ver}/ + update LATEST |
Branch main |
Build + upload to nightly/ (no version check) |
| Other branches | ❌ Abort |
| Remote host | skaldserver (SSH alias → 192.168.1.100, user dguiducci, key id_ed25519_skaldserver) |
| Remote path | /var/www/builds.skaldagent.net/ |
Setup SSH
| Step | Command |
|---|---|
| Key created | ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_skaldserver |
~/.ssh/config alias |
Host skaldserver → HostName 192.168.1.100 User dguiducci IdentityFile ~/.ssh/id_ed25519_skaldserver |
| Installed on server | cat ~/.ssh/id_ed25519_skaldserver.pub → ~/.ssh/authorized_keys on the NiPoGi |
| MCP SSH registered | mcp__ssh__add_alias → alias skaldserver (auth: key, sudo: prompt) |
Operational notes
- Builds with whisper included (no
--no-default-featureslike on Linux) - The tarball is uploaded via SCP (
scp+sshfor LATEST) install.sh/install-nightly.shalready support macOS ARM64 (launchd)- Service homepage at
http://192.168.1.100:8086— updated with 📦 Builds card → after editing the file, rundocker restart homepage(bind mount:rodoesn't propagate live)
Next steps
- Script
ci/package-macos.shto build and deploy from MacBook (release + nightly) - Test the script on
mainbranch (nightly) - Test the script on
releasebranch (release) - Create
releasebranch on Gitea with branch protection (PR via UI) - Test release workflow with a PR
macOS support
Supported: macOS ARM64 (Apple Silicon M1+), Intel not supported.
| Aspect | Status | Notes |
|---|---|---|
Install script (install.sh) |
✅ | Auto-detects macOS, uses launchd |
Nightly install (install-nightly.sh) |
✅ | Same logic |
Uninstall script (uninstall.sh) |
✅ | Handles launchctl |
Package script (ci/package.sh) |
✅ | Accepts --os darwin, strips best-effort |
| Binary | ⏳ Not yet built | Build natively on MacBook, deploy to builds.skaldagent.net |
How to build for macOS (on MacBook)
cargo build --release -p skald-setup -p skald # includes whisper
./ci/package.sh --version v0.1.0 --os darwin --arch arm64 \
--target-dir target/release --output dist/
Upload the resulting dist/skald-circle-v0.1.0-darwin-arm64.tar.gz to the NiPoGi's builds.skaldagent.net/releases/v0.1.0/ directory.
Cross-compilation from NiPoGi (research notes 🧪)
Cross-compiling for aarch64-apple-darwin from the NiPoGi using zig + cargo-zigbuild was attempted but hit blockers.
| Component | Location | Notes |
|---|---|---|
| Zig | ~/.local/bin/zig (symlink to /tmp/zig-linux-x86_64-0.14.0/zig) |
v0.14.0, installed manually |
| macOS SDK | /opt/MacOSX/MacOSX11.3.sdk |
From phracker/MacOSX-SDKs (GitHub) |
| Rust targets | aarch64-apple-darwin |
via rustup target add |
cargo-zigbuild |
~/.cargo/bin/cargo-zigbuild |
v0.23.0 |
| zig wrapper scripts | /tmp/zig-wrap-cxx.sh, /tmp/zig-ar-wrap.sh |
Handle OpenSSL/Clang flags + SDK paths |
What works:
- ✅ Rust std compilation for macOS target
- ✅ OpenSSL compilation from source (via wrapper that remaps
--target=and provides SDK headers) - ✅ Rust dependency compilation (tree-sitter, sqlx, tokio, etc.)
- ✅ Single-file C programs compile and link correctly
What's blocked:
- ❌
zig ccsegfaults with-F(framework search path) on Linux → can't link against macOS frameworks (CoreFoundation, Security) - ❌
zig cccan't find frameworks without-F - ❌
libsqlite3-sysbuild.rs bug:is_applecheckshost.contains("apple") && target.contains("apple")→ forces OpenSSL linkage instead of CommonCrypto on cross-compile (needs upstream fix orOPENSSL_DIRworkaround)
The fix would be:
- Upstream fix to
libsqlite3-sysbuild.rs (target.contains("apple")only) - Zig fix for
-Fsegfault, or useld64instead of zig's linker
Conclusion: Cross-compilation is fragile. Build natively on MacBook for now.