:PROPERTIES: :ID: f4196b35-5438-4da3-9d34-511291f482b5 :END: #+title: azos/nextcloud #+filetags: :project: :knowledge: :nextcloud: Backlink: [[id:bf4dde56-0967-4374-94ed-771301e47c66][azos]] * Architecture =features/nextcloud/default.nix= (home-manager module, =azos.nextcloud-client= / =azos.nextcloud-mount=) mounts a Nextcloud webdav folder at =~/synced-docs= via =rclone mount= as a systemd user service (=nextcloud-mount.service=, =WantedBy = default.target=, not =graphical-session.target= — the latter never activates under EXWM since nothing explicitly starts it). Credentials come from =pass= at service start (not baked into the Nix store): password from =pass zakobar.com=, Nextcloud *user ID* (distinct from login name on this server) from =pass nextcloud-username/aner=. See [[id:09ae970c-2995-43d9-a597-47bb904c98de][azos/pass]]. Gated on YubiKey smartcard presence via the shared =azos-card-present= overlay (=overlays/default.nix=, wraps =gpg --card-status=) — same silent presence check used by the mbsync mail-sync timer (=features/mail/default.nix=). No card -> loop quietly until plugged in, no pinentry prompt either way. =Restart = always= means unplugging/replugging the key remounts naturally. Added in commit 04427dd "Nextcloud sync, also zapzap" (2026-10-11), same commit that bumped azos-core to add the zapzap WhatsApp client (=azos-core/features/station/whatsapp.nix=) — unrelated features landed together. * Gotchas ** PATH-override crash loop (fixed 2026-10-11) =nextcloud-mount.service= crash-looped (exit code 127, every 10s) because =Service.Environment = "PATH=${pkgs.fuse3}/bin:${pkgs.fuse}/bin";= in =features/nextcloud/default.nix= *replaces* the service's PATH rather than appending. Every command in the mount script used absolute =${pkgs.X}/bin/...= paths except the YubiKey-poll loop's bare =sleep 10= — which couldn't be found under the restricted PATH, so bash exited 127. Ran fine manually (inherits full interactive PATH), which is why it looked like it should work. Fix: =${pkgs.coreutils}/bin/sleep 10= instead of bare =sleep 10=. General lesson logged on the [[id:bf4dde56-0967-4374-94ed-771301e47c66][azos]] root — any systemd unit that restricts PATH this way needs every bare command in its script converted to an absolute nix-store reference, not just the ones the override was added for. ** Same crash loop recurred with a different command (2026-10-12) =nextcloud-mount.service= was 127-looping again, but this time the culprit was =head=, not =sleep=: =password=$(pass zakobar.com | head -1)= and the matching =uid=$(pass nextcloud-username/aner | head -1)= line both piped into bare =head -1=, which isn't on the restricted PATH either. Under =set -eu=, the pipeline's exit status is =head='s (127), which aborts the script. Fixed by changing both to =${pkgs.coreutils}/bin/head -1=. Confirms the general lesson above needs to be applied exhaustively: fixing the one bare command that happened to be caught isn't enough — *every* token in the script that isn't an absolute =/nix/store/...= path is a latent 127. Next time this crash-loops, visually scan the whole script rather than assuming the previously-fixed command is the only one. Still 127'd after deploying the =head= fix — turned out =mkdir -p "$mountpoint"= (the very first line of the script, before the YubiKey loop) was *also* bare and had been bare the whole time, just masked because it never got reached/tested in isolation before. Fixed to =${pkgs.coreutils}/bin/mkdir -p=. Verified by extracting the deployed =writeShellScript= output, manually running it under the exact restricted =PATH=/= from =systemctl --user show nextcloud-mount.service -p Environment=, with =bash -x= to see exactly which line 127'd — this is the reliable way to debug this script going forward rather than guessing from =journalctl= alone (which only reports the exit code, not which line). ** A third, different bug after that: non-setuid fusermount3 (2026-10-12) After the =mkdir= fix the script got all the way to =rclone mount=, which then failed with "fusermount3: mount failed: Operation not permitted" (exit 1, not 127 — a new failure mode). Root cause: NixOS's real, usable =fusermount3= is the *setuid-root* one at =/run/wrappers/bin/fusermount3= (installed via =security.wrappers=); the plain =${pkgs.fuse3}/bin/fusermount3= in the nix store has no setuid bit and can't perform the mount syscall (lacks CAP_SYS_ADMIN). =rclone mount= looks up =fusermount3= via =PATH= internally rather than an absolute path, and the service's =Service.Environment = "PATH=...";= override pointed only at the raw =pkgs.fuse3=/=pkgs.fuse= bin dirs, never =/run/wrappers/bin= — so it always resolved to the broken non-setuid binary. =ExecStop= had the identical bug (hardcoded to the non-setuid =${pkgs.fuse3}/bin/fusermount3 -u ...=). Fix: prepend =/run/wrappers/bin= to the =PATH= override, and point =ExecStop= at =/run/wrappers/bin/fusermount3= directly. Verified by manually exporting the rclone env vars and PATH and running =rclone mount= by hand — failed identically without =/run/wrappers/bin=, succeeded (real files visible) with it. General lesson for *any* systemd unit that overrides =PATH= and touches FUSE: =/run/wrappers/bin= must be included, not just the raw package's =/bin= — several NixOS tools (fusermount, sudo, etc.) only work from their setuid wrapper, which lives there, not in the nix store path itself. I initially misdiagnosed this same "Operation not permitted" error as an artifact of my own sandboxed test shell lacking mount capabilities — it was not; always re-test with the real setuid wrapper on PATH before concluding "not reproducible" for FUSE-related permission errors. Notably, the *symptom* this time didn't look like a crash-looping systemd service at first glance — it looked like the Nextcloud desktop GUI client (the separate =services.nextcloud-client=, webflow login) showing only the Activity/notifications view with no file browser. That GUI client authenticates fine on its own (confirmed via =~/.config/Nextcloud/logs=: AccountState =CredentialsNotReady -> Connected=) — it's unrelated to file access. Actual file access goes through the rclone webdav mount at =~/synced-docs=, which was empty because this crash loop kept it unmounted. When the GUI client looks "connected but empty", check =systemctl --user status nextcloud-mount.service= rather than assuming an auth/account problem. * Related: zapzap notification delivery (confirmed working, no bug) Same session also investigated zapzap (WhatsApp client, added alongside this feature) not showing desktop notifications. Confirmed the whole pipeline is correctly wired — see [[id:c52036a9-25ec-4b8b-8b72-90fcd9ea65dd][azos/exwm]] for the EDNC/D-Bus notification architecture note. No bug found; likely explanation if it recurs is WhatsApp Web's standard behavior of suppressing notifications for a chat that's already focused/open in the window.