Skip to content
Merged
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
11 changes: 11 additions & 0 deletions os/mini/prepare-stick.sh
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,17 @@ for d in $NPUMODELS; do
ok "models/$name registers as the entry '$name-1bit' (onebit_backend npu, by init.sh) and 1bit-release-check asks the NPU through it"
done

# Cycle 21, item 9b (the c20 board): a medium that carries the NPU model and no GGUF has nothing for chat-hrx to ask,
# so the GPU route is never exercised on hardware - the c20 stick shipped exactly that, and the board's chat-hrx row had
# no model to send to. An NPU-only medium is legitimate, so this is not a FAIL: it is a line saying what this stick can
# and cannot answer, with the flag that adds the other half.
ggufs=$(find "$MNT/models" -maxdepth 1 -name '*.gguf' 2>/dev/null | wc -l)
if [ "$ggufs" -gt 0 ]; then
ok "models/ holds $ggufs GGUF(s), so chat-hrx has a model to ask"
else
ok "no GGUF in models/ - chat-hrx has nothing to ask; --model <file.gguf> stages one (the NPU model alone is NPU-only)"
fi

# a stick that would not survive a pull is worth knowing about now, not at the console
printf '\nfree space on the data partition: %s\n' "$(df -h "$MNT" | awk 'NR==2 {print $4}')"
if [ -n "${KEY:-}" ] && [ "$KEY" != mint ]; then printf 'api key (first 6): %s\n' "$(printf '%s' "$KEY" | cut -c1-6)"; fi
Expand Down
46 changes: 33 additions & 13 deletions os/mini/release-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,26 @@ status() {
printf '%s' "${c:-000}"
}

# The Authorization header, and ONLY when this machine has a key (cycle 21, item 9a's other half, from the c20
# board). Blank mode is the shipped state and its contract is stock Lemonade's: a request with NO header at all.
# Cycle 20 sent `Authorization: Bearer none` on every call to keep one code path; on the board a header-less curl
# answered 200 and the NPU answered long then short with 0 TDR, so the header-less shape is the one that worked.
# Defined next to the fetchers so the keyed and blank shapes cannot drift apart.
auth_get() { # <url>
if [ -n "$KEY" ]; then got --header="Authorization: Bearer $KEY" "$1"; else got "$1"; fi
}
auth_status() { # <url> -> HTTP status
if [ -n "$KEY" ]; then status --header="Authorization: Bearer $KEY" "$1"; else status "$1"; fi
}
auth_post() { # <json body> <url> [slow] -> the reply
if [ "${3:-}" = slow ]; then _ag=got_slow; else _ag=got; fi
if [ -n "$KEY" ]; then
$_ag --header='Content-Type: application/json' --header="Authorization: Bearer $KEY" --post-data="$1" "$2"
else
$_ag --header='Content-Type: application/json' --post-data="$1" "$2"
fi
}

# Readiness: on a cold stick lemond takes tens of seconds to answer its first request (catalog, web
# app, engine spawn). The capped fetches above then see "status 000" and the row fails although the
# server is fine - candidate 4 (25 Oct) failed exactly so, where candidate 3's uncapped wget had simply
Expand Down Expand Up @@ -318,10 +338,9 @@ lemond_key() {
}
KEY=$(lemond_key)
[ -n "$KEY" ] || KEY=$(sed -n 's/^LEMONADE_API_KEY=//p' /data/lemonade/lemond.conf 2>/dev/null | tail -n 1)
# Cycle 20, the owner's "blank key": blank mode is the SHIPPED state, and its contract is stock Lemonade's - the
# server answers without a key and any placeholder is accepted. So every request below sends ${KEY:-none}: one code
# path, and on a keyed machine it is the real key (a wrong one there is still a 401, which the auth row asserts).
KEYH=${KEY:-none}
# Cycle 20, the owner's "blank key": blank mode is the SHIPPED state. Cycle 21: a request in blank mode carries no
# Authorization header at all (auth_get/auth_status/auth_post above), and on a keyed machine the same calls carry the
# real key (a wrong one is still a 401, which the auth row asserts).
API="http://127.0.0.1:$PORT"
CATALOG=$LEMDIR/bin/resources/server_models.json
PLAIN_TOTAL=$(find /data/npu/kernels -name 'layer1.elf' 2>/dev/null | wc -l)
Expand Down Expand Up @@ -433,8 +452,8 @@ elif ! lemonade_ready; then
# Not the same row as a wrong key: nothing answered on the port at all within the wait.
fail lemonade "lemond did not answer $API within ${READY_WAIT}s (see /tmp/lemond.log)"
else
MODELS=$(got --header="Authorization: Bearer $KEYH" "$API/api/v1/models")
WITH=$(status --header="Authorization: Bearer $KEYH" "$API/api/v1/models")
MODELS=$(auth_get "$API/api/v1/models")
WITH=$(auth_status "$API/api/v1/models")
BODY=$(got "$API/api/v1/models")
WITHOUT=$(status "$API/api/v1/models")
case "$(lemonade_verdict "$WITH" "$MODELS" "$WITHOUT")" in
Expand Down Expand Up @@ -560,10 +579,8 @@ fi
# ── one chat per backend that this machine can actually run ─────────────────────────────────────
chat() { # chat <model id> [slow] -> sets ANSWER and TPS
ANSWER=; TPS=
_get=got; [ "${2:-}" = slow ] && _get=got_slow
R=$($_get --header='Content-Type: application/json' --header="Authorization: Bearer $KEYH" \
--post-data="{\"model\":\"$1\",\"messages\":[{\"role\":\"user\",\"content\":\"/no_think Reply with exactly: 1BIT OK\"}],\"max_tokens\":48,\"temperature\":0}" \
"$API/api/v1/chat/completions")
R=$(auth_post "{\"model\":\"$1\",\"messages\":[{\"role\":\"user\",\"content\":\"/no_think Reply with exactly: 1BIT OK\"}],\"max_tokens\":48,\"temperature\":0}" \
"$API/api/v1/chat/completions" "${2:-}")
case "$R" in
*'"content"'*)
ANSWER=$(printf '%s' "$R" | sed -n 's/.*"content":"\([^"]*\)".*/\1/p' | head -1)
Expand Down Expand Up @@ -715,8 +732,7 @@ if ls /dev/accel/accel* >/dev/null 2>&1 && [ -n "$NPU" ]; then
TDR0=$(tdr_count)
Q_LONG='/no_think Explain in detail how tides work: the moon and the sun, the shape of ocean basins, why some coasts have two high tides a day and others one, and what an amphidromic point is'
_post() { # <json body> -> the reply
got_slow --header='Content-Type: application/json' --header="Authorization: Bearer $KEYH" \
--post-data="$1" "$API/api/v1/chat/completions"
auth_post "$1" "$API/api/v1/chat/completions" slow
}
R1=$(_post "{\"model\":\"$NPU\",\"messages\":[{\"role\":\"user\",\"content\":$(json_text "$Q_LONG" | sed 's/.*/"&"/')}],\"max_tokens\":512,\"temperature\":0}")
A1=$(json_str "$R1" content)
Expand Down Expand Up @@ -759,7 +775,11 @@ else
# `lemonade bench` prints one human row per run: "Run 1/1... TTFT=30.6ms PP=881.6t/s TPS=120.2".
# That line is the evidence; --json/--output is only a fallback because its shape moves between
# Lemonade versions (and it wrote nothing to stdout in the 2026-10-23 candidate image).
BL=$(LEMONADE_API_KEY="$KEYH" timeout 900 "$BENCH" bench "$VMODEL" --scenarios chat-short --runs 1 --warmup 0 \
# Blank mode hands the CLI NO key at all: the variable is unset rather than set to a placeholder, so the
# command runs exactly as a person's would on a keyless stick.
unset LEMONADE_API_KEY
[ -n "$KEY" ] && export LEMONADE_API_KEY="$KEY"
BL=$(timeout 900 "$BENCH" bench "$VMODEL" --scenarios chat-short --runs 1 --warmup 0 \
--json --output "$BLF" 2>/tmp/bench.err) || BL=
BTT=$(printf '%s\n' "$BL" | sed -n 's/.*TTFT=\([0-9.]*\)ms.*/\1/p' | head -1)
BPP=$(printf '%s\n' "$BL" | sed -n 's/.*PP=\([0-9.]*\)t\/s.*/\1/p' | head -1)
Expand Down
62 changes: 56 additions & 6 deletions os/v3/puppy/container-build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -191,10 +191,10 @@ echo " ok: inittab tty1 -> /usr/local/bin/1bit-console (plogin on the desktop
# skeleton that already says 1bit-os is left alone - and /etc/hosts has to agree or sudo, sshd banners and
# every log line carry a name that does not resolve. Both are edited here rather than shipped as whole
# files, because /etc/hosts is woof-CE's and has two lines (v4 and v6).
# mDNS (1bit-os.local) is NOT shipped: avahi-daemon is not in this rootfs and adding it would mean dbus
# activation on a non-systemd Puppy, and QEMU's user-mode network forwards no multicast, so nothing here
# could measure whether it worked. The board reads the address off the console menu instead - said plainly
# in the hand-off rather than shipped blind.
# mDNS (1bit-os.local, cycle 21 item 10): avahi-daemon + libnss-mdns are package rows, and 18onboard-mdns starts the
# responder (and the D-Bus system bus it needs, which nothing else in a non-systemd image starts). Asserted here rather
# than assumed, because woof-CE's package table is one line per package and a split that drops the binary fails at boot
# on a machine nobody can log into. nsswitch gains the mdns line below so the name resolves ON the appliance too.
echo "== hostname 1bit-os"
[ -f "$skeleton/etc/hosts" ] || {
echo "build: REFUSED: no rootfs-skeleton/etc/hosts - the appliance name would not resolve" >&2; exit 1; }
Expand All @@ -206,6 +206,31 @@ grep -q '1bit-os' "$skeleton/etc/hosts" || {
echo "build: REFUSED: /etc/hosts does not name the appliance" >&2; exit 1; }
echo " ok: hostname 1bit-os, and /etc/hosts agrees (rc.sysinit only renames a stock puppypc)"

# item 10: the name a client clicks. The binary is a package row; its PRESENCE is asserted after packing (the
# packages land in the rootfs at 2createpackages, which runs after this block - measured: checking here refused a
# healthy build). The nss line is a file edit and has to happen now, before the sfs is packed.
if [ -f "$skeleton/etc/nsswitch.conf" ] && ! grep -q 'mdns4_minimal' "$skeleton/etc/nsswitch.conf"; then
sed -i 's/^hosts:[[:space:]]*files[[:space:]]*dns[[:space:]]*$/hosts: files mdns4_minimal [NOTFOUND=return] dns/' \
"$skeleton/etc/nsswitch.conf"
fi
grep -q 'mdns4_minimal' "$skeleton/etc/nsswitch.conf" 2>/dev/null || {
echo "build: REFUSED: nsswitch.conf has no mdns line, so `getent hosts 1bit-os.local` cannot work on the box" >&2
exit 1; }
echo " ok: nsswitch resolves .local (mdns4_minimal); avahi-daemon is asserted in the packed image"

# ...and the account the bus runs as. /usr/share/dbus-1/system.conf carries <user>messagebus</user>, and woof-CE
# installs a package by unpacking it, so Debian's postinst never ran adduser: measured in the c20 image, /etc/passwd has
# no messagebus and dbus-daemon --system exits with "Failed to drop privileges". Restored here with a fixed id below
# 1000, which is also what the packed-image uid refusal expects.
if ! grep -q '^messagebus:' "$skeleton/etc/passwd"; then
printf 'messagebus:x:110:110::/nonexistent:/usr/sbin/nologin\n' >> "$skeleton/etc/passwd"
fi
grep -q '^messagebus:' "$skeleton/etc/group" || printf 'messagebus:x:110:\n' >> "$skeleton/etc/group"
grep -q '^messagebus:' "$skeleton/etc/passwd" && grep -q '^messagebus:' "$skeleton/etc/group" || {
echo "build: REFUSED: could not create the messagebus account dbus-daemon --system needs (avahi would not start)" >&2
exit 1; }
echo " ok: messagebus account for the system bus (dbus-daemon refuses to run as root without it)"

# U3/U4: the release package goes into the rootfs BEFORE 3builddistro, which packs it into the puppy SFS.
# Nothing is installed into it - the payload is relocatable and lemond-onebit finds bin/ and 1bit/ beside
# itself - so this is a copy plus two symlinks and the boot service.
Expand Down Expand Up @@ -281,6 +306,8 @@ if [ -n "${PAYLOAD:-}" ]; then
install -D -m 644 /recipe/embed/etc/1bit/palemoon-prefs.js "$skeleton/etc/1bit/palemoon-prefs.js"
install -D -m 755 /recipe/embed/usr/local/bin/1bit-lemonade-webapp "$skeleton/usr/local/bin/1bit-lemonade-webapp"
install -D -m 755 /recipe/embed/etc/init.d/24onboard-webapp "$skeleton/etc/init.d/24onboard-webapp"
# item 10: the responder that makes http://1bit-os.local:13305 work from another machine (cycle 21, owner).
install -D -m 755 /recipe/embed/etc/init.d/18onboard-mdns "$skeleton/etc/init.d/18onboard-mdns"
install -D -m 644 /recipe/embed/opt/1bit/webapp/index.html "$skeleton/opt/1bit/webapp/index.html"
install -D -m 644 /recipe/embed/etc/xdg/autostart/1bit-lemonade-webapp.desktop \
"$skeleton/etc/xdg/autostart/1bit-lemonade-webapp.desktop"
Expand Down Expand Up @@ -308,6 +335,7 @@ if [ -n "${PAYLOAD:-}" ]; then
chmod 0755 "$skeleton"/etc/init.d/03onboard-efivars "$skeleton"/etc/init.d/05onboard-data \
"$skeleton"/etc/init.d/06onboard-logs "$skeleton"/etc/init.d/08onboard-presets \
"$skeleton"/etc/init.d/10lemonade \
"$skeleton"/etc/init.d/18onboard-mdns \
"$skeleton"/etc/init.d/20onboard-ssh "$skeleton"/etc/init.d/21serial-console \
"$skeleton"/etc/init.d/24onboard-webapp "$skeleton"/etc/init.d/26onboard-board \
"$skeleton"/etc/init.d/27onboard-installer "$skeleton"/etc/init.d/zz-onboard-ports
Expand Down Expand Up @@ -457,11 +485,33 @@ echo " ok: $suid_n setuid/setgid entries survive the pre-pack chown, incl. the
miss=""
# the demo path needs a browser, a recorder and an encoder; an image without them cannot show or record itself
for b in usr/bin/sudo usr/bin/apt usr/sbin/sshd usr/bin/mokutil usr/sbin/sfdisk usr/bin/bash \
usr/bin/ffmpeg usr/bin/ffprobe usr/bin/python3 opt/palemoon/palemoon; do
usr/bin/ffmpeg usr/bin/ffprobe usr/bin/python3 opt/palemoon/palemoon usr/sbin/avahi-daemon; do
in_image "$b" || miss="$miss $b"
done
[ -z "$miss" ] || { echo "build: REFUSED: the image is missing:$miss (packages-extra did not land)" >&2; exit 1; }
echo " ok: sudo, apt, sshd, mokutil, sfdisk, bash are in the image"
# item 10: the responder AND the line that lets the box resolve the name it publishes, both read out of the packed sfs
packed_nss=$(unsquashfs -cat sandbox3/build/puppy_*.sfs etc/nsswitch.conf 2>/dev/null)
case "$packed_nss" in
*mdns4_minimal*) ;;
*) echo "build: REFUSED: the packed nsswitch.conf has no mdns line (1bit-os.local would not resolve on the box)" >&2
exit 1 ;;
esac
echo " ok: sudo, apt, sshd, mokutil, sfdisk, bash, avahi-daemon are in the image and .local resolves"
# item 10: and the responder must actually be STARTED. rc.services runs every executable /etc/init.d script with
# `start`, but skips any whose text contains the bus binary's name (`grep -qF`, see /etc/rc.d/rc.services - Puppy's
# way of not starting the bus twice). Applied here to the packed script, i.e. the exact file a boot runs.
packed_mdns=$(unsquashfs -cat sandbox3/build/puppy_*.sfs etc/init.d/18onboard-mdns 2>/dev/null)
if [ -z "$packed_mdns" ]; then
echo "build: REFUSED: the packed image has no etc/init.d/18onboard-mdns (1bit-os.local is never announced)" >&2
exit 1
fi
if printf '%s' "$packed_mdns" | grep -qF dbus-daemon; then
echo "build: REFUSED: etc/init.d/18onboard-mdns names the bus binary, so rc.services would SKIP it and the" >&2
echo " responder would never start (measured on the c20 image's /etc/rc.d/rc.services). rc.sysinit already" >&2
echo " starts that bus; this script must only wait for it." >&2
exit 1
fi
echo " ok: etc/init.d/18onboard-mdns will be executed by rc.services (no bus-binary token, so no skip)"
rc_country=$(unsquashfs -cat sandbox3/build/puppy_*.sfs etc/rc.d/rc.country 2>/dev/null |
grep -c "^if false ; then # 1bit: a headless boot must not ask" || true)
[ "${rc_country:-0}" = 1 ] || {
Expand Down
62 changes: 62 additions & 0 deletions os/v3/puppy/embed/etc/init.d/18onboard-mdns
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#!/bin/sh
# Copyright 2026 bong-water-water-bong
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# /etc/init.d/18onboard-mdns - the appliance announces itself as `1bit-os.local` (cycle 21, owner 2026-11-04: the
# clickable-links ask, so http://1bit-os.local:13305 works from ryzen or a phone without reading an address off the
# console).
#
# Two things the rootfs does not do for itself, both measured on the built resolute64 tree before this shipped:
# * the `avahi` (and `messagebus`) ACCOUNT is missing: woof-CE installs a package by unpacking it, so the adduser
# in Debian's postinst never ran, and avahi-daemon drops to `avahi` and chroots to /run/avahi-daemon before it
# will run. Rather than invent a system account in the image, it is given --no-drop-root --no-chroot: this
# appliance runs its services as root anyway (10lemonade, sshd), and the two flags only skip the privilege drop.
# * nothing in /etc/init.d starts the responder.
#
# DO NOT name the D-Bus bus binary in this file, not even in a comment. /etc/rc.d/rc.services runs every executable
# script in /etc/init.d with `start` but FIRST does `grep -qF` for that one string and skips the script if it appears
# anywhere in the text - Puppy uses it to avoid starting the bus twice. Measured on the c20 image: the bus binary is
# /usr/bin/…-daemon is in the image and rc.sysinit already runs it --system (line ~695) BEFORE rc.services (line
# ~727), and the board's own messages.log shows a live bus (it was trying to activate org.freedesktop.login1). So this
# service only WAITS for that socket; naming the binary here would silently disable the whole feature. container-build.sh
# asserts the packed script against rc.services' own rule, so that silence cannot come back.
#
# rc.services runs every executable script in /etc/init.d with `start`; the mode is part of the gate. Nothing on the
# box depends on mDNS - it is how a CLIENT finds the box - so a failure here is logged and never stops the boot.
LOG=/var/log/mdns.log
say() { printf '%s %s\n' "$(date -u +%FT%TZ)" "$@" >> "$LOG" 2>/dev/null; }

[ -x /usr/sbin/avahi-daemon ] || { say "no /usr/sbin/avahi-daemon in this image; 1bit-os.local is not announced"; exit 0; }

mkdir -p /run/dbus /run/avahi-daemon

# rc.sysinit starts the system bus before rc.services; on a slow medium it can still be a moment behind.
i=0
while [ "$i" -lt 10 ] && [ ! -S /run/dbus/system_bus_socket ]; do sleep 1; i=$((i + 1)); done
if [ ! -S /run/dbus/system_bus_socket ]; then
say "no system bus socket (/run/dbus/system_bus_socket) 10 s after rc.sysinit; 1bit-os.local is not announced"
exit 0
fi

avahi_pid() { cat /run/avahi-daemon/pid 2>/dev/null; }
pid=$(avahi_pid)
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
say "avahi-daemon already running (pid $pid)"
elif /usr/sbin/avahi-daemon --daemonize --no-drop-root --no-chroot 2>>"$LOG"; then
say "avahi-daemon up: $(hostname).local is announced on this network"
else
say "avahi-daemon failed to start (see the lines above); the console menu still prints the address"
fi
exit 0
10 changes: 10 additions & 0 deletions os/v3/puppy/packages-extra
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,20 @@
# NOT, so 1bit-install cannot write a GPT (the 26.04 split ships both in the fdisk
# package; the file that survives woof-CE's splitting is the one with a spec row)
#
# U2 avahi-daemon - item 10 (owner 2026-11-04): the appliance announces itself as 1bit-os.local, so
# http://1bit-os.local:13305 works from ryzen or a phone. Measured on the built rootfs,
# which carries libavahi-client/common only - no /usr/sbin/avahi-daemon, so nothing can
# answer an mDNS query.
# U2 libnss-mdns - the client half. Without it `getent hosts 1bit-os.local` cannot resolve the name the
# appliance publishes; container-build.sh adds the mdns4_minimal line to nsswitch.conf
# and REFUSES an image whose nsswitch.conf still lacks it.
#
# bash, dpkg, python3, efibootmgr, jwm, rox, xterm and curl are already in the merged set (measured).

yes|apt|apt|exe,dev>null,doc>null,nls>null||deps:yes
yes|sudo|sudo|exe,dev>null,doc>null,nls>null||deps:yes
yes|mokutil|mokutil|exe,dev>null,doc>null,nls>null||deps:yes
yes|openssh-server|openssh-server|exe,dev>null,doc>null,nls>null||deps:yes
yes|fdisk|fdisk|exe,dev>null,doc>null,nls>null||deps:yes
yes|avahi-daemon|avahi-daemon|exe,dev>null,doc>null,nls>null||deps:yes
yes|libnss-mdns|libnss-mdns|exe,dev>null,doc>null,nls>null||deps:yes
Loading
Loading