Skip to content

os/v3: the appliance announces 1bit-os.local, and a blank-key check stops asking for a key - #953

Merged
bong-water-water-bong merged 14 commits into
mainfrom
os/v3-c21-mdns
Nov 4, 2026
Merged

bong-water-water-bong merged 14 commits into
mainfrom
os/v3-c21-mdns

Conversation

@bong-water-water-bong

@bong-water-water-bong bong-water-water-bong commented Nov 4, 2026 •

Copy link
Copy Markdown
Collaborator

Cycle 21 items 10, 9a's header half and 9b (c20 board, TOS3-C18-items.md) plus pi-release's TOS3-NOTE-c20-prepstick.md.

Stacked on #951 (100867df), which owns item 9a's model-list half (#945) with its own self-tests — merge #951 first; after it lands this rebases to 5 files.

closes #952

Root causes, measured

Item 10 — nothing in the image can answer an mDNS query. Measured in the built resolute64 rootfs: libavahi-client/libavahi-common are present, /usr/sbin/avahi-daemon is not, and nsswitch.conf has no mdns line. So http://1bit-os.local:13305 (owner: "clickable links via ryzen") could never resolve. Two halves: the responder (package row + /etc/init.d/18onboard-mdns) and the client half (libnss-mdns + mdns4_minimal). container-build.sh refuses an image missing either.

The responder's own trap, found by extracting the c20 image's puppy sfs (c49946f): /etc/rc.d/rc.services runs each /etc/init.d script with start only after grep -qF dbus-daemon "$script" — a script whose text names the bus binary is skipped outright, comments included. The first version of this service started the bus itself, so it named the binary, so it would have been silently skipped on every boot and 1bit-os.local would never have been announced. The premise was wrong too: rc.sysinit already starts the bus (--system, line ~695) before running rc.services (line ~727), and the board's own messages.log proves a live bus. So the service now only waits (bounded, 10 s) for /run/dbus/system_bus_socket and starts the responder. container-build.sh applies rc.services' own rule to the packed script and refuses the image if it would be skipped — my first attempt at the fix still tripped it (one comment line spelled the path), which is the assertion doing its job. Sandbox check, three arms: no socket → says so, starts nothing; socket + avahi ok → "avahi-daemon up"; socket + avahi failing → logged, boot continues.

Item 9a, header half — blank mode asked for a key it does not have. The shipped default is blank key (owner, c20: "blank key"), whose contract is stock Lemonade: no header at all. Cycle 20 sent Authorization: Bearer none on the chat, bench and NPU requests to keep one code path. #951 fixes the model list but not this. auth_get/auth_status/auth_post build the header only when the machine has a key; lemonade bench gets LEMONADE_API_KEY unset, not a placeholder.

Item 9b — the medium said nothing about what it cannot answer. The c20 stick carried the NPU model and no GGUF, so chat-hrx had nothing to ask. prepare-stick --check now prints that as its own line with the flag that fixes it (no GGUF in models/ - chat-hrx has nothing to ask; --model <file.gguf> stages one).

Why the VM gate let 9a through. vm-check's U4 release-check row was INFO: it printed FAIL models and passed. It now asserts RESULT: PASS — the same line the board reads.

Before / after, on the request log

~/wt/os3i/rc-blank-repro.sh runs the real release-check.sh against a stub lemond in an unprivileged sandbox (CPU-only, no device, no /data touched) and records every request's Authorization header plus the bench row's environment.

arm rows requests the check made
before (origin/main) FAIL models, SKIP gguf, SKIP chat, SKIP bench 3 × GET /models — no POST at all
after, blank PASS models, PASS chat (1BIT OK @42 tok/s), PASS bench, SKIP gguf (no GPU in the sandbox) POST /chat/completions auth=<none>
after, keyed same rows PASS GET/POST … auth=Bearer 70f057…

