diff --git a/.claude/adr/0005-stock-firefox-policy-extensions.md b/.claude/adr/0005-stock-firefox-policy-extensions.md new file mode 100644 index 0000000..cce789e --- /dev/null +++ b/.claude/adr/0005-stock-firefox-policy-extensions.md @@ -0,0 +1,37 @@ +--- +status: accepted +--- + +# Stock Firefox with policy-installed extensions + +The browser Module ships **stock mainline Firefox** (`pkgs.firefox`, the release train), and installs its three extensions — an ad and content blocker, the operator's password manager, and a video sponsor-skipper — through Mozilla's enterprise `ExtensionSettings` policy, keyed by add-on id with an install URL and `installation_mode = "force_installed"`. +Firefox fetches each signed add-on from Mozilla's add-on site at runtime and enables it automatically. + +We chose this over an ESR, unbranded, or Developer Edition build carrying hash-pinned add-on packages from the Nix store. +Stock mainline Firefox refuses to load unsigned locally-built add-ons, so the pinned-package path forces the browser variant: it works only on a build that relaxes signature enforcement, which the mainline release does not. +Pairing the variant to the extension mechanism makes this the pivotal, hard-to-reverse decision — the choice of build dictates the whole extension story — so it is recorded here rather than left implicit in the Module. + +The trade-off is deliberate. +The policy path gives up build-time reproducibility of the extension binaries, and needs network on first launch to populate them, in exchange for staying on current mainline Firefox with add-ons that are actually enabled and no new flake input. + +## Considered Options + +- **Stock mainline Firefox with policy-installed extensions** (chosen). + Current release train, no signature-enforcement caveat, no extra flake input. + The extension binaries are fetched signed at runtime rather than pinned, so their exact versions are not reproducible from the flake and first launch needs network. +- **ESR or unbranded Firefox with hash-pinned add-on packages** (e.g. via a NUR add-ons input). + Rejected: it buys reproducible extension binaries but drags in an older or unusual browser variant to satisfy the signature check the mainline build enforces, plus a new flake input to maintain, for a browser the operator wants on the mainline feature and security cadence. +- **Stock Firefox with extensions installed by hand.** + Rejected: the state would live outside the flake, would not survive a reimage, and defeats the point of declaring the browser at all. + +## Consequences + +- The Module needs no new flake input and no add-on package set. + The extension list is three id/URL pairs under the enterprise policy. +- Extension versions are whatever Mozilla currently serves, not a pinned hash, so the browser tracks upstream add-on updates automatically and the flake does not gate them. +- First launch after a fresh build requires network to fetch the add-ons. + An offline first boot comes up with the extensions not yet present, populating them once online. +- Moving to a pinned-package posture later would mean changing the browser variant as well, since the two are coupled — the reason this is captured as a decision rather than a detail. +- The no-pinned-package rule scopes to the three functional extensions, which is where the signature-enforcement conflict bites. + Nord chrome theming comes from the Stylix Firefox Color add-on, a signed add-on that Stylix pins and manages, so it loads on stock mainline Firefox and adds no flake input of ours. + That is a bounded, deliberate exception, not a reversal: it is what lets the browser be themed from the shared Stylix scheme without hand-written chrome CSS. diff --git a/.claude/spec/firefox.md b/.claude/spec/firefox.md new file mode 100644 index 0000000..e73fa2b --- /dev/null +++ b/.claude/spec/firefox.md @@ -0,0 +1,122 @@ +## Problem Statement + +Neogaia now boots into the keyboard-driven Hyprland desktop, but the session ships no web browser. +The operator lives in this desktop daily and needs a browser, yet a stock browser install would arrive un-themed, telemetry-on, cluttered with sponsored surfaces, and requiring a round of manual clicking to reach a usable state. +That manual state would also be invisible to the flake and would not survive a reimage or transfer to the future desktop Host. + +The operator wants the browser configured the same way as the rest of the system: declared once, hardened and themed by default, and reproduced automatically on any Host that runs the desktop. + +## Solution + +Add Firefox as a new single-purpose desktop Module, configured entirely through home-manager's `programs.firefox`, and fold it into the desktop aggregator so the browser is part of the daily-drivable session rather than a separate opt-in. + +Ship stock mainline Firefox, hardened and de-monetized through locked enterprise policies, with a small fixed set of extensions force-installed by policy from Mozilla's add-on site. +Default search to DuckDuckGo over a lean, pruned engine list. +Theme the browser Nord from the same single Stylix source that themes the rest of the graphical layer, with no hand-maintained browser CSS. +Register Firefox as the system default handler for web links. + +Leave the most personal, frequently-changing state — bookmarks and container tabs — to Firefox's own runtime management rather than declaring it, keeping the Module lean and avoiding the destructive overwrite those declarative options impose. + +Because the Module joins the aggregator, it comes up on any Host with the desktop enabled: neogaia now, and the future desktop Host for free, with no per-Host browser flag. + +## User Stories + +1. As the operator, I want a web browser present the moment the desktop comes up, so that a daily-drivable session includes browsing without a separate install step. +2. As the operator, I want the browser expressed as one more enable in the desktop aggregator, so that any Host running the desktop inherits it and the future desktop Host adopts it without rework. +3. As the operator, I want the browser configured declaratively alongside every other Module, so that it is reproduced identically on reimage and never depends on manual post-install clicking. +4. As the operator, I want to stay on current mainline Firefox rather than an older release train, so that I get up-to-date browser features and security without maintaining an unusual package variant. +5. As the operator, I want a fixed set of extensions installed and actually enabled automatically, so that ad-blocking, password management, and sponsor-skipping work on first launch with no add-on hunting. +6. As the operator, I want ad and content blocking, so that pages are lighter and less hostile. +7. As the operator, I want my password manager available in the browser, so that credentials autofill without me reaching for another app. +8. As the operator, I want sponsor segments skipped in videos, so that watching is uninterrupted. +9. As the operator, I want telemetry, studies, the read-it-later widget, and the sponsored surfaces on the new-tab and address bar turned off and kept off, so that the browser is quiet, private, and un-monetized without me policing settings. +10. As the operator, I want the browser to stop offering to save logins and to stop nagging about being the default, so that it does not fight the password manager or interrupt me. +11. As the operator, I want Firefox accounts and sync disabled, so that no account surface appears for a feature I do not use. +12. As the operator, I want DuckDuckGo as the default search with only a lean set of engines present, so that search is private and uncluttered. +13. As the operator, I want the browser themed Nord from the same source as the rest of the desktop, so that it coheres with the bar, launcher, and lock screen without me hand-theming it. +14. As the operator, I want the browser registered as the system default for web links, so that links opened from the bar, notifications, the launcher, or the terminal land in it. +15. As the operator, I want bookmarks and container tabs left to the browser itself, so that the things I add in the moment are never wiped by a rebuild. +16. As the operator, I want the whole Host to still build green under the existing check, so that I gain confidence before switching a live machine. + +## Implementation Decisions + +**Module and placement** + +- A new single-purpose Module is added under the desktop group, namespaced to mirror its location per the Namespace convention, exposing one `enable` option guarded by the Enable convention. +- The Module is configured entirely through the primary user's home-manager `programs.firefox`, matching every other user-facing Module in the repo; no NixOS-level Firefox program integration and no manual package override are used. +- The desktop aggregator turns the Module on at default priority alongside the terminal and the other session pieces, so a single desktop flag brings the browser up while a Host retains the ability to override it. + The browser is treated as an essential application of the session rather than optional plumbing, following the precedent that the aggregator already enables the terminal. + +**Package variant and extension mechanism** + +- The package is stock mainline Firefox, not ESR, unbranded, or Developer Edition. +- Extensions are installed through Mozilla's enterprise policy `force_installed`, keyed by add-on id with an install URL, so the browser fetches the signed add-on from Mozilla's add-on site and enables it automatically. +- Nix-built or hash-pinned add-on packages are not used, because stock Firefox refuses unsigned locally-built add-ons; the trade-off — losing build-time reproducibility of the extension binaries in exchange for current mainline Firefox with add-ons that are actually enabled and no new flake input — is accepted deliberately. + This decision is the pivotal, hard-to-reverse one and is called out for an ADR in Further Notes. + +**Extensions** + +- Three extensions are force-installed: an ad and content blocker, the operator's password manager, and a video sponsor-skipper. +- All three are self-contained web extensions, so no native messaging host is wired. + +**Hardening** + +- Hardening is split across two mechanisms by intent: things with a corresponding enterprise policy are set as locked policies so they cannot be toggled back in the UI, and the remainder are set as ordinary profile preferences. +- Locked policies turn off telemetry and studies and data reporting, turn off the read-it-later widget, stop the browser offering to save logins, stop the default-browser check, strip the sponsored shortcuts, sponsored stories, and snippets from the new-tab page, and disable Firefox accounts and sync. +- Profile preferences turn off sponsored address-bar suggestions and tidy the new-tab surface. +- Fingerprinting resistance is deliberately left off, because it breaks enough everyday browsing to be a deliberate per-need choice rather than a baseline. + +**Search** + +- A single profile is declared, named as the default profile. +- The default engine is DuckDuckGo, and the engine list is pruned to a lean set with the general-purpose commercial engines removed; the removed engines remain reachable through DuckDuckGo's bang syntax. +- Declaring search requires the module's authoritative-overwrite acknowledgement, so engines added later through the UI are not preserved across a rebuild; this is accepted as the point of declaring search. + +**Theming** + +- The browser is themed by enabling the Stylix Firefox target against the declared profile, driven from the same single Nord scheme that themes the rest of the graphical layer. +- No hand-written browser chrome CSS is shipped; a small chrome-CSS layer remains a later addition on top of Stylix if deeper chrome restyling is ever wanted. +- The Firefox Stylix target is enabled from within the browser Module, mirroring how the theming Module already sets per-Module Stylix targets, so the target only takes effect when the browser is enabled. + +**Default browser** + +- Firefox is registered as the default handler for the web-link schemes and HTML through the primary user's home-manager mime-association config, placed in the browser Module. + +**Existing conventions already satisfied** + +- The Waybar workspace indicator already carries a window-rewrite icon mapping for Firefox, so the graphical-application icon convention needs no change. + +## Testing Decisions + +- A good test asserts externally-observable evaluation and build success of the whole Host, not the internals of the Module. + This mirrors the desktop and laptop-MVI stance, where the config-merge model makes the whole-Host build the meaningful unit and the highest available seam. +- Primary seam, required and existing: the neogaia Host evaluates and its system toplevel builds under the flake check. + Building the toplevel drives the Auto-loader discovering the new Module, the aggregator fan-out, home-manager wrapping the Firefox package with the policies, extensions, search, and preferences baked in, the Stylix Firefox target wiring, and the default-handler association, surfacing nearly all config-authoring errors short of launching the browser. +- Cheap targeted checks: evaluate specific configuration paths to confirm the aggregator fans the browser enable out, the rendered policies carry the three force-installed extensions, the default search engine resolves to DuckDuckGo, and the Stylix Firefox target is on, reusing the repo's existing lightweight eval-probe pattern. +- No Module-level unit tests are added; there is no seam below the whole-Host build worth testing here, and the prior art is the desktop and laptop-MVI build-the-toplevel checks. +- The genuine end-to-end confirmation is manual and irreducible: switch the configuration on neogaia, launch the browser, and confirm the three extensions are present and enabled, the Nord theme is applied, DuckDuckGo is the default search, and a link opened from another application lands in the browser. + A browser cannot self-test headless, but unlike a reimage this is reversible, so verification is done by living in it with a safety net of a generation rollback or the desktop flag. +- The build validates that the policy document is well-formed and baked into the package, but not that Firefox accepts every policy key semantically, since the policy document is only text until the browser loads it; that last mile is part of the manual confirmation. + +## Out of Scope + +- ESR, unbranded, and Developer Edition Firefox variants, and the nix-built or hash-pinned add-on path they would enable; the reproducibility trade-off was weighed and declined. +- Native messaging hosts of any kind, including a password-manager native connector and desktop browser-integration bridges; none of the chosen extensions need one. +- Declarative bookmarks and declarative container tabs, both deliberately left to the browser's own runtime state. +- The Multi-Account Containers extension and the contextual-identity preference it relies on. +- Multiple Firefox profiles; a single default profile carries everything. +- Custom or Nix-oriented search engines beyond the lean pruned set. +- Hand-written browser chrome CSS and any deep chrome-layout restyling such as compact tabs or a hidden title bar. +- Fingerprinting resistance and any harder privacy posture that routinely breaks everyday browsing. +- Firefox accounts and sync. +- Any second browser, and any per-Host browser divergence; the future desktop Host inherits this same Module unchanged. +- Any Skeleton, Auto-loader, or secret-wiring change; the Module uses the existing plumbing unchanged. + +## Further Notes + +- The stock-Firefox-plus-policy-extensions decision is hard to reverse — the variant dictates the entire extension mechanism — surprising without context, since a Nix-purist default would expect hash-pinned add-on packages, and the product of a real trade-off between reproducibility and current mainline Firefox with working extensions. + It should be recorded as an ADR. +- Home-manager's `programs.firefox` was confirmed to expose a top-level policies option that merges into the wrapper's enterprise policies, which is what lets the whole Module, hardening included, live under home-manager rather than needing the NixOS-level program integration. +- The declarative search, bookmarks, and containers options all share one authoritative-overwrite model: the browser owns those files at runtime, so declaring them means home-manager overwrites them wholesale and runtime-added entries do not survive a rebuild. + This is why bookmarks and containers are left out and why declaring search is an explicit acknowledgement. +- The extension set fetches signed add-ons from Mozilla's add-on site at runtime rather than from the Nix store, so first launch after a fresh build needs network to populate the extensions; this is inherent to the policy-based path chosen over the pinned-package path. diff --git a/.claude/tasks/0032-firefox-browser.md b/.claude/tasks/0032-firefox-browser.md new file mode 100644 index 0000000..d72b431 --- /dev/null +++ b/.claude/tasks/0032-firefox-browser.md @@ -0,0 +1,71 @@ +--- +spec: firefox +--- + +## What to build + +Add Firefox as a new single-purpose Module under the desktop group, configured entirely through the primary user's home-manager `programs.firefox`, and fold it into the desktop aggregator so the browser comes up as part of the daily-drivable session on any Host that enables the desktop. + +The browser is stock mainline Firefox, hardened and de-monetized through locked enterprise policies, carrying a small fixed set of extensions force-installed by policy from Mozilla's add-on site. +Search defaults to DuckDuckGo over a lean, pruned engine list. +The browser is themed Nord from the same single Stylix source as the rest of the graphical layer, and registered as the system default handler for web links. +Bookmarks and container tabs are deliberately not declared, leaving that state to the browser's own runtime management. + +The end-to-end result: a Host with the desktop enabled boots into a session where the browser is present, launchable, themed to match, telemetry-quiet, has its extensions installed and enabled on first launch, uses DuckDuckGo, and receives links opened from other applications. + +Scope details, all following the domain conventions (Namespace convention, Enable convention, aggregator fan-out): + +- **Module and placement.** One `enable` option namespaced to mirror the file's location under the desktop group, guarded by the Enable convention. Configured only through home-manager `programs.firefox`; no NixOS-level Firefox program integration and no manual package override. The desktop aggregator turns it on at default priority alongside the terminal, so the single desktop flag brings it up while a Host can still override it. +- **Package and extensions.** Stock mainline Firefox, not ESR/unbranded/Developer Edition. The three extensions — an ad and content blocker, the operator's password manager, and a video sponsor-skipper — are installed through the enterprise `force_installed` policy keyed by add-on id with an install URL, so Firefox fetches the signed add-on and enables it automatically. No Nix-built or hash-pinned add-on packages, and no native messaging host. +- **Hardening.** Split by intent: policy-backed items are set as locked policies (telemetry, studies, and data reporting off; read-it-later widget off; offer-to-save-logins off; default-browser check off; sponsored shortcuts, stories, and snippets stripped from the new-tab page; Firefox accounts and sync disabled), and the rest as ordinary profile preferences (sponsored address-bar suggestions off, new-tab surface tidied). Fingerprinting resistance stays off. +- **Search.** A single profile, named the default. DuckDuckGo as the default engine, the engine list pruned to a lean set with the general-purpose commercial engines removed, using the module's authoritative-overwrite acknowledgement. +- **Theming.** Enable the Stylix Firefox target against the declared profile, driven from the shared Nord scheme, set from within this Module (mirroring how the theming Module already sets per-Module Stylix targets). No hand-written browser chrome CSS. +- **Default browser.** Register Firefox as the default handler for the web-link schemes and HTML through the user's home-manager mime-association config, placed in this Module. + +Record the pivotal, hard-to-reverse decision — stock Firefox plus policy-installed extensions over an ESR/unbranded build with hash-pinned add-on packages — as an ADR, following the ADR format and the next ADR number. + +## Acceptance criteria + +- [x] A new Firefox Module exists under the desktop group with a single `enable` option, namespaced to mirror its directory per the Namespace convention and guarded per the Enable convention; `modules.desktop.firefox.enable` resolves. +- [x] The Module is configured only through the primary user's home-manager `programs.firefox`; there is no NixOS-level Firefox program integration and no manual package override. +- [x] The desktop aggregator enables the Module at default priority, so `modules.desktop.enable` brings the browser up and a Host can still override the single flag; neogaia carries it through the desktop flag with no per-Host browser line. +- [x] The package is stock mainline Firefox (not ESR, unbranded, or Developer Edition). +- [x] The three extensions are force-installed via enterprise policy keyed by add-on id with an install URL: the ad and content blocker, the password manager, and the video sponsor-skipper; no native messaging host is declared. +- [x] Locked policies turn off telemetry, studies, and data reporting; turn off the read-it-later widget; stop offer-to-save-logins; stop the default-browser check; strip sponsored shortcuts, stories, and snippets from the new-tab page; and disable Firefox accounts and sync. +- [x] Profile preferences turn off sponsored address-bar suggestions and tidy the new-tab surface; fingerprinting resistance is left off. +- [x] A single default profile is declared with DuckDuckGo as the default search engine and the engine list pruned to a lean set (general-purpose commercial engines removed), using the search authoritative-overwrite acknowledgement. +- [x] The Stylix Firefox target is enabled against the declared profile from within this Module, driven from the shared Nord scheme; no hand-written browser chrome CSS is shipped. +- [x] Firefox is registered as the default handler for the web-link schemes and HTML through the user's home-manager mime-association config. +- [x] An ADR (next number) records the stock-Firefox-plus-policy-extensions decision over an ESR/unbranded build with hash-pinned add-on packages, following the ADR format. +- [x] `nix flake check` builds `checks.x86_64-linux.neogaia` green, with the new file staged so evaluation sees it. +- [-] Manual confirmation on neogaia: after switching, the browser launches with the three extensions present and enabled, the Nord theme applied, DuckDuckGo as default search, and a link opened from another application lands in it. + +## Implementation Notes + +The module lives at `modules/desktop/firefox.nix`, declares `modules.desktop.firefox.enable`, and is fanned out by the desktop aggregator at `lib.mkDefault true`. +Everything is configured through `home-manager.users..programs.firefox`, with no NixOS-level program integration and no `package` override, so it stays on stock `pkgs.firefox` (built as `firefox-152.0.6`, the mainline release train). + +The three force-installed extensions are keyed by their real add-on ids, verified against Mozilla's AMO API rather than guessed: uBlock Origin (`uBlock0@raymondhill.net`), Proton Pass (`78272b6fa58f4a1abaac99321d503a20@proton.me`), and SponsorBlock (`sponsorBlocker@ajay.app`). +Proton Pass is the operator's password manager, per ADR 0002 and the sops spec. + +Search pruning deviated from a first pass that merely omitted the commercial engines. +Omission does not remove them: home-manager's search module writes `search.json.mozlz4`, but Firefox reconciles its locale's app-provided engines back in for any not present in the file, so the general-purpose commercial engines would reappear. +The lean set is instead reached by explicitly hiding them with `.metaData.hidden = true` (the module's documented builtin-hiding idiom), confirmed by decoding the built `search.json.mozlz4`: it carries `_metaData.hidden` on google, bing, ebay, and amazon, with `defaultEngineId = "ddg"`. +DuckDuckGo and Wikipedia remain visible; the hidden engines stay reachable through DuckDuckGo bangs. +Engines are referenced by their current id form (`ddg`, `google`, …), which the module maps from the old display names — `default = "ddg"` is correct, not `"DuckDuckGo"`. + +The decision to ship stock Firefox with policy-installed extensions over an ESR/unbranded build with hash-pinned add-ons is recorded as ADR 0005. + +The final acceptance criterion is marked `[-]` rather than `[x]`: it is the irreducible manual confirmation the spec calls out (a browser cannot self-test headless), deferred to the operator on the live machine after switching, not dropped work. +Every automatable check — the whole-Host toplevel build, and eval probes for the aggregator fan-out, the three force-installed ids, the DuckDuckGo default, the hidden commercial engines, the Stylix Firefox target, and the mime handlers — passes. + +### Follow-ups from the manual confirmation + +The manual launch surfaced two runtime problems the build could not, both since fixed and verified by deploying the home generation. + +Home-manager activation was failing outright: Firefox writes `~/.config/mozilla/firefox/profiles.ini` itself on first launch, and home-manager refuses to clobber the pre-existing file, so the whole generation failed and the declarative profile never deployed — search and theming were absent while the policy-driven extensions and mime handlers, which do not touch the profile, still worked. +Fixed by setting `force = true` on the generated `profiles.ini` home.file entry so home-manager owns it and deploys the `default` profile. + +The base Stylix Firefox target themes only fonts and the reader view, not the toolbar and tabs, so the chrome did not look Nord. +Fixed by enabling `stylix.targets.firefox.colorTheme`, which recolours the chrome from the shared scheme through the Stylix-managed Firefox Color add-on, with `profiles.default.extensions.force = true` to acknowledge the managed extension-settings store. +ADR 0005 is updated to scope its no-pinned-add-on rule to the functional extensions and record the signed theming add-on as a deliberate exception. diff --git a/CLAUDE.md b/CLAUDE.md index 2487f22..cb51915 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -57,8 +57,9 @@ The domain model (Host, Module, Skeleton, Auto-loader, Enable convention, overla Editing a skill in place fails; its source is `modules/agents/claude-code/skills//` here, applied by a rebuild. Creating a new file under `~/.claude/skills/` succeeds silently and is the trap — it stays outside the repo and reaches no other machine. Copying out of that tree needs `cp -rL` plus `chmod -R u+w`: a plain `cp -r` copies the symlinks, putting store paths into the destination, and dereferenced files keep the store's read-only mode. -- `home-manager.users..home.file` is keyed by **absolute** path, not by a path relative to the home directory. - Evaluating `home.file.".claude/CLAUDE.md"` fails with "does not provide attribute"; the working key is `home.file."/home/alexion/.claude/CLAUDE.md"`. +- `home-manager.users..home.file` is keyed by whatever path string the **defining module wrote**, absolute or relative, not by one canonical form. + A module that writes `home.file."/home/alexion/.claude/CLAUDE.md"` is reachable only at that absolute key, while `programs.firefox` writes relative keys such as `home.file.".config/mozilla/firefox/profiles.ini"` reachable only at the relative form. + The other form fails with "does not provide attribute", so overriding an entry (e.g. setting `.force = true` on it) requires matching the writer's exact key. List the real keys with `nix eval --json .#nixosConfigurations..config.home-manager.users..home.file --apply builtins.attrNames` rather than guessing one. A key's `.source` is the input file, whose store path differs from the deployed symlink's target (home-manager copies it to a `hm_`-prefixed path) even though the contents match. - nixpkgs `vimPlugins.nord-nvim` is `shaunsingh/nord.nvim` (no `require("nord").setup()`); the config wants `gbprod/nord.nvim`, which is packaged as `vimPlugins.gbprod-nord`. @@ -103,3 +104,8 @@ The domain model (Host, Module, Skeleton, Auto-loader, Enable convention, overla - A `nix build` or `nix flake check` on a **dirty** tree evaluates the working-copy content of tracked files, not what is committed, and only warns `Git tree ... is dirty`. A green check on a dirty tree therefore proves nothing about the commit. To verify a commit, build once on a clean tree (nothing uncommitted), where the absence of the dirty warning confirms the build reflects `HEAD`. +- home-manager's `programs.firefox` declarative `search` with `force = true` does **not** prune Firefox's built-in engines by omission. + The overwrite writes `search.json.mozlz4` with only the engines listed, but Firefox reconciles its locale's app-provided engines back in for any not present in the file, so Google, Bing, and the rest reappear. + To actually remove a builtin, list it explicitly with `.metaData.hidden = true` — an engine entry carrying only `metaData` is treated as a builtin rather than a custom engine. + Engines are referenced by their current id, which the module maps from the old display names, so the default is `default = "ddg"`, not `"DuckDuckGo"` (the latter only warns and migrates). + Prove the result by decoding the built file: `mozlz4a -d ` shows the `_metaData.hidden` flags and `defaultEngineId`. diff --git a/modules/desktop/desktop.nix b/modules/desktop/desktop.nix index e1cc899..8277da3 100644 --- a/modules/desktop/desktop.nix +++ b/modules/desktop/desktop.nix @@ -10,6 +10,7 @@ in # any one of them while the single flag above enables the whole desktop. config = lib.mkIf cfg.enable { modules.desktop.clipboard.enable = lib.mkDefault true; + modules.desktop.firefox.enable = lib.mkDefault true; modules.desktop.hyprland.enable = lib.mkDefault true; modules.desktop.hyprland.hyprlock.enable = lib.mkDefault true; modules.desktop.hyprland.hypridle.enable = lib.mkDefault true; diff --git a/modules/desktop/firefox.nix b/modules/desktop/firefox.nix new file mode 100644 index 0000000..d86d7f9 --- /dev/null +++ b/modules/desktop/firefox.nix @@ -0,0 +1,124 @@ +{ config, lib, ... }: +# Firefox as the desktop browser: stock mainline, hardened and de-monetized by policy. +let + cfg = config.modules.desktop.firefox; + user = config.user.name; + + # A force-installed extension, keyed at the call site by the add-on's own id. + # Firefox fetches the signed add-on from Mozilla's site and enables it automatically. + forceInstalled = slug: { + install_url = "https://addons.mozilla.org/firefox/downloads/latest/${slug}/latest.xpi"; + installation_mode = "force_installed"; + }; +in +{ + options.modules.desktop.firefox.enable = lib.mkEnableOption "Firefox as the desktop browser"; + + config = lib.mkIf cfg.enable { + home-manager.users.${user} = hm: { + programs.firefox = { + enable = true; + + # Enterprise policies: enforced and unchangeable from the browser UI. + policies = { + DisableTelemetry = true; + DisableFirefoxStudies = true; + DisablePocket = true; + OfferToSaveLogins = false; + DontCheckDefaultBrowser = true; + DisableFirefoxAccounts = true; + + # Strip the monetized surfaces from the new-tab page. + FirefoxHome = { + SponsoredTopSites = false; + SponsoredPocket = false; + Snippets = false; + }; + + # An ad and content blocker, the operator's password manager, and a + # video sponsor-skipper. All three are self-contained web extensions. + ExtensionSettings = { + "uBlock0@raymondhill.net" = forceInstalled "ublock-origin"; + "78272b6fa58f4a1abaac99321d503a20@proton.me" = forceInstalled "proton-pass"; + "sponsorBlocker@ajay.app" = forceInstalled "sponsorblock"; + }; + }; + + profiles.default = { + isDefault = true; + + # The Nord chrome theme is a declared extension setting, so home-manager + # owns the extension-settings store, overwriting runtime changes to it. + extensions.force = true; + + # Stylix's Nord mapping paints the selected address-bar result a + # near-white grey, leaving its light text unreadable. Darken that one + # highlight to the Nord selection grey, from the same scheme. + extensions.settings."FirefoxColor@mozilla.com".settings.theme.colors.popup_highlight = + let + c = hm.config.lib.stylix.colors; + in + lib.mkForce { + r = c."base03-rgb-r"; + g = c."base03-rgb-g"; + b = c."base03-rgb-b"; + }; + + settings = { + # Auto-enable the sideloaded Firefox Color add-on that carries the + # Nord chrome theme, which Firefox otherwise leaves disabled. + # The functional extensions come through policy and are unaffected. + "extensions.autoDisableScopes" = 0; + + # Sponsored surfaces the policies above do not reach. + "browser.urlbar.suggest.quicksuggest.sponsored" = false; + "browser.newtabpage.activity-stream.showSponsored" = false; + "browser.newtabpage.activity-stream.showSponsoredTopSites" = false; + }; + + search = { + # Declaring search overwrites Firefox's own engine list wholesale, + # so engines added later in the UI do not survive a rebuild. + force = true; + default = "ddg"; + + # The general-purpose commercial engines, hidden to leave a lean + # DuckDuckGo-and-Wikipedia list. They stay reachable through bangs. + # An engine carrying only metaData is treated as a builtin. + engines = { + google.metaData.hidden = true; + bing.metaData.hidden = true; + ebay.metaData.hidden = true; + "amazondotcom-us".metaData.hidden = true; + }; + order = [ "ddg" ]; + }; + }; + }; + + # Nord chrome from the shared Stylix scheme, against the one profile. + # colorTheme recolours the toolbar and tabs, which the target does not do + # on its own, through the Stylix-managed Firefox Color add-on. + stylix.targets.firefox = { + enable = true; + profileNames = [ "default" ]; + colorTheme.enable = true; + }; + + # Links opened from other applications land in Firefox. + xdg.mimeApps = { + enable = true; + defaultApplications = { + "text/html" = "firefox.desktop"; + "application/xhtml+xml" = "firefox.desktop"; + "x-scheme-handler/http" = "firefox.desktop"; + "x-scheme-handler/https" = "firefox.desktop"; + }; + }; + + # Firefox writes profiles.ini itself on first launch, so home-manager is + # told to own the file rather than fail activation refusing to clobber it. + home.file."${hm.config.programs.firefox.configPath}/profiles.ini".force = true; + }; + }; +}