Skip to content

feat: add Custom Node Manager#9047

Open
Pfannkuchensack wants to merge 33 commits intoinvoke-ai:mainfrom
Pfannkuchensack:feature/custom-node-manager
Open

feat: add Custom Node Manager#9047
Pfannkuchensack wants to merge 33 commits intoinvoke-ai:mainfrom
Pfannkuchensack:feature/custom-node-manager

Conversation

@Pfannkuchensack
Copy link
Copy Markdown
Collaborator

Summary

Adds a Custom Node Manager as a new tab in the sidebar, allowing users to install, manage, and remove community node packs directly from the UI — no manual file copying or restarts required.

Backend:

  • New API router (/api/v2/custom_nodes/) with endpoints for list, install (git clone), uninstall, and reload
  • Runtime node loading/unloading via InvocationRegistry.unregister_pack() — no restart needed
  • Automatic pip install of requirements.txt dependencies
  • Automatic detection and import of workflow .json files from node pack repos (tagged with node-pack:<name> for filtering, removed on uninstall)
  • OpenAPI schema cache invalidation so the workflow editor reflects changes immediately

Frontend:

  • New "Nodes" tab with circuit icon (PiCircuitryBold) in the sidebar between Models and Queue
  • Two-panel layout matching the Model Manager pattern:
    • Left: installed node packs list with reload and per-pack uninstall
    • Right: tabbed install UI (Git Repository URL / Scan Folder) with install log table
  • Security warning banner about trusting node pack authors
  • RTK Query endpoints with cache tag invalidation for CustomNodePacks, Schema, and Workflow

Docs:

  • User guide: docs/nodes/customNodeManager.md
  • Developer guide: docs/nodes/creatingNodePack.md (repo structure, __init__.py, workflows, best practices)

Related Issues / Discussions

N/A — new feature

QA Instructions

  1. Start the dev server (backend + frontend)
  2. Click the new Nodes tab (circuit icon) in the left sidebar
  3. Install a node pack: paste a Git URL into the "Git Repository URL" tab and click Install. Verify:
    • The pack appears in the left panel with node count and type badges
    • If the repo contains workflow .json files, they appear in the workflow library (tagged node-pack:<name>)
    • The install log at the bottom shows the result
  4. Reload: click the Reload button and verify it picks up manually added packs
  5. Uninstall: click Uninstall on a pack and verify:
    • The pack disappears from the list
    • The nodes are no longer available in the workflow editor (no restart needed)
    • Imported workflows from that pack are removed from the library
  6. Edge cases: try installing with an invalid URL, a repo without __init__.py, an already-installed pack

Merge Plan

No special considerations. No DB schema changes — workflows are stored via the existing workflow library API.

Checklist

  • The PR has a short but descriptive title, suitable for a changelog
  • Tests added / updated (if applicable) — 21 backend + 10 frontend tests
  • ❗Changes to a redux slice have a corresponding migration
  • Documentation added / updated (if applicable) — user guide + developer guide
  • Updated What's New copy (if doing a release after this PR)

…from the UI

Adds a new "Nodes" tab (circuit icon) to the sidebar with a two-panel layout:
- Left: list of installed custom node packs with reload and uninstall
- Right: tabbed install UI (Git URL / Scan Folder) with install log

Backend API endpoints (POST install, DELETE uninstall, POST reload, GET list)
handle git clone, pip dependency install, runtime node loading/unloading,
and automatic workflow import from node pack repositories. Workflows are
tagged with node-pack:<name> and removed on uninstall.

Includes user and developer documentation, plus 31 tests (21 backend, 10 frontend).
@github-actions github-actions Bot added api python PRs that change python files invocations PRs that change invocations frontend PRs that change frontend files python-tests PRs that change python tests docs PRs that change docs labels Apr 12, 2026
@Pfannkuchensack
Copy link
Copy Markdown
Collaborator Author

image image

@iwr-redmond
Copy link
Copy Markdown

One of the most annoying problems with ComfyUI's custom nodes is conflicting dependencies, which has gotten so bad that ComfyUI is now developing an isolation solution to mitigate it. Has there been any thought at this stage about how to protect the base install from incompatible dependencies in requirements.txt?

@Pfannkuchensack
Copy link
Copy Markdown
Collaborator Author

