1121 Commits

Author SHA1 Message Date
librelad
e944a33d8f feat(uninstall): honor --unattended to skip the confirm prompt
runFullUninstall always prompted for `DELETE LIBREPORTAL`, so it couldn't
be driven non-interactively. Honor the existing global --unattended flag
(init_unattended_mode) to skip the prompt; an interactive `init.sh
uninstall` still requires it.

This lets the deploy helper do a clean teardown (`init.sh --unattended
uninstall`) for a full reinstall instead of `rm -rf /docker`. The brute
wipe left the task-processor systemd service running against a deleted
runtime dir; init.sh's idempotent service setup then saw an unchanged
unit and skipped the restart, so the reinstalled WebUI container was
never started. The uninstall stops the service and tears down the
rootless daemon + users in order, so the follow-up install behaves like
a true first install.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-24 20:43:19 +01:00
librelad
8712be2631 Merge claude/2 2026-05-24 20:20:25 +01:00
librelad
fee195c995 Merge claude/1 2026-05-24 20:20:22 +01:00
librelad
b9ae512d31 auto: session-start commit — 2 file(s) at 2026-05-24 20:20:21 2026-05-24 20:20:21 +01:00
librelad
cb6301dc01 auto: session-start commit — 2 file(s) at 2026-05-24 20:20:20 2026-05-24 20:20:21 +01:00
librelad
a9c83b06b0 Merge claude/1 2026-05-24 20:01:52 +01:00
librelad
c63cb4a2a7 fix(install): broad sudo during install, tighten to scoped only after
The install hands the heavy setup to the manager (completeInitMessage:
sudo -u libreportal 'libreportal run install') — creating the
docker-install user, rootless setup, apt, sysctl — which needs broad root.
initUsers was installing the SCOPED sudoers up front, so that handoff died
with 'sudo: a password is required' on useradd. Fix: initUsers installs a
temporary NOPASSWD: ALL for the install phase; completeInitMessage calls
the new initScopedSudoers to tighten to the runtime allowlist only after
the install succeeds (on failure, broad sudo is left so the manual
'libreportal run install' retry works). This restores the documented
'kill NOPASSWD:ALL AFTER the runtime is set up' ordering.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-24 20:01:52 +01:00
librelad
d7aae3f47e Merge claude/1 2026-05-24 19:56:09 +01:00
librelad
9f0fa7ae31 fix(uninstall): fully remove both users' homes (terminate session + rm backstop)
The validation teardown left /home/libreportal orphaned: userdel -r skips
the home when the user still has a live session/processes, and the manager
only got a pkill (not a loginctl terminate) before userdel. Now both users
get disable-linger + terminate-user + pkill before userdel -r, plus an
explicit rm -rf /home/<user> backstop.

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-24 19:56:09 +01:00
librelad
f6fba03444 Merge claude/1 2026-05-24 19:54:11 +01:00
librelad
93284cdb39 feat(uninstall): add 'init.sh uninstall' — full, guarded teardown
A single 'sudo bash init.sh uninstall' that permanently removes the whole
LibrePortal footprint, behind a typed 'DELETE LIBREPORTAL' confirmation:
- stops + removes the task-processor service
- best-effort graceful container removal, then tears down the rootless
  docker setup + the install user's session (linger/terminate/pkill)
- removes the out-of-/docker footprint (/usr/local/lib/libreportal +
  /usr/local/bin/libreportal, /etc/sudoers.d, the systemd unit, the
  sysctl drop-ins, restic/kopia/ufw-docker, /root/init.sh)
- rm -rf /docker
- removes the libreportal + dockerinstall users + subuid/subgid ranges

Runs as root (the entrypoint root-check enforces it — and the scoped
sudoers can no longer self-remove anyway); self-contained (only init.sh's
inline helpers, so it works as it deletes /docker); ordered so containers/
daemon stop before the users are removed. Leaves docker/compose/apt deps
and SSH config in place (no lockout). Mirrors FOOTPRINT.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-24 19:54:11 +01:00
librelad
65937e8108 Merge claude/1 2026-05-24 19:40:35 +01:00
librelad
15fc42c858 refactor(layout): consolidate out-of-/docker files + fix sysctl dir
Organise the system footprint outside /docker:
- All LibrePortal executables now live together in /usr/local/lib/libreportal/
  (root:root): the 7 root helpers AND the CLI wrapper. /usr/local/bin/libreportal
  becomes a symlink onto $PATH. run_privileged._runRootHelper, init.sh
  (initRootHelpers + scoped-sudoers Cmnd_Alias + command setup) all point there.
  The wrapper is now root-owned too (manager can't tamper with its entrypoint).
- Fix a real bug: rootless sysctl settings were written to /etc/sysctl/99-custom.conf,
  a dir  does NOT read, so net.ipv4.ip_unprivileged_port_start /
  kernel.unprivileged_userns_clone never persisted across reboot. Moved to
  /etc/sysctl.d/99-libreportal-rootless.conf (the existing
  reload now actually applies them). Consistent libreportal* naming.
- Drop dead fqdn_file=/root/libreportal-fqdn.txt global (never used).
- Add FOOTPRINT.md: a manifest of every file LibrePortal places outside /docker
  (doubles as an uninstall checklist).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-24 19:40:35 +01:00
librelad
d8cad2677d Merge claude/1 2026-05-24 19:22:22 +01:00
librelad
cd4fd55a6d feat(desudo): helper-ize backup-engine + app-config installs; retire standalone WireGuard
Bring the remaining deferred subsystems under the scoped sudoers, and drop
the one that's redundant.

Backup engines + app configs -> root-owned helpers (same pattern as
ownership/dns/ssh/socket/svc):
- scripts/system/libreportal-bininstall: install <restic|kopia> — does the
  whole pkg-manager/signed-download install itself for a fixed, validated
  engine name (no blanket sudo apt-get/install). restic_install/kopia_install
  call it.
- scripts/system/libreportal-appcfg: {adguard-auth <user> <bcrypt>|
  crowdsec-priority|owncloud-config <public> <host> <ip> <public_ip>} —
  faithful ports of the AdGuard yaml / CrowdSec bouncer / ownCloud config.php
  rewrites, fixed paths + validated args. adguard_auth/crowdsec_fix_priority/
  owncloud_setup_config call it.
- run_privileged: runBinInstall / runAppCfg; init.sh installs + allowlists both.

Retire standalone (host-level) WireGuard — it's a duplicate of the
containerized containers/wireguard app (+ headscale mesh), its slirp4netns
speed rationale is largely moot with a better rootless net backend / typical
WAN-bound throughput, and it was the heaviest host-root subsystem (apt +
sysctl + iptables + /etc/wireguard), the worst fit for the rootless/
least-privilege direction:
- moved scripts/wireguard/ + manage_wireguard.sh + check_wireguard.sh to
  scripts/unused/; dropped the install-path call, the Tools menu 'w' entry,
  and the requirement check; removed the half-built libreportal-wg helper.
- generate_arrays.sh now also skips system/ (root-owned helpers, never
  sourced); arrays regenerated (files_wireguard.sh pruned).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-24 19:22:22 +01:00
librelad
048e967ec1 Merge claude/1 2026-05-24 18:58:48 +01:00
librelad
32cdf96c13 fix(webui): re-apply tag processors after the WebUI compose template copy
installLibrePortalImageWebUI copyFolder's the template docker-compose.yml
(raw #LIBREPORTAL|TAG|VALUE placeholders) over the runtime one on every
WebUI build — including rebuilds/updates. On a fresh install the following
dockerInstallApp substitutes them, but on a rebuild (libreportal already
installed) nothing did, so the at-rest compose kept raw placeholders and a
plain 'docker compose' against it failed ("invalid boolean:
HEALTHCHECK_DATA", etc.) — it only worked because up_app.sh self-heals at
CLI start time. Re-run the tag processors (initializeAppVariables +
dockerConfigSetupFileWithData, the same heal up_app.sh uses) right after
the copy when libreportal is already installed, so the runtime compose is
always fully substituted at rest.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-24 18:58:48 +01:00
librelad
a2bdcf0e4f Merge claude/1 2026-05-24 18:48:16 +01:00
librelad
c9e6afea79 feat(desudo): init.sh installs the SCOPED sudoers by default — kill NOPASSWD:ALL
Replace the NOPASSWD: ALL drop-in with a validated, scoped grant:
  - (dockerinstall) NOPASSWD:SETENV: ALL   (data plane; rootless-confined)
  - (root) NOPASSWD: the 5 root-owned /usr/local/sbin/libreportal-* helpers
    + a fixed system-binary allowlist (systemctl/ufw/ufw-docker/nft/sysctl/
    loginctl/service)
No bash/su/tee/cp/chmod/chown/sed/mv/rm/install — none of the
root-equivalent primitives. Also: drop '-G sudo' from the manager useradd
(privileges come from the user-specific drop-in, not group membership),
and defensively remove legacy broad grants on re-run (a NOPASSWD: ALL line
appended to the main /etc/sudoers + sudo-group membership).

Validated live end-to-end as the manager: app lifecycle, webui generate,
ownership reconcile, ssh/dns/socket/svc helpers, task service, data-plane
drop (incl. -E for backups) all denial-free; sudo bash / sudo cat shadow /
arbitrary sudo chown all denied.

Residual (still raw runSystem file-primitives, denied under the scoped
grant until they get helpers / docker-exec rework): owncloud/adguard/
crowdsec app-config edits, wireguard-standalone, restic/kopia binary
self-install. These are opt-in/deferred features.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-24 18:48:16 +01:00
librelad
12476b507a Merge claude/1 2026-05-24 18:45:33 +01:00
librelad
ac163e3808 fix(desudo): don't run init.sh install-mode detect/write when sourced
start.sh sources init.sh for its function defs at runtime (Model A). The
top-level install-mode auto-detect + initUpdateConfigOption write ran on
every source, rewriting CFG_INSTALL_MODE via 'sudo sed' on the
manager-owned config — denied under the scoped sudoers (the last
per-command 'a password is required'), and spurious '"Auto-detected ..."'
noise. Gate both on BASH_SOURCE==$0 (executed directly only); also drop
the needless sudo from initUpdateConfigOption (config is manager-owned).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-24 18:45:33 +01:00
librelad
6431e7abbe Merge claude/1 2026-05-24 18:40:19 +01:00
librelad
6bb04533fa fix(desudo): manager->self sudo drops -> runAsManager (scoped-sudoers safe)
The scoped sudoers grants the manager (root) and (dockerinstall) but NOT
(itself), so the many 'sudo -u $sudo_user_name <cmd>' calls (crontab,
git/update, reinstall, swapfile, …) failed with 'a password is required'
once per CLI command. runAsManager runs the command plainly when already
the manager (the runtime case) and only sudo -u's when root (install
time), so it's correct in both contexts and needs no sudoers self-grant.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-24 18:40:19 +01:00
librelad
4f2fd251fa Merge claude/1 2026-05-24 18:38:19 +01:00
librelad
13d2c15074 fix(desudo): de-sudo config scan so the manager runtime loads CFG
scan_files used 'sudo find' to enumerate config files to source. Under the
scoped sudoers that's denied, so NO configs got sourced -> CFG_DOCKER_INSTALL_TYPE
ended up empty -> runFileOp/runFileWrite fell back to the manager branch and
every container-path write failed. Root cause of the 'sudo: a password is
required' + 'tee: Permission denied' storm when running under the scoped grant.

- configs/ scan (manager-owned): plain find
- app_configs scan (/docker/containers, docker-install-owned, not list-readable
  by the manager): runFileOp find (enumerate as that user; manager still sources
  each .config, which is o+r). 'containers' install templates stay plain find.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-24 18:38:19 +01:00
librelad
2d9450fed4 Merge claude/1 2026-05-24 18:28:56 +01:00
librelad
9af2465ffe feat(desudo): socket + systemd-svc helpers; route traefik/db chowns + svc
Move the last runtime-critical root file-primitive subsystems behind
root-owned helpers so the type switcher + task service work under a scoped
sudoers:

- scripts/system/libreportal-socket: {rootless|rooted} {on|off} chmod of
  the docker sockets (paths computed from config, not caller-supplied;
  exit 3 = absent so the *_found flags come from its exit code)
- scripts/system/libreportal-svc: GENERATES + installs the systemd unit
  from config (mode/uid/baked manager) — never accepts unit content from
  the caller (arbitrary unit = root). Idempotent install/enable/restart.
- ownership helper: add db-own + app-file <app> <relpath> actions
- run_privileged: runSocket / runSvc
- set_socket_permissions -> runSocket; webui_install_systemd -> runSvc
  (+ crontab cleanup runs as the manager directly, no sudo -u self)
- before_start: db chown -> runOwnership db-own; traefik cert/yml ->
  runOwnership app-file (retires updateFileOwnership/changeRootOwnedFile)
- init.sh installs all five helpers

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-24 18:28:56 +01:00
librelad
c6ba3b0ed8 Merge claude/1 2026-05-24 18:21:46 +01:00
librelad
d17e8814d0 feat(desudo): root-owned DNS + host-SSH-access helpers
Two more runtime root file-primitive subsystems moved behind self-
validating root-owned helpers so the scoped sudoers needn't grant blanket
sudo sed/tee/cp on /etc (which is root-equivalent — sudo arg wildcards
match across '/', so even path-scoped entries are bypassable):

- scripts/system/libreportal-dns: {clear|add <ip>} — edits /etc/resolv.conf
  only, validates the IP argument
- scripts/system/libreportal-ssh-access: authorized_keys + sshd
  PasswordAuthentication management, with the lockout guards moved INTO the
  helper (the trust boundary) so a compromised manager can't bypass them
- run_privileged: _runRootHelper dispatcher + runResolv / runSshAccess
  (runOwnership now uses it too)
- init.sh: initRootHelpers installs all three helpers root:root 0755 with
  the manager name baked in
- setup_dns -> runResolv (+ ping de-sudo'd, works unprivileged); host_access
  + webui_ssh_access -> runSshAccess

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-24 18:21:46 +01:00
librelad
dd414a6e73 Merge claude/1 2026-05-24 18:16:23 +01:00
librelad
46622cd2f9 feat(desudo): root-owned ownership helper (no blanket sudo chown needed)
Under Model A the runtime runs as the manager, so establishing the
/docker ownership model needs root. Granting the manager a blanket
'sudo chown'/'sudo chmod' in the scoped sudoers would be root-equivalent
(chown /etc/sudoers, ...). Introduce a self-contained, root-owned helper
that performs only a FIXED set of reconciles on FIXED LibrePortal paths,
with owners derived from config + a baked manager name (never the caller)
and a strictly-validated app-name argument.

- scripts/system/libreportal-ownership: the helper (actions: reconcile,
  traversal, containers-top, app-perms, webui, taskdir, app-data-nobody)
- run_privileged: runOwnership wrapper (sudo the installed helper; run the
  bundled copy directly when already root mid-install)
- init.sh: installOwnershipHelper bakes the manager name and installs it
  root:root 0755 to /usr/local/sbin (manager can't modify it)
- libreportal_folders/app_folder/app_update_specifics/task processor:
  delegate the ownership chowns to runOwnership instead of runSystem chown

This removes chown/chmod-on-/docker from the runtime sudo surface, a
prerequisite for a non-root-equivalent scoped sudoers.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-24 18:16:23 +01:00
librelad
85fd086281 Merge claude/1 2026-05-24 18:09:20 +01:00
librelad
78e7651ea0 feat(desudo): run start.sh AS the manager (Model A flip) + fix exposed writes
The CLI wrapper already runs as the manager (libreportal) but then did
'sudo ./start.sh', so the whole runtime executed as root — the reason
NOPASSWD:ALL was load-bearing. Drop that sudo so start.sh runs as the
manager; also drop the now-redundant sudo from the wrapper's own
manager-owned ops (config sed, /docker/configs + /docker/install
mkdir/cp/chown/rm, 'sudo -u libreportal' git clone, chmod). Only the
'cp -f init.sh /root/' copies stay root.

Running as the manager surfaced data-plane writes that only worked under
root; fixed to be owner-correct:
- webui_system_metrics: .metrics_{cpu,net}_prev state via runFileWrite
- atomicWriteWebUI: path-aware temp+chmod+mv (atomic same-dir rename as
  the path owner) instead of bare >/mv
- webui_app_config last_update trigger via runFileWrite

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-24 18:09:20 +01:00
librelad
99e3bde574 Merge claude/1 2026-05-24 18:03:36 +01:00
librelad
21afae2eff refactor(desudo): drop runtime root from docker_run, sqlite guards, restores
- docker_run: in rooted mode run docker AS the manager via the docker
  group (no sudo); the type=='sudo' branch was unreachable dead code
- 8 db helpers: fix 'command -v sudo sqlite3' guard to 'command -v
  sqlite3' (bodies already query via runInstallOp)
- restic/kopia single-file dump: write target_file via runBackupOp tee
  (as the backup user, matching the snapshot-restore path) instead of
  root tee
- adguard auth: root-owned scratch via runSystem mktemp

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-24 18:03:36 +01:00
librelad
f13a5bc548 Merge claude/1 2026-05-24 18:01:51 +01:00
librelad
0b27ed1072 refactor(desudo): funnel backup-engine privilege drop through runBackupOp
The borg/restic/kopia engines all dropped to the dedicated backup user
via scattered 'sudo -E -u $docker_install_user'. Centralize that into a
single runBackupOp helper so the backup subsystem has one audit point and
the scoped sudoers needs only the (dockerinstall) drop rule.

Also:
- owncloud config heredoc tees -> runSystem (container-UID file)
- webui_display_logins: fix the broken 'command -v sudo sqlite3' guard
  to 'command -v sqlite3' (body already runs sqlite3 via runInstallOp)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-24 18:01:51 +01:00
librelad
0dc86e62f6 Merge claude/1 2026-05-24 18:00:19 +01:00
librelad
8b14f26125 refactor(desudo): route scattered runtime sudo through privilege helpers
Convert the remaining ad-hoc 'sudo' calls across the data plane to the
run_privileged helpers so every file op lands as the correct owner with
no blanket root:

- DB/configs (manager-owned): db_list_all_apps, delete_db_file,
  install_sqlite, cli_webui_commands -> runInstallOp
- containers (dockerinstall-owned): scan_container_socket, delete_data,
  webui_task_files, webui_app_log, webui_config_patch,
  application_missing_variables, uninstall_app -> runFileOp/runFileWrite
- genuine root: passwd, tailscale, ufw-docker, sysctl grep, systemd
  unit read, authorized_keys read, nobody chown -> runSystem
- interactive editors and 'id -u': drop sudo entirely (run as caller)
- owncloud/adguard container-UID config edits -> runSystem (funnel;
  docker-exec rework deferred)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-24 18:00:19 +01:00
librelad
fb87d0e687 Merge claude/1 2026-05-24 17:55:07 +01:00
librelad
eea5b41e68 fix(rootless): tagsManager in-place edit via runFileOp/runInstallOp
tagsManagerUpdateUniversalTag did a bare 'sed -i "$file_path"' — works only
because start.sh runs as root today; under Model-A-as-manager the manager
can't create sed's temp file in the dockerinstall-owned containers dir
(permission denied). Make the in-place edit run AS the file's owner: classify
by path (containers/<app> -> runFileOp, manager configs/templates ->
runInstallOp), like createTouch. The awk read stays unescalated (config/compose
are world-readable). Unblocks running the whole app as the manager for tag ops.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-24 17:55:07 +01:00
librelad
574146c75e Merge claude/1 2026-05-24 17:37:14 +01:00
librelad
4ee231ae9f refactor(de-sudo): wireguard -> runSystem, traefik -> runFileOp
Wireguard standalone touches /etc/wireguard + sysctl exclusively (genuine
root) -> runSystem for all its mkdir/chmod/sed/rm/grep/tee/qrencode. Traefik
dynamic configs live under containers/traefik (docker-install-owned) ->
runFileOp/runFileWrite (whitelist.yml, protectionauth.yml, the router-rewrite
awk|tee|mv in port_subdomains). sudo -u drops left.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-24 17:37:14 +01:00
librelad
c2cb75cfd9 Merge claude/1 2026-05-24 17:35:09 +01:00
librelad
2c907b25c2 refactor(de-sudo): compose/setup/run misc off raw sudo
- copy_build_context: rsync/cp/rm -> runFileOp (writes the deployed tree AS the
  container owner with --no-owner); drop the now-redundant runSystem chown.
- setup_lock: .setup_complete is in the docker-install-owned frontend/data ->
  runFileOp touch/chmod/rm (drop the chown).
- tags_processor_docker_installation 'user:' enable + update_compose_yml
  jail.local -> runFileOp (deployed compose/config under containers).
- crontab_clear: clear the manager's own crontab via runInstallOp.
- reinstall: cp init.sh to /root -> runSystem (genuine root path).
- create_successful_run_file: drop the pointless sudo echo -> runInstallWrite to
  /docker/run.txt.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-24 17:35:09 +01:00
librelad
db681fbcd1 Merge claude/1 2026-05-24 17:30:25 +01:00
librelad
5ceef2df6a refactor(de-sudo): config/password processors off raw sudo
scanConfigsForRandomPassword iterates $configs_dir (manager-owned), so the
placeholder grep/sed/awk on the config file -> runInstallOp. The bcrypt export
log ($containers_dir/bcrypt.txt) is docker-install-owned, so its touch/chmod/
sed/grep/append -> runFileOp/runFileWrite (NOT runInstallOp). Covers all
password_replace*/password_user_replace/password_update_all and bcrypt/*.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-24 17:30:25 +01:00
librelad
cf148327fd Merge claude/1 2026-05-24 17:24:44 +01:00
librelad
33107c4f27 refactor(de-sudo): rework generic file/folder helpers to path-aware ownership
The old copy/move helpers ran 'sudo cp/mv X Y; sudo chown $user_name Y' (root +
arbitrary chown). Rework them to write AS the destination's owner — no root, no
chown — classifying by dest path like createTouch: /docker/containers/<app> ->
runFileOp (docker install user), manager-owned control plane -> runInstallOp.
The $user_name arg is now advisory (the path decides). Covers copyFile/copyFiles/
copyFolder/copyFolders/moveFile; copyResource is always containers -> runFileOp;
createFolders' non-container branch -> runInstallOp; updateFileOwnership (an
arbitrary user1:user2 chown) -> runSystem. Confirmed by callers (containers vs
$docker_dir/backup_install_dir/configs dests). Removes a class of root data ops
+ arbitrary-chown from the runtime.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-24 17:24:44 +01:00
librelad
9a694115ac Merge claude/1 2026-05-24 17:14:31 +01:00