Skip to content

fix(plugin-loader): auto-detect new dependencies via requirements.txt hash#355

Merged
ChuckBuilds merged 6 commits into
mainfrom
fix/dep-hash-marker-auto-install
May 30, 2026
Merged

fix(plugin-loader): auto-detect new dependencies via requirements.txt hash#355
ChuckBuilds merged 6 commits into
mainfrom
fix/dep-hash-marker-auto-install

Conversation

@ChuckBuilds

@ChuckBuilds ChuckBuilds commented May 29, 2026

Copy link
Copy Markdown
Owner

Summary

  • The .dependencies_installed marker was an empty file — adding a new package to requirements.txt (e.g. astral in ledmatrix-weather v2.3.0) never triggered a pip re-install on existing installs because the marker already existed
  • The marker now stores a SHA-256 hash of requirements.txt; a hash mismatch (or missing marker) automatically triggers pip install and writes the updated hash
  • store_manager._install_dependencies() also writes the hash marker after a store install/update, preventing a redundant pip run on the next startup

Root cause

User reported ModuleNotFoundError: No module named 'astral' after updating the weather plugin. The astral package was added to requirements.txt in v2.3.0, but plugin_loader.install_dependencies() saw an existing (empty) marker file and skipped pip entirely.

Test plan

  • Install a plugin that has a requirements.txt — confirm .dependencies_installed now contains a SHA-256 hash string instead of being empty
  • Modify requirements.txt (add/change a package) — confirm next restart triggers pip install and updates the hash
  • Unchanged requirements.txt across restarts — confirm pip is skipped (debug log: "requirements unchanged")
  • Install/update a plugin via plugin store — confirm marker is written and pip is not re-run on subsequent startup

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Performance
    • Skips reinstalling unchanged plugin requirements by recording a SHA-256 fingerprint, speeding plugin load and reducing startup time.
  • Stability
    • Writes a fingerprint marker after successful install; marker-write failures are logged but do not cause install to be treated as failed.
  • Security / Behavior
    • Optional containment check enforces installation only into a trusted plugins directory when configured, and installation errors surface with clearer context.

Review Change Stack

… of empty marker

The .dependencies_installed marker was an empty file, so adding a new
package to requirements.txt (e.g. astral in ledmatrix-weather v2.3.0)
never triggered a pip re-install on existing installs — the file existed
so the check returned early.

The marker now stores a SHA-256 hash of requirements.txt. On every plugin
load, the loader compares the current hash to the stored one; a mismatch
(or missing marker) triggers pip install and writes the new hash.
store_manager._install_dependencies() also writes the hash marker after a
store install/update so the loader skips a redundant pip run on next boot.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Compute SHA-256 of each plugin's requirements.txt, store the hash in .dependencies_installed, and skip dependency installation when the stored hash matches. PluginLoader adds optional plugins_dir containment validation and forwards it from PluginManager; both loader and store manager persist the hash marker.

Changes

Dependency installation optimization

Layer / File(s) Summary
Hash generation and persistence in StoreManager
src/plugin_system/store_manager.py
Imports hashlib and, after a successful pip3 install, computes a SHA-256 of requirements.txt and writes it to .dependencies_installed; marker write failures are logged at debug level but do not fail the installation path.
Hash-based install skipping and containment in PluginLoader
src/plugin_system/plugin_loader.py
Adds hashlib; install_dependencies now accepts plugins_dir, verifies plugin_dir containment when provided, compares current requirements.txt SHA-256 against .dependencies_installed to skip reinstalling unchanged deps, runs pip install when needed, and writes the computed hash to the marker (also on the system-managed-packages non-zero pip RECORD path) and sets marker permissions.
Loader signature and forward from PluginManager
src/plugin_system/plugin_loader.py, src/plugin_system/plugin_manager.py
PluginLoader.load_plugin signature/docstring accepts plugins_dir and forwards it into install_dependencies; PluginManager.load_plugin passes plugins_dir=self.plugins_dir when invoking the loader.
sequenceDiagram
  participant PluginManager
  participant PluginLoader
  participant Pip as pip3
  participant FS as Filesystem
  PluginManager->>PluginLoader: load_plugin(..., plugins_dir)
  PluginLoader->>PluginLoader: install_dependencies(plugin_dir, plugins_dir)
  PluginLoader->>FS: read `.dependencies_installed` and `requirements.txt`, compute SHA-256
  alt hashes match
    PluginLoader->>PluginLoader: skip pip install
  else
    PluginLoader->>Pip: run `pip install -r requirements.txt`
    Pip-->>PluginLoader: exit status / RECORD
    PluginLoader->>FS: write `.dependencies_installed` with new hash and set permissions
  end
Loading

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding SHA-256 hashing of requirements.txt to auto-detect dependency changes and trigger reinstalls when needed.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/dep-hash-marker-auto-install

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread src/plugin_system/plugin_loader.py Fixed
Comment thread src/plugin_system/plugin_loader.py Fixed
Comment thread src/plugin_system/plugin_loader.py Fixed
Comment thread src/plugin_system/plugin_loader.py Fixed
@codacy-production

