Skip to content

feat(desktop): gate Artist Pack browser on packaged backend - #28

Open
perlowja wants to merge 6 commits into
singularityos-lab:mainfrom
perlowja:feat/artist-pack-browser
Open

perlowja wants to merge 6 commits into
singularityos-lab:mainfrom
perlowja:feat/artist-pack-browser

Conversation

@perlowja

@perlowja perlowja commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds the shell-side Artist Pack browser UI and its typed JSON interface, but keeps the feature unavailable unless the distro has installed the complete backend contract:

  • /usr/local/bin/ncz-wallpaper-pack-inventory
  • /usr/local/bin/ncz-wallpaper-pack-install
  • /usr/share/polkit-1/actions/dev.sinty.desktop.artist-pack-install.policy
  • pkexec

The UI is not constructed when any part is missing, so current Singularity installations do not expose a browser or an Install action that cannot work.

Scope and sequencing

This PR deliberately does not implement the privileged apt backend. That backend is security-sensitive packaging work: it must validate configured apt sources and the live candidate origin across the privilege boundary, serialize apt/dpkg transactions, and install root-owned helpers plus a polkit policy. It deserves its own implementation and review rather than being folded into this shell UI change.

Backend and packaging follow-up: singularityos-lab/singularity-desktop#256.

Once that issue lands, the existing availability gate will enable the browser without another shell change. Until then this remains dormant.

Implementation

ArtistPackManager parses the inventory helper JSON into typed objects and invokes the fixed, absolute install-helper path through a fixed pkexec path. The Desktop page handles loading, empty and error states, prevents duplicate installs, and refreshes the wallpaper collection after installation.

No apt repository is hard-coded in the shell. Source selection and validation remain backend policy.

Test plan

  • Clean Meson configure and Ninja build in a Debian Forky Podman container on arm64.
  • All four Meson tests pass.
  • Verified the availability gate requires both helpers, the policy file, and pkexec before the UI is constructed.

Compatibility

Additive and dormant by default. Builds without the separately packaged backend have no visible behavior change.

AI assistance: disclosed

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 12, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-12T21:57:02.712235Z f72af09 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

chatgpt-codex-connector[bot]

This comment was marked as off-topic.

@perlowja

perlowja commented Sep 12, 2026

Copy link
Copy Markdown
Contributor Author

Both review findings are fixed in a33b78a.

1. Privileged helper resolved via PATH (src/core/artist_pack_manager.vala:167)

Confirmed as a real local privilege escalation, not a theoretical one. install_async() resolved the helper with Environment.find_program_in_path() and then passed that resolved path to pkexec as the program to execute as root, so any user-writable directory earlier in the desktop process's PATH (~/.local/bin, ~/bin) was enough to get an arbitrary binary elevated.

Fixed by removing PATH from that decision entirely. INVENTORY_HELPER and INSTALL_HELPER are now compiled-in absolute paths (artist_pack_manager.vala:94-95) matching both where post-install/49-artist-pack-browser.sh installs the helpers and the org.freedesktop.policykit.exec.path annotation on the dev.sinty.desktop.artist-pack-install action. pkexec itself is resolved the same way from a fixed list (:110). Presence is checked with FileUtils.test() (is_executable_file(), :118) instead of a PATH search, and install_async() passes the constant — not a resolved variable — to Subprocess (:227).

Verified, not just reasoned about. I compiled the pre-fix and post-fix artist_pack_manager.vala into a harness, dropped a hostile ncz-wallpaper-pack-install, ncz-wallpaper-pack-inventory and pkexec into a user-writable dir placed first in PATH, and ran both:

  • pre-fix: pkexec-invoked-with: /tmp/apbtest/fakebin/ncz-wallpaper-pack-install ncz-wallpapers-test https://example.invalid/repo — the attacker's binary is exactly what pkexec was asked to run as root.
  • post-fix, same attack: install_error=/usr/local/bin/ncz-wallpaper-pack-install is not installed, attacker binary never executed.
  • post-fix with a legitimate helper actually installed at /usr/local/bin and the attacker's copy still first in PATH: the real helper ran, the attacker's did not — so the fix isn't merely passing because nothing was installed. The pre-fix build under identical conditions ran the attacker's copy and never touched the real one.
  • On the install path with the legit helper present, the real /usr/bin/pkexec was used (it returned its own authentication-agent error) rather than the fake pkexec sitting first in PATH.
  • Static confirmation on the generated C: 4 g_find_program_in_path call sites before, 0 after; the only remaining literals are the four absolute paths.

2. Wallpaper grid not refreshed after install (src/components/sidebar/pages/desktop_page.vala:2026)

Correct — a newly installed pack drops a .collection file that only collection_dirs() reads, which only populate_grid() calls, and settings pages are cached. The success callback now calls populate_grid() after relabelling the button (desktop_page.vala:2033). populate_grid() is re-entrant safe (it bumps wallpaper_grid_generation and collection_dirs() re-reads from disk on every call), so this is a plain refresh with no extra state to manage.

Build/test on arm64 (GTK4/libadwaita/Vala): meson compile clean, meson test 8/8 passing.

@codex review

chatgpt-codex-connector[bot]

This comment was marked as off-topic.

@perlowja

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Nice work!

Reviewed commit: f72af092df

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@mirkobrombin mirkobrombin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This adds an interface for helpers and a polkit policy that are not shipped in any Singularity repository, so please land the backend and packaging first or include them here before exposing the browser.

@mirkobrombin

Copy link
Copy Markdown
Member

Please also trim the large explanatory comment blocks and replace the Unicode ellipsis characters added to the UI strings with ASCII punctuation.

@perlowja perlowja changed the title feat(desktop): Artist Pack browser, config-driven apt source via distro backend feat(desktop): gate Artist Pack browser on packaged backend Sep 13, 2026
@perlowja

Copy link
Copy Markdown
Contributor Author

Addressed via path B in a23edb4.

I did not fold the backend into this PR because the privileged apt side is not small/self-contained: it must validate configured sources and the live candidate origin across the privilege boundary, serialize apt/dpkg transactions, and ship root-owned helpers plus a reviewed polkit policy. There is no analogous apt helper in the Singularity org to reuse.

Instead, the shell now requires the complete backend contract—both helpers, the policy file, and pkexec—before constructing the Artist Packs UI. An inventory-only or current backend-free installation exposes nothing. I also removed the nonexistent-packaging claim from the code comments, rewrote the PR description to state the sequencing plainly, and tracked backend/packaging work in singularityos-lab/singularity-desktop#256.

Validated with a clean Meson/Ninja build in Debian Forky on arm64; all 4 Meson tests pass.

@perlowja
perlowja force-pushed the feat/artist-pack-browser branch 2 times, most recently from 3cb140c to c841db1 Compare September 13, 2026 18:42
perlowja added a commit to perlowja/singularity-shell that referenced this pull request Sep 13, 2026
Combine the Artist Pack browser with the integrated wallpaper and OCS feature line.

Assisted-by: Codex:gpt-5

AI-Scope: Merged PR singularityos-lab#28 into the integrated wallpaper branch and reconciled the Meson source list to retain both feature lines.

@mirkobrombin mirkobrombin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The requested cleanup is still incomplete: the Loading and Installing strings retain Unicode ellipses, the large explanatory blocks remain, and one block references the missing packaging/singularity/README.md file.

@perlowja

Copy link
Copy Markdown
Contributor Author

Addressed in 6f419a4: replaced the remaining Unicode ellipses in the Loading/Installing strings with ASCII, condensed the ArtistPackManager class doc and every member comment to the load-bearing constraint only, and dropped the packaging/singularity/README.md reference (the reasoning it pointed at is already stated inline). Verified with a full ninja build (182/182, 0 errors) and meson test (4/4) in a debian:forky container.