Blank mode sends no header and still answers; keyed mode still sends the real key. release-check.sh --self-test: PASS (includes #951's three models_list cases).

Gates

bash -n/sh -n clean, tools/copyright.py --check rc=0, no new shellcheck warning per file vs the base (release-check.sh stays at its 5 pre-existing). The new file os/v3/puppy/embed/etc/init.d/18onboard-mdns carries the full Apache notice.

Not claimed: that avahi actually starts under Puppy's init in a booted image, and that a peer on the LAN resolves 1bit-os.local — the first is the U2 mdns vm-check row, the second needs multicast from outside the guest, which QEMU user networking does not carry. Both go to the board.

bong-water-water-bong added 2 commits November 4, 2026 02:00
… healthy server is not reported dead (#945)

`#934` split the lemonade row into a blank branch (the shipped cycle-20 default) and a keyed
branch. The keyed branch assigned `MODELS`; the blank branch assigned only `BODY` and `WITHOUT`, and
`MODELS` is the ONE input every row below reads (`models`, `have_model`, `have_model_ci`, `pick`, and
therefore `chat`/`chat-hrx`/`chat-npu`/`bench`). On the appliance that made a live server report
`FAIL Lemonade did not answer` and skip every chat row: the c20 board was 19/1 with
`u4-no-hw-skips` downstream of it.

Root cause is the split itself, not the missing line: a mode can set the probe body and forget the
list. So the list is now chosen in ONE place, by a pure selector, after the mode branch:

    MODELS=$(models_list "${MODELS:-}" "$BODY")

`models_list` takes the keyed body when there is one and the keyless body otherwise - which IS the
list in blank mode (`/api/v1/models` answers the same records keyless when a key is not configured).

Before / after, driving the script's real lemonade block with the network stubbed (healthy server,
cycle-20 blank mode):

    before  PASS lemonade answers ... without a key
            FAIL models Lemonade did not answer, so the model list cannot be checked
            MODELS=[]
    after   PASS lemonade answers ... without a key
            MODELS=[{"data":[{"id":"user.Qwen3-0.6B-NPU-1bit",...}]}]
            models row: proceeds with 61 bytes of model list

and keyed mode still takes the authenticated list (not the keyless one).

Self-test: three new arms in `--self-test` (`models_list` blank / keyed / nothing answered) - the
selector is pure for the same reason `lemonade_verdict_blank` is. Negative control: deleting
`models_list` fails all three arms (`command not found`, rc 1); `bash -n` and the pre-existing 24
arms are clean on both sides (`release-check self-test PASS`). `tools/copyright.py --check` rc 0,
no new files.

Closes #945.

Not claimed: the appliance-side board. The A/B on the frozen c20 image is in the issue; this fixes
the branch that produced it and is proven device-free above.
…tops asking for a key

Item 10 (owner 2026-11-04 ~01:3x: clickable links via ryzen) plus item 9a's other half and 9b from
the c20 board (TOS3-C18-items.md), stacked on #951 for the model-list half of 9a.

10  mDNS: the built rootfs carries libavahi-client/common and no /usr/sbin/avahi-daemon, and nothing
    in the image answers an mDNS query, so http://1bit-os.local:13305 could never work. Adds the
    avahi-daemon + libnss-mdns package rows, /etc/init.d/18onboard-mdns (starts the D-Bus system bus
    first - PID 1 is not systemd, and avahi exits without a bus), the mdns4_minimal nsswitch line, and
    container-build.sh refusals for both halves. vm-check gains a 'U2 mdns' row.

 9a  the header half #951 does not cover: blank mode (the shipped default) sent 'Authorization: Bearer
     none' on the chat, bench and NPU requests. Blank mode's contract is stock Lemonade - no header at
    all, which is the shape the board measured (header-less /models 200, NPU long then short, 0 TDR).
    auth_get/auth_status/auth_post build the header only when the machine has a key, and `lemonade
    bench` gets LEMONADE_API_KEY unset rather than a placeholder.

 9b  prepare-stick says what the medium can answer: a stick with the NPU model and no GGUF has nothing
    for chat-hrx to ask, which is how the c20 stick shipped.

vm-check's 'U4 release-check' row now fails on the RESULT line the board reads instead of recording it
as INFO - that row is what let 9a through the VM gate.

closes #952
@github-actions

github-actions Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

PR Reviewer Guide 🔍

(Review updated until commit 34702f8)

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis 🔶

951 - Partially compliant

Compliant requirements:

  • Split the lemonade row into a blank branch and a keyed branch
  • The keyed branch assigns MODELS
  • The blank branch assigns only BODY and WITHOUT
  • The MODELS variable is the ONE input every row below reads
  • Fix the blank branch to set MODELS using models_list function
  • Ensure that models_list takes the keyed body when there is one and the keyless body otherwise
  • Add self-tests for models_list in blank, keyed, and nothing answered scenarios

Non-compliant requirements:

  • None

Requires further human verification:

  • None

945 - Partially compliant

Compliant requirements:

  • Fix the release check's models row to never fail on the shipped blank-key appliance
  • The blank branch never sets the model list, causing healthy servers to be reported as dead
  • Ensure that the blank branch sets MODELS to the same value as the keyed branch

Non-compliant requirements:

  • None

Requires further human verification:

  • None

952 - Partially compliant

Compliant requirements:

  • The appliance should be reachable by a clickable link from ryzen or a phone, i.e. http://1bit-os.local:13305
  • The image should have both the mDNS responder and client name resolution
  • The image should refuse to be published without both halves
  • The vm-check should assert that the name resolves on the box

Non-compliant requirements:

  • None

Requires further human verification:

  • None
⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Incorrect Key Handling

The change to use auth_get, auth_status, and auth_post functions instead of directly using $KEYH for authorization headers introduces a potential issue where the key handling logic might not correctly differentiate between blank and keyed modes. Specifically, in blank mode, the KEY variable is unset, but the functions may not handle this correctly, potentially leading to incorrect behavior when making requests.

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
}
Potential Race Condition in mDNS Service

The 18onboard-mdns service waits for the D-Bus system bus socket but does not explicitly check if the service has started successfully. If the service fails to start due to an issue with the D-Bus socket or other dependencies, the appliance may not announce itself correctly, leading to connectivity issues.

# and root-owned, because `cp -a` carries the host user: we run as root in here, and git refuses a repository owned
# by another uid ("detected dubious ownership in repository at /work/woof-CE") - which merge2out hits when it undoes
# its VCS workarounds. Measured on the first cycle-16 build: that step failed once the fresh copy replaced the old
# root-owned tree. It also keeps the image's ownership policy true from the first file.
chown -R 0:0 /work/woof-CE
cd /work/woof-CE
# OWNERSHIP (pi-prs, blocker) second half: 3builddistro unpacks the PET packages into rootfs-complete AFTER the
# skeleton chown below, and mksquashfs keeps whatever ownership it finds - the frugalpup docs arrived owned by
# 6975/2030 and the packed-SFS check refused exactly those three files on cycle 16d. So the trees are normalised
# immediately before woof-CE packs them. -all-root is not available here (SFSCOMP is woof-CE's own), so the tree is
# chowned - but a bare `chown -R` ALSO strips every setuid/setgid bit it touches (POSIX: chown clears them), and
# that is what the c20 board hit as "Activated service 'org.freedesktop.login1' failed: The permission of the setuid
# helper is not correct" - measured, the built tree has 12 setuid helpers and the packed image had none, so passwd,
# su and the dbus launch helper could not work. /recipe/fix-ownership.sh does chown+restore and refuses a tree whose
# count changed. The patch is idempotent, counted, and `bash -n`'d, because a silently unapplied patch would be a
# silent regression of a security fix.
if ! grep -q '1bit: ownership before pack' woof-code/3builddistro; then
    sed -i \

…s any script that names the bus

Measured on the c20 image itself (its puppy sfs, extracted): /etc/rc.d/rc.services does

    grep -qF dbus-daemon $service_script || $service_script start

so a script whose TEXT contains that string is skipped outright - and comments count, because it greps the file.
18onboard-mdns started the D-Bus system bus itself, so it named the binary, so it would have been skipped and
1bit-os.local would have been silently unannounced on every boot. The trap's shape is the bad one: nothing fails.

The premise was wrong too, which is what makes the fix smaller rather than bigger: rc.sysinit already starts the
bus (`--system`, line ~695) BEFORE it runs rc.services (line ~727), and the board's own messages.log proves a live
bus (it was trying to activate org.freedesktop.login1). So the service now only WAITS for /run/dbus/system_bus_socket
(bounded, 10 s) and starts the responder; the dead bus-starting branch and the now-unreachable fallbacks are gone.

The gate is in the build, not in a comment: container-build.sh reads the PACKED etc/init.d/18onboard-mdns and refuses
the image if it is missing or if rc.services' own rule would skip it. My first attempt at this fix still tripped it
(one comment line spelled the path), which is the assertion doing its job.

Checked in a sandbox (unprivileged unshare, no device, host /run,/var,/usr untouched), three arms: no socket -> says
so and starts nothing; socket + avahi ok -> "avahi-daemon up" (announced); socket + avahi failing -> logs it, boot
continues.
@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown

Persistent review updated to latest commit c49946f

@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown

Preparing review...

@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown

Persistent review updated to latest commit c49946f

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

#953 ready for review at c49946f. Stacked on #951 (100867d) - merge that first, then this rebases to 5 files. Item 9a's model-list half is #951's (I dropped mine rather than duplicate it); this carries the header half it misses, plus 9b and item 10 (closes #952).
Correction to my own PR body, found after the review: the mDNS service must NOT start the D-Bus bus - rc.services skips any /etc/init.d script whose text contains the bus binary's name, and rc.sysinit already starts it. The commit c49946f is that fix, and container-build.sh now refuses an image whose responder would be skipped.

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY on content at c49946f96 — but #953 contains #951's whole diff, so they are not both mergeable as they stand.

Merge order, first

#961's fix aside, the os/mini/release-check.sh hunks in this PR are the same change as #951 (models_list, the three self-test arms, MODELS=$(models_list "${MODELS:-}" "$BODY")). #951 is a strict subset: this PR has all of it plus the auth_get/auth_status/auth_post header change, the mDNS init script, prepare-stick item 9b, container-build.sh and vm-check.sh.

So: merge #953 and close #951 as superseded, or merge #951 first and rebase #953 onto it (git will drop the duplicate hunk). Landing both as they are means the second one conflicts over text that is already identical.

What I verified here

  • bash -n clean on every shell file it touches; the new 18onboard-mdns init script included.
  • The models_list half I already verified on #951 by running it: blank mode gives the keyless list, keyed mode is unchanged, and two negative controls (definition removed; keyed precedence dropped) both turn the self-test red with rc 1.
  • The blank-mode auth change is the one with behavioural risk, and the reasoning is right: cycle 20 sent Authorization: Bearer none, and the board's own evidence is that a header-less request is what answers 200 and keeps the NPU TDR-free. Keeping the keyed and blank shapes in three small helpers rather than at every call site is the correct way to stop them drifting.
  • prepare-stick item 9b is ok in both branches, deliberately: an NPU-only medium is legitimate, so this is a statement about what the stick can answer, not a pass/fail.

Not run

The mDNS announcement and the container-build/vm-check rows need a real image build and a boot, which is the appliance lane's gate, not something I can do on this box. packages-extra only adds the avahi/mDNS package lines and container-build.sh gains the checks around them; both are read, not executed.

bong-water-water-bong pushed a commit that referenced this pull request Nov 4, 2026
…R for phones, and the mDNS name

Owner, 2026-11-04 01:3x: "due to being headless, can you make the links clickable, because i'm on kvm and
i want use it through ryzen", plus a QR code of the web UI URL and the mDNS name on the CONNECT block.

The TUI already made no tty1 assumption, but three things assumed the VGA console, so using it from a
terminal emulator was strictly worse than being at the box: the URLs were text nobody can click, the only
way to reach the web UI from a phone was to retype 27 characters off a screen, and `q` was refused even
where a prompt is harmless. Each of the three now depends on the terminal it actually finds itself on,
decided in one place and reported in the footer:

- OSC 8 hyperlinks on every URL in CONNECT when TERM is a terminal emulator (xterm-256color, tmux, kitty,
  ...), none when it is `linux`/`dumb` (VGA console, JetKVM, serial - there the escapes would be printed).
  ratatui has no hyperlink style, so the links are written AFTER the frame at the cells `ui::draw` reports:
  the drawn text is the link target, which `every_hyperlink_covers_the_url_that_is_drawn_there` asserts off
  the rendered frame at 80x25 and 240x67. A URL that wrapping split is not a link - half a URL opens the
  wrong thing. The link is re-stated every frame and styled with the row's own white+bold, so it keeps the
  pixel it had plus the underline (an `\x1b[4m` alone would inherit whatever colour the last cell left).

- A QR code of the web UI URL beside the CONNECT rows, two modules per cell (17 rows for the appliance's
  27-character URL: version 2, ECC L), drawn BLACK ON WHITE because a scanner wants dark-on-light and a
  terminal's dark background is the inversion many phone scanners refuse. Decoded end to end, not reasoned
  about: zxing-cpp on the PNG built from the 240x67 render's own cells returns
  `http://192.168.50.110:13305`.

- `http://1bit-os.local:13305` on the CONNECT block, with `(mDNS)` when the box's `avahi-daemon` is up and
  `(mDNS - avahi not running)` when it is not, so the row never promises a name that will not answer. The
  responder itself is pi-os3's half (engine #953, still open) - this PR reads it, it does not install it.

- `q` quits when started from an ssh session (sshd sets SSH_CONNECTION/SSH_TTY; a tty1 respawn sets
  neither) and still says "q stays" on tty1, which keeps the promise that console never drops to a bare
  prompt. The footer says which one you are looking at, from the same fact the key uses.

Not done on purpose: the QR is NOT at 80x25. The code needs 33 columns and 17 rows; at 80x25 the eight
CONNECT rows, the ten MENU rows, the three log rows and the footer already account for all 25 console rows,
so there is nothing to put it in without deleting one of the things the owner asked for. It is drawn
wherever it fits (tty1 is 240x67; an ssh window is normally wider than 104 columns), and at 80x25 the URL
stays plain text - which is also the size the QEMU gate reads, so `U2 menu connect` is unaffected.

Measured (pty, the musl release binary from build-static.sh, d225399adbf7a119, 1550384 bytes):
- TERM=xterm-256color at 80x25: 3 distinct OSC 8 targets written per frame - web UI, api base, mDNS name -
  each at the cell its text occupies; TERM=linux: 0 bytes of OSC 8, frames byte-identical to the previous
  build at the URL cells (checked, not assumed).
- `q` with SSH_CONNECTION set: exit 0. `q` without it (env -u SSH_CONNECTION -u SSH_TTY): the screen stays
  and the footer says "q stays: no bare prompt here (7 shell, 9 reboot)".
- 240x67: the panel is 33 columns at x=206, rows 3-19, all nine menu rows and 40 log rows still below it.
- cargo test 42/42 (10 new: the links cover their own URL, a split URL is not a link, the QR is drawn only
  where it fits, the QR block is square 33x17 with a quiet zone, the mDNS row in both states at both sizes,
  the footer in both worlds, the ssh/hyperlink predicates), clippy --all-targets clean, fmt clean,
  tools/copyright.py --check rc 0.

New dependency: `qrcodegen` 1.8 (Nayuki's reference encoder, MIT, no transitive dependencies, ~700 lines).
It is compiled into the static binary on the build host; the image build still only *installs* the binary
(CONSOLE_TUI_BIN), so the appliance gains no toolchain and no runtime dependency.
@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown

Persistent review updated to latest commit 6de2eee

@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown

Preparing review...

bong-water-water-bong pushed a commit that referenced this pull request Nov 4, 2026
…R for phones, and the mDNS name

Owner, 2026-11-04 01:3x: "due to being headless, can you make the links clickable, because i'm on kvm and
i want use it through ryzen", plus a QR code of the web UI URL and the mDNS name on the CONNECT block.

The TUI already made no tty1 assumption, but three things assumed the VGA console, so using it from a
terminal emulator was strictly worse than being at the box: the URLs were text nobody can click, the only
way to reach the web UI from a phone was to retype 27 characters off a screen, and `q` was refused even
where a prompt is harmless. Each of the three now depends on the terminal it actually finds itself on,
decided in one place and reported in the footer:

- OSC 8 hyperlinks on every URL in CONNECT when TERM is a terminal emulator (xterm-256color, tmux, kitty,
  ...), none when it is `linux`/`dumb` (VGA console, JetKVM, serial - there the escapes would be printed).
  ratatui has no hyperlink style, so the links are written AFTER the frame at the cells `ui::draw` reports:
  the drawn text is the link target, which `every_hyperlink_covers_the_url_that_is_drawn_there` asserts off
  the rendered frame at 80x25 and 240x67. A URL that wrapping split is not a link - half a URL opens the
  wrong thing. The link is re-stated every frame and styled with the row's own white+bold, so it keeps the
  pixel it had plus the underline (an `\x1b[4m` alone would inherit whatever colour the last cell left).

- A QR code of the web UI URL beside the CONNECT rows, two modules per cell (17 rows for the appliance's
  27-character URL: version 2, ECC L), drawn BLACK ON WHITE because a scanner wants dark-on-light and a
  terminal's dark background is the inversion many phone scanners refuse. Decoded end to end, not reasoned
  about: zxing-cpp on the PNG built from the 240x67 render's own cells returns
  `http://192.168.50.110:13305`.

- `http://1bit-os.local:13305` on the CONNECT block, with `(mDNS)` when the box's `avahi-daemon` is up and
  `(mDNS - avahi not running)` when it is not, so the row never promises a name that will not answer. The
  responder itself is pi-os3's half (engine #953, still open) - this PR reads it, it does not install it.

- `q` quits when started from an ssh session (sshd sets SSH_CONNECTION/SSH_TTY; a tty1 respawn sets
  neither) and still says "q stays" on tty1, which keeps the promise that console never drops to a bare
  prompt. The footer says which one you are looking at, from the same fact the key uses.

Not done on purpose: the QR is NOT at 80x25. The code needs 33 columns and 17 rows; at 80x25 the eight
CONNECT rows, the ten MENU rows, the three log rows and the footer already account for all 25 console rows,
so there is nothing to put it in without deleting one of the things the owner asked for. It is drawn
wherever it fits (tty1 is 240x67; an ssh window is normally wider than 104 columns), and at 80x25 the URL
stays plain text - which is also the size the QEMU gate reads, so `U2 menu connect` is unaffected.

Measured (pty, the musl release binary from build-static.sh, d225399adbf7a119, 1550384 bytes):
- TERM=xterm-256color at 80x25: 3 distinct OSC 8 targets written per frame - web UI, api base, mDNS name -
  each at the cell its text occupies; TERM=linux: 0 bytes of OSC 8, frames byte-identical to the previous
  build at the URL cells (checked, not assumed).
- `q` with SSH_CONNECTION set: exit 0. `q` without it (env -u SSH_CONNECTION -u SSH_TTY): the screen stays
  and the footer says "q stays: no bare prompt here (7 shell, 9 reboot)".
- 240x67: the panel is 33 columns at x=206, rows 3-19, all nine menu rows and 40 log rows still below it.
- cargo test 42/42 (10 new: the links cover their own URL, a split URL is not a link, the QR is drawn only
  where it fits, the QR block is square 33x17 with a quiet zone, the mDNS row in both states at both sizes,
  the footer in both worlds, the ssh/hyperlink predicates), clippy --all-targets clean, fmt clean,
  tools/copyright.py --check rc 0.

New dependency: `qrcodegen` 1.8 (Nayuki's reference encoder, MIT, no transitive dependencies, ~700 lines).
It is compiled into the static binary on the build host; the image build still only *installs* the binary
(CONSOLE_TUI_BIN), so the appliance gains no toolchain and no runtime dependency.
@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown

Persistent review updated to latest commit 5aae34c

@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown

Persistent review updated to latest commit 3db4549

@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown

Persistent review updated to latest commit 07ffab9

@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown

Persistent review updated to latest commit 407f224

@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown

Persistent review updated to latest commit 5c5994c

@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown

Persistent review updated to latest commit 34702f8

@bong-water-water-bong
bong-water-water-bong merged commit b8a07df into main Nov 4, 2026
9 checks passed
@bong-water-water-bong
bong-water-water-bong deleted the os/v3-c21-mdns branch November 4, 2026 08:14
bong-water-water-bong added a commit that referenced this pull request Nov 4, 2026
…R for phones, and the mDNS name (#962)

* os/v3/puppy: the console TUI is usable over ssh - clickable URLs, a QR for phones, and the mDNS name

Owner, 2026-11-04 01:3x: "due to being headless, can you make the links clickable, because i'm on kvm and
i want use it through ryzen", plus a QR code of the web UI URL and the mDNS name on the CONNECT block.

The TUI already made no tty1 assumption, but three things assumed the VGA console, so using it from a
terminal emulator was strictly worse than being at the box: the URLs were text nobody can click, the only
way to reach the web UI from a phone was to retype 27 characters off a screen, and `q` was refused even
where a prompt is harmless. Each of the three now depends on the terminal it actually finds itself on,
decided in one place and reported in the footer:

- OSC 8 hyperlinks on every URL in CONNECT when TERM is a terminal emulator (xterm-256color, tmux, kitty,
  ...), none when it is `linux`/`dumb` (VGA console, JetKVM, serial - there the escapes would be printed).
  ratatui has no hyperlink style, so the links are written AFTER the frame at the cells `ui::draw` reports:
  the drawn text is the link target, which `every_hyperlink_covers_the_url_that_is_drawn_there` asserts off
  the rendered frame at 80x25 and 240x67. A URL that wrapping split is not a link - half a URL opens the
  wrong thing. The link is re-stated every frame and styled with the row's own white+bold, so it keeps the
  pixel it had plus the underline (an `\x1b[4m` alone would inherit whatever colour the last cell left).

- A QR code of the web UI URL beside the CONNECT rows, two modules per cell (17 rows for the appliance's
  27-character URL: version 2, ECC L), drawn BLACK ON WHITE because a scanner wants dark-on-light and a
  terminal's dark background is the inversion many phone scanners refuse. Decoded end to end, not reasoned
  about: zxing-cpp on the PNG built from the 240x67 render's own cells returns
  `http://192.168.50.110:13305`.

- `http://1bit-os.local:13305` on the CONNECT block, with `(mDNS)` when the box's `avahi-daemon` is up and
  `(mDNS - avahi not running)` when it is not, so the row never promises a name that will not answer. The
  responder itself is pi-os3's half (engine #953, still open) - this PR reads it, it does not install it.

- `q` quits when started from an ssh session (sshd sets SSH_CONNECTION/SSH_TTY; a tty1 respawn sets
  neither) and still says "q stays" on tty1, which keeps the promise that console never drops to a bare
  prompt. The footer says which one you are looking at, from the same fact the key uses.

Not done on purpose: the QR is NOT at 80x25. The code needs 33 columns and 17 rows; at 80x25 the eight
CONNECT rows, the ten MENU rows, the three log rows and the footer already account for all 25 console rows,
so there is nothing to put it in without deleting one of the things the owner asked for. It is drawn
wherever it fits (tty1 is 240x67; an ssh window is normally wider than 104 columns), and at 80x25 the URL
stays plain text - which is also the size the QEMU gate reads, so `U2 menu connect` is unaffected.

Measured (pty, the musl release binary from build-static.sh, d225399adbf7a119, 1550384 bytes):
- TERM=xterm-256color at 80x25: 3 distinct OSC 8 targets written per frame - web UI, api base, mDNS name -
  each at the cell its text occupies; TERM=linux: 0 bytes of OSC 8, frames byte-identical to the previous
  build at the URL cells (checked, not assumed).
- `q` with SSH_CONNECTION set: exit 0. `q` without it (env -u SSH_CONNECTION -u SSH_TTY): the screen stays
  and the footer says "q stays: no bare prompt here (7 shell, 9 reboot)".
- 240x67: the panel is 33 columns at x=206, rows 3-19, all nine menu rows and 40 log rows still below it.
- cargo test 42/42 (10 new: the links cover their own URL, a split URL is not a link, the QR is drawn only
  where it fits, the QR block is square 33x17 with a quiet zone, the mDNS row in both states at both sizes,
  the footer in both worlds, the ssh/hyperlink predicates), clippy --all-targets clean, fmt clean,
  tools/copyright.py --check rc 0.

New dependency: `qrcodegen` 1.8 (Nayuki's reference encoder, MIT, no transitive dependencies, ~700 lines).
It is compiled into the static binary on the build host; the image build still only *installs* the binary
(CONSOLE_TUI_BIN), so the appliance gains no toolchain and no runtime dependency.

* os/v3/puppy: pin the QR's rows as a known answer (pi-prs' nits on #962)

Review: "the one sentence in the module that would make a reader trust the drawing is the one thing the
suite does not do" (nit 1), and nit 2's "worth keeping the comment that says the matrix is the encoder's and
not ours".

Nit 1 was right about the phrase, not quite about the coverage: `blocks()` did have a known-answer test
(a hand-drawn 2x2), but its name (`two_module_rows_become_one_text_row`) did not say so and the doc comment
pointed at "a known-answer test" without naming it - so the reader was asked to take it on faith. Both
halves are fixed:

- `two_module_rows_become_one_text_row` -> `blocks_draws_two_module_rows_per_text_row` (the drawing rule
  now reads in the name).
- New `a_known_url_renders_to_these_exact_rows`: `code("http://192.168.50.110:13305")` against the 17 rows
  of 33 cells written out literally. That is the known answer rather than the fit check
  (`the_appliances_own_url_fits_the_block_the_layout_reserves`, kept - it is about the layout's box, not
  the drawing). A change to the encoder, the ECC level (L), the version (2) or the quiet zone now fails the
  build instead of shipping a code nobody can read; the literal block also makes the quiet zone and the
  half-block rule visible in the test itself.

Nit 2, kept and made explicit: the module doc now says the matrix is the encoder's, that the drawing is
what this module owns, that a scan is not asserted by any test - and how it WAS measured: the 240x67 frame
rendered to a PNG and read back with zxing-cpp returned `http://192.168.50.110:13305` (2026-11-04), with
"re-run that by hand if the encoder/ECC/quiet zone changes". The README says the same next to the feature.

Measured on this head: cargo test 43/43 (1 new), clippy --all-targets -D warnings clean, fmt --check clean,
tools/copyright.py --check rc 0, build-static.sh ok (static musl binary fc71d2c720f97213, 1550360 B). The
two snapshots are untouched by this commit - no drawn cell changes.

---------

Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg>
bong-water-water-bong pushed a commit that referenced this pull request Nov 4, 2026
The page told a reader 'mDNS is not claimed' and to read the address off the box. Cycle 21
item 10 (#953, merged b8a07df) ships the responder, asserts it at build time and prints it in
the console's CONNECT block, so that sentence is now false in the direction that costs a
reader a step. Said what the sources say, with the two limits kept: the announced name is
$(hostname).local (so it follows a rename), the CONNECT row says when avahi is not running,
and a peer resolving it over multicast is still not claimed on this page.
bong-water-water-bong added a commit that referenced this pull request Nov 4, 2026
… announced but never answers (#1016)

* os/v3/puppy: the firewall drops every mDNS query, so 1bit-os.local is announced but never answers

The c22 board (4 Nov 12:5x AST) logged `avahi-daemon up: 1bit-os.local is announced on this network` and a raw
mDNS query from ryzen got no answer. #952/#953 put the responder in the image; what was never opened is the path
for a *query*, and the cause is the same `rc.firewall` that `zz-onboard-ports` already works around for tcp.

Read out of the packed c22 puppy SFS, `etc/init.d/rc.firewall`:

    -P INPUT DROP
    -A INPUT -p ALL -d 224.0.0.1 -j DROP
    -A INPUT -p UDP -j udp_inbound        (drops 137/138, accepts DHCP 67->68, then -j RETURN)
    -A INPUT -m pkttype --pkt-type broadcast -j DROP
    -A OUTPUT -p ALL -j ACCEPT

so every NEW inbound UDP packet falls through to the DROP policy, and a query on 5353 is exactly that; the
appliance's own announcements leave fine, which is why avahi reported success while nothing answered. This file
already opens tcp/13305 and tcp/22 for the same reason - mDNS is the UDP half. LAN-only, the same ranges
hosts.allow already names below plus link-local for avahi's no-lease fallback, ahead of udp_inbound and the
broadcast drop, idempotent via -C, and in `zz-onboard-ports` rather than `18onboard-mdns`, whose name sorts
before `rc.firewall` and would be wiped by it.

Measured in a booted c22 image (`~/wt/os3i/c23-mdns-probe6.sh`, one controlled announcement burst per arm, the
rules' own packet counters):

  arm                                   policy DROP        the fix's INPUT rule
  shipped firewall                      5 -> 12 (+7/2095)  absent
  + one `-I INPUT 1 ... dport 5353`     12 (unchanged)     0 -> 7 packets / 2095 bytes
  rule removed again                    12 -> 19 (+7/4963) absent

the same seven packets that were dropped are the ones the rule accepts, byte for byte. Two caveats stated rather
than implied: those packets are avahi's own multicast announcements looped back (IP_MULTICAST_LOOP), not a query
from a peer - same packet class, same chain, and a peer's query is what the board row must still prove; and QEMU
user networking cannot carry inbound UDP at all (a `hostfwd=udp` delivers nothing even with the firewall
flushed), which is why this is measured on the appliance's own interface.

Gate: a `U3 mdns rule` row in vm-check.sh that fails an image without the rule.

closes #1011

* os/v3/puppy: a self-check for the inbound rules, so the mDNS block cannot regress silently

`mdns-ports-selftest.sh` runs the shipped `zz-onboard-ports` against a stub `iptables` (a rule table in a file, `-C`
answered from it): no root, no ruleset, no device. It asserts four `udp/5353` inserts with the four intended
sources, that a second run inserts nothing, and that the two TCP ports this file already opened are still opened.

Measured both ways before committing:

    shipped script    run 1: 6 inserts, 4 of them udp/5353   run 2: 0 inserts, 4 -C   -> RESULT: PASS
    origin/main's     run 1: 2 inserts, 0 of them udp/5353                            -> RESULT: FAIL

`sh -n`, `shellcheck -S warning` and `tools/copyright.py --check` are clean.

---------

Co-authored-by: pi-os3 <pi-os3@1bit>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

os/v3/puppy: the appliance cannot answer an mDNS query, so 1bit-os.local never resolves

1 participant