So far, I haven't seen many Invoke nodes that require "requirements.txt". I'd rather not make a big deal out of it while it's not too serious.

@iwr-redmond
Copy link
Copy Markdown

At the moment custom node authors have a variety of dependency installation mechanisms. Here are some examples from my list of custom nodes:

A simple start would be to warn users when additional packages are being installed that it might break their system. Another option that isn't too onerous would be to use the pip constraints feature to preserve the integrity of the main package.

@JPPhoto
Copy link
Copy Markdown
Collaborator

JPPhoto commented Apr 14, 2026

A simple start would be to warn users when additional packages are being installed that it might break their system. Another option that isn't too onerous would be to use the pip constraints feature to preserve the integrity of the main package.

AFAIK Invoke doesn't install node requirements automatically; that's left to the user to do manually and should be documented by the node author in some fashion. @Pfannkuchensack That's still the case, right?

@Pfannkuchensack
Copy link
Copy Markdown
Collaborator Author

Automatic pip install of requirements.txt dependencies
This pr will install the dependencies if there is any.

But i think we should make that a option and not Automatic. A Checkbox with default to dont install Automatic

@JPPhoto
Copy link
Copy Markdown
Collaborator

JPPhoto commented Apr 14, 2026

@Pfannkuchensack Before I review, does this support updating nodes (via git pull or other mechanism)? How do to that should be documented somewhere, if not. Or if it's trivial to add in, it would be nice to see a "Check and Update" to the left of "Uninstall".

@Pfannkuchensack
Copy link
Copy Markdown
Collaborator Author

Update is not build in right now. but could be easy added.

@JPPhoto
Copy link
Copy Markdown
Collaborator

JPPhoto commented Apr 14, 2026

Automatic pip install of requirements.txt dependencies
This pr will install the dependencies if there is any.

But i think we should make that a option and not Automatic. A Checkbox with default to dont install Automatic

Yes, and also maybe a dialog that tells you what's going to be installed with constraints of not downgrading already installed versions or causing conflicts.

In truth, I think even this feels wrong and overly complex, and it will become a maintenance headache. I actually have a node package that has all sorts of requirements but pins the version of Invoke!

@Pfannkuchensack
Copy link
Copy Markdown
Collaborator Author

We need to maintain the venv that is right. But i think if a node has a bad/wrong dependencie that is a node author thing.

@iwr-redmond
Copy link
Copy Markdown

iwr-redmond commented Apr 14, 2026

I agree that pushing as much responsibility off to node authors as possible is the right way to do things. Enforcing sensible limits that protect the venv will make third parties more likely to follow the best practices that are needed, since painting outside the lines will prevent their extensions from being installable from within the UI.

AFAIK Invoke doesn't install node requirements automatically

In this PR, there is an installer function here.

@iwr-redmond
Copy link
Copy Markdown

iwr-redmond commented Apr 14, 2026

Some other feedback:

  • You may wish to consider adding Dulwich as a dependency so that users (especially on Windows) do not need to have git installed for custom node installation to work
  • Since most Invoke users access the application through the Launcher, shouldn't the extension installer use uv pip after setting the VIRTUAL_ENV environment variable to $INVOKEAI_ROOT/.venv?

@JPPhoto
Copy link
Copy Markdown
Collaborator

JPPhoto commented Apr 14, 2026

We need to maintain the venv that is right. But i think if a node has a bad/wrong dependencie that is a node author thing.

I agree to both, but we cannot have nodes fighting each other for dependencies. Just as models are validated before installing, nodes should be flagged as incompatible (or invalid) if they introduce conflicting dependencies. That does mean that if node A requires foo==1.8 and node B requires foo<=1.6, one or the other can be installed but not both. I'm fairly sure you can get this information out of uv pip. But I definitely don't think it's appropriate to let people install node requirements if we haven't fleshed this out thoroughly.

So, my recommendation prior to reviewing this PR is to remove the automatic requirements.txt or pyproject.toml installation for version 1 and to have a detection with a toast that informs the user that they will have to perform an additional requirements installation using that node's documentation as the guide.

@JPPhoto
Copy link
Copy Markdown
Collaborator

JPPhoto commented Apr 14, 2026

(Plus, add in the automatic search for updates feature just to make everything complex yet again.)

@JPPhoto
Copy link
Copy Markdown
Collaborator

JPPhoto commented Apr 17, 2026

Findings

  • Medium: non-admin users can still land on the Custom Nodes screen through persisted UI state even after the navbar gate was added. invokeai/frontend/web/src/features/ui/store/uiTypes.ts:4 still allows activeTab: 'customNodes', that state is persisted through invokeai/frontend/web/src/features/ui/store/uiSlice.ts:98, and invokeai/frontend/web/src/features/ui/components/AppContent.tsx:40 renders CustomNodesTabAutoLayout whenever the active tab is customNodes with no permission check. The new gate only hides the nav button in invokeai/frontend/web/src/features/ui/components/VerticalNavBar.tsx:42; it does not sanitize persisted state or guard the tab content itself. Once that tab renders, invokeai/frontend/web/src/features/customNodes/CustomNodesList.tsx:64 immediately calls the admin-only list endpoint, and the install pane renders admin-only actions as well. The evidence chain is: persisted activeTab can still be customNodes -> tab content is rendered unconditionally -> mounted components call admin-only endpoints -> a non-admin can still hit an unauthorized management screen and trigger 403-backed requests. To expose this issue, add a test that rehydrates UI state with activeTab='customNodes' for a non-admin user in multiuser mode and verifies the app redirects to an allowed tab or suppresses the Custom Nodes content entirely.

Weaker Concerns

  • invokeai/tests/app/routers/test_custom_nodes.py has grown from 21 to 27 passing tests and now covers the manifest-based uninstall fix well, but it still does not include route-level auth tests for list_custom_node_packs, install_custom_node_pack, uninstall_custom_node_pack, or reload_custom_nodes in invokeai/app/api/routers/custom_nodes.py. The auth hardening looks correct by inspection, but there is still no API test that would catch a future removal of AdminUserOrDefault. To expose this issue, add tests that verify non-admin callers are rejected and admin callers succeed for all four endpoints.

  • invokeai/frontend/web/src/features/customNodes/useIsCustomNodesEnabled.ts:16 is the new permission hook, but I do not see a corresponding frontend test for the multiuser/admin matrix. Given that the PR now relies on that hook for the navbar gate in invokeai/frontend/web/src/features/ui/components/VerticalNavBar.tsx:25, the absence of a logic-level test leaves the frontend permission behavior easier to regress than it should be. To expose this issue, add a logic test that covers single-user mode, multiuser admin, and multiuser non-admin inputs for useIsCustomNodesEnabled.

…add auth regression tests

- Suppress CustomNodesTabAutoLayout render and redirect to generate via
  navigationApi.switchToTab when a non-admin user lands on a persisted
  customNodes tab
- Add TestCustomNodesAuthorization with 10 route-level tests verifying
  unauthenticated (401), non-admin (403), and admin (200) for list,
  install, uninstall, and reload endpoints
- Add decision-matrix test for useIsCustomNodesEnabled covering
  single-user, multiuser admin, multiuser non-admin, and unloaded user
@JPPhoto
Copy link
Copy Markdown
Collaborator

JPPhoto commented Apr 17, 2026

Two more things, minor:

  • invokeai/frontend/web/src/features/customNodes/useIsCustomNodesEnabled.test.ts:9 does not test the actual hook; it tests a local reimplementation of the decision table. That still gives some regression value, but it can drift from invokeai/frontend/web/src/features/customNodes/useIsCustomNodesEnabled.ts:16 if the hook later adds behavior around loading state, selector wiring, or setup-status handling. To expose this issue, add a logic-level test around the real exported implementation or refactor the decision into a shared pure helper that both the hook and the test import.

  • tests/app/routers/test_multiuser_authorization.py:1873 and tests/app/routers/test_multiuser_authorization.py:1877 only verify admin success for list and reload. The new tests do a good job covering unauthenticated and non-admin rejection for all four custom-node routes, but they still do not prove the happy path for admin install and uninstall. I am keeping this below finding level because the route auth boundary itself is now well covered and the missing positive cases are more about regression completeness than a demonstrated defect. To expose this issue, add tests that mock filesystem and subprocess behavior so admin callers can successfully exercise install and uninstall as well.

Extract getIsCustomNodesEnabled so test imports the real logic
instead of a local reimplementation. Add install/uninstall
admin-success tests with mocked filesystem/subprocess.
@JPPhoto
Copy link
Copy Markdown
Collaborator

JPPhoto commented Apr 18, 2026

Findings

  • Medium: invokeai/frontend/web/src/features/customNodes/useIsCustomNodesEnabled.ts:31 introduces a startup-time false negative that can incorrectly kick allowed single-user users out of the Custom Nodes tab. The hook treats missing setupStatus as if multiuser_enabled were true, and invokeai/frontend/web/src/features/ui/components/AppContent.tsx:38 immediately redirects any customNodes active tab to generate when the hook returns false. The evidence chain is: initial RTK Query state can be "not loaded yet" -> the hook defaults that state to multiuser mode -> isCustomNodesEnabled becomes false when user?.is_admin is still undefined -> persisted activeTab='customNodes' is redirected away before setup status resolves -> a legitimate single-user session loses its restored tab and never returns to Custom Nodes automatically. The new test file invokeai/frontend/web/src/features/customNodes/useIsCustomNodesEnabled.test.ts:20 covers only the pure helper's explicit inputs and does not cover the hook's "setup status not loaded yet" behavior that triggers this redirect. To expose this issue, add a logic-level test that rehydrates activeTab='customNodes' with setup status initially unresolved in single-user mode and verifies the app does not redirect away once that unresolved state is still compatible with allowed access.

Weaker Concerns

  • invokeai/frontend/web/src/features/customNodes/ScanNodesForm.tsx:12 derives the displayed "Nodes directory" from data?.node_packs?.[0]?.path, so the scan-folder pane can only show the directory if at least one node pack is already installed. But the copy in invokeai/frontend/web/public/locales/en.json:3326 says users can place node packs in the nodes directory and reload, which is most useful precisely when the list is empty. The evidence chain is: scan pane only knows the directory through the first installed pack -> empty install state means no first pack -> nodesDir stays null -> the UI hides the directory label on the first-use path where the user most needs it. I am keeping this below finding level because it is a UX gap, not a correctness or security defect. To expose this issue, add a logic or API-backed test that covers the zero-installed-packs state and verifies the scan-folder pane can still surface the configured nodes directory.

@JPPhoto
Copy link
Copy Markdown
Collaborator

JPPhoto commented Apr 18, 2026

I took another look!

invokeai/frontend/web/src/features/customNodes/useIsCustomNodesEnabled.ts:35 now returns an optimistic true whenever setupStatus has not loaded yet, which fixes the single-user redirect bug but reopens the non-admin exposure path during startup. invokeai/frontend/web/src/features/ui/components/VerticalNavBar.tsx:42 uses that value to show the Custom Nodes tab, and invokeai/frontend/web/src/features/ui/components/AppContent.tsx:51 uses it to render CustomNodesTabAutoLayout. That tab immediately mounts admin-only screens through invokeai/frontend/web/src/features/ui/components/tabs/CustomNodesManagerTab.tsx:8, and invokeai/frontend/web/src/features/customNodes/CustomNodesList.tsx:64 then calls the admin-only list endpoint. The evidence chain is: unresolved setup status -> optimistic isCustomNodesEnabled=true for all users -> non-admin multiuser user can see and open the tab before the query resolves -> frontend issues admin-only requests and then relies on backend 403s. This is a real regression in the frontend permission boundary, even if the server still blocks the operation.

To expose this issue, add a logic-level test that simulates multiuser mode with a non-admin user while setupStatus is still unresolved and verifies the Custom Nodes tab and content do not become reachable during that loading window.

I think this behavior needs to be better defined:

  • loading: do not show the tab yet, do not redirect yet
  • known + allowed: show/render it
  • known + denied: hide it and redirect away if active

The easiest approach is to stop using a single boolean for both of these concerns:

  • "Should I avoid redirecting a legitimate persisted single-user tab while auth/setup is still loading?"
  • "Should I expose admin-only UI right now?"

Those need different answers. Thus a simple resolution is to:

  • Keep the optimistic behavior for the redirect path.
  • Keep the UI gate conservative until setup status is known.

Practically, split useIsCustomNodesEnabled() into two states, isCustomNodesKnown and isCustomNodesAllowed.

Then:

  • In VerticalNavBar, only show the tab when isCustomNodesAllowed is true.
  • In AppContent, only redirect away from customNodes once permission is known and denied.

…ding window

useIsCustomNodesEnabled now returns { isKnown, isAllowed } so the navbar
hides the tab conservatively (isAllowed=false while loading) while the
redirect only fires once the decision is definitive (isKnown && !isAllowed),
preventing both the non-admin flash and the single-user kickout.
@JPPhoto
Copy link
Copy Markdown
Collaborator

JPPhoto commented Apr 19, 2026

invokeai/frontend/web/src/features/customNodes/useIsCustomNodesEnabled.test.ts:38 still does not exercise the actual hook or the actual consumers. It validates a simulated contract that mirrors the hook logic, which is useful, but it can still drift from the real behavior in invokeai/frontend/web/src/features/customNodes/useIsCustomNodesEnabled.ts:42, invokeai/frontend/web/src/features/ui/components/VerticalNavBar.tsx:25, and invokeai/frontend/web/src/features/ui/components/AppContent.tsx:33. The good news is that the implementation itself now looks coherent: loading hides the tab, loading suppresses content, and redirect only happens once denial is known. To make sure there aren't regressions in the future, add a logic-level test around the real exported permission object flow or refactor the permission-state derivation into a shared pure helper that both the hook and the tests consume directly.

I'm continuing with my UI/UX testing now that functionality looks right.

Tests now import deriveCustomNodesPermission directly instead
of mirroring the hook logic in a local simulateHook, so hook
and tests can never drift.
@JPPhoto JPPhoto self-requested a review April 19, 2026 16:19
Copy link
Copy Markdown
Collaborator

@JPPhoto JPPhoto left a comment

Choose a reason for hiding this comment

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

I'm hitting a case where I installed a node via git@github.com:JPPhoto/film-grain-node.git - this is showing up in the listing with 0 nodes until I restart Invoke.

The only outstanding question I have: What is the .invokeai_pack_manifest.json file in installed node directories, and is this necessary?

@JPPhoto JPPhoto self-requested a review April 19, 2026 16:23
@JPPhoto
Copy link
Copy Markdown
Collaborator

JPPhoto commented Apr 19, 2026

This was discovered by removing a pack and then reinstalling.

Thus, the likely failure is stale module cache on uninstall/reinstall:

  • invokeai/app/api/routers/custom_nodes.py:298 only removes sys.modules[pack_name] on uninstall.
  • It does not remove submodules like pack_name.foo, pack_name.nodes, etc.
  • On reinstall, invokeai/app/api/routers/custom_nodes.py:349 loads the root package again, but if the pack's invocations are defined in submodules, Python can reuse the cached submodules instead of re-executing them.
  • The invocation decorators in invokeai/app/invocations/baseinvocation.py:771 only register nodes when those class definitions run.
  • So after uninstall -> reinstall, the pack directory exists, but the invocation classes do not get re-registered, which matches your symptom: pack shows up with 0 nodes, and a full process restart fixes it because sys.modules starts clean.

To expose this issue, add a test that installs a pack whose invocation lives in a submodule, uninstalls it, reinstalls it, and verifies the invocation registry is repopulated without restarting.

The fix is straightforward:

  • on uninstall, delete the whole module subtree from sys.modules, not just the root module
  • practically: remove any key equal to pack_name or starting with pack_name + "."

There is one caveat:

  • if a pack imports code through a different top-level module name than the cloned directory name, there may be an additional naming mismatch, but your uninstall/reinstall path is already enough to prove the cache-clearing bug.

Only removing sys.modules[pack_name] left submodules cached, so
reinstall reused them and the @invocation decorators never re-
registered the nodes — the pack loaded with 0 nodes until a
full process restart. Now _purge_pack_modules strips the root
and every pack_name.* key.
Only removing sys.modules[pack_name] left submodules cached, so
reinstall reused them and the @invocation decorators never re-
registered the nodes — the pack loaded with 0 nodes until a
full process restart. Now _purge_pack_modules strips the root
and every pack_name.* key.
Copy link
Copy Markdown
Collaborator

@JPPhoto JPPhoto left a comment

Choose a reason for hiding this comment

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

All fixed, good to merge!

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

Labels

api docs PRs that change docs frontend PRs that change frontend files invocations PRs that change invocations python PRs that change python files python-tests PRs that change python tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants