Skip to main content

max / alloy

Ship no flatpak remote, and say so when the first install has nowhere to go Picking a catalog is picking who the user's software comes from, so Alloy picks none. The code already assumed that - SpecBox::remote is an Option and every literal flathub was in the test module - so what was missing was the sentence a user gets on a fresh machine. flatpak's own error there reads as the app being missing, which sends someone hunting for a typo in an app id that is correct. The sandboxed backend now asks which remotes are configured when the spec names none, and refuses with a message that says Alloy ships no catalog and names flatpak remote-add. The probe is a seam on the backend so the branch is testable, and an unanswerable probe never blocks an install: a flatpak that could not answer is not evidence that there is nowhere to install from. That also makes the ruling safe against a remote arriving from the flatpak package's own data files, which was the one thing this could not be confirmed against without a booted image. The preset comment, alloy-open's no-browser help and manual chapter 7 all named a specific catalog and no longer do.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-28 17:55 UTC
Commit: 9fc128d72b83b39817c8385c3101d8b02891cb9f
Parent: 86675d8
9 files changed, +185 insertions, -44 deletions
M docs/IMAGE.md +1 -1
@@ -108,7 +108,7 @@
108 108
109 109 **A package added here gets a line in `crates/alloy/credits.toml`.** The installer's last screen names the projects Alloy ships and their licenses, off a hand-curated manifest rather than a generated closure, so nothing adds itself. The manifest is embedded in the console binary with `include_str!`, which means the page cannot go missing on installer media or a read-only deployment and also means a manifest edit needs a rebuild. Its own header says which license to record: for anything packaged out of Rust or Go, Fedora's `%{LICENSE}` is the whole vendored closure rather than the project's own terms, so read upstream's LICENSE for those and use `rpm -q --qf '%{LICENSE}'` only for the C packages.
110 110
111 - The browser ships as an RPM baked into the image: one code path, no first-boot delay, and no first-run network dependency. It is Firefox, from Fedora's own repos, and it carries exactly one file of Alloy configuration (`/etc/firefox/pref/alloy.js`, anti-features removed and one hidden control restored; see [STACK.md](STACK.md#browser)). The `flatpak` client is included so users can pull Flathub-only apps on demand post-install; no Flatpaks are provisioned at build or first-boot time.
111 + The browser ships as an RPM baked into the image: one code path, no first-boot delay, and no first-run network dependency. It is Firefox, from Fedora's own repos, and it carries exactly one file of Alloy configuration (`/etc/firefox/pref/alloy.js`, anti-features removed and one hidden control restored; see [STACK.md](STACK.md#browser)). The `flatpak` client is included so users can pull sandboxed-only apps on demand post-install; no Flatpaks and no remote are provisioned at build or first-boot time, because which catalog to trust is the user's call.
112 112
113 113 ## Size
114 114
M docs/STACK.md +2 -2
@@ -74,7 +74,7 @@
74 74
75 75 ### Xwayland: **disabled**, ruled 2026-08-21
76 76
77 - The session ships `xwayland disable`. X11 has no isolation between clients: any X client can read every other client's keystrokes, window contents and clipboard, because that was the design. Running an X server inside a Wayland session hands that model back to anything that asks, and "anything that asks" includes a Flathub app that Alloy otherwise presents as sandboxed.
77 + The session ships `xwayland disable`. X11 has no isolation between clients: any X client can read every other client's keystrokes, window contents and clipboard, because that was the design. Running an X server inside a Wayland session hands that model back to anything that asks, and "anything that asks" includes a flatpak app that Alloy otherwise presents as sandboxed.
78 78
79 79 Breaking Steam, most Electron applications and older Java toolkits is the intent rather than a cost to be mitigated. Alloy has been a Wayland-only session since the pivot, and this makes the stack honest about it.
80 80
@@ -142,7 +142,7 @@
142 142
143 143 No custom egui file manager planned. Scope is too large (file ops, permissions, drag-drop, thumbnails, archives, mounts, trash, search, batch ops, associations) for a marquee-app slot, and the pivot moved off graphical authored surfaces anyway.
144 144
145 - Rejected: broot (Rust TUI, useful as a complement but a different model), nautilus / dolphin (not Rust), cosmic-files (was the pre-pivot GUI fallback; dropped along with the rest of the graphical stack). Users who want a graphical file manager install one themselves with `flatpak install flathub com.system76.CosmicFiles` (or thunar, nautilus): Flathub is where ad-hoc applications come from, and Alloy provisions none of them.
145 + Rejected: broot (Rust TUI, useful as a complement but a different model), nautilus / dolphin (not Rust), cosmic-files (was the pre-pivot GUI fallback; dropped along with the rest of the graphical stack). Users who want a graphical file manager install one themselves with `flatpak install com.system76.CosmicFiles` (or thunar, nautilus) from a remote they added: sandboxed apps are where ad-hoc applications come from, and Alloy provisions neither the apps nor the catalog they come from.
146 146
147 147 ## Text editor
148 148
@@ -96,15 +96,24 @@
96 96 pages, build your image with `TRIM=keep` (chapter 2) and they come back along
97 97 with every translation.
98 98
99 - ## Flathub
99 + ## Sandboxed apps
100 100
101 - The `flatpak` client is in the image and no Flatpaks are provisioned. Adding
102 - Flathub and pulling an app is the ordinary thing:
101 + The `flatpak` client is in the image, no Flatpaks are provisioned, and **no
102 + remote is configured**. That last part is deliberate: a remote is a catalog,
103 + and a catalog is a decision about who your software comes from. Alloy does not
104 + make that one for you.
103 105
104 - flatpak install flathub org.example.SomeApp
106 + So the first step is yours. Add whichever catalog you want:
105 107
106 - Alloy ships no graphical file manager, so this is where one comes from if you
107 - want one.
108 + flatpak remote-add --user --if-not-exists <name> <url>
109 +
110 + Then apps install from it in the ordinary way:
111 +
112 + flatpak install <name> org.example.SomeApp
113 +
114 + `alloy pkg` uses the same remotes; a box entry can name one, and one that does
115 + not is resolved across whatever you have added. Alloy ships no graphical file
116 + manager, so this is where one comes from if you want one.
108 117
109 118 ## What a sandboxed app does not get
110 119
@@ -114,7 +123,7 @@
114 123 filesystem. GPU access stays, because a video player or a browser without it is
115 124 not usable.
116 125
117 - That is a smaller sandbox than Flathub assumes, so some apps will misbehave. Give
126 + That is a smaller sandbox than most catalogs assume, so some apps will misbehave. Give
118 127 back what one actually needs, per app:
119 128
120 129 flatpak override --user org.example.SomeApp --filesystem=~/Projects
@@ -15,7 +15,7 @@
15 15
16 16 There is no graphical file manager in the image. Apps that need to open or save
17 17 a file get a portal dialog, which is what a browser uses, so the daily case is
18 - covered. If you want one anyway, it comes from Flathub (chapter 7).
18 + covered. If you want one anyway, it comes from a flatpak remote you add (chapter 7).
19 19
20 20 ## Editing: helix
21 21
@@ -82,7 +82,7 @@
82 82 **Is there a graphical file manager, launcher, or editor?**
83 83
84 84 No. yazi, `Mod+D`, and helix are the answers. If you want a graphical file
85 - manager, Flathub has several and chapter 7 explains how to install one.
85 + manager, chapter 7 explains how to add a catalog and install one.
86 86
87 87 **How stable is this?**
88 88
@@ -131,6 +131,6 @@
131 131 # one command from the browser the image would have carried.
132 132 printf 'No browser is installed, so this link cannot be opened: %s\n' "$url" >&2
133 133 say "Install one with: rpm-ostree install firefox
134 - Or, sandboxed: flatpak install fedora org.mozilla.firefox
134 + Or, sandboxed: flatpak install org.mozilla.firefox, from a remote you have added
135 135 Or rebuild the image with BROWSER=firefox to bake one in."
136 136 exit 3
@@ -222,7 +222,7 @@
222 222 #[default]
223 223 Firefox,
224 224 /// No browser in the image. Still a real answer: someone who wants to
225 - /// install their own from Flathub should not pay for one they will remove.
225 + /// install their own from a flatpak remote should not pay for one they will remove.
226 226 None,
227 227 }
228 228
@@ -534,7 +534,7 @@
534 534 backends.push(std::boxed::Box::new(Podman));
535 535 }
536 536 if Invocation::new("flatpak").arg("--version").probe() {
537 - backends.push(std::boxed::Box::new(Flatpak));
537 + backends.push(std::boxed::Box::new(Flatpak::new()));
538 538 }
539 539 backends
540 540 }
@@ -923,7 +923,61 @@
923 923
924 924 // ---- flatpak: the sandboxed level ----
925 925
926 - pub(crate) struct Flatpak;
926 + pub(crate) struct Flatpak {
927 + /// How the backend asks the machine which remotes are configured.
928 + ///
929 + /// A seam rather than a direct call because it is the one place a backend
930 + /// reads the machine outside `list`, and the branch it feeds — the message
931 + /// a first install on a fresh machine gets — has to be testable without a
932 + /// flatpak installation to arrange. `None` means the question could not be
933 + /// asked, which never blocks an install: a probe that failed is not
934 + /// evidence that there is nowhere to install from.
935 + remotes: fn() -> Option<Vec<String>>,
936 + }
937 +
938 + impl Flatpak {
939 + pub(crate) fn new() -> Self {
940 + Self {
941 + remotes: configured_remotes,
942 + }
943 + }
944 + }
945 +
946 + /// The remotes flatpak would resolve an app against, or `None` if it could not
947 + /// be asked.
948 + fn configured_remotes() -> Option<Vec<String>> {
949 + let raw = Invocation::new("flatpak")
950 + .args(["remotes", "--columns=name"])
951 + .capture_quiet()
952 + .ok()?;
953 + Some(parse_remotes(&raw))
954 + }
955 +
956 + /// One remote per non-empty line, which is what `--columns=name` prints.
957 + fn parse_remotes(raw: &str) -> Vec<String> {
958 + raw.lines()
959 + .map(str::trim)
960 + .filter(|line| !line.is_empty())
961 + .map(ToString::to_string)
962 + .collect()
963 + }
964 +
965 + /// What a sandboxed install says on a machine that has no remote configured.
966 + ///
967 + /// Alloy ships the flatpak client and no catalog: choosing a catalog is
968 + /// choosing who the user's software comes from, and that is the user's call
969 + /// (wiki `alloy-byo-principle`). So this is a decision the console has to own
970 + /// out loud — flatpak's own "no remote refs found" reads as the app being
971 + /// missing rather than as there being nowhere to look.
972 + fn no_remote_message(app: &str) -> String {
973 + format!(
974 + "no flatpak remote is configured, so there is nowhere to install {app} from. \
975 + Alloy ships the flatpak client and no catalog, because which catalog you \
976 + install from is your choice, not the image's. Add one with \
977 + `flatpak remote-add --user --if-not-exists <name> <url>`, or name a remote \
978 + in the box's spec entry"
979 + )
980 + }
927 981
928 982 /// Columns requested from `flatpak list`, in the order the parser reads them.
929 983 ///
@@ -1012,8 +1066,16 @@
1012 1066 let app = spec.app(name)?;
1013 1067 let mut invocation =
1014 1068 Invocation::new("flatpak").args(["install", Scope::User.flag(), "--noninteractive"]);
1015 - if let Some(remote) = &spec.remote {
1016 - invocation = invocation.arg(remote);
1069 + match &spec.remote {
1070 + Some(remote) => invocation = invocation.arg(remote),
1071 + // Only when the spec names none: a box that declares its remote
1072 + // says where it comes from, and flatpak's own error is the right
1073 + // one if that remote is not configured.
1074 + None => {
1075 + if (self.remotes)().is_some_and(|remotes| remotes.is_empty()) {
1076 + anyhow::bail!(no_remote_message(app));
1077 + }
1078 + }
1017 1079 }
1018 1080 Ok(invocation.arg(app))
1019 1081 }
@@ -2490,7 +2552,7 @@
2490 2552 // here even though it never does for podman.
2491 2553 #[test]
2492 2554 fn every_flatpak_app_is_sandboxed_even_when_ad_hoc() {
2493 - let boxes = Flatpak.parse(FLATPAK, &Spec::default()).unwrap();
2555 + let boxes = flatpak().parse(FLATPAK, &Spec::default()).unwrap();
2494 2556 assert_eq!(boxes.len(), 2);
2495 2557 for boxed in &boxes {
2496 2558 assert_eq!(boxed.level, Some(Level::Sandboxed));
@@ -2503,7 +2565,7 @@
2503 2565 // is what identifies it to flatpak.
2504 2566 #[test]
2505 2567 fn a_sandboxed_row_shows_the_name_and_carries_the_app_id() {
2506 - let boxes = Flatpak.parse(FLATPAK, &Spec::default()).unwrap();
2568 + let boxes = flatpak().parse(FLATPAK, &Spec::default()).unwrap();
2507 2569 let inkscape = boxes
2508 2570 .iter()
2509 2571 .find(|b| b.source == "org.inkscape.Inkscape")
@@ -2526,7 +2588,7 @@
2526 2588 )
2527 2589 .unwrap();
2528 2590
2529 - let boxes = Flatpak.parse(FLATPAK, &spec).unwrap();
2591 + let boxes = flatpak().parse(FLATPAK, &spec).unwrap();
2530 2592 let inkscape = boxes
2531 2593 .iter()
2532 2594 .find(|b| b.source == "org.inkscape.Inkscape")
@@ -2548,7 +2610,7 @@
2548 2610 // An app with no human name still needs an identifiable row.
2549 2611 #[test]
2550 2612 fn a_flatpak_row_with_a_missing_name_falls_back_to_the_app_id() {
2551 - let boxes = Flatpak
2613 + let boxes = flatpak()
2552 2614 .parse("org.example.Thing\t\tflathub\tuser\n", &Spec::default())
2553 2615 .unwrap();
2554 2616 assert_eq!(boxes[0].name, "org.example.Thing");
@@ -2558,7 +2620,7 @@
2558 2620 // the two rows of the fixture must not come out of the parser alike.
2559 2621 #[test]
2560 2622 fn a_sandboxed_row_carries_the_installation_it_was_found_in() {
2561 - let boxes = Flatpak.parse(FLATPAK, &Spec::default()).unwrap();
2623 + let boxes = flatpak().parse(FLATPAK, &Spec::default()).unwrap();
2562 2624 let inkscape = boxes
2563 2625 .iter()
2564 2626 .find(|b| b.source == "org.inkscape.Inkscape")
@@ -2570,7 +2632,7 @@
2570 2632 assert_eq!(inkscape.scope, Some(Scope::User));
2571 2633 assert_eq!(tasks.scope, Some(Scope::System));
2572 2634 assert_eq!(
2573 - Flatpak.remove(tasks).display(),
2635 + flatpak().remove(tasks).display(),
2574 2636 "flatpak uninstall --system dev.edfloreshz.Tasks",
2575 2637 "a system app is removed from the system installation, not the user one"
2576 2638 );
@@ -2581,7 +2643,7 @@
2581 2643 // way it did before the column was read, rather than guessing at `--user`.
2582 2644 #[test]
2583 2645 fn an_unrecognized_installation_leaves_removal_unqualified() {
2584 - let boxes = Flatpak
2646 + let boxes = flatpak()
2585 2647 .parse(
2586 2648 "org.example.Thing\tThing\tflathub\tmy-ssd\n",
2587 2649 &Spec::default(),
@@ -2589,7 +2651,7 @@
2589 2651 .unwrap();
2590 2652 assert_eq!(boxes[0].scope, None);
2591 2653 assert_eq!(
2592 - Flatpak.remove(&boxes[0]).display(),
2654 + flatpak().remove(&boxes[0]).display(),
2593 2655 "flatpak uninstall org.example.Thing"
2594 2656 );
2595 2657 }
@@ -2605,8 +2667,8 @@
2605 2667 let without =
2606 2668 Spec::parse("[box.thing]\nlevel = \"sandboxed\"\napp = \"org.example.T\"\n").unwrap();
2607 2669 for argv in [
2608 - create_argv(&Flatpak, &with_remote, "inkscape"),
2609 - create_argv(&Flatpak, &without, "thing"),
2670 + create_argv(&flatpak(), &with_remote, "inkscape"),
2671 + create_argv(&flatpak(), &without, "thing"),
2610 2672 ] {
2611 2673 assert!(argv.contains(" --user "), "got: {argv}");
2612 2674 assert!(!argv.contains("--system"), "got: {argv}");
@@ -2617,8 +2679,13 @@
2617 2679 // not read as a parse failure.
2618 2680 #[test]
2619 2681 fn empty_flatpak_output_parses_to_an_empty_list() {
2620 - assert!(Flatpak.parse("", &Spec::default()).unwrap().is_empty());
2621 - assert!(Flatpak.parse("\n\n", &Spec::default()).unwrap().is_empty());
2682 + assert!(flatpak().parse("", &Spec::default()).unwrap().is_empty());
2683 + assert!(
2684 + flatpak()
2685 + .parse("\n\n", &Spec::default())
2686 + .unwrap()
2687 + .is_empty()
2688 + );
2622 2689 }
2623 2690
2624 2691 // ---- commands ----
@@ -2692,26 +2759,26 @@
2692 2759
2693 2760 #[test]
2694 2761 fn flatpak_commands_address_the_app_id() {
2695 - let boxes = Flatpak.parse(FLATPAK, &Spec::default()).unwrap();
2762 + let boxes = flatpak().parse(FLATPAK, &Spec::default()).unwrap();
2696 2763 let inkscape = boxes
2697 2764 .iter()
2698 2765 .find(|b| b.source == "org.inkscape.Inkscape")
2699 2766 .unwrap();
2700 2767
2701 2768 assert_eq!(
2702 - Flatpak.list().display(),
2769 + flatpak().list().display(),
2703 2770 "flatpak list --app --columns=application,name,origin,installation"
2704 2771 );
2705 2772 assert_eq!(
2706 - Flatpak.remove(inkscape).display(),
2773 + flatpak().remove(inkscape).display(),
2707 2774 "flatpak uninstall --user org.inkscape.Inkscape"
2708 2775 );
2709 2776 assert_eq!(
2710 - Flatpak.enter(inkscape).display(),
2777 + flatpak().enter(inkscape).display(),
2711 2778 "flatpak run --command=sh org.inkscape.Inkscape"
2712 2779 );
2713 - assert!(Flatpak.start(inkscape).is_none());
2714 - assert!(Flatpak.stop(inkscape).is_none());
2780 + assert!(flatpak().start(inkscape).is_none());
2781 + assert!(flatpak().stop(inkscape).is_none());
2715 2782 }
2716 2783
2717 2784 // ---- exporting ----
@@ -2875,7 +2942,7 @@
2875 2942 #[test]
2876 2943 fn flatpak_refuses_to_export_and_says_the_app_is_already_there() {
2877 2944 let spec = export_spec();
2878 - let err = exports(&Flatpak, &spec, "inkscape")
2945 + let err = exports(&flatpak(), &spec, "inkscape")
2879 2946 .unwrap_err()
2880 2947 .to_string();
2881 2948 assert!(err.contains("already exports"), "{err}");
@@ -2921,6 +2988,16 @@
2921 2988 .unwrap()
2922 2989 }
2923 2990
2991 + /// A flatpak backend on an ordinary machine: one remote configured.
2992 + ///
2993 + /// Every test that is not about the remote probe wants this, since a
2994 + /// machine with a catalog is what a user who added one has.
2995 + fn flatpak() -> Flatpak {
2996 + Flatpak {
2997 + remotes: || Some(vec!["example".to_string()]),
2998 + }
2999 + }
3000 +
2924 3001 fn create_argv(backend: &dyn Backend, spec: &Spec, name: &str) -> String {
2925 3002 let (_, entry) = spec.resolve(name).unwrap();
2926 3003 backend.create(name, entry).unwrap().display()
@@ -2957,7 +3034,7 @@
2957 3034 fn a_sandboxed_box_is_installed_from_its_declared_remote() {
2958 3035 let spec = creation_spec();
2959 3036 assert_eq!(
2960 - create_argv(&Flatpak, &spec, "inkscape"),
3037 + create_argv(&flatpak(), &spec, "inkscape"),
2961 3038 "flatpak install --user --noninteractive flathub org.inkscape.Inkscape"
2962 3039 );
2963 3040 }
@@ -2968,11 +3045,65 @@
2968 3045 let spec = Spec::parse("[box.thing]\nlevel = \"sandboxed\"\napp = \"org.example.Thing\"\n")
2969 3046 .unwrap();
2970 3047 assert_eq!(
2971 - create_argv(&Flatpak, &spec, "thing"),
3048 + create_argv(&flatpak(), &spec, "thing"),
2972 3049 "flatpak install --user --noninteractive org.example.Thing"
2973 3050 );
2974 3051 }
2975 3052
3053 + // Alloy ships the flatpak client and no catalog, so the first sandboxed
3054 + // install on a fresh machine has nowhere to resolve from. Flatpak's own
3055 + // error for that reads as the app being missing, which sends a user
3056 + // looking for a typo in an app id that is correct.
3057 + #[test]
3058 + fn a_first_install_with_no_remote_configured_says_alloy_ships_none() {
3059 + let spec = Spec::parse("[box.thing]\nlevel = \"sandboxed\"\napp = \"org.example.Thing\"\n")
3060 + .unwrap();
3061 + let backend = Flatpak {
3062 + remotes: || Some(Vec::new()),
3063 + };
3064 + let (_, entry) = spec.resolve("thing").unwrap();
3065 + let err = backend.create("thing", entry).unwrap_err().to_string();
3066 + assert!(err.contains("org.example.Thing"), "names the app: {err}");
3067 + assert!(err.contains("remote-add"), "names what to run: {err}");
3068 + }
3069 +
3070 + // A box that declares its remote says where it comes from, so the probe is
3071 + // not consulted and flatpak's own error is the right one if that remote is
3072 + // missing.
3073 + #[test]
3074 + fn a_declared_remote_is_installed_from_without_asking_what_is_configured() {
3075 + let spec = creation_spec();
3076 + let backend = Flatpak {
3077 + remotes: || panic!("the probe must not run when the spec names a remote"),
3078 + };
3079 + assert_eq!(
3080 + create_argv(&backend, &spec, "inkscape"),
3081 + "flatpak install --user --noninteractive flathub org.inkscape.Inkscape"
3082 + );
3083 + }
3084 +
3085 + // A probe that could not run is not evidence of anything. Refusing on it
3086 + // would turn a flatpak that failed to answer into an install the user
3087 + // cannot make.
3088 + #[test]
3089 + fn an_unanswerable_probe_does_not_block_the_install() {
3090 + let spec = Spec::parse("[box.thing]\nlevel = \"sandboxed\"\napp = \"org.example.Thing\"\n")
3091 + .unwrap();
3092 + let backend = Flatpak { remotes: || None };
3093 + assert_eq!(
3094 + create_argv(&backend, &spec, "thing"),
3095 + "flatpak install --user --noninteractive org.example.Thing"
3096 + );
3097 + }
3098 +
3099 + // `--columns=name` prints one remote per line and nothing else; a machine
3100 + // with none prints an empty body rather than a header.
3101 + #[test]
3102 + fn remotes_are_read_one_per_line() {
3103 + assert_eq!(parse_remotes("fedora\nflathub\n"), ["fedora", "flathub"]);
3104 + assert!(parse_remotes("\n \n").is_empty());
3105 + }
3106 +
2976 3107 // The level decides which source field is required, so a spec that omits it
2977 3108 // is only wrong at creation time. The error has to name the box and the
2978 3109 // field, since the file may declare a dozen of them.
@@ -2991,7 +3122,7 @@
2991 3122 );
2992 3123
2993 3124 let (_, sandboxed) = spec.resolve("nothing").unwrap();
2994 - let err = Flatpak
3125 + let err = flatpak()
2995 3126 .create("nothing", sandboxed)
2996 3127 .unwrap_err()
2997 3128 .to_string();
@@ -3061,7 +3192,7 @@
3061 3192 #[test]
3062 3193 fn each_level_has_exactly_one_backend() {
3063 3194 for level in [Level::Host, Level::Workspace, Level::Sandboxed] {
3064 - let implementors = [&Podman as &dyn Backend, &Flatpak]
3195 + let implementors = [&Podman as &dyn Backend, &flatpak()]
3065 3196 .iter()
3066 3197 .filter(|backend| backend.implements(level))
3067 3198 .count();
@@ -9,9 +9,10 @@
9 9 # (see docs/CONTINUITY.md — Alloy does not phone home
10 10 # before the user says yes).
11 11 #
12 - # The base image ships its browser as an RPM; Flathub apps are
13 - # user-installed on demand (see docs/STACK.md), so no first-boot
14 - # Flatpak provisioning is needed.
12 + # The base image ships its browser as an RPM; sandboxed apps are
13 + # user-installed on demand from a remote the user adds (Alloy names
14 + # no catalog — see docs/STACK.md), so no first-boot Flatpak
15 + # provisioning is needed.
15 16 #
16 17 # Format: `enable|disable <unit>`. First match wins; entries without
17 18 # a match fall through to /usr/lib/systemd/system-preset/ defaults.