Skip to content

release: v0.3.0 - operations (unified CLI, cloud images, metrics, security baseline) - #2

Merged
engineering87 merged 11 commits into
mainfrom
release/0.3
Jul 7, 2026
Merged

release: v0.3.0 - operations (unified CLI, cloud images, metrics, security baseline)#2
engineering87 merged 11 commits into
mainfrom
release/0.3

Conversation

@engineering87

Copy link
Copy Markdown
Owner

Brings main up to v0.3.0, the operations release. Full notes in
docs/releases/v0.3.0.md; decision log updated in docs/architecture.md.

Scope

  • Unified sibilla CLI as the single entry point (status, model,
    tls, metrics, webui, connect), with the sibilla-* commands kept as
    aliases. sibilla status is a real health view (engine, served
    models, disk, GPU, gateway) and exits nonzero on failure; sibilla model rm and prune reclaim disk without touching the served
    model.
  • Cloud image: a qcow2 baked from the official Ubuntu cloud image,
    published for amd64 and arm64. Deployers attach their own
    cloud-init user-data; engine and model detection moved into
    llmd-firstboot so it runs on the deployed hardware. The arm64
    variant builds on GitHub's native arm64 runners, with the llmfit
    deb repackaged from the aarch64 musl release.
  • Observability opt-in: sibilla metrics enable serves Prometheus
    metrics at /metrics/gateway behind the same bearer token; Grafana
    dashboard and scrape config in docs/observability/. vLLM's native
    /metrics passes through; Ollama at the pinned 0.31.1 has none
    (verified in source).
  • Security baseline: ufw on from first boot (SSH and gateway port
    only, webui port closed by default, --acme opens 80/443 itself),
    unattended security updates, systemd sandbox extended to caddy,
    firstboot and both containers.

Notable fixes

  • Caddy's HTTP instrumentation is opt-in per server even in 2.6: the
    renderer now emits the servers-metrics global option, and the site
    body moved into a route block so the metrics endpoint can never be
    served before the 401 check.
  • cloud-init bake runs as a real bash script via write_files
    (string-form runcmd runs under dash, which killed the ERR trap),
    with its output routed to the serial console.
  • llmfit tarballs verified against their published sha256; llmd-hw
    and llmd-firstboot declare their real curl dependency.

Verification

The full pipeline is green: ISO build, BIOS and UEFI boot, unattended
install, inference, TLS and auth matrix, multi-model, webui plumbing,
metrics endpoint (authenticated, surviving TLS switches), firewall
and sandbox assertions, prune invariants; plus the cloud image built
and deployed with a real user seed on amd64 and arm64. Documented
limits: the arm64 deploy runs under TCG (no KVM on GitHub's arm64
runners), so its token generation is informational; the Open WebUI
interface and vLLM on physical GPUs stay outside CI.

After the squash merge, tag v0.3.0 on main: the release build attaches
the split ISO plus both cloud images with per-arch checksums.