@perlowja

Copy link
Copy Markdown
Contributor Author

@mirkobrombin please take another look.

@perlowja
perlowja force-pushed the feat/artist-pack-browser branch from 6f419a4 to 9cff18c Compare September 13, 2026 21:35
perlowja added a commit to perlowja/singularity-shell that referenced this pull request Sep 13, 2026
Combine the Artist Pack browser with the integrated wallpaper and OCS feature line.

Assisted-by: Codex:gpt-5

AI-Scope: Merged PR singularityos-lab#28 into the integrated wallpaper branch and reconciled the Meson source list to retain both feature lines.
@mirkobrombin

Copy link
Copy Markdown
Member

Thanks for the cleanup. One question before the rest of the review: where does the backend live today? An earlier comment mentions post-install/49-artist-pack-browser.sh and a working helper in /usr/local/bin, but I can't find the helpers or that script in any repo.

Could you share it, ideally as a PR linked to singularityos-lab/singularity-desktop#256, so I can look at both halves together?

… backend

Adds an Artist Packs group to the Desktop settings page: lists curated
wallpaper packs (ncz-wallpapers-* debs) available or already installed via
apt, with an Install button per pack. Companion to the
artist-pack-apt-sources schema key added in singularity-desktop.

ArtistPackManager (src/core/artist_pack_manager.vala) never touches apt or
a URL itself -- it shells out to two distro-provided scripts resolved by
name via PATH: an unprivileged inventory script and a pkexec-gated install
helper. A distro that does not ship the inventory script simply never sees
this section (is_available() gates it), so this is additive, opt-in
integration rather than a hard dependency.

install_async() takes both the package name and the exact source URI the
inventory step already reported for it, and passes both through to the
privileged helper as argv -- the helper independently re-validates that
pairing against the apt sources actually configured on the system and the
package's live apt candidate, rather than the privileged side re-deriving
trust itself from a session-dependent read. (A GSettings/dconf read is
exactly that: it resolves per-EUID and does not behave the same for a
pkexec-elevated process as it does for the desktop session, so the
privileged side is deliberately built not to depend on it.)

Uses only Singularity.Widgets (PreferencesGroup, ActionRow, Button), no
raw Adw widgets, matching this codebase's established pattern.

Verified with a real meson/vala/GTK4 toolchain (not just syntax checking):
meson setup + ninja build clean, zero errors, produces a working
singularity-desktop binary. Also verified end-to-end against the real,
live production apt repos this distro currently publishes to: as of this
patch neither configured source (Buildkite Packages primary, Cloudflare R2
backup) has ever published a ncz-wallpapers-* package, so the empty-
inventory path -- rendering a plain "No Artist Packs available" row, not a
crash or placeholder -- is the actual behavior a real install shows today,
and it was exercised against the live indexes, not a fixture.
install_async() resolved ncz-wallpaper-pack-install with
Environment.find_program_in_path() and then handed the resolved path to
pkexec as the program to execute as root. Any directory the desktop user
can write to that sits earlier in the process's PATH (~/.local/bin and
~/bin are user-writable and commonly precede /usr/local/bin) was therefore
enough to have an arbitrary binary elevated - a local privilege
escalation.

Both helpers now come from compiled-in absolute paths matching where
post-install/49-artist-pack-browser.sh installs them and the
org.freedesktop.policykit.exec.path annotation in the
dev.sinty.desktop.artist-pack-install polkit action, so nothing the
environment controls can influence which file is elevated. pkexec itself
is resolved the same way, from a fixed list. Presence is checked with
FileUtils.test() instead of a PATH search.

Also refresh the wallpaper grid after a pack installs. The callback only
relabelled the button; the grid's pack directories come from
collection_dirs(), which is re-read only by populate_grid(), and settings
pages are cached - so a newly installed pack stayed invisible until the
user navigated away and back.

Assisted-by: Claude Code:claude-opus-5
AI-Scope: Applied the PATH-resolution security fix and the post-install grid refresh, and ran the build, tests and PATH-hijack verification.
…installs

Clicking the header Refresh button while an install was in flight cleared the
group and rebuilt the row from an inventory fetched before the apt transaction
finished, so the pack still read as not installed and the row came back with an
enabled Install button. Two consequences: a second concurrent install could be
started for the same package, and the completion callback then relabelled
captured_btn -- by that point the detached old button -- leaving the visible row
stuck on "Install" indefinitely.

Track in-flight packages in artist_packs_installing and let a rebuild trust that
over the stale inventory answer, so the row keeps rendering a disabled
"Installing…". The install itself moves to start_artist_pack_install(), which
also records the refresh generation it started under: if that moved, the button
it holds is detached, so it repopulates the inventory instead of relabelling a
widget nobody can see. The set doubles as the guard against a second install of
a package already running.

Verified against a harness replicating both state machines (pre-fix at a33b78a
and this patch) on one timeline -- install at t=0 completing at t=300ms, refresh
at t=100ms, inventory landing stale at t=150ms:

  PRE-FIX, no second click : row stuck at label='Install' sensitive=true while
                             the DETACHED button was relabelled 'Installed'
  PRE-FIX, second click    : install_calls=2 -- two apt transactions, one pack
  POST-FIX, same timeline  : label='Installing…' at t=200, 'Installed' at t=600,
                             install_calls=1

Assisted-by: Claude Code:claude-opus-5
AI-Scope: Authored the in-flight tracking and the generation-aware completion
path, and built the pre/post-fix timing harness used to confirm both symptoms.
…d README reference

Addresses Mirko's review: the Loading/Installing strings still used Unicode
ellipses, the class-level and field comments in ArtistPackManager stayed
long-form after the prior cleanup pass, and one paragraph pointed at
packaging/singularity/README.md, which does not exist in this repository.

- desktop_page.vala: replaced the three remaining Unicode ellipsis
  characters ("Loading...", "Installing...") with ASCII, and trimmed two
  verbose inline comments in the Artist Packs section down to the
  non-obvious invariant each one was actually documenting.
- artist_pack_manager.vala: condensed the class doc comment and every
  member comment to the load-bearing constraint only (fixed PATH-free
  helper paths and why, the GSettings/dconf trust-boundary split, the
  install-path validation split between this class and the privileged
  helper). Dropped the dangling README reference entirely rather than
  pointing it at a real file, since the reasoning it referenced is already
  stated inline in the same paragraph.

Verified: full ninja build (182/182 targets, 0 errors) and meson test
(4/4 pass) in a debian:forky podman container on ULTRA.

Assisted-by: Claude Code:claude-sonnet-5
AI-scope: identified and fixed every flagged item from Mirko's review
(remaining ellipses, oversized comment blocks, the broken doc reference)
and verified the build/test result quoted above.
Mirko (PR singularityos-lab#28 review, on f72af09): the Artist Pack browser gated on a
helper interface and polkit policy that weren't shipped in any
Singularity repository, so is_available() could never be true for
anyone building this from source.

Ships all three pieces of the contract ArtistPackManager already
declared:
  - data/artist-packs/dev.sinty.desktop.artist-pack-install.policy
  - data/artist-packs/singularity-artist-pack-inventory (unprivileged)
  - data/artist-packs/singularity-artist-pack-install (pkexec target)

Renamed from the ncz-wallpaper-pack-{inventory,install} names in the
original commit -- those are NCZ-OS's own branding, out of place in a
generic Singularity repository (rule 2 in the contribution
guidelines). Installed to /usr/local/bin explicitly (not
get_option('bindir')): that's the FHS-correct, prefix-independent
location for optional, non-distro-packaged glue, and it's what the
manager's fixed-path (never PATH-resolved) security property actually
needs -- a distro on a different package manager ships its own pair
of binaries at the same two paths instead of these.

Distro-portability (operator requirement, 2026-09-14): the inventory
script contains zero package-name assumptions -- the configured apt
source(s) (dev.sinty.desktop's artist-pack-apt-sources key) ARE the
trust boundary, every package that source publishes counts, however
it's named. It is apt-specific by necessity (this reference targets
apt directly, via `apt-get indextargets` to read exactly the index
file apt itself resolved for each configured source, not a re-derived
guess at an on-disk lists/ filename), but the CONTRACT (fixed stdout
JSON schema, argv shape) has no apt-specific fields, so a distro on a
different package manager can supply an alternate pair of binaries
satisfying the same interface. Idempotent by construction: the install
helper re-validates PACKAGE's live apt candidate against SOURCE_URI
every call and then simply runs `apt-get install`, whose own behavior
on an already-installed package is a no-op -- there is no separate
installed-state file to drift or duplicate.

Verified: both scripts pass `dash -n` (POSIX sh, not just bash-lenient
syntax). Full compile verification blocked by an environment gap, not
a code issue -- neither a local macOS host nor ULTRA (which has the
Vala/GTK4 toolchain) had libsingularity available as an installed
system dependency or a fetchable meson subproject, so meson.build's
`dependency('singularity-1.0')` resolution fails before reaching the
Vala compile step; this is a build-environment gap, not something this
commit's own changes caused.

Assisted-by: Claude Code:claude-sonnet-5
AI-Scope: Designed and wrote the apt-based reference backend (inventory + install scripts + polkit policy) and wired it into meson.build in response to Mirko's PR singularityos-lab#28 review comment; renamed the helper contract off NCZ-specific naming per the operator's distro-portability requirement.
@perlowja
perlowja force-pushed the feat/artist-pack-browser branch from 9cff18c to 2196b51 Compare September 14, 2026 15:46
@perlowja

Copy link
Copy Markdown
Contributor Author

Addressed both threads on this branch (now at 2196b51):

  • String/comment cleanup: this was already fixed in 9cff18c (a prior push) — grep -nP '…|—|–' <changed files> returns nothing, and the dead packaging/singularity/README.md reference is gone. Looks like this just needed a re-review ping rather than more work.

  • Backend/packaging not shipped: shipped all three pieces the interface already declared — data/artist-packs/singularity-artist-pack-inventory (unprivileged), data/artist-packs/singularity-artist-pack-install (pkexec target), and data/artist-packs/dev.sinty.desktop.artist-pack-install.policy, wired into meson.build's install rules. Renamed off the original ncz-wallpaper-pack-* naming (NCZ-OS-specific, doesn't belong in a generic Singularity repo) to singularity-artist-pack-*. Installed to /usr/local/bin explicitly rather than get_option('bindir') — that's the FHS-correct, prefix-independent location this contract's fixed-path (never-PATH-resolved) security property actually needs.

    Distro-portability: the inventory script has zero package-name assumptions — the configured apt source(s) (artist-pack-apt-sources) are the trust boundary, every package that source publishes counts however it's named. It targets apt directly (via apt-get indextargets, reading exactly the index file apt itself resolved for each source, not a guessed-at lists/ path), but the CONTRACT — fixed stdout JSON schema, argv shape — has no apt-specific fields, so a distro on a different package manager can ship its own pair of binaries at the same two paths. Idempotent by construction: the install helper re-validates the package's live apt candidate against the caller-supplied source URI on every call, then just runs apt-get install — no separate installed-state file to drift.

    Verified: both scripts pass dash -n (POSIX sh). Full compile hit the same environment gap noted on feat(wallpaper): OCS browser for importing wallpaper packs #26 (libsingularity unavailable on my build hosts) — not a defect in this diff, noting for honesty.

Also rebased onto current origin/main (this branch predated #25 and #27; no real conflicts beyond the two already-merged UI changes both branches independently touched).

Assisted-by: Claude Code:claude-sonnet-5

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants