Record update delivery as a per-body offer and local candidate activation after a live shell push killed an active conversation.
15 KiB
TASK 25 — One gitea repo for every SouveraineOS package
Status: 2026-07-30 — Pixel3Arch now publishes every device package.
packages.yml builds + signs all of them into the additive edge release,
so pacman -Sl souveraine-aarch64 is the complete live manifest: 14 packages
(souveraine, -callaudio, -q6voiced, -ucm-blueline, -updater, upower-souveraine,
pulse-native-provider, souveraine-osk-switch, hexagonrpc-blueline,
tqftpserv-blueline, blueline-edge-sense, souveraine-stevia,
souveraine-squeekboard, usb-signaller). Two deferred on purpose: hyprgrass
(Hyprland plugin, phased out for viewtop) and libssc-souveraine (phone runs
upstream libssc; our fork isn't deployed). Goal achieved: push → pacman -Syu on the phone, no hand-carried packages. The kernel (kernel.yml) is the
last holdout — it still ships a raw .pkg.tar.zst with no repo-add --sign.
The problem in one line
souveraine's CI is the only pipeline that does the last mile — repo-add --sign into a per-arch pacman database. Every other repo stops at "here is
a file," so those packages have to be copied to the phone and pacman -U'd
by hand.
| Repo | CI | Publishes | pacman -Syu-able |
|---|---|---|---|
| souveraine | ci.yml (primary/public) |
signed pkgs + souveraine-{arch}.db |
yes |
| Pixel3Arch | kernel.yml (main) |
raw .pkg.tar.zst, no db, no sig |
no |
| firefox-arm | build.yml |
nothing published yet | no |
| culver | ci.yml |
tests only | no |
| souveraine-player | none | — | no |
| upower | none — rides souveraine as a submodule | (into souveraine's db) | yes |
upower is the proof the pattern generalises: a separate repo, built by souveraine's CI, folded into the same database. That is the shape everything else should take.
The design — one database, many producers
Do NOT add a fourth pipeline. Make souveraine-{arch}.db the single archive
and have every producer publish into it.
-
Pixel3Arch
kernel.ymlgains the last mile. It already builds a valid package on the archdev runner and asserts the built hash against the gitea tip. Add therepo-add --include-sigs --sign --key "$ARCHIVE_KEY"step and publish into the sharededgerelease —souveraine/.gitea/workflows/ci.ymllines ~248-256 are the reference implementation. Kernel updates then arrive bypacman -Syu; flashing becomes a recovery tool, not a release channel.repo-add --signis NOT the first blocker (verified 2026-07-25; parked by Casey the same day). Three things sit in front of it:- The job has never once passed. 11 runs, 11 failures, none since
Jul 14;
kernel-latestis a 404 and has never been published. The Jul 14 chain (b1d3226→9c51f3f) was successive gitea-credential fixes tobuild.shand the last fix was never re-run — it may already work. pkgveris static.kernel/PKGBUILD:12-13ispkgver=7.1.1/pkgrel=1; the commit lives only in the kernelrelease, so every build is7.1.1-1and-Syucan never see an upgrade.build.sh:27-28hardcodes both artifact filenames — they must become dynamic in the same change.- The obvious fix is non-monotonic.
vercmp 7.1.1.g588929024 7.1.1.gaaa111222→-1: a bare.g<sha>sorts as hex, so a newer kernel can sort below an older one. Use7.1.1.r<count>.g<sha>(verified monotonic), matching souveraine's own scheme. Never+.
Archive shape — Casey chose "additive" 2026-07-25. Publishing into the shared
edgerelease as written above does not work today:ci.ymldeletes the release and tag and recreates it, uploading only its ownpacman-repo(which itrm -rfs first), so anything another producer published there is erased on the next souveraine push. ci.yml must stop delete-and-recreate; each producer then does read-modify-write onsouveraine-<arch>.dbunder a shared archdev flock (repo-addreplaces the same-pkgname entry, so old versions drop out), uploading its own packages plus the refreshed db and deleting only its own superseded assets. Both jobs areruns-on: archdev, so one flock serialises them.Two things that make it easier than expected: the archive key
3CD9E99E…C8E911is already a secret key in casey's gpg keyring on archdev and souveraine's CI signs with it there, so kernel.yml needs no new signing secret; and~/.git-credentialson archdev holds bothoauth2:andcasey:gitea entries. Pixel3Arch CI does still need a token that can write toFimeg/souveraine. - The job has never once passed. 11 runs, 11 failures, none since
Jul 14;
-
squeekboard + stevia are Pixel3Arch packages — once the kernel job is generalised into a "build these packages, sign, publish" job, they are extra entries in it, not new pipelines.
-
firefox-arm publishes into the same db instead of standing alone.
-
Phone wiring lives in
rootfs-overlay/, never only on the device — see "What bit us" below.
Signing key
One archive key signs the whole database; a pacman db cannot coherently mix
signers. Pixel3Arch's CI needs the same $ARCHIVE_KEY secret souveraine's CI
uses. The key asserts "SouveraineOS published this," which is true regardless
of which repo built it — so sharing it is correct, not a compromise.
This is the package archive key and is deliberately shared across machines.
It is NOT the machine/agent identity key: those stay unique per device and are
never generated implicitly (SouveraineOS/docs/FEDERATION.md, identity tiers).
Different keyring, different trust domain — do not conflate them.
Phone-side wiring (done 2026-07-24, live)
/etc/pacman.d/souveraine-edge.conf—[souveraine-aarch64],SigLevel = Required DatabaseRequired. Template fromsouveraine/packaging/arch/souveraine-edge.conf,__ARCH__→aarch64./usr/local/libexec/souveraine-pacman-fetch— XferCommand shim; attaches the gitea netrc credential for10.10.20.120only, every other mirror unchanged./etc/pacman.d/souveraine-gitea.netrc—root:alpm 0640, token, NOT in git.pacman-key --add+--lsign-key 3CD9E99E222C2A174986FC9AFF4949AA20C8E911./etc/pacman.conf—XferCommandunder[options],Includeat the end.
Two traps found doing it
DownloadUser+ a customXferCommandare incompatible. Pacman execs the external command asDownloadUserbut hands it a path inside the root-owned sync dir; it only creates the alpm-owneddownload-*staging dir for its INTERNAL downloader. Result:EACCESon every repo, failing on upstreamcore.dblong before the souveraine repo is reached. UnsetDownloadUser.DisableSandboxFilesystem/DisableSandboxSyscallsare not pacman options. They were present in the phone'spacman.conf, silently ignored, and leftpacman -Sybroken since ~2026-07-23. pacman 7.1 has exactly one:DisableSandbox— and it governs landlock, not the privilege drop, so it is not the fix here either.
What bit us — why the overlay matters
The phone wiring existed once, was configured live on the device, and was lost
at the next reflash. provision-rootfs.sh copies rootfs-overlay/ wholesale
into a freshly extracted rootfs; anything not represented there does not
survive. The phone is Casey's daily driver — a reprovision costs him his live
system, which is exactly why the repo pipeline matters: it replaces reflashing
with in-place updates.
Next stage — the shell surfaces, settings included
Casey 2026-07-24: souveraine-settings should become a versioned app, and the state-manager settings should live in it.
Settings is QML, not a binary — surfaces/quickshell/settings.qml, run via
qs -p. On the phone it sits in ~/.config/quickshell/souveraine/, unowned,
delivered by rsync + deploy.sh. So "version the settings app" is really
package the shell surfaces, and it is harder than the binaries were:
- A package cannot own
$HOME. The tree would install to a system path (/usr/share/quickshell/souveraine/) and the shell's resolution + the composed-tree symlinks (souveraine/→ stockii/) must survive the move. Seeunify-shell-trees-laptop-phone.md, which is the same tree problem. - It is all-or-nothing: splitting settings out alone puts one QML tree behind two delivery mechanisms.
deploy.shbecomes the dev loop, not the delivery path.
Ordering, and why. TASK-19's rule is that every control is a view over the owning service — "no success-shaped switches". The device state machine is still observational (it logs transitions and confidence, drives nothing; idle timeouts remain hypridle's 300s/600s). Settings pages over it would be lying until TASK-15/08 give it actuators. But this is not a hard block on all of it: charge control is real now (TASK-08b, unblocked tonight) and has a live sysfs + D-Bus backend, so a battery/charge page can be honest today.
So: package the surfaces → expose what already actuates (charge, existing pages) → add state-machine controls as TASK-15/08 wire them up.
2026-08-11 — an update is offered to a body, not pushed through its glass
Observed on the Pixel daily driver: deploy.sh --phone passed its lock check,
snapshotted the tree, and still replaced the live QuickShell composition while
Casey was using it. The reload killed an active Souveraine conversation. A
rollback makes the files recoverable; it does not make destroying live session
state an acceptable delivery protocol.
Casey's required development shape is now explicit:
- CI publishes a signed, immutable surface candidate. The laptop never rsyncs that candidate directly over a live body's active tree.
- A connected-device channel may announce that a development update is available and safe to evaluate. USB Signaller is a possible transport for that offer; it is not the update authority and attachment is not consent.
- Each body answers independently. Its local updater acquires the durable maintenance lease, verifies the package and target profile, stages beside the running candidate, and reports what would change.
- Activation is an explicit local transition with conversation/session state preserved or deliberately quiesced, a health check, and an automatic return to the last working candidate. One connected device accepting must not make every connected device switch.
deploy.sh --phone remains a recovery/developer instrument for an explicitly
quiesced target. It is no longer the ordinary update path. TASK-08 owns the
lease and state projection; the updater owns retrieval and candidate
activation; USB Signaller can carry presence and offers only.
The actual inventory (measured 2026-07-25)
379,153 files under /usr /etc /opt /boot; 5,624 owned by nothing. The
same disease as the eleven binaries, just not yet colliding: 16
/usr/bin/blueline-* scripts, the gadget-* set, clatd, mmsdtng,
qmic, qrtr-*, rmtfs, a hand-installed Qt6Contacts cmake tree,
libcmatrix.so.0u (the ldconfig warning on every transaction), and stale
chatty.orig-pacman / chatty.pacman-orig leftovers.
One of them, blueline-proximity-lock, is not merely unpackaged — it is a
running second authority over proximity. Resolved 2026-07-25: it reports
to sessiond and decides nothing. Only its packaging remains, under TASK-27.
pacman -Syu had been blocked for days by exactly one of these files.
Intersecting the unowned set against the 112,051 files shipped by the 199
pending packages gave a conflict set of ONE:
/usr/include/aquamarine/backend/FrameScheduler.hpp, hand-dropped Jul 9 into
a directory whose every other header belongs to aquamarine 0.12.1-1, and
shipped by extra/aquamarine 0.13.0-2. It was also a version-mismatched
header sitting in the include path the whole time. Backed up to
/var/backups/unowned-preupgrade/ and removed.
Compute conflicts, do not guess at them:
pacman -Fy
pacman -Ql | awk '{print $2}' | sed 's#/$##' | sort -u > owned
find /usr /etc /opt /boot -xdev \( -type f -o -type l \) | sort -u > ondisk
comm -13 owned ondisk > unowned
pacman -Qu | awk '{print $1}' > pending
xargs -a pending pacman -Fl | awk '{print "/" $2}' | sed 's#/$##' | sort -u > newfiles
comm -12 unowned newfiles # <- exactly what will abort -Syu
The phone's login shell is zsh — no mapfile; run this under bash -s
over ssh.
Acceptance
- Pushing to any SouveraineOS repo produces a signed package in
souveraine-{arch}without hand-carrying anything. DONE for every device package except the kernel (2026-07-30; Pixel3Archpackages.yml). - The phone installs kernel, upower, squeekboard, stevia and souveraine from
pacman -Syu. DONE except the kernel — squeekboard/stevia publish alongside the rest; the first real upgrade the pipeline delivered wasusb-signaller 0.3.1-2 → 0.3.1-3. - A reprovision restores the repo wiring from
rootfs-overlay/with no manual steps.
Cross-build shapes (landed 2026-07-30)
Every aarch64 package cross-compiles on the archdev runner against
$HOME/aarch64-sysroot. Four distinct shapes live in packages.yml, each its
own block because the incantation differs:
- arch=any (pulse-native-provider, osk-switch): plain native
makepkg. - meson C (hexagonrpc, tqftpserv): a generated
--cross-file+ sysroot pkg-config. Both PKGBUILDs honourMESON_CROSS_FILEinbuild(). - Makefile C (blueline-edge-sense):
CC="aarch64-linux-gnu-gcc --sysroot=… -L…/usr/lib"; the Makefile honoursCC ?=. - meson+Rust / cargo (stevia, squeekboard via each
build-cross.sh; usb-signaller viaCARGO_BUILD_TARGET+ a per-target cross linker).
Two traps worth not re-deriving: (1) --sysroot alone does NOT link on this
toolchain — ld resolves the sysroot libc.so script's = paths against the
wrong prefix and falls back to the host /lib/libc.so.6 ("file in wrong
format"); an explicit -L$SYSROOT/usr/lib makes ld find the right script
first. (2) the C cross-file is generated inline in the workflow; stevia/
squeekboard's own cross-files (~/build/*.ini) are pets-state, failed loud
if missing.
What broke CI and how it was fixed
First green run was 993. Run 992 failed because stale bare VCS clones and
built *.pkg.tar.zst binaries had been committed (pkgs/stevia/stevia/,
pkgs/squeekboard/squeekboard/, 9 package files incl. a 1.7 MB packfile) —
makepkg rejected the clone as "not a clone of ". Fix: git rm'd the junk,
gitignored *.pkg.tar.zst + the VCS checkout dirs, and set SRCDEST
out-of-tree in CI so clones never land in a package dir. usb-signaller's
PKGBUILD was also made cross-aware (it built for the host tuple before).
Connects to
TASK-24 (keyboard: squeekboard/stevia are two of the packages), the
kernel flash flow in PAF/build.md, and
souveraine/packaging/arch/ which already holds the canonical configs.