Comment on lines +562 to +700
needs: build-cloud
strategy:
fail-fast: false
matrix:
include:
- arch: amd64
runner: ubuntu-24.04
# KVM available: inference is asserted
strict: "yes"
- arch: arm64
runner: ubuntu-24.04-arm
# no KVM: TCG-emulated inference is informational only
strict: "no"
runs-on: ${{ matrix.runner }}
timeout-minutes: ${{ matrix.arch == 'arm64' && 150 || 45 }}
steps:
- name: Download cloud image
uses: actions/download-artifact@v4
with:
name: sibillaos-cloud-${{ matrix.arch }}
path: cloud-img
- name: Install QEMU
run: |
sudo apt-get update
if [ "${{ matrix.arch }}" = "arm64" ]; then
sudo apt-get install -y qemu-system-arm qemu-efi-aarch64 qemu-utils xorriso sshpass jq
else
sudo apt-get install -y qemu-system-x86 qemu-utils xorriso sshpass jq
fi
- name: Deploy and probe
run: |
IMG=$(ls cloud-img/*.qcow2 | head -1)
if [ "${{ matrix.arch }}" = "arm64" ]; then
QEMU=qemu-system-aarch64
KVM="-machine virt -cpu max -bios /usr/share/AAVMF/AAVMF_CODE.fd"
if [ -e /dev/kvm ]; then
sudo chmod 666 /dev/kvm
KVM="-machine virt -enable-kvm -cpu host -bios /usr/share/AAVMF/AAVMF_CODE.fd"
fi
else
QEMU=qemu-system-x86_64
KVM=""
if [ -e /dev/kvm ]; then
sudo chmod 666 /dev/kvm
KVM="-enable-kvm -cpu host"
fi
fi
# a deployer-style NoCloud seed: own user, password ssh
mkdir seed
cat > seed/user-data <<'EOF'
#cloud-config
hostname: sibilla-cloud
ssh_pwauth: true
users:
- name: sibilla
plain_text_passwd: sibilla
lock_passwd: false
shell: /bin/bash
groups: sudo
sudo: ALL=(ALL) NOPASSWD:ALL
EOF
printf 'instance-id: sibilla-cloud-ci\n' > seed/meta-data
xorriso -as mkisofs -volid CIDATA -joliet -rock -o seed.iso seed
# shellcheck disable=SC2086
$QEMU $KVM -m 4096 -smp 4 \
-display none -serial file:cloud.log \
-drive "file=$IMG,format=qcow2,if=virtio" \
-drive "file=seed.iso,format=raw,if=virtio,readonly=on" \
-netdev user,id=n0,hostfwd=tcp::8080-:8080,hostfwd=tcp::2222-:22 \
-device virtio-net,netdev=n0 &
QPID=$!
ok=0
# first boot downloads the model; under TCG everything is
# slower, so allow up to 40 minutes
for _ in $(seq 1 240); do
sleep 10
code=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8080/v1/models || true)
if [ "$code" = "401" ]; then
ok=1
break
fi
done
RC=1
if [ "$ok" -eq 1 ]; then
echo "gateway answered 401"
SSH="sshpass -p sibilla ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2222 sibilla@127.0.0.1"
KEY=$($SSH "sudo cat /etc/llmd/apikey" | tail -1)
MODEL=$($SSH "sudo cat /etc/llmd/model" | tail -1)
echo "model: $MODEL"
if [ -n "$KEY" ] && [ -n "$MODEL" ]; then
RC=0
# the API surface is asserted on every architecture
mjson=$(curl -s -H "Authorization: Bearer $KEY" http://127.0.0.1:8080/v1/models) || true
if echo "$mjson" | jq -e --arg m "$MODEL" '.data[] | select(.id == $m)' >/dev/null; then
echo "served models include $MODEL"
else
echo "served models do not include $MODEL: $mjson"
RC=1
fi
if $SSH "sibilla status"; then
true
else
echo "sibilla status failed on the cloud image"
RC=1
fi
# inference: asserted with KVM, informational under TCG
# (emulated token generation is minutes per sentence)
RESP=$(curl -s -m 1500 http://127.0.0.1:8080/v1/chat/completions \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d "{\"model\": \"$MODEL\", \"messages\": [{\"role\": \"user\", \"content\": \"Say ok.\"}], \"max_tokens\": 60}") || true
if echo "$RESP" | jq -e '.choices[0].message | ((.content // "") + (.reasoning // "")) | length > 0' >/dev/null; then
echo "CLOUD INFERENCE OK"
else
if [ "${{ matrix.strict }}" = "yes" ]; then
echo "cloud inference failed"
echo "$RESP" | jq . || echo "$RESP"
RC=1
else
echo "inference not asserted on this architecture (no KVM, TCG emulation)"
echo "$RESP" | head -c 400 || true
echo
fi
fi
else
echo "could not read api key or model over ssh"
fi
else
echo "gateway never came up; console tail:"
tail -80 cloud.log || true
fi
kill "$QPID" 2>/dev/null || true
exit "$RC"
- name: Upload cloud debug artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: cloud-debug-${{ matrix.arch }}
path: cloud.log
@engineering87
engineering87 merged commit 980416d into main Jul 7, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants