swayfx-mosaic/docs/design/STEP20C_HEADLESS_BOOT_GATE2.md

19 KiB
Raw Blame History

STEP20C — Gate 2 headless boot preparation

Status: EXPERIMENTAL. V4 pristine: FAIL before compositor; V4 CORE: NOT_RUN. V5 native: PRISTINE_ALLOCATOR_BLOCKED / CORE_NOT_RUN. The separately approved single V5 sequence consumed its pristine reservation; CORE was never attempted. No sequence PASS, private IPC or rendering proof was obtained. Historical preparation verdicts below are pre-attempt evidence. Any future native attempt requires a new LAB, budget and explicit approval.

V5 first native outcome — 2026-10-17

The reviewed V5 fixture ran once from the approved app.slice launcher after preflight of 12 fixture hashes, 92 manifested files and 98 ELF identities. The supervisor returned REFUSED: pristine failed: CORE NOT_RUN. The retained pristine receipt records verdict FAIL, elapsed 0.361879 seconds and oracle null. The scope executed the Python child and native pristine SwayFX binary: unlike V4, the failure occurred after compositor execution. The mandatory PWD=/tmp correction passed that earlier environment guard.

The relevant diagnostic messages, in order, are:

MESA-EGL: warning: Not allowed to force software rendering when API explicitly selects a hardware device.
[wlr] [render/allocator/udmabuf.c:148] Failed to open /dev/udmabuf: No such file or directory
[wlr] [render/allocator/allocator.c:168] Failed to create allocator
[sway/server.c:308] Failed to create allocator
CHILD REFUSED: compositor exited before private IPC

The immediate failure is allocator creation with the host device absent from the private /dev. The Mesa warning is not established as fatal. Source analysis must distinguish software EGL rendering from allocation of render targets; no effective capability masks were logged. See the offline allocator triage. There is no private IPC, output oracle or rendering evidence. CORE is NOT_RUN, with no ATTEMPT receipt; no result transfers to that binary.

Post-attempt independent observations found both V5 scopes not-found/inactive, their cgroups absent and the pristine runtime empty. Stable SwayFX, QuickShell and greetd remained active with their original process identities. These are historical post-run observations, not authority to manipulate those services. The V5 LAB, raw stdout/stderr, result, units and reservation ledger are sealed historical evidence: never edit or reuse them and never retry pristine there. Only this sanitized documentary summary is published; raw receipts stay private.

V4 native failure and prospective V5 repair — 2026-10-17

The approved V4 pristine attempt at 06:09 CEST failed with REFUSED: pristine failed: CORE NOT_RUN. Its raw logs/pristine.stderr.raw contains CHILD REFUSED: unexpected sandbox environment. The strict comparison dict(os.environ) == CHILD_ENV rejected the Python child before its first compositor Popen. No pristine or CORE compositor, native IPC/socket or rendering was executed. The pristine reservation is consumed and must never be retried in /var/tmp/swayfx-mosaic-step20c-gate2-prep-qglkg45r; CORE was not reserved. The retained result records unit LoadState=not-found / ActiveState=inactive and the cgroup was absent. Historical preparation verdicts below describe pre-attempt evidence only; they do not requalify this failure.

The harmless post-failure Bubblewrap diagnostic at /var/tmp/swayfx-mosaic-step20c-gate2-env-diagnostic-uh25ciwe/RESULT.json returned 0 and measured only extra_keys=["PWD"], PWD="/tmp", no missing keys or changed shared values, and strict_equal=false. The local bwrap(1) contract preserves PWD through --clearenv. This is a reproduced environment cause, not a second native attempt.

V5 will use a new exclusive mode0700 LAB, fresh scope identities and empty reservation ledger. Its sole functional correction makes PWD=/tmp mandatory in CHILD_ENV and explicitly emits --setenv PWD /tmp after --clearenv, keeping --chdir /tmp and the complete dictionary equality guard. V4 and older LABs, including raw failures, remain byte-for-byte unchanged. Offline tests and a harmless Bubblewrap child must pass, as must the read-only plan from Desktop Commander under app.slice and independent review. No compositor may run in this correction mission; a new conversational approval is required for V5. STEP20C remains NEXT; production multi-output NOT_YET_SUPPORTED.

Scope and authority

Prepare a symmetric comparison of the retained pristine SwayFX 0.6 and private Mosaic CORE-C V2. The supervisor stays outside the sandbox; its child enters an ephemeral delegated systemd user scope under app.slice, then Bubblewrap, then the fixed binary and private IPC checks. Reuse the combined benign proof at /var/tmp/swayfx-mosaic-step20c-combined-benign-z4wk7gog: its 22/22 checks qualify that benign child only. Do not reopen historical scientific ledgers.

The original preparation scope excluded changes to production source, C API/ABI, SceneFX patches, packages, UX, Stable compositor, QuickShell, greetd and UWSM. It prohibited installation, session switching, compositor execution (including --version) and live Sway IPC. The later single V5 native sequence had separate approval, now consumed. The current documentation-only mission authorizes a reviewed commit and pushes to origin then github; it authorizes no new compositor or sandbox expansion. Architecture and UX contracts remain unchanged. STEP20A DONE; STEP20B EXPERIMENTAL; STEP20C NEXT; STEP20D–G PLANNED; production multi-output NOT_YET_SUPPORTED. Headless software evidence cannot qualify DRM/GPU, G4, fullscreen, buffer lifetime, complete effects/damage or daily use.

Exact identities and passive preflight

Profile Retained ELF under /var/tmp/swayfx-mosaic-corec-v2-i516p6po SHA256 Build ID
pristine controls/pristine/sway b7fa32116e3c42706b9b02da7df12f27e76d82dd253deb8bdec81519eed145f6 ba9751c82e73d4043b7651354f20936d070c2227
core builds/core-mosaic/sway/swayfx-mosaic 91f48aae2675331e0a2c60ec2fedf983745c521c75232c704a6516a4140801fb d5d304533f2c8db9d738365e9bd7f2c96779ef8d

Pristine dynamically needs libscenefx-0.5.so. CORE embeds private SceneFX and must have no SceneFX DT_NEEDED. Both use system libwlroots-0.20.so with SHA256 7edca9a7d65855addda68e29ba5db94d57c803eb5fa7d0f86db3136b90ca9837. The main build and diagnostic OOM/GPU variants are not substitutes.

Inspect UID, mode, link count, size, SHA, Build ID, ELF class/machine, interpreter, DT_NEEDED, RPATH/RUNPATH and symbols with readelf, never ldd or the target loader. Resolve the transitive library closure passively under /usr/lib, recording real paths and hashes. Inspect EGL vendor JSON, swrast DRI module, XKB and font data separately: DT_NEEDED is not the dlopen/data closure. Missing or drifted inputs are refusals, not permission to rebuild, install or change the binary. Local ABI evidence is exact-pair only; passive resolution does not prove live relocation, renderer initialization or future update compatibility.

Environment and containment contract

The manager control plane alone uses an explicitly reconstructed user bus address. Desktop Commander must be measured under user@1000.service/app.slice; a launcher under session-6.scope is rejected. No PID from a historical report is an authority for the new run. Verify manager Delegate=yes, active app.slice, writable common-ancestor cgroup.procs and memory/pids controllers before spawn.

Bubblewrap uses --unshare-user --unshare-all --disable-userns, --new-session, --die-with-parent, --clearenv; /usr read-only, /bin /lib /lib64 links into /usr, private /proc and /dev, private tmpfs /tmp and /run. Bind only the sealed LAB input read-only and run-specific LAB output/runtime writable. No host HOME, /run/user, /sys, display, seat, bus, configuration, network namespace, /dev/dri or /dev/input is exposed. Close all inherited FDs other than explicit stdio; stdio uses /dev/null and bounded pipes, never desktop sockets or a TTY. The private runtime directory is UID1000 mode0700; all IPC paths belong there.

Do not bind host /etc wholesale. Candidate minimal data are exact EGL vendor JSON (normally under /usr/share/glvnd/egl_vendor.d), /usr/share/X11/xkb and /usr/share/fonts. Fontconfig may require a private minimal fonts.conf referring only to /usr/share/fonts and LAB cache, supplied through FONTCONFIG_FILE. No host NSS/password, machine-id, seat or ld.so cache is needed by assumption: any missing requirement must be observed and reviewed, never silently exposed. /proc/sys and same-UID hostile peers remain security limitations, not a claim of a hardened general execution service.

SceneFX directly calls fx_renderer_create, not wlr_renderer_autocreate. WLR_RENDERER=pixman is therefore not evidence of effective Pixman. The source supports WLR_RENDERER_FORCE_SOFTWARE=1, passing DRM FD -1 into EGL software device selection. Candidate environment: WLR_BACKENDS=headless, WLR_HEADLESS_OUTPUTS=2, WLR_RENDERER_FORCE_SOFTWARE=1, WLR_RENDERER_ALLOW_SOFTWARE=1, LIBGL_ALWAYS_SOFTWARE=1, XWAYLAND disabled by configuration. Effective EGL/renderer/allocator success remains NOT TESTED. No automatic GPU fallback or host device exposure is allowed.

Proposed budget, supervisor and oracle

These are bounds to approve later, not consumed reservations: exactly one pristine attempt, then exactly one core attempt only if pristine passes all checks; no retry. At most two 800x600 headless outputs; 30 seconds per run, TasksMax=64, MemoryMax=1GiB, MemorySwapMax=0, TimeoutStopSec=2s, KillMode=control-group. Both stdout and stderr are drained concurrently with 16MiB maximum per channel; limit overflow or partial output fails the run. Capture budget zero for the minimal IPC boot. Missing proof never means PASS.

Before spawn, journal exact argv, hashes, unit name, unique description, fresh attempt identity and expected cgroup. Reject existing unit, directory, socket or attempt receipt. Persist atomically with fsync before execution. Record Popen-owned PID, startticks and pidfd; match manager InvocationID, description and ControlGroup before any stop. Never kill by guessed/recycled PID, never stop a foreign unit, and never adopt a pre-existing unit. Unknown ownership fails cleanup rather than widening authority. All pidfds/pipes must close. On timeout/cancellation stop only the proven invocation; verify collection, empty cgroup, exited owned processes and absent LAB sockets. Do not rerun.

Inside the sandbox the minimal client is /usr/bin/swaymsg with an explicit -s /runtime/sway-ipc.sock; get_version, get_outputs and get_tree are read-only. The bootstrap must verify socket UID/type/path and the private Wayland socket; no discovery via inherited SWAYSOCK is allowed. Require exactly two active 800x600 outputs and a valid tree/version for each binary; compare symmetric fields without requiring equal Mosaic-specific metadata. Request exit only on the proven private socket, distinguish an exit-IPC disconnect from the compositor exit status, and independently verify cleanup. No popup, fullscreen, G4 or graphics capture is required. An empty workspace does not qualify Mosaic layout behavior, only the minimal native boot/IPC path.

Refusals, tests and decision

The source-only fixture lives in the new private LAB until independent review justifies publication. Its default is a read-only plan: no directory creation, manager mutation, Bubblewrap, socket connection or native target execution. A future launch requires its dedicated flag after separate conversational approval; tests cannot grant that approval. Test production predicates with fake manager/cgroup/procfs and private temporary files, including session scope, wrong ELF/ABI/Build ID, inherited environment/socket, forbidden mounts, unit collision, InvocationID mismatch, recycled PID/startticks, timeout and partial output. No real scope or compositor in offline tests.

Gate Required evidence Preparation status
Exact binaries and passive closure ELF_DEPENDENCIES.json and raw inspection PASS, passive only
Combined benign isolation Prior immutable receipt and 22 checks PASS, benign only
Current launcher and environment Fresh observations and frozen sandbox spec PASS for preparation
Fixture and refusal tests Frozen source hashes, raw tests, independent review PASS, V4 / 27 offline tests
V4 pristine native boot/IPC/cleanup One reservation consumed; refused before compositor FAIL before compositor
V4 CORE native boot/IPC/cleanup Pristine failed; no reservation NOT_RUN
V5 pristine native boot/IPC One approved reservation consumed; allocator failure before IPC PRISTINE_ALLOCATOR_BLOCKED
V5 CORE native boot/IPC Pristine failed; no ATTEMPT CORE_NOT_RUN
V5 post-attempt cleanup Independent scope/cgroup/runtime observations Absent / empty, sampled
Physical GPU, FS/G4 and full STEP20C Outside this tranche NOT TESTED

The private LAB must contain GATE2_PREP_REPORT.md, ELF_DEPENDENCIES.json, SANDBOX_SPEC.md, NATIVE_RUN_REVIEW.md, APPROVAL_GATE2.md, validation logs, SHA manifest and before/after observations. Preserve raw failures. Record the final offline verdict here after validation. Only all closed preparation gates permit GATE2_NATIVE_BOOT_READY_FOR_APPROVAL; otherwise use GATE2_BOOT_PREP_BLOCKED_WITH_EVIDENCE. Those labels describe preparation only; the later V5 native failure above supersedes NOT_RUN for pristine, without qualifying boot/IPC.

Historical preparation evidence — before the V4 attempt

The private source-only fixture is retained in /var/tmp/swayfx-mosaic-step20c-gate2-prep-qglkg45r/fixture, not published as a production tool. FIXTURE_SHA256SUMS_V3 binds eight files. Independent passive review rechecked 96 main ELF entries, 46 supplemental tool entries (overlapping), 1409 data files and all 14 files bound by the prior combined benign receipt. The current fixture inspects 98 unique ELF files, including its Python child. Retained source manifests match 85 SceneFX and 417 Mosaic files. No rebuild or new binary reproducibility claim follows.

The default V3 plan passes from Desktop Commander under app.slice (exit0, launcher_error null) and refuses the Codex session-scope launch path. Both remain PREPARATION_ONLY / native NOT_RUN. The planned native scope uses RuntimeMax25s within the supervisor's 30s total bound, preserving cleanup time.

V2's independent review found an uncollected systemctl-stop auxiliary on its timeout path. The bounded V3 repair reserves one second within the existing deadline and finalizes only the exact owned Popen. Its delta review passes; five independently executed mocked methods pass, and the separate tester passes all 23 offline tests plus eight before/after fixture hashes and AST/local style checks. Raw initial failures and V2 evidence remain retained. Ruff/Black are unavailable and were not installed. Native boot, new sandbox mounts, EGL/renderer/allocator and real cleanup remain NOT TESTED.

The LAB contains the sandbox specification, exact future approval commands, passive inventories, raw logs, review records and final preparation report. The frozen fixture README records its pre-review state; NATIVE_RUN_REVIEW.md and GATE2_PREP_REPORT.md carry later review outcomes without rewriting that sealed source set. The final preparation verdict is recorded below after the pre-freeze audit. No native approval or reservation has been consumed.

Historical V4 preparation outcome (superseded by native refusal)

GATE2_NATIVE_BOOT_READY_FOR_APPROVAL. Native Gate 2 NOT_RUN. The unique deep audit on V3 found an additional passive-child finalization defect in inspect_command on exceptions/cancellation. The single bounded final repair V4 reserves 0.25s inside the existing deadline, finalizes only its owned Popen and reports unproven collection explicitly. Four targeted methods were independently exercised; bounded delta review passes and the separate tester passes all 27 tests and eight V4 hashes before/after. The default V4 plan passes through Desktop Commander with no native execution. Historical V2/V3 failures and the failed V3 audit remain intact; no second deep audit occurred.

Two NON_BLOCKING observations persist: internal IPC/exit auxiliaries rely on the enclosing scope/PID namespace after exceptional local teardown, and the full attempt chain is not simulated end-to-end. They are not runtime evidence. The LAB's APPROVAL_GATE2.md binds the exact V4 fixture, future command and unchanged two-profile identities. No native authorization follows from this preparation verdict; the first native attempt still requires separate approval.

Historical V5 correction evidence — before the native attempt

New exclusive mode0700 LAB: /var/tmp/swayfx-mosaic-step20c-gate2-v5-ge8wddpa. The imported V4 fixture passes all eight source hashes. The sole functional change adds mandatory PWD=/tmp to CHILD_ENV, automatically emitted as an explicit --setenv after --clearenv. child.py, its complete environment equality and --chdir /tmp remain unchanged. The new LAB and scope identities are V5-only; no historical ledger was imported or reopened.

The targeted suite passes 31 tests (27 inherited, four new). The harmless Bubblewrap child returns 0, strict_equal=true, PWD=/tmp, cwd=/tmp, no extra or missing keys and no changed shared values. It exercises child.main's guards with every Popen replaced by a refusal at the native boundary. The sandbox argv/environment prefix is exact V5; only the final Python script/profile is replaced with a harmless -c payload. All seven namespaces differ from host; mount/device/FD/network checks pass. The owned child is reaped with pidfd confirmation, transient directories removed and no remaining sockets. This qualifies the environment correction only, not a native boot.

The read-only plan passes from Desktop Commander under app.slice (rc0, launcher_error=null; 98 ELF inspected passively). The Codex session-scope refusal is retained (rc1). No real scope or IPC was created by these checks. HISTORICAL_COMPARISON.json confirms the five historical LAB trees unchanged. Before the native attempt, V5 receipts ATTEMPT-pristine.json and ATTEMPT-core.json were absent. The later pristine reservation is consumed.

REPORT_V5.md, APPROVAL_V5.md, ENV_DIFF_V5.json, BENIGN_ARGV_V5.json, FIXTURE_SHA256SUMS_V5 and raw logs are retained in that LAB. The independent audit passes with no BLOCKING finding. Its NON_BLOCKING harmless-helper pidfd acquisition cleanup observation received a bounded repair and two mocked failure subcases; the targeted final suite now passes 32 test methods. A fresh harmless Bubblewrap run also passes on the repaired helper. The independent repair-delta review passes (12 source hashes, one mocked method/two failure subcases). The final source/evidence manifests and FINAL_VALIDATION_V5.json retain the closed checks. Verdict: GATE2_V5_READY_FOR_NEW_NATIVE_APPROVAL. The read-only reviewer return is transcribed by the parent in review/REPAIR_REVIEW_V5.md; the unique audit report remains intact. At that preparation stage no native V5 execution was authorized. The separately approved V5 attempt is recorded above: pristine allocator FAIL, CORE NOT_RUN. V4 pristine remains FAIL before compositor; V4 CORE NOT_RUN. STEP20C remains NEXT; production multi-output NOT_YET_SUPPORTED.