codacy-production Bot commented May 29, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 9 complexity · 2 duplication

Metric Results
Complexity 9
Duplication 2

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/plugin_system/plugin_loader.py (1)

189-189: ⚡ Quick win

Consider adopting the error handling pattern from store_manager.py for marker writes.

If marker_path.write_text() fails on lines 189 or 205, the exception is caught by the generic except Exception handler (line 232), logged as "Unexpected error installing dependencies", and the function returns False. However, at this point, the pip3 install has already succeeded—the dependencies are installed, just the marker file couldn't be written.

This creates a misleading error message and unnecessary reinstall attempts on next startup. store_manager.py (lines 1760-1764) demonstrates a better pattern: wrap the marker write in its own try/except, log failures at debug level, and still return True since the actual dependency installation succeeded.

♻️ Suggested refactor to match store_manager.py pattern
         if result.returncode == 0:
-            marker_path.write_text(current_hash, encoding='utf-8')
-            ensure_file_permissions(marker_path, get_plugin_file_mode())
+            try:
+                marker_path.write_text(current_hash, encoding='utf-8')
+                ensure_file_permissions(marker_path, get_plugin_file_mode())
+            except OSError as marker_err:
+                self.logger.debug("Could not write dependency marker for %s: %s", plugin_id, marker_err)
             self.logger.info("Dependencies installed successfully for %s", plugin_id)
             return True

Apply the same pattern on line 205.

Also applies to: 205-205

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/plugin_system/plugin_loader.py` at line 189, The marker_path.write_text
calls in plugin_loader.py (the writes at marker_path.write_text(current_hash)
around lines where dependencies are installed) should be wrapped in their own
try/except blocks so failures to write the marker don’t cause the whole install
flow to be treated as failed; specifically, inside the function that runs pip3
install (look for the install_dependencies / dependency installation block and
the subsequent marker_path.write_text calls), catch exceptions from
marker_path.write_text, log the error at debug level (similar to
store_manager.py’s pattern) and continue to return True because the pip install
succeeded; apply this same try/except pattern to both marker write sites (the
current_hash write near 189 and the other marker write near 205).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/plugin_system/plugin_loader.py`:
- Around line 168-176: The code computes current_hash via
requirements_file.read_bytes() and reads stored_hash via marker_path.read_text()
outside any try/except, which can raise IOErrors on flaky storage; wrap the
read_bytes() and read_text() calls in a try/except (catching OSError/IOError)
inside the function/method that uses current_hash, marker_path,
requirements_file and plugin_id, log clear, contextual error messages via
self.logger.error (include plugin_id and the exception) and return False (or
propagate a controlled error) so the install check fails gracefully instead of
crashing; ensure the log lines reference current_hash, stored_hash, marker_path
and requirements_file to aid remote debugging.

---

Nitpick comments:
In `@src/plugin_system/plugin_loader.py`:
- Line 189: The marker_path.write_text calls in plugin_loader.py (the writes at
marker_path.write_text(current_hash) around lines where dependencies are
installed) should be wrapped in their own try/except blocks so failures to write
the marker don’t cause the whole install flow to be treated as failed;
specifically, inside the function that runs pip3 install (look for the
install_dependencies / dependency installation block and the subsequent
marker_path.write_text calls), catch exceptions from marker_path.write_text, log
the error at debug level (similar to store_manager.py’s pattern) and continue to
return True because the pip install succeeded; apply this same try/except
pattern to both marker write sites (the current_hash write near 189 and the
other marker write near 205).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8200e5fb-ac00-4ed1-9292-27a1c394f522

📥 Commits

Reviewing files that changed from the base of the PR and between f96fdd9 and 6c47005.

📒 Files selected for processing (2)
  • src/plugin_system/plugin_loader.py
  • src/plugin_system/store_manager.py

Comment thread src/plugin_system/plugin_loader.py Outdated
…ling

- Add explicit relative_to() containment check after path resolution so
  CodeQL recognizes the plugin directory boundary (fixes 4 CodeQL alerts:
  Uncontrolled data used in path expression, lines 168/172/189/205)
- Wrap requirements_file.read_bytes() in try/except OSError — on Raspberry
  Pi with flaky SD card storage this can fail; returns False with a clear log
- Wrap marker_path.read_text() in try/except OSError — a corrupted marker
  falls through to a clean reinstall instead of crashing
- Wrap both marker_path.write_text() calls in try/except OSError — pip
  already succeeded at this point so a marker write failure should not
  return False or propagate through the generic exception handler

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread src/plugin_system/plugin_loader.py Fixed
Comment thread src/plugin_system/plugin_loader.py Fixed
Comment thread src/plugin_system/plugin_loader.py Fixed
Comment thread src/plugin_system/plugin_loader.py Fixed
…eQL path-injection

Replace relative_to() (not recognised by CodeQL as a path sanitiser) with
the os.path.realpath() + startswith() pattern that CodeQL explicitly models
as sanitising py/path-injection.

- Add plugins_dir optional param to install_dependencies() and load_plugin()
- PluginManager.load_plugin() passes self.plugins_dir as the trusted anchor;
  install_dependencies() validates that the resolved plugin_dir starts with
  the resolved plugins_dir before any file I/O
- Replace all Path.read_bytes/read_text/write_text/exists with open() and
  os.path.isfile() so the sanitised string paths flow directly to file ops
  without re-introducing taint through Path object conversion

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread src/plugin_system/plugin_loader.py Fixed
Comment thread src/plugin_system/plugin_loader.py Dismissed
Comment thread src/plugin_system/plugin_loader.py Dismissed
Comment thread src/plugin_system/plugin_loader.py Dismissed
Comment thread src/plugin_system/plugin_loader.py Dismissed
Comment thread src/plugin_system/plugin_loader.py Dismissed
Comment thread src/plugin_system/plugin_loader.py Dismissed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/plugin_system/plugin_loader.py`:
- Around line 610-612: The loader currently ignores the boolean result of
self.install_dependencies, so failures during dependency setup (in
install_dependencies) are only logged and the plugin import proceeds; update the
code in the plugin loading path that calls self.install_dependencies(plugin_dir,
plugin_id, plugins_dir=plugins_dir) to check the returned boolean (or exception)
and fail-fast: if install_dependencies returns False or signals an error, stop
loading the plugin (raise an exception or return an error) and ensure the plugin
module import is not attempted for that plugin_id/plugin_dir, providing a clear
error message that includes plugin_id and plugin_dir to aid troubleshooting.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bcbe1366-4070-4287-8f90-8276408ff2d0

📥 Commits

Reviewing files that changed from the base of the PR and between b44ff07 and abade43.

📒 Files selected for processing (2)
  • src/plugin_system/plugin_loader.py
  • src/plugin_system/plugin_manager.py

Comment thread src/plugin_system/plugin_loader.py Outdated
Chuck and others added 2 commits May 30, 2026 10:47
Previously the boolean result was silently discarded, so a failed pip
install would log a warning but continue attempting to import the plugin
module — resulting in a confusing ModuleNotFoundError instead of a clear
dependency failure message.

Now raises PluginError with plugin_id and plugin_dir if dependency
installation fails, stopping the load before the import is attempted.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…h-injection

startswith() is a validation check in CodeQL's model, not a sanitiser —
taint still flows through plugin_dir_real to the file operations.

os.path.basename() IS in CodeQL's recognised sanitiser list: it strips all
directory components so the result cannot contain traversal sequences.
Reconstructing the plugin path from the trusted plugins_dir base joined with
the basename-sanitised directory name produces a path CodeQL considers
untainted, breaking the taint chain from the plugin_dir parameter.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread src/plugin_system/plugin_loader.py Dismissed
Comment thread src/plugin_system/plugin_loader.py Dismissed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/plugin_system/plugin_loader.py`:
- Around line 172-174: The current logic uses os.path.basename(plugin_dir_real)
into safe_dir_name and builds safe_plugin_dir from plugins_dir_real +
safe_dir_name, but os.path.basename can return an empty string for root paths
causing safe_plugin_dir == plugins_dir_real; update the check in
plugin_loader.py (symbols: plugin_dir_real, safe_dir_name, safe_plugin_dir,
plugins_dir_real, os.path.basename) to guard against an empty safe_dir_name
(e.g. treat it as invalid) before checking os.path.isdir — return/skip/raise an
error when safe_dir_name is empty so you never compare or operate on the
plugins_dir_real itself.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fcb7a40d-2456-463f-8c38-1725d15141fc

📥 Commits

Reviewing files that changed from the base of the PR and between 098a738 and dbad01a.

📒 Files selected for processing (1)
  • src/plugin_system/plugin_loader.py

Comment thread src/plugin_system/plugin_loader.py
…lves to fs root

If plugin_dir somehow resolves to '/' or a bare drive root, os.path.basename()
returns '', causing safe_plugin_dir to equal plugins_dir_real and the isdir()
check to pass incorrectly. Reject early with a clear error in that case.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@ChuckBuilds ChuckBuilds merged commit cac9644 into main May 30, 2026
5 checks passed
@ChuckBuilds ChuckBuilds deleted the fix/dep-hash-marker-auto-install branch May 30, 2026 18:56
@desiv-github

Copy link
Copy Markdown

This sounds like what I am getting when I try to enable the Weather plugin.
I just installed this whole thing today. I used the CURL from the GITHUB so I would think I have the newest files.
The screen just shows:
Weather Err
No modules named 'astra
And in the logs it looks like it is complaining that the Astral package isn't installed.
I did look in the Weather plugin requirements file and it does list astral in there.
But apparently it didn't get installed.
I have tried uninstalling and reinstalled the Weather package from the web interface.
Unfortunately I don't know how to manually install the astral package. (I am getting virtual environment errors and I don't know what those are. My Python (pip), as rudimentary as it is, is also really old.
I think based on the fact that the branch has been merged that this should be fixed already?
But I really don't understand GIT much either. I have only done basic pulls.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants