* fix(web): unify operation history tracking for monorepo plugin operations
The operation history UI was reading from the wrong data source
(operation_queue instead of operation_history), install/update records
lacked version details, toggle operations used a type name that didn't
match UI filters, and the Clear History button was non-functional.
- Switch GET /plugins/operation/history to read from OperationHistory
audit log with return type hint and targeted exception handling
- Add DELETE /plugins/operation/history endpoint; wire up Clear button
- Add _get_plugin_version helper with specific exception handling
(FileNotFoundError, PermissionError, json.JSONDecodeError) and
structured logging with plugin_id/path context
- Record plugin version, branch, and commit details on install/update
- Record install failures in the direct (non-queue) code path
- Replace "toggle" operation type with "enable"/"disable"
- Add normalizeStatus() in JS to map completed→success, error→failed
so status filter works regardless of server-side convention
- Truncate commit SHAs to 7 chars in details display
- Fix HTML filter options, operation type colors, duplicate JS init
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(plugins): prevent root-owned files from blocking plugin updates
The root ledmatrix service creates __pycache__ and data cache files
owned by root inside plugin directories. The web service (non-root)
cannot delete these when updating or uninstalling plugins, causing
operations to fail with "Permission denied".
Defense in depth with three layers:
- Prevent: PYTHONDONTWRITEBYTECODE=1 in systemd service + run.py
- Fallback: sudoers rules for rm on plugin directories
- Code: _safe_remove_directory() now uses sudo as last resort,
and all bare shutil.rmtree() calls routed through it
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(security): harden sudo removal with path-validated helper script
Address code review findings:
- Replace raw rm/find sudoers wildcards with a vetted helper script
(safe_plugin_rm.sh) that resolves symlinks and validates the target
is a strict child of plugin-repos/ or plugins/ before deletion
- Add allow-list validation in sudo_remove_directory() that checks
resolved paths against allowed bases before invoking sudo
- Check _safe_remove_directory() return value before shutil.move()
in the manifest ID rename path
- Move stat import to module level in store_manager.py
- Use stat.S_IRWXU instead of 0o777 in chmod fallback stage
- Add ignore_errors=True to temp dir cleanup in finally block
- Use command -v instead of which in configure_web_sudo.sh
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(security): address code review round 2 — harden paths and error handling
- safe_plugin_rm.sh: use realpath --canonicalize-missing for ALLOWED_BASES
so the script doesn't fail under set -e when dirs don't exist yet
- safe_plugin_rm.sh: add -- before path in rm -rf to prevent flag injection
- permission_utils.py: use shutil.which('bash') instead of hardcoded /bin/bash
to match whatever path the sudoers BASH_PATH resolves to
- store_manager.py: check _safe_remove_directory() return before shutil.move()
in _install_from_monorepo_zip to prevent moving into a non-removed target
- store_manager.py: catch OSError instead of PermissionError in Stage 1 removal
to handle both EACCES and EPERM error codes
- store_manager.py: hoist sudo_remove_directory import to module level
- configure_web_sudo.sh: harden safe_plugin_rm.sh to root-owned 755 so
the web user cannot modify the vetted helper script
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(security): validate command paths in sudoers config and use resolved paths
- configure_web_sudo.sh: validate that required commands (systemctl, bash,
python3) resolve to non-empty paths before generating sudoers entries;
abort with clear error if any are missing; skip optional commands
(reboot, poweroff, journalctl) with a warning instead of emitting
malformed NOPASSWD lines; validate helper script exists on disk
- permission_utils.py: pass the already-resolved path to the subprocess
call and use it for the post-removal exists() check, eliminating a
TOCTOU window between Python-side validation and shell-side execution
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>