Compare commits

..
Author SHA1 Message Date
Dr.Lt.Data 2f7d16f382 Escape untrusted Manager UI content 2026-09-19 08:07:44 +09:00
21 changed files with 13309 additions and 19284 deletions
-45
View File
@@ -458,51 +458,6 @@ Changes take effect after a **restart** (no hot reload).
> outdated ComfyUI deployments.
### Flagged CNR versions: `allow_flagged_nodepack_install`
Manager checks the version status in the existing CNR install response; no
additional Registry request is needed. Active versions continue to install
normally, subject to the existing security policy. Registry refusals (including
HTTP 404) still prevent installation.
Flagged versions must also satisfy the existing security policy and Registry
checks. In addition, they require either **all** `--listen` addresses to be
loopback, such as `127.0.0.1` or `::1`, or the explicit override below.
A mixed loopback/non-loopback listener is non-local.
For a **trusted private network**, administrators can explicitly allow flagged
versions on non-loopback listeners:
```ini
[default]
allow_flagged_nodepack_install = true
```
The default is `false`; only the case-insensitive value `true` enables it.
Restart ComfyUI after editing the setting. This option does not detect private
networks and is independent of `network_mode`. It does not bypass
`security_level`, `allow_git_url_install`, or `allow_pip_install`.
When the flagged-version check denies an action, the UI reports that the current
security configuration does not allow it and directs users to the terminal.
Only the terminal provides the loopback-only `--listen` examples, the private
network override setting, and the instruction to restart ComfyUI after changes.
Enabling an already installed version does not query the Registry or reinstall it.
Older deferred switches without a stored status require loopback-only listeners
or the override. Otherwise they are rejected without changing the installed pack;
request the installation again so Manager can check its current Registry status.
The check applies to server-initiated installation, reinstallation, version switches, updates,
and snapshot restores. A policy refusal preserves the existing nodepack.
Scheduled version switches save the returned status and check the current
listener and setting again at startup, before downloading or running scripts.
Server-scheduled snapshot restores inherit the permission computed from the
server's current listener and setting. Standalone `cm-cli` commands remain local
administrator operations and do not use the server listener policy.
Saved statuses are not refreshed from the Registry.
# Disclaimer
* This extension simply provides the convenience of installing custom nodes and does not guarantee their proper functioning.
+453 -1367
View File
File diff suppressed because it is too large Load Diff
+780 -2336
View File
File diff suppressed because it is too large Load Diff
+8586 -8761
View File
File diff suppressed because it is too large Load Diff
+1 -2
View File
@@ -129,7 +129,6 @@ class NodeVersion:
id: str
version: str
download_url: str
status: str = ''
def map_node_version(api_node_version):
@@ -161,7 +160,6 @@ def map_node_version(api_node_version):
download_url=api_node_version.get(
"downloadUrl", ""
), # Provide a default value if 'downloadUrl' is missing
status=api_node_version.get('status', ''),
)
@@ -183,6 +181,7 @@ def install_node(node_id, version=None):
response = requests.get(url, verify=not manager_util.bypass_ssl)
if response.status_code == 200:
# Convert the API response to a NodeVersion object
return map_node_version(response.json())
else:
return None
+70 -140
View File
@@ -44,7 +44,7 @@ import manager_migration
from node_package import InstalledNodePackage
version_code = [3, 42]
version_code = [3, 41]
version_str = f"V{version_code[0]}.{version_code[1]}" + (f'.{version_code[2]}' if len(version_code) > 2 else '')
@@ -953,10 +953,10 @@ class UnifiedManager:
return res
def reserve_cnr_switch(self, target, zip_url, from_path, to_path, no_deps, status=''):
def reserve_cnr_switch(self, target, zip_url, from_path, to_path, no_deps):
script_path = os.path.join(manager_startup_script_path, "install-scripts.txt")
with open(script_path, "a") as file:
obj = [target, "#LAZY-CNR-SWITCH-SCRIPT", zip_url, from_path, to_path, no_deps, get_default_custom_nodes_path(), sys.executable, status]
obj = [target, "#LAZY-CNR-SWITCH-SCRIPT", zip_url, from_path, to_path, no_deps, get_default_custom_nodes_path(), sys.executable]
file.write(f"{obj}\n")
print(f"Installation reserved: {target}")
@@ -982,45 +982,35 @@ class UnifiedManager:
return result
def _get_cnr_install_info(self, node_id, version_spec):
node_info = cnr_utils.install_node(node_id, version_spec)
if node_info is not None and node_info.status == 'NodeVersionStatusFlagged' and not manager_funcs.is_flagged_install_allowed():
logging.error(manager_util.FLAGGED_NODEPACK_INSTALL_GUIDANCE)
raise PermissionError(manager_util.FLAGGED_NODEPACK_INSTALL_ERROR)
return node_info
def cnr_switch_version(self, node_id, version_spec=None, instant_execution=False, no_deps=False, return_postinstall=False):
if instant_execution:
return self.cnr_switch_version_instant(node_id, version_spec, instant_execution, no_deps, return_postinstall)
else:
return self.cnr_switch_version_lazy(node_id, version_spec, no_deps, return_postinstall)
def cnr_switch_version_lazy(self, node_id, version_spec=None, no_deps=False, return_postinstall=False):
"""
switch between cnr version (lazy mode)
"""
result = ManagedResult('switch-cnr')
try:
node_info = self._get_cnr_install_info(node_id, version_spec)
except PermissionError as exc:
return result.fail(str(exc))
node_info = cnr_utils.install_node(node_id, version_spec)
if node_info is None or not node_info.download_url:
return result.fail(f'not available node: {node_id}@{version_spec}')
return self._cnr_switch_version(node_id, node_info, instant_execution, no_deps, return_postinstall)
version_spec = node_info.version
def _cnr_switch_version(self, node_id, node_info, instant_execution=False, no_deps=False, return_postinstall=False):
"""Execute a switch using the checked CNR response."""
if self.active_nodes[node_id][0] == node_info.version:
if self.active_nodes[node_id][0] == version_spec:
return ManagedResult('skip').with_msg("Up to date")
if instant_execution:
return self._cnr_switch_version_instant(node_id, node_info, no_deps, return_postinstall)
return self._cnr_switch_version_lazy(node_id, node_info, no_deps, return_postinstall)
def _cnr_switch_version_lazy(self, node_id, node_info, no_deps=False, return_postinstall=False):
"""Reserve a switch using the validated install response."""
result = ManagedResult('switch-cnr')
version_spec = node_info.version
zip_url = node_info.download_url
from_path = self.active_nodes[node_id][1]
target = node_id
to_path = os.path.join(get_default_custom_nodes_path(), target)
def postinstall():
return self.reserve_cnr_switch(target, zip_url, from_path, to_path, no_deps, node_info.status)
return self.reserve_cnr_switch(target, zip_url, from_path, to_path, no_deps)
if return_postinstall:
return result.with_postinstall(postinstall)
@@ -1030,12 +1020,23 @@ class UnifiedManager:
return result
def _cnr_switch_version_instant(self, node_id, node_info, no_deps=False, return_postinstall=False):
"""Apply a switch using the validated install response."""
result = ManagedResult('switch-cnr')
version_spec = node_info.version
def cnr_switch_version_instant(self, node_id, version_spec=None, instant_execution=True, no_deps=False, return_postinstall=False):
"""
switch between cnr version
"""
# 1. download
result = ManagedResult('switch-cnr')
node_info = cnr_utils.install_node(node_id, version_spec)
if node_info is None or not node_info.download_url:
return result.fail(f'not available node: {node_id}@{version_spec}')
version_spec = node_info.version
if self.active_nodes[node_id][0] == version_spec:
return ManagedResult('skip').with_msg("Up to date")
archive_name = f"CNR_temp_{str(uuid.uuid4())}.zip" # should be unpredictable name - security precaution
download_path = os.path.join(get_default_custom_nodes_path(), archive_name)
manager_downloader.basic_download_url(node_info.download_url, get_default_custom_nodes_path(), archive_name)
@@ -1080,7 +1081,7 @@ class UnifiedManager:
result.target = version_spec
def postinstall():
res = self.execute_install_script(f"{node_id}@{version_spec}", install_path, instant_execution=True, no_deps=no_deps)
res = self.execute_install_script(f"{node_id}@{version_spec}", install_path, instant_execution=instant_execution, no_deps=no_deps)
return res
if return_postinstall:
@@ -1297,24 +1298,10 @@ class UnifiedManager:
if 'comfyui-manager' in node_id.lower():
return result.fail(f"ignored: installing '{node_id}'")
try:
node_info = self._get_cnr_install_info(node_id, version_spec)
except PermissionError as exc:
return result.fail(str(exc))
node_info = cnr_utils.install_node(node_id, version_spec)
if node_info is None or not node_info.download_url:
return result.fail(f'not available node: {node_id}@{version_spec}')
result = self._cnr_install(node_id, node_info, instant_execution, no_deps, return_postinstall)
# Keep the requested version in the public result.
if result.target is not None:
result.target = version_spec
return result
def _cnr_install(self, node_id, node_info, instant_execution=False, no_deps=False, return_postinstall=False):
"""Install using the checked CNR response."""
result = ManagedResult('install-cnr')
version_spec = node_info.version
archive_name = f"CNR_temp_{str(uuid.uuid4())}.zip" # should be unpredictable name - security precaution
download_path = os.path.join(get_default_custom_nodes_path(), archive_name)
@@ -1503,13 +1490,21 @@ class UnifiedManager:
else:
version_spec = self.resolve_unspecified_version(node_id)
if version_spec in ('unknown', 'nightly'):
if version_spec == 'unknown' or version_spec == 'nightly':
try:
repo_url = await self._get_git_install_url(node_id, version_spec, channel, mode)
except InvalidChannel as exc:
return ManagedResult('fail').fail(f'Invalid channel is used: {exc.channel}')
except LookupError as exc:
return ManagedResult('install').fail(str(exc))
custom_nodes = await self.get_custom_nodes(channel, mode)
except InvalidChannel as e:
return ManagedResult('fail').fail(f'Invalid channel is used: {e.channel}')
the_node = custom_nodes.get(node_id)
if the_node is not None:
if version_spec == 'unknown':
repo_url = the_node['files'][0]
else: # nightly
repo_url = the_node['repository']
else:
result = ManagedResult('install')
return result.fail(f"Node '{node_id}@{version_spec}' not found in [{channel}, {mode}]")
if self.is_enabled(node_id, version_spec):
return ManagedResult('skip').with_target(f"{node_id}@{version_spec}")
@@ -1517,17 +1512,24 @@ class UnifiedManager:
elif self.is_disabled(node_id, version_spec):
return self.unified_enable(node_id, version_spec)
elif version_spec in ('unknown', 'nightly'):
return self._install_git(node_id, version_spec, repo_url, instant_execution, no_deps, return_postinstall)
elif version_spec == 'unknown' or version_spec == 'nightly':
if version_spec == 'nightly':
# disable cnr nodes
if self.is_enabled(node_id, 'cnr'):
self.unified_disable(node_id, False)
# Check before changing the existing pack, then reuse this response.
try:
node_info = self._get_cnr_install_info(node_id, version_spec)
except PermissionError as exc:
return ManagedResult('install-cnr').fail(str(exc))
if node_info is None or not node_info.download_url:
return ManagedResult('install-cnr').fail(f'not available node: {node_id}@{version_spec}')
version_spec = node_info.version
to_path = os.path.abspath(os.path.join(get_default_custom_nodes_path(), node_id))
res = self.repo_install(repo_url, to_path, instant_execution=instant_execution, no_deps=no_deps, return_postinstall=return_postinstall)
if res.result:
if version_spec == 'unknown':
self.unknown_active_nodes[node_id] = repo_url, to_path
elif version_spec == 'nightly':
cnr_utils.generate_cnr_id(to_path, node_id)
self.active_nodes[node_id] = 'nightly', to_path
else:
return res
return res.with_target(version_spec)
if self.is_enabled(node_id, 'nightly'):
# disable nightly nodes
@@ -1540,77 +1542,17 @@ class UnifiedManager:
if self.is_disabled(node_id, "cnr"):
# enable and switch version if cnr is disabled (not specified version)
self.unified_enable(node_id, "cnr")
return self._cnr_switch_version(node_id, node_info, instant_execution, no_deps, return_postinstall)
return self.cnr_switch_version(node_id, version_spec, no_deps=no_deps, return_postinstall=return_postinstall)
if self.is_enabled(node_id, "cnr"):
return self._cnr_switch_version(node_id, node_info, instant_execution, no_deps, return_postinstall)
return self.cnr_switch_version(node_id, version_spec, no_deps=no_deps, return_postinstall=return_postinstall)
res = self._cnr_install(node_id, node_info, instant_execution, no_deps, return_postinstall)
res = self.cnr_install(node_id, version_spec, instant_execution=instant_execution, no_deps=no_deps, return_postinstall=return_postinstall)
if res.result:
self.active_nodes[node_id] = version_spec, res.to_path
return res
async def _get_git_install_url(self, node_id, version_spec, channel, mode):
custom_nodes = await self.get_custom_nodes(channel, mode)
node = custom_nodes.get(node_id)
if node is None:
raise LookupError(f"Node '{node_id}@{version_spec}' not found in [{channel}, {mode}]")
return node['files'][0] if version_spec == 'unknown' else node['repository']
def _install_git(self, node_id, version_spec, repo_url, instant_execution=False, no_deps=False, return_postinstall=False):
if version_spec == 'nightly':
# disable cnr nodes
if self.is_enabled(node_id, 'cnr'):
self.unified_disable(node_id, False)
to_path = os.path.abspath(os.path.join(get_default_custom_nodes_path(), node_id))
res = self.repo_install(repo_url, to_path, instant_execution=instant_execution, no_deps=no_deps, return_postinstall=return_postinstall)
if res.result:
if version_spec == 'unknown':
self.unknown_active_nodes[node_id] = repo_url, to_path
elif version_spec == 'nightly':
cnr_utils.generate_cnr_id(to_path, node_id)
self.active_nodes[node_id] = 'nightly', to_path
else:
return res
return res.with_target(version_spec)
async def reinstall_by_id(self, node_id, version_spec, channel=None, mode=None):
if 'comfyui-manager' in node_id.lower():
return ManagedResult('skip').fail(f"ignored: installing '{node_id}'")
if version_spec in ('unknown', 'nightly'):
try:
repo_url = await self._get_git_install_url(node_id, version_spec, channel, mode)
except InvalidChannel as exc:
return ManagedResult('fail').fail(f'Invalid channel is used: {exc.channel}')
except LookupError as exc:
return ManagedResult('install').fail(str(exc))
else:
try:
node_info = self._get_cnr_install_info(node_id, version_spec)
except PermissionError as exc:
return ManagedResult('install-cnr').fail(str(exc))
if node_info is None or not node_info.download_url:
return ManagedResult('install-cnr').fail(f'not available node: {node_id}@{version_spec}')
removed = self.unified_uninstall(node_id, version_spec == 'unknown')
if not removed.result:
return removed
for item in removed.items:
path = item if version_spec == 'unknown' else item[1]
self.processed_install.discard(os.path.join(path, 'install.py'))
if version_spec in ('unknown', 'nightly'):
return self._install_git(node_id, version_spec, repo_url)
result = self._cnr_install(node_id, node_info)
if result.result:
self.active_nodes[node_id] = node_info.version, result.to_path
return result
unified_manager = UnifiedManager()
@@ -1721,10 +1663,6 @@ class ManagerFuncs:
def __init__(self):
pass
def is_flagged_install_allowed(self):
# Server-scheduled restore children inherit only the resolved permission.
return os.environ.get('_COMFYUI_MANAGER_CNR_ALLOW_FLAGGED', 'true') == 'true'
def get_current_preview_method(self):
return "none"
@@ -1765,7 +1703,6 @@ WRITTEN_CONFIG_KEYS = (
'db_mode',
'allow_git_url_install',
'allow_pip_install',
'allow_flagged_nodepack_install',
)
@@ -1940,7 +1877,6 @@ def read_config():
'db_mode': default_conf.get('db_mode', 'cache').lower(),
'allow_git_url_install': get_bool('allow_git_url_install', False),
'allow_pip_install': get_bool('allow_pip_install', False),
'allow_flagged_nodepack_install': get_bool('allow_flagged_nodepack_install', False),
}
manager_migration.force_security_level_if_needed(result)
return result
@@ -1972,7 +1908,6 @@ def read_config():
'db_mode': 'cache', # local | cache | remote
'allow_git_url_install': False,
'allow_pip_install': False,
'allow_flagged_nodepack_install': False,
}
manager_migration.force_security_level_if_needed(result)
return result
@@ -3380,20 +3315,15 @@ async def restore_snapshot(snapshot_path, git_helper_extras=None):
skip_node_packs.append(f"{x[0]}@{x[1]}")
elif not ps.result:
failed.append(f"{x[0]}@{x[1]}")
logging.error(ps.msg)
# Do not retry processed switches as fresh installs.
checked_cnr = {node_id for node_id, _ in todo_checkout}
# install listed cnr nodes
for k, v in cnr_info.items():
if 'comfyui-manager' in k or k in checked_cnr:
if 'comfyui-manager' in k:
continue
ps = await unified_manager.install_by_id(k, version_spec=v, instant_execution=True, return_postinstall=True)
if ps.action == 'install-cnr' and ps.result:
installed_node_packs.append(f"{k}@{v}")
elif not ps.result:
failed.append(f"{k}@{v}")
logging.error(ps.msg)
if ps is not None and ps.result:
if hasattr(ps, 'postinstall'):
+10 -19
View File
@@ -162,9 +162,6 @@ async def get_risky_level(files, pip_packages):
class ManagerFuncsInComfyUI(core.ManagerFuncs):
def is_flagged_install_allowed(self):
return core.get_config()['allow_flagged_nodepack_install'] or manager_util.is_loopback_listener(args.listen)
def get_current_preview_method(self):
if args.preview_method == latent_preview.LatentPreviewMethod.Auto:
return "auto"
@@ -520,7 +517,7 @@ async def task_worker():
global model_result
global tasks_in_progress
async def do_install(item, operation) -> str:
async def do_install(item) -> str:
ui_id, node_spec_str, channel, mode, skip_post_install = item
try:
@@ -530,10 +527,7 @@ async def task_worker():
return f"Cannot resolve install target: '{node_spec_str}'"
node_name, version_spec, is_specified = node_spec
if operation == 'reinstall':
res = await core.unified_manager.reinstall_by_id(node_name, version_spec, channel, mode)
else:
res = await core.unified_manager.install_by_id(node_name, version_spec, channel, mode, return_postinstall=skip_post_install)
res = await core.unified_manager.install_by_id(node_name, version_spec, channel, mode, return_postinstall=skip_post_install)
# discard post install if skip_post_install mode
if res.action not in ['skip', 'enable', 'install-git', 'install-cnr', 'switch-cnr']:
@@ -580,7 +574,7 @@ async def task_worker():
base_res['msg'] = 'success'
return base_res
base_res['msg'] = res.msg or f"An error occurred while updating '{node_name}'."
base_res['msg'] = f"An error occurred while updating '{node_name}'."
logging.error(f"\nERROR: An error occurred while updating '{node_name}'. (res.result={res.result}, res.action={res.action})")
return base_res
except Exception:
@@ -733,8 +727,8 @@ async def task_worker():
tasks_in_progress.add((kind, item[0]))
try:
if kind in ('install', 'reinstall'):
msg = await do_install(item, kind)
if kind == 'install':
msg = await do_install(item)
elif kind == 'install-model':
msg = await do_install_model(item)
elif kind == 'update':
@@ -1419,7 +1413,8 @@ async def import_fail_info(request):
@routes.post("/manager/queue/reinstall")
async def reinstall_custom_node(request):
return await _queue_node_install(request, "reinstall")
await uninstall_custom_node(request)
await install_custom_node(request)
@routes.post("/manager/queue/reset")
@@ -1449,20 +1444,16 @@ async def queue_count(request):
@routes.post("/manager/queue/install")
async def install_custom_node(request):
return await _queue_node_install(request, "install")
async def _queue_node_install(request, operation):
if not is_allowed_security_level('middle'):
logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW)
return web.Response(status=403, text="A security error has occurred. Please check the terminal logs")
json_data = await request.json()
# CNR version status is checked by the worker before installation.
# non-nightly cnr is safe
risky_level = None
cnr_id = json_data.get('id')
skip_post_install = False if operation == 'reinstall' else json_data.get('skip_post_install')
skip_post_install = json_data.get('skip_post_install')
git_url = None
@@ -1535,7 +1526,7 @@ async def _queue_node_install(request, operation):
return web.Response(status=404, text="A security error has occurred. Please check the terminal logs")
install_item = json_data.get('ui_id'), node_spec_str, json_data['channel'], json_data['mode'], skip_post_install
task_queue.put((operation, install_item))
task_queue.put(("install", install_item))
return web.Response(status=200)
+1 -47
View File
@@ -3,8 +3,6 @@ description:
`manager_util` is the lightest module shared across the prestartup_script, main code, and cm-cli of ComfyUI-Manager.
"""
import traceback
import ipaddress
import socket
import aiohttp
import json
@@ -30,50 +28,6 @@ cache_dir = os.path.join(comfyui_manager_path, '.cache') # This path is also up
use_uv = False
bypass_ssl = False
FLAGGED_NODEPACK_INSTALL_ERROR = (
'This action is not allowed by the current security configuration. '
'See the terminal for details.'
)
FLAGGED_NODEPACK_INSTALL_GUIDANCE = (
'Installation of this flagged CNR version is blocked. To satisfy the additional '
'flagged-version requirement, choose one of the following:\n'
'1. Run ComfyUI with loopback-only listeners, for example --listen 127.0.0.1 '
'or --listen ::1. All configured listen addresses must be loopback.\n'
' Do not use bare --listen or any non-loopback address, such as 0.0.0.0 or ::.\n'
'2. For a trusted private network, set allow_flagged_nodepack_install = true '
'in the [default] section of ComfyUI-Manager\'s config.ini.\n'
'Restart ComfyUI after changing the listener or configuration. '
'All other installation security checks still apply.'
)
def is_loopback_listener(listen_address: str) -> bool:
"""All addresses bound by --listen must resolve exclusively to loopback."""
if not isinstance(listen_address, str) or not listen_address:
return False
for address in listen_address.split(','):
address = address.strip()
if not address:
return False
try:
if ipaddress.ip_address(address).is_loopback:
continue
except ValueError:
pass
try:
resolved = socket.getaddrinfo(address, None, type=socket.SOCK_STREAM)
except OSError:
return False
if not resolved or any(not ipaddress.ip_address(item[4][0]).is_loopback for item in resolved):
return False
return True
def is_cnr_install_allowed(status: str, allow_flagged: bool, listen_address: str) -> bool:
"""Only flagged versions require loopback or the private-network opt-in."""
return status != 'NodeVersionStatusFlagged' or bool(allow_flagged) or is_loopback_listener(listen_address)
def add_python_path_to_env():
if platform.system() != "Windows":
sep = ':'
@@ -756,4 +710,4 @@ def restore_pip_snapshot(pips, options):
if res != 0:
failed.append(x)
print(f"Installation failed for pip packages: {failed}")
print(f"Installation failed for pip packages: {failed}")
+1 -23
View File
@@ -6249,28 +6249,6 @@
"filename": "Qwen-Image-InstantX-ControlNet-Inpainting.safetensors",
"url": "https://huggingface.co/Comfy-Org/Qwen-Image-InstantX-ControlNets/resolve/main/split_files/controlnet/Qwen-Image-InstantX-ControlNet-Inpainting.safetensors",
"size": "2.54GB"
},
{
"name": "Anime Eye Detector (YOLOv8n)",
"type": "Ultralytics",
"base": "yolov8",
"save_path": "ultralytics/bbox",
"description": "Anime eye detector (YOLOv8n). Detects eyes in anime-style character art for use with BboxDetectorSEGS/Detailer workflows.",
"reference": "https://huggingface.co/killjoyelite/anime-eye-yolov8",
"filename": "eye_yolov8n.pt",
"url": "https://huggingface.co/killjoyelite/anime-eye-yolov8/resolve/6bceece59aa38f7fe9d3867f3e2931b11c7ff9a1/eye_yolov8n.pt",
"size": "6.24MB"
},
{
"name": "Anime Eye Detector (YOLOv8s)",
"type": "Ultralytics",
"base": "yolov8",
"save_path": "ultralytics/bbox",
"description": "Anime eye detector (YOLOv8s). Detects eyes in anime-style character art for use with BboxDetectorSEGS/Detailer workflows.",
"reference": "https://huggingface.co/killjoyelite/anime-eye-yolov8",
"filename": "eye_yolov8s.pt",
"url": "https://huggingface.co/killjoyelite/anime-eye-yolov8/resolve/6bceece59aa38f7fe9d3867f3e2931b11c7ff9a1/eye_yolov8s.pt",
"size": "22.5MB"
}
]
}
}
+381 -63
View File
@@ -1,67 +1,5 @@
{
"custom_nodes": [
{
"author": "magicwang1111",
"title": "ComfyUI-GPT-image (OpenAI/Relay) [NAME CONFLICT]",
"id": "comfyui-gpt-image-openai-relay",
"reference": "https://github.com/magicwang1111/ComfyUI-GPT-image",
"files": [
"https://github.com/magicwang1111/ComfyUI-GPT-image"
],
"install_type": "git-clone",
"description": "Generate and edit images in ComfyUI with OpenAI GPT Image models. Includes dedicated Generate and Edit nodes, multi-image reference input, size presets, and support for both the official OpenAI API and OpenAI-compatible relay endpoints."
},
{
"author": "Yuan-ManX",
"title": "ComfyUI-Bagel [NAME CONFLICT]",
"reference": "https://github.com/Yuan-ManX/ComfyUI-Bagel",
"files": [
"https://github.com/Yuan-ManX/ComfyUI-Bagel"
],
"install_type": "git-clone",
"description": "ComfyUI-Bagel is now available in ComfyUI, BAGEL is an open‑source multimodal foundation model with 7B active parameters (14B total) trained on large‑scale interleaved multimodal data. [w/Don't install together with neverbiasu/ComfyUI-BAGEL simultaneously.]"
},
{
"author": "liming123332",
"title": "ComfyUI-Ranbooru [NAME CONFLICT]",
"reference": "https://github.com/liming123332/ComfyUI-Ranbooru",
"files": [
"https://github.com/liming123332/ComfyUI-Ranbooru"
],
"install_type": "git-clone",
"description": "Custom node that migrates WebUI Forge's Ranbooru plugin to ComfyUI, enabling searches across Booru sites to generate positive/negative prompts with tag cleaning, rating sorting, and caching support. (Description by CC)"
},
{
"author": "katalist-ai",
"title": "comfyUI-nsfw-detection [NAME CONFLICT]",
"id": "nsfw-detection",
"reference": "https://github.com/katalist-ai/comfyUI-nsfw-detection",
"files": [
"https://github.com/katalist-ai/comfyUI-nsfw-detection"
],
"install_type": "git-clone",
"description": "Nodes: NudenetDetector"
},
{
"author": "AIFSH",
"title": "DiffSynth-ComfyUI [NAME CONFLICT]",
"reference": "https://github.com/AIFSH/DiffSynth-ComfyUI",
"files": [
"https://github.com/AIFSH/DiffSynth-ComfyUI"
],
"install_type": "git-clone",
"description": "a custom node for [a/DiffSynth-Studio](https://github.com/modelscope/DiffSynth-Studio)"
},
{
"author": "flyghtxmz",
"title": "ComfyUI-CFG-Ctrl [NAME CONFLICT]",
"reference": "https://github.com/flyghtxmz/ComfyUI-CFG-Ctrl",
"files": [
"https://github.com/flyghtxmz/ComfyUI-CFG-Ctrl"
],
"install_type": "git-clone",
"description": "ComfyUI custom node implementing SMC-CFG (Sliding Mode Control Classifier-Free Guidance), a nonlinear control-theory approach to CFG that prevents instability and semantic overshooting at high guidance scales. (Description by CC)"
},
{
"author": "trusapien",
"title": "ComfyUI-Trusapien",
@@ -183,6 +121,16 @@
"install_type": "git-clone",
"description": "A small pack of utility nodes including CLIP/VAE fallbacks, Anima model detection, and multi image preview.\nNOTE: The files in the repo are not organized."
},
{
"author": "TechnoWarrior2",
"title": "comfyui-live-preview [WIP]",
"reference": "https://github.com/TechnoWarrior2/comfyui-live-preview",
"files": [
"https://github.com/TechnoWarrior2/comfyui-live-preview"
],
"install_type": "git-clone",
"description": "ComfyUI node displaying large floating overlay window with live denoising step previews as image generates, similar to A1111 and Forge.\nNOTE: The files in the repo are not organized."
},
{
"author": "HuajunPeng1982",
"title": "comfyui-grsai-storyboard",
@@ -613,6 +561,16 @@
"install_type": "git-clone",
"description": "ComfyUI utility nodes including SamplerPipeIn, SamplerPipeOut, and VAENames for workflow management. (Description by CC)"
},
{
"author": "pollockjj",
"title": "JAXTools",
"reference": "https://github.com/pollockjj/ComfyUI-JAXTools",
"files": [
"https://github.com/pollockjj/ComfyUI-JAXTools"
],
"install_type": "git-clone",
"description": "JAX-accelerated image processing nodes for ComfyUI via dm_pix sealed worker isolation"
},
{
"author": "ptmff",
"title": "ComfyUI-Nodes8734",
@@ -1013,6 +971,16 @@
"install_type": "git-clone",
"description": "A lightweight utility node for ComfyUI that detects aspect ratio of input images and selects preset resolutions with multiplier support.\nNOTE: The files in the repo are not organized."
},
{
"author": "MengYe-Z",
"title": "ComfyUI-MultiPointWhiteBalance [WIP]",
"reference": "https://github.com/MengYe-Z/ComfyUI-MultiPointWhiteBalance",
"files": [
"https://github.com/MengYe-Z/ComfyUI-MultiPointWhiteBalance"
],
"install_type": "git-clone",
"description": "A custom node pack for e-commerce product photography with precise white balance and exposure correction using multi-point sampling.\nNOTE: The files in the repo are not organized."
},
{
"author": "Mervent",
"title": "comfyui-fast-lut [WIP]",
@@ -1213,6 +1181,16 @@
"install_type": "git-clone",
"description": "Image selector node for ComfyUI.\nNOTE: The files in the repo are not organized."
},
{
"author": "saganaki22",
"title": "Kitten-TTS [NAME CONFLICT]",
"reference": "https://github.com/Saganaki22/ComfyUI-KittenTTS",
"files": [
"https://github.com/Saganaki22/ComfyUI-KittenTTS"
],
"install_type": "git-clone",
"description": "ComfyUI custom node for KittenTTS - Ultra-lightweight text-to-speech (CUDA / CPU)"
},
{
"author": "opj161",
"title": "comfyui-comfyui-lmstudio [NAME CONFLICT]",
@@ -1875,6 +1853,16 @@
"install_type": "git-clone",
"description": "ComfyUI Frontend Vue Basic is custom node that demonstrate how to use vue as frontend framework along with primevue and vue-i18n, cooperating with ComfyUI API. Needs ComfyUI Frontend 1.25.0 or later."
},
{
"author": "vjumpkung",
"title": "comfyui-mosaic-blur [NAME CONFLICT]",
"reference": "https://github.com/vjumpkung/comfyui-mosaic-blur",
"files": [
"https://github.com/vjumpkung/comfyui-mosaic-blur"
],
"install_type": "git-clone",
"description": "ComfyUI custom node for applying mosaic blur effects. (Description by CC)"
},
{
"author": "richardctw",
"title": "ComfyUI-ImageFillBackgroundColor",
@@ -1925,6 +1913,16 @@
"install_type": "git-clone",
"description": "NODES: CLIPModelSelectionNode, CLIPVisionModelSelectionNode, CheckpointSelectionNode, DiffusionModelSelectionNode, FilteredCLIPModelsNode, ... (13 total)"
},
{
"author": "pollockjj",
"title": "ComfyUI-StabilityTest",
"reference": "https://github.com/pollockjj/ComfyUI-StabilityTest",
"files": [
"https://github.com/pollockjj/ComfyUI-StabilityTest"
],
"install_type": "git-clone",
"description": "ComfyUI stability testing node. (Description by CC)"
},
{
"author": "Tr1dae",
"title": "ComfyUI-MobileSAM [WIP]",
@@ -1995,6 +1993,16 @@
"install_type": "git-clone",
"description": "Custom ComfyUI nodes for Google Gemini AI with text generation and image generation capabilities using the Python SDK."
},
{
"author": "Saganaki22",
"title": "ComfyUI-NovaSR",
"reference": "https://github.com/Saganaki22/ComfyUI-NovaSR",
"files": [
"https://github.com/Saganaki22/ComfyUI-NovaSR"
],
"install_type": "git-clone",
"description": "ComfyUI node for NovaSR - Ultra-fast audio super resolution (3600x realtime, 50KB model)"
},
{
"author": "GokuHiki",
"title": "ComfyUI-Goku-Tools",
@@ -2235,6 +2243,16 @@
"install_type": "git-clone",
"description": "NODES: GetSimilarity, LPIPSSimilarity"
},
{
"author": "StevenBaby",
"title": "comfyui-tools",
"reference": "https://github.com/StevenBaby/comfyui-tools",
"files": [
"https://github.com/StevenBaby/comfyui-tools"
],
"install_type": "git-clone",
"description": "IntParameterNode: A Node contain 4 int parameters to quick switch input to other nodes."
},
{
"author": "zhu798542746",
"title": "comfyui_model [UNSAFE]",
@@ -2349,7 +2367,6 @@
"author": "huhu-tiger",
"title": "ComfyUI-RemoteResource",
"reference": "https://github.com/huhu-tiger/ComfyUI-RemoteResource",
"reference2": "https://github.com/huhu-tiger/ComfyUI-RemoteDownload",
"files": [
"https://github.com/huhu-tiger/ComfyUI-RemoteResource"
],
@@ -2676,6 +2693,16 @@
"install_type": "git-clone",
"description": "ComfyUI custom node applying image transformations (scaling, rotation, translation) around a pivot point with optional canvas expansion and pivot visualization. (Description by CC)\nNOTE: The files in the repo are not organized."
},
{
"author": "OhSeongHyeon",
"title": "comfyui-random-image-size",
"reference": "https://github.com/OhSeongHyeon/comfyui-random-image-size",
"files": [
"https://github.com/OhSeongHyeon/comfyui-random-image-size"
],
"install_type": "git-clone",
"description": "ComfyUI Random Image Size"
},
{
"author": "Enferlain",
"title": "ComfyUI-Model-Comparison [WIP]",
@@ -2906,6 +2933,16 @@
"install_type": "git-clone",
"description": "ComfyUI custom node providing customizable prompt generation capabilities. (Description by CC)"
},
{
"author": "JBKing514",
"title": "map_comfyui [WIP]",
"reference": "https://github.com/JBKing514/map_comfyui",
"files": [
"https://github.com/JBKing514/map_comfyui"
],
"install_type": "git-clone",
"description": "A custom node implementing the Manifold Alignment Protocol (MAP) within ComfyUI, transforming diffusion sampling into a measurable and visualizable geometric process. (Description by CC)\nNOTE: The files in the repo are not organized."
},
{
"author": "binarystatic",
"title": "ComfyUI-BinarystaticMasterSeed",
@@ -3216,6 +3253,16 @@
"install_type": "git-clone",
"description": "A simple ComfyUI node that lets you choose one text from up to 10 inputs.\nNOTE: The files in the repo are not organized."
},
{
"author": "Isi-dev",
"title": "ComfyUI_DeleteModelPassthrough_ExecutionFlowControl [WIP]",
"reference": "https://github.com/Isi-dev/ComfyUI_DeleteModelPassthrough_ExecutionFlowControl",
"files": [
"https://github.com/Isi-dev/ComfyUI_DeleteModelPassthrough_ExecutionFlowControl"
],
"install_type": "git-clone",
"description": "Memory management custom node that deletes specific models from VRAM and RAM while passing through other input types unchanged, useful for low-memory environments. (Description by CC)"
},
{
"author": "K1maran",
"title": "ComfyUI-Kimaran",
@@ -3386,6 +3433,16 @@
"install_type": "git-clone",
"description": "Quality-of-life utility nodes for ComfyUI providing smart switches for dynamic data routing, path helpers for consistent file naming, and image utilities including Qwen vision model preparation.\nNOTE: The files in the repo are not organized."
},
{
"author": "huhu-tiger",
"title": "ComfyUI-RemoteDownload",
"reference": "https://github.com/huhu-tiger/ComfyUI-RemoteDownload",
"files": [
"https://github.com/huhu-tiger/ComfyUI-RemoteDownload"
],
"install_type": "git-clone",
"description": "NODES: LoadImageFromRemote"
},
{
"author": "basenc",
"title": "Comfyui-Nodes-basenc [UNSAFE]",
@@ -3466,6 +3523,16 @@
"install_type": "git-clone",
"description": "This repo has different functions to help dealing with huggingface data"
},
{
"author": "ilslli",
"title": "Comfyui-IV2Z",
"reference": "https://github.com/ilslli/Comfyui-IV2Z",
"files": [
"https://github.com/ilslli/Comfyui-IV2Z"
],
"install_type": "git-clone",
"description": "Modify the VideoCombine node of [a/ComfyUI-VideoHelperSuite](https://github.com/Kosinkadink/ComfyUI-VideoHelperSuite) so that, after encoding the video, it outputs the result as a compressed archive."
},
{
"author": "PladsElsker",
"title": "comfyui-krita [WIP]",
@@ -3557,6 +3624,16 @@
"install_type": "git-clone",
"description": "NODES: 'URL: Load Media', 'Image: Save To Disk (No UI)', 'S3: Upload Presigned', ..."
},
{
"author": "akashch1512",
"title": "ComfyUI_UguuUploader",
"reference": "https://github.com/akashch1512/ComfyUI_UguuUploader",
"files": [
"https://github.com/akashch1512/ComfyUI_UguuUploader"
],
"install_type": "git-clone",
"description": "NODES: Uguu.se Video Uploader\nNOTE: The files in the repo are not organized."
},
{
"author": "vadimcro",
"title": "string_by_index",
@@ -4349,6 +4426,16 @@
"install_type": "git-clone",
"description": "NODES: S3 Upload File, File Explorer, Save Image Export Path, Save Video Export Path, Save Audio Export Path, S3 Combine Images to Video, Multi-Line Text Input, ...\nNOTE: The files in the repo are not organized.[w/This extension has a vulnerability that allows arbitrary access to local files from remote.]"
},
{
"author": "kesslerdev",
"title": "Comfyui : kesslerdev_comfyui_nodes [UNSAFE]",
"reference": "https://github.com/kesslerdev/kesslerdev_comfyui_nodes",
"files": [
"https://github.com/kesslerdev/kesslerdev_comfyui_nodes"
],
"install_type": "git-clone",
"description": "NODES: Multiline Text Choice (Kesslerdev), Environment Variable Getter (Kesslerdev)[w/This nodepack includes a node that can remotely read arbitrary environment variables.]"
},
{
"author": "panchovial-max",
"title": "ComfyUI-ListHelper [NAME CONFLICT]",
@@ -4900,6 +4987,16 @@
"install_type": "git-clone",
"description": "A ComfyUI custom node implementation of ByteDance's USO (Unified Style and Subject-Driven Generation) model, which enables advanced style transfer and subject preservation using FLUX.\nNOTE: The files in the repo are not organized."
},
{
"author": "numq",
"title": "comfyui-camera-capture-node",
"reference": "https://github.com/numq/comfyui-camera-capture-node",
"files": [
"https://github.com/numq/comfyui-camera-capture-node"
],
"install_type": "git-clone",
"description": "NODES: Camera Capture"
},
{
"author": "pickles",
"title": "PyPromptGenerator [UNSAFE]",
@@ -5000,6 +5097,16 @@
"install_type": "git-clone",
"description": "NODES: INT Constant, Simple Int Math Handle, Simple Json Array Handle, Simple Json Object Handle, Select Image Size, Video Time & FPS, Video Frame Size, Wan22 Step Handle, Prompt Selector String, Prompt Example Node, Prompt Join or List, Prompt List, CRD Audio Length Node, ..."
},
{
"author": "Saganaki22",
"title": "ComfyUI YTDL Nodes [WIP]",
"reference": "https://github.com/Saganaki22/ComfyUI-ytdl_nodes",
"files": [
"https://github.com/Saganaki22/ComfyUI-ytdl_nodes"
],
"install_type": "git-clone",
"description": "Custom ComfyUI nodes for downloading, converting, and previewing audio/video from YouTube and 1,000+ other platforms"
},
{
"author": "comfyscript",
"title": "ComfyUI-CloudClient",
@@ -5020,6 +5127,16 @@
"install_type": "git-clone",
"description": "A custom node for ComfyUI that automatically uploads generated images to SharePoint or OneDrive document libraries.[w/This nodepack contains a node that can write files to an arbitrary path.]"
},
{
"author": "KoinnAI",
"title": "ComfyUI Dynamic Prompting Simplified [WIP]",
"reference": "https://github.com/KoinnAI/ComfyUI-DynPromptSimplified",
"files": [
"https://github.com/KoinnAI/ComfyUI-DynPromptSimplified"
],
"install_type": "git-clone",
"description": "A minimal dynamic prompting + mirrored wildcards node for ComfyUI.\nNOTE: The files in the repo are not organized."
},
{
"author": "jtrue",
"title": "MaskTools",
@@ -5411,6 +5528,16 @@
"install_type": "git-clone",
"description": "This custom node Qwen3 designed to integrate with a GPT-based system under the category GPT_QWEN/Qwen. It serves as an interface to interact with the Qwen language model, specifically the 'Qwen/Qwen3-4B-Instruct-2507' variant."
},
{
"author": "MatthewClayHarrison",
"title": "MetaMan - Universal AI Image Metadata Manager [UNSAFE]",
"reference": "https://github.com/MatthewClayHarrison/ComfyUI-MetaMan",
"files": [
"https://github.com/MatthewClayHarrison/ComfyUI-MetaMan"
],
"install_type": "git-clone",
"description": "First universal metadata system for AI image generation, with template-driven architecture allowing easy extension to new services; comprehensive dependency tracking with automatic download resolution; lossless conversion between platform formats where possible; future-proof design with extensible schema and validation system.[w/This nodepack has a vulnerability that allows remote access to arbitrary file paths.]"
},
{
"author": "idoru",
"title": "Filestash Upload Node [UNSAFE]",
@@ -5781,6 +5908,16 @@
"install_type": "git-clone",
"description": "This is a text translation node using a local SLM (Microsoft Phi-4) for ComfyUI."
},
{
"author": "DenRakEiw",
"title": "Denrakeiw Nodes [WIP]",
"reference": "https://github.com/DenRakEiw/DenRakEiw_Nodes",
"files": [
"https://github.com/DenRakEiw/DenRakEiw_Nodes"
],
"install_type": "git-clone",
"description": "A custom nodepack for ComfyUI that provides utility nodes for image generation and manipulation.\nNOTE: The files in the repo are not organized."
},
{
"author": "ahmedbana",
"title": "json-creator [WIP]",
@@ -6261,6 +6398,16 @@
"install_type": "git-clone",
"description": "NODES: Whisper + AudioTranslate, Google Translate Node"
},
{
"author": "edgerunner",
"title": "ComfyUI Queue Manager [WIP]",
"reference": "https://github.com/QuietNoise/ComfyUI-Queue-Manager",
"files": [
"https://github.com/QuietNoise/ComfyUI-Queue-Manager"
],
"install_type": "git-clone",
"description": "An extension supporting more streamlined prompt queue management."
},
{
"author": "fylrid2",
"title": "lockValue",
@@ -7033,6 +7180,16 @@
"install_type": "git-clone",
"description": "NODES: Random Line from File (Seeded), Keyword Prompt Composer, Simple Prompt Combiner (5x), Boolean Switch (Any), Select Line from TxT (Any), Multiline Text Input, Flux 2M Aspect Ratios, SDXL Aspect Ratios, Room Mapper, ..."
},
{
"author": "pacchikAI",
"title": "ImagePromptBatch [UNSAFE]",
"reference": "https://github.com/pacchikAI/ImagePromptBatch",
"files": [
"https://github.com/pacchikAI/ImagePromptBatch"
],
"install_type": "git-clone",
"description": "NODES: Load Image and Prompt[w/This includes a node that can read the contents of a `.csv` file from an arbitrary path.]"
},
{
"author": "papcorns",
"title": "ComfyUI-Papcorns-Node-UploadToGCS",
@@ -7743,6 +7900,16 @@
"install_type": "git-clone",
"description": "ComfyUI extensions for sending prompt progress to webhook"
},
{
"author": "ashllay",
"title": "ComfyUI_MoreComfy",
"reference": "https://github.com/ashllay/ComfyUI_MoreComfy",
"files": [
"https://github.com/ashllay/ComfyUI_MoreComfy"
],
"install_type": "git-clone",
"description": "NODES: MC Switch Seed/Image/Latent/Model/String, MC Alter Seed, MC Set Tile Size, MC Get Image Size, MC Multi Concat"
},
{
"author": "gordon1chuge2623",
"title": "ComfyUI_seal_migration [WIP]",
@@ -7913,6 +8080,16 @@
"install_type": "git-clone",
"description": "NODES: Add Text To Image, Adjust Hex Brightness, Adjust Transparency By Mask, Align Images with Mask, Align Restore Json, Binarize Mask, Blend ICLight, ..."
},
{
"author": "alexgenovese",
"title": "ComfyUI-Reica",
"reference": "https://github.com/alexgenovese/ComfyUI-Reica",
"files": [
"https://github.com/alexgenovese/ComfyUI-Reica"
],
"install_type": "git-clone",
"description": "NODES: Reica Text Image Display, Flux Image Generator, Reica GCP: Read Image, Reica GCP: Write Image & Get URL, Reica API: Send HTTP Notification"
},
{
"author": "Stable-X",
"title": "ComfyUI-Hi3DGen",
@@ -8024,6 +8201,16 @@
"install_type": "git-clone",
"description": "Nodes: Provides nodes to execute arbitrary Python code snippets and Resize images directly within ComfyUI workflows. [w/This node allows you to execute arbitrary code via the workflow.]"
},
{
"author": "ashllay",
"title": "ComfyUI_MoreComfy",
"reference": "https://github.com/ashllay/ComfyUI_MoreComfy",
"files": [
"https://github.com/ashllay/ComfyUI_MoreComfy"
],
"install_type": "git-clone",
"description": "NODES: MC Switch Seed, MC Alter Seed, MC Set Tile Size, MC Multi Concat"
},
{
"author": "ayaoayaoayaoaya",
"title": "ComfyUI-KLUT-DeepSeek-API [WIP]",
@@ -8124,6 +8311,16 @@
"install_type": "git-clone",
"description": "A lightweight nodepack for ComfyUI that adds a few handy nodes that I use in my workflows"
},
{
"author": "CeeVeeR",
"title": "ComfyUi-Text-Tiler",
"reference": "https://github.com/CeeVeeR/ComfyUi-Text-Tiler",
"files": [
"https://github.com/CeeVeeR/ComfyUi-Text-Tiler"
],
"install_type": "git-clone",
"description": "NODES: Text Tiler"
},
{
"author": "Dreamshot-io",
"title": "ComfyUI-Extend-Resolution",
@@ -8635,6 +8832,26 @@
"install_type": "git-clone",
"description": "NODES: Add Grid Boundaries, Pack Frames, Unpack Frames, Resize Frame"
},
{
"author": "Northerner1",
"title": "ComfyUI_North_Noise [WIP]",
"reference": "https://github.com/Northerner1/ComfyUI_North_Noise",
"files": [
"https://github.com/Northerner1/ComfyUI_North_Noise"
],
"install_type": "git-clone",
"description": "NODES: Unsampler"
},
{
"author": "kimara-ai",
"title": "ComfyUI-Kimara-AI-Image-From-URL [WIP]",
"reference": "https://github.com/kimara-ai/ComfyUI-Kimara-AI-Image-From-URL",
"files": [
"https://github.com/kimara-ai/ComfyUI-Kimara-AI-Image-From-URL"
],
"install_type": "git-clone",
"description": "Load image from URL and downscale to desired megapixels. Set megapixels to 0 for no downscaling."
},
{
"author": "tc8M4lF3s88",
"title": "comfy-tif-support",
@@ -9467,6 +9684,16 @@
"install_type": "git-clone",
"description": "A set of nodes to edit videos using the Hunyuan Video model"
},
{
"author": "watarika",
"title": "ComfyUI-exit [UNSAFE]",
"reference": "https://github.com/watarika/ComfyUI-exit",
"files": [
"https://github.com/watarika/ComfyUI-exit"
],
"install_type": "git-clone",
"description": "A custom node that terminates ComfyUI after a specified number of seconds. Use this node if you want Google Colab to automatically terminate after mass generation. It is necessary to disconnect and delete the Google Colab runtime on the Notebook side."
},
{
"author": "backearth1",
"title": "Comfyui-MiniMax-Video [WIP]",
@@ -9618,6 +9845,16 @@
"install_type": "git-clone",
"description": "NODES: SaveImageOSS."
},
{
"author": "Matrix-King-Studio",
"title": "ComfyUI-MoviePy",
"reference": "https://github.com/Matrix-King-Studio/ComfyUI-MoviePy",
"files": [
"https://github.com/Matrix-King-Studio/ComfyUI-MoviePy"
],
"install_type": "git-clone",
"description": "NODES: Image Clip Node, Audio Duration Node, Save Video Node,..."
},
{
"author": "Big Idea Technology",
"title": "ComfyUI-Movie-Tools [WIP]",
@@ -9952,6 +10189,16 @@
"install_type": "git-clone",
"description": "NODES:LETM Save Image, ETM Load Image From Local"
},
{
"author": "m-ai-studio",
"title": "mai-prompt-progress",
"reference": "https://github.com/m-ai-studio/mai-prompt-progress",
"files": [
"https://github.com/m-ai-studio/mai-prompt-progress"
],
"install_type": "git-clone",
"description": "ComfyUI extensions for sending prompt progress to webhook"
},
{
"author": "neeltheninja",
"title": "ComfyUI-TempFileDeleter [UNSAFE]",
@@ -10166,6 +10413,16 @@
"install_type": "git-clone",
"description": "comfyui-photobooth-customnode"
},
{
"author": "netanelben",
"title": "comfyui-text2image-customnode",
"reference": "https://github.com/netanelben/comfyui-text2image-customnode",
"files": [
"https://github.com/netanelben/comfyui-text2image-customnode"
],
"install_type": "git-clone",
"description": "comfyui-text2image-customnode"
},
{
"author": "netanelben",
"title": "comfyui-camera2image-customnode",
@@ -10579,6 +10836,16 @@
"install_type": "git-clone",
"description": "A powerful debugging tool designed to provide in-depth analysis of your environment and dependencies by exposing API endpoints. This tool allows you to inspect environment variables, pip packages, python info and dependency trees, making it easier to diagnose and resolve issues in your ComfyUI setup.[w/This tool may expose sensitive system information if used on a public server]"
},
{
"author": "denislov",
"title": "Comfyui_AutoSurvey",
"reference": "https://github.com/denislov/Comfyui_AutoSurvey",
"files": [
"https://github.com/denislov/Comfyui_AutoSurvey"
],
"install_type": "git-clone",
"description": "Nodes:AutoSurvey, WriteOutline, WriteSection, ChatModel, QueryKnowledge, ManageDatabase, AddDoc2Knowledge"
},
{
"author": "leoleelxh",
"title": "ComfyUI-MidjourneyNode-leoleexh",
@@ -10938,6 +11205,17 @@
"install_type": "git-clone",
"description": "Nodes:Lora Block Weight Regex Loader"
},
{
"author": "jtydhr88",
"title": "ComfyUI-Unique3D [WIP]",
"id": "unique3d",
"reference": "https://github.com/jtydhr88/ComfyUI-Unique3D",
"files": [
"https://github.com/jtydhr88/ComfyUI-Unique3D"
],
"install_type": "git-clone",
"description": "ComfyUI Unique3D is custom nodes that running [a/AiuniAI/Unique3D](https://github.com/AiuniAI/Unique3D) into ComfyUI."
},
{
"author": "kycg",
"title": "comfyui-Kwtoolset",
@@ -10960,6 +11238,16 @@
"install_type": "git-clone",
"description": "This Python script is an optional add-on to the Comfy UI stable diffusion client."
},
{
"author": "immersiveexperience",
"title": "ie-comfyui-color-nodes",
"reference": "https://github.com/immersiveexperience/ie-comfyui-color-nodes",
"files": [
"https://github.com/immersiveexperience/ie-comfyui-color-nodes"
],
"install_type": "git-clone",
"description": "Custom ComfyUI nodes for simple color correction."
},
{
"author": "LZpenguin",
"title": "ComfyUI-Text",
@@ -11264,6 +11552,16 @@
"install_type": "git-clone",
"description": "ComfyUI extensions for better [a/Captain](https://github.com/blib-la/captain) integration."
},
{
"author": "ejektaflex",
"title": "ComfyUI-Ty",
"reference": "https://github.com/ejektaflex/ComfyUI-Ty",
"files": [
"https://github.com/ejektaflex/ComfyUI-Ty"
],
"install_type": "git-clone",
"description": "Nodes:Lora Block Weight Regex Loader"
},
{
"author": "christian-byrne",
"title": "Python Interpreter ComfyUI Node [UNSAFE]",
@@ -11385,6 +11683,16 @@
"install_type": "git-clone",
"description": "[a/https://huggingface.co/deepseek-ai](https://huggingface.co/deepseek-ai)"
},
{
"author": "GentlemanHu",
"title": "ComfyUI-Notifier",
"reference": "https://github.com/GentlemanHu/ComfyUI-Notifier",
"files": [
"https://github.com/GentlemanHu/ComfyUI-Notifier"
],
"install_type": "git-clone",
"description": "Nodes:GentlemanHu_Notifier"
},
{
"author": "nat-chan",
"title": "Transceiver📡",
@@ -11585,6 +11893,16 @@
"install_type": "git-clone",
"description": "Nodes:GH Tools Image Sizer, GH Tools Simple Scale. Simple quality of life Tools for ComfyUI. Basically, If it makes my life easier, it will be here. The list will grow over time."
},
{
"author": "Beinsezii",
"title": "comfyui-amd-go-fast",
"reference": "https://github.com/Beinsezii/comfyui-amd-go-fast",
"files": [
"https://github.com/Beinsezii/comfyui-amd-go-fast"
],
"install_type": "git-clone",
"description": "See details: [a/link](https://github.com/Beinsezii/comfyui-amd-go-fast?tab=readme-ov-file)"
},
{
"author": "SeedV",
"title": "ComfyUI-SeedV-Nodes [UNSAFE]",
File diff suppressed because it is too large Load Diff
+1604 -1394
View File
File diff suppressed because it is too large Load Diff
+318 -327
View File
@@ -1,316 +1,5 @@
{
"custom_nodes": [
{
"author": "Jokimbe",
"title": "ComfyUI-DrawThings-gRPC [DEPRECATED]",
"reference": "https://github.com/Jokimbe/ComfyUI-DrawThings-gRPC",
"files": [
"https://github.com/Jokimbe/ComfyUI-DrawThings-gRPC"
],
"install_type": "git-clone",
"description": "Connect to any Draw Things gRPC server"
},
{
"author": "akashch1512",
"title": "ComfyUI_UguuUploader [REMOVED]",
"reference": "https://github.com/akashch1512/ComfyUI_UguuUploader",
"files": [
"https://github.com/akashch1512/ComfyUI_UguuUploader"
],
"install_type": "git-clone",
"description": "NODES: Uguu.se Video Uploader\nNOTE: The files in the repo are not organized."
},
{
"author": "CeeVeeR",
"title": "ComfyUi-Text-Tiler [REMOVED]",
"reference": "https://github.com/CeeVeeR/ComfyUi-Text-Tiler",
"files": [
"https://github.com/CeeVeeR/ComfyUi-Text-Tiler"
],
"install_type": "git-clone",
"description": "NODES: Text Tiler"
},
{
"author": "ilslli",
"title": "Comfyui-IV2Z [REMOVED]",
"reference": "https://github.com/ilslli/Comfyui-IV2Z",
"files": [
"https://github.com/ilslli/Comfyui-IV2Z"
],
"install_type": "git-clone",
"description": "Modify the VideoCombine node of [a/ComfyUI-VideoHelperSuite](https://github.com/Kosinkadink/ComfyUI-VideoHelperSuite) so that, after encoding the video, it outputs the result as a compressed archive."
},
{
"author": "TK22-26",
"title": "Inspect Image Overlay [REMOVED]",
"reference": "https://github.com/TK22-26/comfyui-inspect-image-overlay",
"files": [
"https://github.com/TK22-26/comfyui-inspect-image-overlay"
],
"install_type": "git-clone",
"description": "ComfyUI node that opens generated images in a fullscreen inspector overlay."
},
{
"author": "space-nuko",
"title": "OpenPose Editor [NOT MAINTAINED]",
"id": "openpose-editor",
"reference": "https://github.com/space-nuko/ComfyUI-OpenPose-Editor",
"files": [
"https://github.com/space-nuko/ComfyUI-OpenPose-Editor"
],
"install_type": "git-clone",
"description": "A port of the openpose-editor extension for stable-diffusion-webui. NOTE: Requires [a/this ComfyUI patch](https://github.com/comfyanonymous/ComfyUI/pull/711) to work correctly"
},
{
"author": "Zuellni",
"title": "Zuellni/ComfyUI-Custom-Nodes [DEPRECATED]",
"reference": "https://github.com/Zuellni/ComfyUI-Custom-Nodes",
"files": [
"https://github.com/Zuellni/ComfyUI-Custom-Nodes"
],
"install_type": "git-clone",
"description": "Nodes: DeepFloyd, Filter, Select, Save, Decode, Encode, Repeat, Noise, Noise"
},
{
"author": "StevenBaby",
"title": "comfyui-tools [REMOVED]",
"reference": "https://github.com/StevenBaby/comfyui-tools",
"files": [
"https://github.com/StevenBaby/comfyui-tools"
],
"install_type": "git-clone",
"description": "IntParameterNode: A Node contain 4 int parameters to quick switch input to other nodes."
},
{
"author": "saganaki22",
"title": "Kitten-TTS [NAME CONFLICT] [REMOVED]",
"reference": "https://github.com/Saganaki22/ComfyUI-KittenTTS",
"files": [
"https://github.com/Saganaki22/ComfyUI-KittenTTS"
],
"install_type": "git-clone",
"description": "ComfyUI custom node for KittenTTS - Ultra-lightweight text-to-speech (CUDA / CPU)"
},
{
"author": "Saganaki22",
"title": "ComfyUI-NovaSR [REMOVED]",
"reference": "https://github.com/Saganaki22/ComfyUI-NovaSR",
"files": [
"https://github.com/Saganaki22/ComfyUI-NovaSR"
],
"install_type": "git-clone",
"description": "ComfyUI node for NovaSR - Ultra-fast audio super resolution (3600x realtime, 50KB model)"
},
{
"author": "Saganaki22",
"title": "ComfyUI YTDL Nodes [WIP] [REMOVED]",
"reference": "https://github.com/Saganaki22/ComfyUI-ytdl_nodes",
"files": [
"https://github.com/Saganaki22/ComfyUI-ytdl_nodes"
],
"install_type": "git-clone",
"description": "Custom ComfyUI nodes for downloading, converting, and previewing audio/video from YouTube and 1,000+ other platforms"
},
{
"author": "pollockjj",
"title": "JAXTools [REMOVED]",
"reference": "https://github.com/pollockjj/ComfyUI-JAXTools",
"files": [
"https://github.com/pollockjj/ComfyUI-JAXTools"
],
"install_type": "git-clone",
"description": "JAX-accelerated image processing nodes for ComfyUI via dm_pix sealed worker isolation"
},
{
"author": "pollockjj",
"title": "ComfyUI-StabilityTest [REMOVED]",
"reference": "https://github.com/pollockjj/ComfyUI-StabilityTest",
"files": [
"https://github.com/pollockjj/ComfyUI-StabilityTest"
],
"install_type": "git-clone",
"description": "ComfyUI stability testing node. (Description by CC)"
},
{
"author": "denislov",
"title": "Comfyui_AutoSurvey [REMOVED]",
"reference": "https://github.com/denislov/Comfyui_AutoSurvey",
"files": [
"https://github.com/denislov/Comfyui_AutoSurvey"
],
"install_type": "git-clone",
"description": "Nodes:AutoSurvey, WriteOutline, WriteSection, ChatModel, QueryKnowledge, ManageDatabase, AddDoc2Knowledge"
},
{
"author": "saganaki22",
"title": "ZONOS2 TTS [REMOVED]",
"reference": "https://github.com/Saganaki22/Zonos2_TTS-ComfyUI",
"files": [
"https://github.com/Saganaki22/Zonos2_TTS-ComfyUI"
],
"install_type": "git-clone",
"description": "Native ComfyUI nodes for Zyphra ZONOS2 text-to-speech and voice cloning with AIMDO memory tracking"
},
{
"author": "saganaki22",
"title": "WavTTS [REMOVED]",
"reference": "https://github.com/Saganaki22/WavTTS-ComfyUI",
"files": [
"https://github.com/Saganaki22/WavTTS-ComfyUI"
],
"install_type": "git-clone",
"description": "ComfyUI custom nodes for WavTTS zero-shot text-to-speech with real ComfyUI memory tracking"
},
{
"author": "saganaki22",
"title": "Step Audio EditX TTS [REMOVED]",
"reference": "https://github.com/Saganaki22/ComfyUI-Step_Audio_EditX_TTS",
"files": [
"https://github.com/Saganaki22/ComfyUI-Step_Audio_EditX_TTS"
],
"install_type": "git-clone",
"description": "Professional voice cloning and audio editing node for ComfyUI using Step Audio EditX"
},
{
"author": "Saganaki22",
"title": "SoulX-Singer [REMOVED]",
"reference": "https://github.com/Saganaki22/ComfyUI-SoulX-Singer",
"files": [
"https://github.com/Saganaki22/ComfyUI-SoulX-Singer"
],
"install_type": "git-clone",
"description": "ComfyUI custom nodes for SoulX-Singer - Zero-Shot Singing Voice Synthesis"
},
{
"author": "Saganaki22",
"title": "Seedream4 Replicate [REMOVED]",
"reference": "https://github.com/Saganaki22/ComfyUI-Seedream4_Replicate",
"files": [
"https://github.com/Saganaki22/ComfyUI-Seedream4_Replicate"
],
"install_type": "git-clone",
"description": "ComfyUI node for Seedream 4 image generation via Replicate API with support for text-to-image and image-to-image generation"
},
{
"author": "saganaki22",
"title": "MOSS-TTS [REMOVED]",
"reference": "https://github.com/Saganaki22/Moss_TTS-ComfyUI",
"files": [
"https://github.com/Saganaki22/Moss_TTS-ComfyUI"
],
"install_type": "git-clone",
"description": "ComfyUI custom nodes for OpenMOSS MOSS-TTS Local Transformer v1.5 with model catalog downloads, Whisper prefix transcription, continuation, voice cloning, and AIMDO memory tracking"
},
{
"author": "Saganaki22",
"title": "Maya1 TTS [REMOVED]",
"reference": "https://github.com/Saganaki22/ComfyUI-Maya1_TTS",
"files": [
"https://github.com/Saganaki22/ComfyUI-Maya1_TTS"
],
"install_type": "git-clone",
"description": "ComfyUI node for Maya1 TTS - Expressive voice generation with 20+ emotions, voice design, and SNAC neural codec"
},
{
"author": "saganaki22",
"title": "LongCat AudioDiT TTS [REMOVED]",
"reference": "https://github.com/Saganaki22/ComfyUI-LongCat-AudioDIT-TTS",
"files": [
"https://github.com/Saganaki22/ComfyUI-LongCat-AudioDIT-TTS"
],
"install_type": "git-clone",
"description": "ComfyUI custom nodes for LongCat-AudioDiT TTS - zero-shot and voice cloning diffusion TTS"
},
{
"author": "saganaki22",
"title": "Ideogram 4 Prompter [REMOVED]",
"reference": "https://github.com/Saganaki22/ideogram4_prompter-ComfyUI",
"files": [
"https://github.com/Saganaki22/ideogram4_prompter-ComfyUI"
],
"install_type": "git-clone",
"description": "ComfyUI custom nodes for Ideogram 4 Magic Prompt and Ideogram 4 resolution selection"
},
{
"author": "saganaki22",
"title": "Higgs v3 TTS [REMOVED]",
"reference": "https://github.com/Saganaki22/Higgs_v3-TTS-ComfyUI",
"files": [
"https://github.com/Saganaki22/Higgs_v3-TTS-ComfyUI"
],
"install_type": "git-clone",
"description": "ComfyUI custom nodes for Higgs Audio v3 TTS with native inference, voice cloning, multi-speaker dialogue, longform chunking, and AIMDO DynamicVRAM support"
},
{
"author": "Saganaki22",
"title": "Foundation-1 [REMOVED]",
"reference": "https://github.com/Saganaki22/ComfyUI-Foundation-1",
"files": [
"https://github.com/Saganaki22/ComfyUI-Foundation-1"
],
"install_type": "git-clone",
"description": "ComfyUI custom nodes for Foundation-1 | structured text-to-sample generation for music production"
},
{
"author": "Saganaki22",
"title": "dotWaveform [REMOVED]",
"reference": "https://github.com/Saganaki22/ComfyUI-dotWaveform",
"files": [
"https://github.com/Saganaki22/ComfyUI-dotWaveform"
],
"install_type": "git-clone",
"description": "ComfyUI node for generating animated dotted waveform visualizations with multiple animation styles including teardrop bars"
},
{
"author": "saganaki22",
"title": "Dots TTS [REMOVED]",
"reference": "https://github.com/Saganaki22/Dots-TTS-ComfyUI",
"files": [
"https://github.com/Saganaki22/Dots-TTS-ComfyUI"
],
"install_type": "git-clone",
"description": "ComfyUI custom nodes for rednote-hilab dots.tts with model loading, generation, voice cloning, Whisper transcription, progress tracking, and AIMDO memory visualization"
},
{
"author": "Saganaki22",
"title": "AudioSR [REMOVED]",
"reference": "https://github.com/Saganaki22/ComfyUI-AudioSR",
"files": [
"https://github.com/Saganaki22/ComfyUI-AudioSR"
],
"install_type": "git-clone",
"description": "ComfyUI node for AudioSR - Versatile Audio Super Resolution upscales audio to 48kHz using latent diffusion"
},
{
"author": "pollockjj",
"title": "comfyui-sealedcuda [REMOVED]",
"reference": "https://github.com/pollockjj/ComfyUI-SealedCuda",
"files": [
"https://github.com/pollockjj/ComfyUI-SealedCuda"
],
"install_type": "git-clone",
"description": "Sealed-worker CUDA image nodes (NATTEN, fused-ssim) sourced from prebuilt cuda-wheels"
},
{
"author": "lvquan",
"title": "CS Hybrid 4K Image-to-Video [REMOVED]",
"reference": "https://github.com/lvqunx/comfyui-cs-hybrid",
"files": [
"https://github.com/lvqunx/comfyui-cs-hybrid"
],
"install_type": "git-clone",
"description": "Compressed-sensing hybrid 4K image-to-video: RAFT flow transfer, dual-channel Laplacian recompose, CS tile scheduler, keyframe refresh helpers."
},
{
"author": "leylahviolet",
"title": "iolet Tools 💅 [REMOVED]",
"reference": "https://github.com/leylahviolet/ComfyUI-Violet-Tools",
"files": [
"https://github.com/leylahviolet/ComfyUI-Violet-Tools"
],
"install_type": "git-clone",
"description": "Aesthetic-focused custom nodes for ComfyUI with sophisticated style and prompt management"
},
{
"author": "mahshid1378",
"title": "ThinkSound_Wrapper [REMOVED]",
@@ -2040,6 +1729,206 @@
"install_type": "git-clone",
"description": "🤗🤗🤗Comfyui Universal Translation Plugin (no longer requires adding various nodes, directly add translation function on the existing nodes), allowing Comfyui to support Chinese input and automatic translation for any long text input box, while adding error translation function (calling Baidu Translate), achieving translation freedom![w/This nodepack turns your ComfyUI server into a resource in the cloud.]"
},
{
"author": "Slix-M-Lestragg",
"title": "comfyui-enhanced [WIP]",
"reference": "https://github.com/Slix-M-Lestragg/comfyui-enhanced",
"files": [
"https://github.com/Slix-M-Lestragg/comfyui-enhanced"
],
"install_type": "git-clone",
"description": "A collection of enhanced nodes for ComfyUI that provide powerful additional functionality to your workflows.\nNOTE: The files in the repo are not organized."
},
{
"author": "Linsoo",
"title": "ComfyUI-Linsoo-Custom-Nodes",
"reference": "https://github.com/Linsoo/ComfyUI-Linsoo-Custom-Nodes",
"files": [
"https://github.com/Linsoo/ComfyUI-Linsoo-Custom-Nodes"
],
"install_type": "git-clone",
"description": "NODES: Linsoo Save Image, Linsoo Load Image (In development.. not working), Linsoo Empty Latent Image, Linsoo Multi Inputs, Linsoo Multi Outputs"
},
{
"author": "shinich39",
"title": "comfyui-nothing-happened",
"reference": "httphttps://github.com/shinich39/comfyui-nothing-happened",
"files": [
"https://github.com/shinich39/comfyui-nothing-happened"
],
"description": "Save image and keep metadata.",
"install_type": "git-clone"
},
{
"author": "AhBumm",
"title": "ComfyUI_MangaLineExtraction",
"reference": "https://github.com/AhBumm/ComfyUI_MangaLineExtraction-hf",
"files": [
"https://github.com/AhBumm/ComfyUI_MangaLineExtraction-hf"
],
"description": "p1atdev/MangaLineExtraction-hf as a node in comfyui",
"install_type": "git-clone"
},
{
"author": "fuzr0dah",
"title": "comfyui-sceneassembly",
"reference": "https://github.com/fuzr0dah/comfyui-sceneassembly",
"files": [
"https://github.com/fuzr0dah/comfyui-sceneassembly"
],
"install_type": "git-clone",
"description": "A bunch of nodes I created that I also find useful."
},
{
"author": "amamisonlyuser",
"title": "MixvtonComfyui [WIP]",
"reference": "https://github.com/amamisonlyuser/MixvtonComfyui",
"files": [
"https://github.com/amamisonlyuser/MixvtonComfyui"
],
"install_type": "git-clone",
"description": "NODES: CXH_Leffa_Viton_Load, CXH_Leffa_Viton_Run\nNOTE: The files in the repo are not organized."
},
{
"author": "A4P7J1N7M05OT",
"title": "ComfyUI-VAELoaderSDXLmod",
"reference": "https://github.com/A4P7J1N7M05OT/ComfyUI-VAELoaderSDXLmod",
"files": [
"https://github.com/A4P7J1N7M05OT/ComfyUI-VAELoaderSDXLmod"
],
"install_type": "git-clone",
"description": "NODES: Modified SDXL VAE Loader, Empty Latent Image Variable"
},
{
"author": "pixixai",
"title": "ComfyUI_Pixix-Tools [UNSAFE/WIP]",
"reference": "https://github.com/pixixai/ComfyUI_pixixTools",
"files": [
"https://github.com/pixixai/ComfyUI_pixixTools"
],
"install_type": "git-clone",
"description": "Load Text (from folder)\nNOTE: The files in the repo are not organized.[w/The contents of files from arbitrary paths can be read remotely through this node.]"
},
{
"author": "brandonkish",
"title": "comfyUI-extractable-text [WIP]",
"reference": "https://github.com/brandonkish/comfyUI-extractable-text",
"files": [
"https://github.com/brandonkish/comfyUI-extractable-text"
],
"install_type": "git-clone",
"description": "NODES: Save Image With Description, Save Image To Folder, Load Image With Description, LoRA Testing Node, Get Smaller Of Two Numbers, Get Larger Of Two Numbers\nNOTE: The files in the repo are not organized."
},
{
"author": "Randomwalkforest",
"title": "Comfyui-Koi-Toolkit",
"reference": "https://github.com/Randomwalkforest/Comfyui-Koi-Toolkit",
"files": [
"https://github.com/Randomwalkforest/Comfyui-Koi-Toolkit"
],
"install_type": "git-clone",
"description": "Koi Toolkit provides advanced nodes"
},
{
"author": "lfelipegg",
"title": "lfgg_custom_nodes [WIP]",
"reference": "https://github.com/lfelipegg/lfgg_custom_nodes",
"files": [
"https://github.com/lfelipegg/lfgg_custom_nodes"
],
"install_type": "git-clone",
"description": "NODES: ModelMergeCombos\nNOTE: The files in the repo are not organized."
},
{
"author": "Dehypnotic",
"title": "Save MP3 node for ComfyUI [UNSAFE]",
"reference": "https://github.com/Dehypnotic/comfyui-save-mp3",
"files": [
"https://github.com/Dehypnotic/comfyui-save-mp3"
],
"install_type": "git-clone",
"description": "Simple, flexible MP3 saver with bitrate options and handy path/filename templates.[w/This nodepack contains a node that has a vulnerability allowing write to arbitrary file paths.]"
},
{
"author": "octapus8085",
"title": "OpenAI-comfyui-O",
"reference": "https://github.com/Spicely/Comfyui-File-Utils",
"files": [
"https://github.com/Spicely/Comfyui-File-Utils"
],
"install_type": "git-clone",
"description": "This plugin provides multiple file-handling and utility nodes for ComfyUI, including: image saving, audio saving, video saving, video composition, audio-to-subtitle conversion, and random number generation nodes. These nodes not only process files but also return their absolute file paths.\nNOTE: The files in the repo are not organized.[w/This nodepack contains a node that has a vulnerability allowing write to arbitrary file paths.]"
},
{
"author": "pizurny",
"title": "ComfyUI-Just-DWPose [WIP]",
"reference": "https://github.com/pizurny/ComfyUI-Just-DWPose",
"files": [
"https://github.com/pizurny/ComfyUI-Just-DWPose"
],
"install_type": "git-clone",
"description": "An advanced DWPose annotator for ComfyUI with TorchScript and ONNX backends, featuring comprehensive pose detection, bone validation, temporal smoothing, and custom visualization tools."
},
{
"author": "aistudynow",
"title": "comfyui-wilddragon-nodes [WIP]",
"reference": "https://github.com/aistudynow/comfyui-wilddragon-nodes",
"files": [
"https://github.com/aistudynow/comfyui-wilddragon-nodes"
],
"install_type": "git-clone",
"description": "Image Face Crop (2025), Person Selector, Pose Face Filter, Person BBox Tracker, Face Restore & Blend, ...\nNOTE: The files in the repo are not organized."
},
{
"author": "rzasharp79",
"title": "ComfyUI--SolarFlare",
"reference": "https://github.com/rzasharp79/ComfyUI--SolarFlare",
"files": [
"https://github.com/rzasharp79/ComfyUI--SolarFlare"
],
"install_type": "git-clone",
"description": "NODES: Qwen Image, ..."
},
{
"author": "90cube",
"title": "CBCanvas Node for ComfyUI [WIP]",
"reference": "https://github.com/90cube/Comfyui-CBcanvas",
"files": [
"https://github.com/90cube/Comfyui-CBcanvas"
],
"install_type": "git-clone",
"description": "Canvas node with intelligent aspect ratio control - 13 preset aspect ratios from ultra-wide (21:9) to ultra-tall (9:21).\nNOTE: The files in the repo are not organized."
},
{
"author": "nohikomiso",
"title": "ComfyUI-ImageFolderPicker [UNSAFE]",
"reference": "https://github.com/nohikomiso/ComfyUI-ImageFolderPicker",
"files": [
"https://github.com/nohikomiso/ComfyUI-ImageFolderPicker"
],
"install_type": "git-clone",
"description": "Custom ComfyUI node for browsing local server folders and selecting images via thumbnail display in a grid interface. (Description by CC)[w/This nodepack has a vulnerability that allows it to retrieve a list of files from arbitrary paths.]"
},
{
"author": "PozzettiAndrea",
"title": "ComfyUI-CameraAnalysis",
"reference": "https://github.com/PozzettiAndrea/ComfyUI-CameraAnalysis",
"files": [
"https://github.com/PozzettiAndrea/ComfyUI-CameraAnalysis"
],
"install_type": "git-clone",
"description": "Extracts camera intrinsic parameters from image EXIF data."
},
{
"author": "LSDJesus",
"title": "ComfyUI-Luna-Collection [WIP]",
"reference": "https://github.com/LSDJesus/ComfyUI-Luna-Collection",
"files": [
"https://github.com/LSDJesus/ComfyUI-Luna-Collection"
],
"install_type": "git-clone",
"description": "Collection of finetuned and custom nodes for Pyrite and I\nNOTE: The files in the repo are not organized."
},
{
"author": "dorpxam",
"title": "ComfyUI-LTXVideoLoRA [REMOVED]",
@@ -2684,7 +2573,7 @@
},
{
"author": "BlackVortexAI",
"title": "BV Nodes [REMOVED]",
"title": "BV Nodes [DEPRECATED]",
"reference": "https://github.com/BlackVortexAI/ComfyUI-BVortexNodes",
"files": [
"https://github.com/BlackVortexAI/ComfyUI-BVortexNodes"
@@ -3287,7 +3176,7 @@
},
{
"author": "vsaan212",
"title": "ComfyUI Subject Selector [REMOVED]",
"title": "ComfyUI Subject Selector [DEPRECATED]",
"reference": "https://github.com/vsaan212/ComfyUI_subjectselector",
"files": [
"https://github.com/vsaan212/ComfyUI_subjectselector"
@@ -3619,6 +3508,16 @@
"install_type": "git-clone",
"description": "A simple custom node for ComfyUI that allows you to compare two images (or batches of images) side-by-side within the UI."
},
{
"author": "APZmedia",
"title": "NormalMapLightEstimator [REMOVED]",
"reference": "https://github.com/APZmedia/Comfyui-LightDirection-estimation",
"files": [
"https://github.com/APZmedia/Comfyui-LightDirection-estimation"
],
"install_type": "git-clone",
"description": "A ComfyUI custom node for estimating light direction and quality from normal maps using luma masking. The system analyzes surface normals to infer lighting information for downstream tasks like adaptive relighting, directional masking, or stylized effects."
},
{
"author": "APZmedia",
"title": "ComfyUI APZmedia PSD Tools [REMOVED]",
@@ -3802,7 +3701,7 @@
},
{
"author": "SlackinJack",
"title": "distrifuser_comfyui [REMOVED]",
"title": "distrifuser_comfyui [DEPRECATED]",
"reference": "https://github.com/SlackinJack/distrifuser_comfyui",
"files": [
"https://github.com/SlackinJack/distrifuser_comfyui"
@@ -3812,7 +3711,7 @@
},
{
"author": "SlackinJack",
"title": "asyncdiff_comfyui [REMOVED]",
"title": "asyncdiff_comfyui [DEPRECATED]",
"reference": "https://github.com/SlackinJack/asyncdiff_comfyui",
"files": [
"https://github.com/SlackinJack/asyncdiff_comfyui"
@@ -3900,6 +3799,16 @@
"install_type": "git-clone",
"description": "This node generates a professional prompt from an input text for modern video AI models (e.g., Alibaba Wan 2.2) via the OpenRouter API."
},
{
"author": "perilli",
"title": "apw_nodes [REMOVED]",
"reference": "https://github.com/alessandroperilli/APW_Nodes",
"files": [
"https://github.com/alessandroperilli/APW_Nodes"
],
"install_type": "git-clone",
"description": "A custom node suite to augment the capabilities of the [a/AP Workflows for ComfyUI](https://perilli.com/ai/comfyui/)\nNOTE: See [a/Open Creative Studio Nodes](https://github.com/alessandroperilli/OCS_Nodes)"
},
{
"author": "greengerong",
"title": "ComfyUI-Lumina-Video [REMOVED]",
@@ -3910,6 +3819,16 @@
"install_type": "git-clone",
"description": "This is a video generation plugin implementation for ComfyUI based on the Lumina Video model."
},
{
"author": "SatadalAI",
"title": "Combined Upscale Node for ComfyUI [REMOVED]",
"reference": "https://github.com/SatadalAI/SATA_UtilityNode",
"files": [
"https://github.com/SatadalAI/SATA_UtilityNode"
],
"install_type": "git-clone",
"description": "Combined_Upscale is a custom ComfyUI node designed for high-quality image enhancement workflows. It intelligently combines model-based upscaling with efficient CPU-based resizing, offering granular control over output dimensions and quality. Ideal for asset pipelines, UI prototyping, and generative workflows.\nNOTE: The files in the repo are not organized."
},
{
"author": "netroxin",
"title": "Netro_wildcards [REMOVED]",
@@ -4174,6 +4093,16 @@
"install_type": "git-clone",
"description": "A collection of custom nodes for ComfyUI that provide various input and output capabilities."
},
{
"author": "NicholasKao1029",
"title": "comfyui-pixxio [REMOVED]",
"reference": "https://github.com/NicholasKao1029/comfyui-pixxio",
"files": [
"https://github.com/NicholasKao1029/comfyui-pixxio"
],
"install_type": "git-clone",
"description": "NODES: Auto-Upload Image to Pixxio Collection, Load Image from Pixx.io"
},
{
"author": "ComfyUI-Workflow",
"title": "ComfyUI OpenAI Nodes [REMOVED]",
@@ -4195,6 +4124,16 @@
"install_type": "git-clone",
"description": "This is a node to convert workflows to cyuai api available nodes."
},
{
"author": "1H-hobit",
"title": "ComfyUI_InternVL3 [REMOVED]",
"reference": "https://github.com/1H-hobit/ComfyUI_InternVL3",
"files": [
"https://github.com/1H-hobit/ComfyUI_InternVL3"
],
"install_type": "git-clone",
"description": "ComfyUI for [a/InternVL](https://github.com/OpenGVLab/InternVL)"
},
{
"author": "spacepxl",
"title": "ComfyUI-Florence-2 [DEPRECATED]",
@@ -4401,6 +4340,17 @@
"install_type": "git-clone",
"description": "ComfyUI extension for parsing Civitai PNG workflows and automatically downloading missing models"
},
{
"author": "DriftJohnson",
"title": "DJZ-Nodes [REMOVED]",
"id": "DJZ-Nodes",
"reference": "https://github.com/MushroomFleet/DJZ-Nodes",
"files": [
"https://github.com/MushroomFleet/DJZ-Nodes"
],
"install_type": "git-clone",
"description": "AspectSize and other nodes"
},
{
"author": "DriftJohnson",
"title": "KokoroTTS Node [REMOVED]",
@@ -4564,6 +4514,16 @@
"install_type": "git-clone",
"description": "Custom nodes for loading and previewing media from URLs in ComfyUI."
},
{
"author": "pureexe",
"title": "DiffusionLight-ComfyUI [REMOVED]",
"reference": "https://github.com/pureexe/DiffusionLight-ComfyUI",
"files": [
"https://github.com/pureexe/DiffusionLight-ComfyUI"
],
"install_type": "git-clone",
"description": "DiffusionLight (Turbo) implemented in ComfyUI"
},
{
"author": "gondar-software",
"title": "comfyui-custom-padding [REMOVED]",
@@ -4716,7 +4676,7 @@
},
{
"author": "Kur0butiMegane",
"title": "Comfyui-StringUtils [REMOVED]",
"title": "Comfyui-StringUtils [DEPRECATED]",
"reference": "https://github.com/Kur0butiMegane/Comfyui-StringUtils",
"files": [
"https://github.com/Kur0butiMegane/Comfyui-StringUtils"
@@ -5041,6 +5001,26 @@
"install_type": "git-clone",
"description": "Nodes: MPN Switch, MPN Reroute, PN Switch. This is a set of custom nodes for ComfyUI. Mainly focus on control switches."
},
{
"author": "IfnotFr",
"title": "ComfyUI-Connect [REMOVED]",
"reference": "https://github.com/IfnotFr/ComfyUI-Connect",
"files": [
"https://github.com/IfnotFr/ComfyUI-Connect"
],
"install_type": "git-clone",
"description": "Transform your ComfyUI into a powerful API, exposing all your saved workflows as ready-to-use HTTP endpoints."
},
{
"author": "KurtHokke",
"title": "ComfyUI_KurtHokke-Nodes [REMOVED]",
"reference": "https://github.com/KurtHokke/ComfyUI_KurtHokke-Nodes",
"files": [
"https://github.com/KurtHokke/ComfyUI_KurtHokke-Nodes"
],
"install_type": "git-clone",
"description": "ComfyUI_KurtHokke-Nodes"
},
{
"author": "SpatialDeploy",
"title": "ComfyUI-Voxels [REMOVED]",
@@ -5185,7 +5165,7 @@
},
{
"author": "Makki_Shizu",
"title": "ComfyUI-SaveAnimatedGIF [REMOVED]",
"title": "ComfyUI-SaveAnimatedGIF [DEPRECATED]",
"id": "SaveAnimatedGIF",
"reference": "https://github.com/MakkiShizu/ComfyUI-SaveAnimatedGIF",
"files": [
@@ -5501,7 +5481,7 @@
},
{
"author": "42lux",
"title": "ComfyUI-safety-checker [REMOVED]",
"title": "ComfyUI-safety-checker [DEPRECATED]",
"reference": "https://github.com/42lux/ComfyUI-safety-checker",
"files": [
"https://github.com/42lux/ComfyUI-safety-checker"
@@ -5689,7 +5669,7 @@
},
{
"author": "zmwv823",
"title": "ComfyUI-Sana [REMOVED]",
"title": "ComfyUI-Sana [DEPRECATED]",
"reference": "https://github.com/zmwv823/ComfyUI-Sana",
"files": [
"https://github.com/zmwv823/ComfyUI-Sana"
@@ -5730,7 +5710,7 @@
},
{
"author": "huangyangke",
"title": "ComfyUI-Kolors-IpadapterFaceId [REMOVED]",
"title": "ComfyUI-Kolors-IpadapterFaceId [DEPRECATED]",
"reference": "https://github.com/huangyangke/ComfyUI-Kolors-IpadapterFaceId",
"files": [
"https://github.com/huangyangke/ComfyUI-Kolors-IpadapterFaceId"
@@ -5740,7 +5720,7 @@
},
{
"author": "zmwv823",
"title": "ComfyUI_Ctrlora [REMOVED]",
"title": "ComfyUI_Ctrlora [DEPRECATED]",
"reference": "https://github.com/zmwv823/ComfyUI_Ctrlora",
"files": [
"https://github.com/zmwv823/ComfyUI_Ctrlora"
@@ -5761,7 +5741,7 @@
},
{
"author": "DonBaronFactory",
"title": "ComfyUI-Cre8it-Nodes [REMOVED]",
"title": "ComfyUI-Cre8it-Nodes [DEPRECATED]",
"reference": "https://github.com/DonBaronFactory/ComfyUI-Cre8it-Nodes",
"files": [
"https://github.com/DonBaronFactory/ComfyUI-Cre8it-Nodes"
@@ -5791,7 +5771,7 @@
},
{
"author": "palant",
"title": "Extended Save Image for ComfyUI [REMOVED]",
"title": "Extended Save Image for ComfyUI [DEPRECATED]",
"reference": "https://github.com/palant/extended-saveimage-comfyui",
"files": [
"https://github.com/palant/extended-saveimage-comfyui"
@@ -5810,6 +5790,17 @@
"install_type": "git-clone",
"description": "ComfyUI Node - Hugging Face repositories GTP2 Prompt"
},
{
"author": "yushan777",
"title": "Y7 Nodes for ComfyUI [REMOVED]",
"id": "y7nodes",
"reference": "https://github.com/yushan777/ComfyUI-Y7Nodes",
"files": [
"https://github.com/yushan777/ComfyUI-Y7Nodes"
],
"install_type": "git-clone",
"description": "Nodes:Count_Tokens_(Y7)"
},
{
"author": "city96",
"title": "SD-Advanced-Noise [DEPRECATED]",
@@ -6061,7 +6052,7 @@
},
{
"author": "shinich39",
"title": "comfyui-pkg39 [REMOVED]",
"title": "comfyui-pkg39 [DEPRECATED]",
"reference": "https://github.com/shinich39/comfyui-pkg39",
"files": [
"https://github.com/shinich39/comfyui-pkg39"
@@ -6071,7 +6062,7 @@
},
{
"author": "dnl13",
"title": "ComfyUI-dnl13-seg [REMOVED]",
"title": "ComfyUI-dnl13-seg [DEPRECATED]",
"reference": "https://github.com/dnl13/ComfyUI-dnl13-seg",
"files": [
"https://github.com/dnl13/ComfyUI-dnl13-seg"
@@ -6397,7 +6388,7 @@
},
{
"author": "shinich39",
"title": "comfyui-text-pipe-39 [REMOVED]",
"title": "comfyui-text-pipe-39 [DEPRECATED]",
"reference": "https://github.com/shinich39/comfyui-text-pipe-39",
"files": [
"https://github.com/shinich39/comfyui-text-pipe-39"
@@ -6507,7 +6498,7 @@
},
{
"author": "ccvv804",
"title": "ComfyUI StableCascade using diffusers for Low VRAM [REMOVED]",
"title": "ComfyUI StableCascade using diffusers for Low VRAM [DEPRECATED]",
"reference": "https://github.com/ccvv804/ComfyUI-DiffusersStableCascade-LowVRAM",
"files": [
"https://github.com/ccvv804/ComfyUI-DiffusersStableCascade-LowVRAM"
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+3 -21
View File
@@ -629,12 +629,6 @@ if os.path.exists(restore_snapshot_path):
if 'COMFYUI_FOLDERS_BASE_PATH' not in new_env:
new_env["COMFYUI_FOLDERS_BASE_PATH"] = comfy_path
from comfy.cli_args import args
allow_flagged = default_conf.get('allow_flagged_nodepack_install', '').lower() == 'true'
new_env['_COMFYUI_MANAGER_CNR_ALLOW_FLAGGED'] = str(
allow_flagged or manager_util.is_loopback_listener(args.listen)
).lower()
cmd_str = [sys.executable, cm_cli_path, 'restore-snapshot', restore_snapshot_path]
exit_code = process_wrap(cmd_str, custom_nodes_base_path, handler=msg_capture, env=new_env)
@@ -683,18 +677,9 @@ def execute_lazy_install_script(repo_path, executable):
process_wrap(install_cmd, repo_path, env=new_env)
def execute_lazy_cnr_switch(target, zip_url, from_path, to_path, no_deps, custom_nodes_path, status=''):
def execute_lazy_cnr_switch(target, zip_url, from_path, to_path, no_deps, custom_nodes_path):
import uuid
import shutil
from comfy.cli_args import args
allow_flagged = default_conf.get('allow_flagged_nodepack_install', '').lower() == 'true'
if not manager_util.is_cnr_install_allowed(status or 'NodeVersionStatusFlagged', allow_flagged, args.listen):
if not status:
logging.error("Cannot execute reserved CNR switch for '%s': stored Registry status is missing. Request the installation again so its current status can be checked.", target)
else:
logging.error(manager_util.FLAGGED_NODEPACK_INSTALL_GUIDANCE)
return False
# 1. download
archive_name = f"CNR_temp_{str(uuid.uuid4())}.zip" # should be unpredictable name - security precaution
@@ -742,8 +727,6 @@ def execute_lazy_cnr_switch(target, zip_url, from_path, to_path, no_deps, custom
with open(tracking_info_file, "w", encoding='utf-8') as file:
file.write('\n'.join(list(extracted)))
return True
script_executed = False
@@ -799,9 +782,8 @@ def execute_startup_script():
execute_lazy_install_script(script[0], script[2])
elif script[1] == "#LAZY-CNR-SWITCH-SCRIPT":
status = script[8] if len(script) > 8 else ''
if execute_lazy_cnr_switch(script[0], script[2], script[3], script[4], script[5], script[6], status):
execute_lazy_install_script(script[3], script[7])
execute_lazy_cnr_switch(script[0], script[2], script[3], script[4], script[5], script[6])
execute_lazy_install_script(script[3], script[7])
elif script[1] == "#LAZY-DELETE-NODEPACK":
execute_lazy_delete(script[2])
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyui-manager"
description = "ComfyUI-Manager provides features to install and manage custom nodes for ComfyUI, as well as various functionalities to assist with ComfyUI."
version = "3.42"
version = "3.41"
license = { file = "LICENSE.txt" }
requires-python = ">=3.9"
dependencies = ["GitPython", "PyGithub", "matrix-nio", "transformers", "huggingface-hub>0.20", "typer", "rich", "typing-extensions", "toml", "uv", "chardet", "packaging", "nh3>=0.3.7"]
-400
View File
@@ -1,400 +0,0 @@
"""Real ComfyUI HTTP/queue/install tests with a local CNR service and harmless ZIPs.
Run with E2E_ROOT pointing to the existing ComfyUI + venv fixture. Each server
uses an isolated base directory; no installed nodes or user config are changed.
"""
import asyncio
import ast
import io
import json
import os
import socket
import subprocess
import threading
import time
import uuid
import zipfile
from contextlib import contextmanager
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from types import SimpleNamespace
from urllib.parse import parse_qs, urlparse
import aiohttp
import pytest
import requests
E2E_ROOT = Path(os.environ.get('E2E_ROOT', '/nonexistent'))
REPO_ROOT = Path(__file__).resolve().parents[2]
pytestmark = pytest.mark.skipif(
not (E2E_ROOT / 'venv/bin/python').is_file(), reason='E2E_ROOT is required',
)
PACKS = ['fixture-active', 'fixture-flagged', 'fixture-pending', 'fixture-banned', 'fixture-latest', 'fixture-switch', 'fixture-reinstall', 'fixture-update', 'fixture-snapshot']
@pytest.fixture(scope='module')
def registry():
calls = []
class Handler(BaseHTTPRequestHandler):
def log_message(self, *args):
pass
def do_GET(self):
parsed = urlparse(self.path)
calls.append(parsed.path + ('?' + parsed.query if parsed.query else ''))
parts = parsed.path.strip('/').split('/')
version = parse_qs(parsed.query).get('version', ['2.0.0'])[0]
node = parts[1] if len(parts) > 1 else ''
base = f'http://127.0.0.1:{self.server.server_port}'
def metadata(node, version):
status = {'1.0.0': 'Active', '1.1.0': 'Active', '2.0.0': 'Flagged', '3.0.0': 'Pending'}[version]
return {'id': f'{node}-{version}', 'node_id': node, 'version': version,
'status': 'NodeVersionStatus' + status, 'dependencies': [],
'downloadUrl': f'{base}/zip/{node}/{version}'}
status = 200
if parts[0] == 'zip':
node, version = parts[1:]
archive = io.BytesIO()
with zipfile.ZipFile(archive, 'w') as z:
z.writestr('__init__.py', 'NODE_CLASS_MAPPINGS = {}\n')
z.writestr('pyproject.toml', f'[project]\nname = "{node}"\nversion = "{version}"\n')
z.writestr('install.py', 'from pathlib import Path\n'
f'Path("installed.txt").write_text("{version}")\n')
body = archive.getvalue()
elif parsed.path == '/nodes':
body = json.dumps({'nodes': [
{'id': name, 'name': name, 'description': '', 'status': 'NodeStatusActive',
'repository': f'https://example.invalid/{name}',
'publisher': {'id': 'fixture', 'name': 'Fixture'},
'latest_version': metadata(name, '2.0.0')}
for name in PACKS], 'totalPages': 1}).encode()
elif len(parts) == 3 and parts[0] == 'nodes' and parts[2] == 'install':
if node == 'fixture-banned':
status, body = 404, b'{"message":"Not found"}'
else:
body = json.dumps(metadata(node, version)).encode()
else:
status, body = 404, b'{}'
self.send_response(status)
self.send_header('Content-Length', str(len(body)))
self.send_header('Content-Type', 'application/zip' if parts[0] == 'zip' else 'application/json')
self.end_headers()
self.wfile.write(body)
server = ThreadingHTTPServer(('127.0.0.1', 0), Handler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
yield SimpleNamespace(url=f'http://127.0.0.1:{server.server_port}', calls=calls)
server.shutdown()
server.server_close()
thread.join()
# Redirect only the external Registry in both ComfyUI and its restore subprocess.
REGISTRY_BOOTSTRAP = '''
import asyncio, json, os, sys, urllib.request
sys.path.insert(0, os.environ['CM_E2E_MANAGER_GLOB'])
import manager_core, cnr_utils, manager_util
cnr_utils.base_url = os.environ['CM_E2E_REGISTRY']
with urllib.request.urlopen(cnr_utils.base_url + '/nodes') as response:
manager_util.save_to_cache(cnr_utils.base_url + '/nodes', json.load(response))
asyncio.run(manager_core.unified_manager.reload('cache', dont_wait=False))
manager_core.unified_manager.custom_node_map_cache[(manager_core.normalize_channel('default'), 'cache')] = manager_core.NormalizedKeyDict()
'''
BOOTSTRAP = '''
import os, runpy, sys
sys.argv = [os.environ['CM_E2E_MAIN'], *sys.argv[1:]]
import comfy.options
comfy.options.enable_args_parsing()
''' + REGISTRY_BOOTSTRAP + "\nrunpy.run_path(sys.argv[0], run_name='__main__')\n"
@contextmanager
def running_server(root, listen, override, registry, security='normal'):
custom_nodes = root / 'custom_nodes'
custom_nodes.mkdir(exist_ok=True)
mount = custom_nodes / 'comfyui-manager'
if not mount.exists():
mount.symlink_to(REPO_ROOT, target_is_directory=True)
config_dir = root / 'user/__manager'
config_dir.mkdir(parents=True, exist_ok=True)
(config_dir / 'config.ini').write_text(
'[default]\nnetwork_mode = offline\nuse_uv = false\n'
f'security_level = {security}\nallow_flagged_nodepack_install = {override}\n'
)
# cm-cli.py inherits this test-only IO redirection from its real parent.
hook = root / 'sitecustomize.py'
hook.write_text("import os, sys\nif os.path.basename(sys.argv[0]) == 'cm-cli.py':\n"
" from comfy.cli_args import args\n"
" args.base_directory = os.environ['CM_E2E_BASE']\n"
+ '\n'.join(' ' + line for line in REGISTRY_BOOTSTRAP.splitlines()))
with socket.socket() as sock:
sock.bind(('127.0.0.1', 0))
port = sock.getsockname()[1]
base = f'http://127.0.0.1:{port}'
env = dict(os.environ, PYTHONUNBUFFERED='1',
PYTHONPATH=os.pathsep.join([str(root), str(E2E_ROOT / 'comfyui')]),
COMFYUI_PATH=str(E2E_ROOT / 'comfyui'), COMFYUI_FOLDERS_BASE_PATH=str(root),
CM_E2E_BASE=str(root), CM_E2E_MAIN=str(E2E_ROOT / 'comfyui/main.py'),
CM_E2E_MANAGER_GLOB=str(REPO_ROOT / 'glob'), CM_E2E_REGISTRY=registry.url)
command = [str(E2E_ROOT / 'venv/bin/python'), '-c', BOOTSTRAP, '--cpu',
'--base-directory', str(root), '--listen', listen, '--port', str(port),
'--database-url', f'sqlite:///{root / "comfyui.db"}']
log_path = root / ('server-' + uuid.uuid4().hex + '.log')
with log_path.open('w') as log:
process = subprocess.Popen(command, env=env, cwd=E2E_ROOT / 'comfyui', stdout=log, stderr=subprocess.STDOUT)
try:
deadline = time.monotonic() + 90
while time.monotonic() < deadline:
assert process.poll() is None, log_path.read_text()[-6000:]
try:
if requests.get(base + '/system_stats', timeout=1).status_code == 200:
break
except requests.RequestException:
pass
time.sleep(.2)
else:
pytest.fail(log_path.read_text()[-6000:])
assert requests.get(base + '/manager/version', timeout=5).status_code == 200, log_path.read_text()[-6000:]
assert 'PRESTARTUP FAILED' not in log_path.read_text(), log_path.read_text()
yield SimpleNamespace(root=root, base=base, listen=listen, override=override,
security=security, registry=registry, log=log_path, env=env)
finally:
process.terminate()
try:
process.wait(timeout=10)
except subprocess.TimeoutExpired:
process.kill()
process.wait(timeout=5)
@pytest.mark.parametrize('operation', ['install', 'restore-snapshot'])
def test_direct_cli_installs_flagged_without_server_options(tmp_path, registry, operation):
with running_server(tmp_path, '0.0.0.0', False, registry) as server:
env = server.env
command = [str(E2E_ROOT / 'venv/bin/python'), str(REPO_ROOT / 'cm-cli.py'), operation]
if operation == 'install':
command += ['fixture-flagged@2.0.0', '--mode', 'cache']
else:
snapshot = tmp_path / 'direct-snapshot.json'
snapshot.write_text(json.dumps({'cnr_custom_nodes': {'fixture-flagged': '2.0.0'},
'git_custom_nodes': {}, 'file_custom_nodes': []}))
command.append(str(snapshot))
command += ['--user-directory', str(tmp_path / 'user')]
before = len(registry.calls)
result = subprocess.run(command, env=env, cwd=E2E_ROOT / 'comfyui',
capture_output=True, text=True, timeout=60)
output = result.stdout + result.stderr
assert result.returncode == 0, output
installed = tmp_path / 'custom_nodes/fixture-flagged'
assert 'version = "2.0.0"' in (installed / 'pyproject.toml').read_text(), output
if operation == 'install':
assert (installed / 'installed.txt').read_text() == '2.0.0', output
assert sum('/install' in call for call in registry.calls[before:]) == 1
assert 'Installation of this flagged CNR version is blocked' not in output
@pytest.fixture(scope='module', params=[
('127.0.0.1', False, 'normal'), ('0.0.0.0', False, 'normal'),
('0.0.0.0', True, 'normal'), ('0.0.0.0', True, 'strong'),
('127.0.0.1', False, 'strong'),
], ids=lambda row: '-'.join(map(str, row)))
def comfy_server(request, tmp_path_factory, registry):
listen, override, security = request.param
root = tmp_path_factory.mktemp('flagged-v3')
with running_server(root, listen, override, registry, security) as server:
yield server
def install(server, node, version, operation='install'):
async def request_and_wait():
ui_id = uuid.uuid4().hex
params = {'id': node, 'version': '1.0.0', 'selected_version': version,
'mode': 'cache', 'channel': 'default', 'ui_id': ui_id,
'repository': 'https://example.invalid/fixture'}
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=40)) as session:
# Subscribe first: a fast worker can emit completion before POST returns.
async with session.ws_connect(server.base + '/ws?clientId=' + ui_id) as ws:
async with session.post(server.base + '/manager/queue/' + operation, json=params) as response:
if response.status != 200:
return {'http': response.status, 'message': await response.text()}
async with session.post(server.base + '/manager/queue/start') as response:
response.raise_for_status()
while True:
event = await ws.receive_json(timeout=30)
data = event.get('data', {})
if event.get('type') == 'cm-queue-status' and data.get('status') == 'done':
if ui_id in data.get('nodepack_result', {}):
return {'http': 200, 'message': data['nodepack_result'][ui_id]}
return asyncio.run(request_and_wait())
def assert_terminal_guidance(log):
for detail in ('--listen 127.0.0.1', '--listen ::1', '0.0.0.0', '[default]',
'allow_flagged_nodepack_install = true', 'config.ini',
'trusted private network', 'Restart ComfyUI'):
assert detail in log
def assert_flagged_denial(result, log):
assert result['message'] == ('This action is not allowed by the current security configuration. '
'See the terminal for details.'), result
assert_terminal_guidance(log)
@pytest.mark.parametrize('node,version,flagged', [
('fixture-active', '1.0.0', False), ('fixture-flagged', '2.0.0', True),
('fixture-pending', '3.0.0', False), ('fixture-banned', '1.0.0', False),
('fixture-latest', 'latest', True),
])
def test_install_policy(comfy_server, node, version, flagged):
server = comfy_server
log_before = len(server.log.read_text())
before = len(server.registry.calls)
result = install(server, node, version)
calls = server.registry.calls[before:]
strong = server.security == 'strong'
allowed = not strong and node != 'fixture-banned' and (
not flagged or server.override or server.listen == '127.0.0.1')
marker = server.root / 'custom_nodes' / node / 'installed.txt'
assert marker.exists() is allowed, (result, server.log.read_text()[-6000:])
assert sum('/install' in call for call in calls) == (0 if strong else 1), calls
assert any(call.startswith('/zip/') for call in calls) is allowed
if allowed:
assert result['message'] == 'success', result
assert marker.read_text() == ('2.0.0' if version == 'latest' else version)
elif strong:
assert result['http'] == 403, result
elif flagged:
assert_flagged_denial(result, server.log.read_text()[log_before:])
@pytest.mark.parametrize('operation', ['install', 'reinstall', 'update'])
def test_existing_pack_policy(comfy_server, operation):
server = comfy_server
if server.security == 'strong':
pytest.skip('Existing-pack transitions require an installed pack')
node = {'install': 'fixture-switch', 'reinstall': 'fixture-reinstall', 'update': 'fixture-update'}[operation]
assert install(server, node, '1.0.0')['message'] == 'success'
marker = server.root / 'custom_nodes' / node / 'installed.txt'
log_before = len(server.log.read_text())
before = len(server.registry.calls)
result = install(server, node, '2.0.0', operation)
calls = server.registry.calls[before:]
allowed = server.override or server.listen == '127.0.0.1'
assert sum('/install' in call for call in calls) == 1, calls
if allowed:
assert result['message'] == 'success', result
if operation == 'reinstall':
assert marker.read_text() == '2.0.0', server.log.read_text()[-6000:]
else:
assert marker.read_text() == '1.0.0'
assert 'NodeVersionStatusFlagged' in (server.root / 'user/__manager/startup-scripts/install-scripts.txt').read_text()
else:
assert_flagged_denial(result, server.log.read_text()[log_before:])
assert marker.read_text() == '1.0.0'
assert any(call.startswith('/zip/') for call in calls) is (allowed and operation == 'reinstall')
@pytest.mark.parametrize('restart_listen,restart_override,allowed', [
('0.0.0.0', False, False), ('0.0.0.0', True, True), ('127.0.0.1', False, True),
])
@pytest.mark.parametrize('stored_status', [True, False])
def test_restart_uses_current_policy(tmp_path, registry, restart_listen, restart_override, allowed, stored_status):
with running_server(tmp_path, '127.0.0.1', False, registry) as server:
assert install(server, 'fixture-switch', '1.0.0')['message'] == 'success'
assert install(server, 'fixture-switch', '2.0.0')['message'] == 'success'
if not stored_status:
reservation = tmp_path / 'user/__manager/startup-scripts/install-scripts.txt'
record = ast.literal_eval(reservation.read_text().strip())
assert record[1] == '#LAZY-CNR-SWITCH-SCRIPT'
assert len(record) == 9
reservation.write_text(repr(record[:8]) + '\n')
marker = tmp_path / 'custom_nodes/fixture-switch/installed.txt'
original = (marker.read_bytes(), marker.stat().st_mtime_ns)
before = len(registry.calls)
with running_server(tmp_path, restart_listen, restart_override, registry) as server:
calls = registry.calls[before:]
assert not any('/install' in call for call in calls), calls
assert any(call.startswith('/zip/') for call in calls) is allowed
if allowed:
assert marker.read_text() == '2.0.0', server.log.read_text()[-6000:]
else:
assert (marker.read_bytes(), marker.stat().st_mtime_ns) == original
if stored_status:
assert_terminal_guidance(server.log.read_text())
else:
assert 'stored Registry status is missing' in server.log.read_text()
assert 'Request the installation again' in server.log.read_text()
@pytest.mark.parametrize('surface,payload,flag', [
('git_url', {'url': 'https://example.invalid/fixture'}, 'allow_git_url_install'),
('pip', {'packages': 'fixture-do-not-install'}, 'allow_pip_install'),
])
def test_other_install_flags_remain_required(comfy_server, surface, payload, flag):
server = comfy_server
before = len(server.registry.calls)
response = requests.post(server.base + '/customnode/install/' + surface, json=payload, timeout=10)
assert response.status_code == 403, response.text
assert response.json() == {'error': flag}
assert server.registry.calls[before:] == []
@pytest.mark.parametrize('existing', [False, True])
@pytest.mark.parametrize('version,listen,override,allowed', [
('1.1.0', '0.0.0.0', False, True),
('2.0.0', '0.0.0.0', False, False),
('2.0.0', '0.0.0.0', True, True),
('2.0.0', '127.0.0.1', False, True),
])
def test_scheduled_snapshot_inherits_parent_policy(tmp_path, registry, existing, version, listen, override, allowed):
installed = tmp_path / 'custom_nodes/fixture-snapshot'
marker = installed / 'installed.txt'
reservation = tmp_path / 'user/__manager/startup-scripts/restore-snapshot.json'
with running_server(tmp_path, '127.0.0.1', False, registry) as server:
if existing:
assert install(server, 'fixture-snapshot', '1.0.0')['message'] == 'success'
assert marker.read_text() == '1.0.0'
original = (marker.read_bytes(), marker.stat().st_mtime_ns)
snapshot = tmp_path / 'user/__manager/snapshots/flagged-policy.json'
snapshot.write_text(json.dumps({'cnr_custom_nodes': {'fixture-snapshot': version},
'git_custom_nodes': {}, 'file_custom_nodes': []}))
before = len(registry.calls)
response = requests.post(server.base + '/snapshot/restore',
json={'target': snapshot.stem}, timeout=10)
assert response.status_code == 200, response.text
assert reservation.read_bytes() == snapshot.read_bytes()
assert registry.calls[before:] == []
if existing:
assert (marker.read_bytes(), marker.stat().st_mtime_ns) == original
else:
assert not installed.exists()
before = len(registry.calls)
with running_server(tmp_path, listen, override, registry) as server:
log = server.log.read_text()
calls = registry.calls[before:]
assert 'Restore snapshot done.' in log, log[-6000:]
assert 'Restore snapshot failed.' not in log, log[-6000:]
assert 'Error in sitecustomize' not in log, log[-6000:]
assert not reservation.exists()
assert sum('/install' in call for call in calls) == 1, (calls, log[-6000:])
assert any(call.startswith('/zip/') for call in calls) is allowed
if allowed:
assert f'version = "{version}"' in (installed / 'pyproject.toml').read_text(), log[-6000:]
assert 'allow_flagged_nodepack_install' not in log
else:
assert_terminal_guidance(log)
if existing:
assert (marker.read_bytes(), marker.stat().st_mtime_ns) == original
assert '1.0.0' in (installed / 'pyproject.toml').read_text()
else:
assert not installed.exists()
-325
View File
@@ -1,325 +0,0 @@
"""Behavioral checks against real v3 modules in an isolated ComfyUI runtime."""
import pytest
from test_install_flags_config import _run_child
PRELUDE = '''
import asyncio, io, logging, pathlib, zipfile
from types import SimpleNamespace
from unittest.mock import AsyncMock, Mock
import cnr_utils, manager_util
from comfy.cli_args import args
args.listen = '0.0.0.0'
core = manager_core
core.manager_funcs.is_flagged_install_allowed = lambda: False
config = core.get_config()
config['allow_flagged_nodepack_install'] = False
nodes = pathlib.Path(tmp) / 'custom_nodes'
nodes.mkdir()
core.get_default_custom_nodes_path = lambda: str(nodes)
manager = core.UnifiedManager()
core.unified_manager = manager
terminal = io.StringIO()
logging.getLogger().addHandler(logging.StreamHandler(terminal))
status = 'NodeVersionStatusFlagged'
http = 200
get = Mock(side_effect=lambda *a, **k: SimpleNamespace(status_code=http, json=lambda: {
'id': 'version-id', 'node_id': 'fixture', 'version': '2.0.0', 'status': status,
'downloadUrl': 'https://example.invalid/fixture.zip',
}))
cnr_utils.requests.get = get
def download(url, directory, name):
with zipfile.ZipFile(pathlib.Path(directory) / name, 'w') as archive:
archive.writestr('__init__.py', 'VERSION = "2.0.0"')
archive.writestr('pyproject.toml', '[project]\\nname = "fixture"\\nversion = "2.0.0"\\n')
core.manager_downloader.download_url = Mock(side_effect=download)
core.manager_downloader.basic_download_url = core.manager_downloader.download_url
manager.execute_install_script = Mock(return_value=True)
'''
def run(body):
stub = '''
comfy = types.ModuleType('comfy')
cli_args = types.ModuleType('comfy.cli_args')
cli_args.args = types.SimpleNamespace(listen='127.0.0.1')
sys.modules['comfy'] = comfy
sys.modules['comfy.cli_args'] = cli_args
'''
return _run_child(stub + PRELUDE + body)
@pytest.mark.parametrize('permission,allowed', [(None, True), ('true', True), ('false', False)])
def test_cli_install_uses_only_parent_permission(permission, allowed):
data = run(f'''
permission = {permission!r}
os.environ.pop('_COMFYUI_MANAGER_CNR_ALLOW_FLAGGED', None)
if permission is not None:
os.environ['_COMFYUI_MANAGER_CNR_ALLOW_FLAGGED'] = permission
core.manager_funcs = core.ManagerFuncs()
args.listen = '127.0.0.1' if permission == 'false' else '0.0.0.0'
config['allow_flagged_nodepack_install'] = permission == 'false'
result = asyncio.run(manager.install_by_id('fixture', '2.0.0'))
print(json.dumps({{'ok': result.result, 'queries': get.call_count,
'downloads': core.manager_downloader.download_url.call_count}}))
''')
assert data == {'ok': allowed, 'queries': 1, 'downloads': int(allowed)}
@pytest.mark.parametrize('status,permission,allowed', [
('Active', False, True), ('Pending', False, True),
('Flagged', False, False), ('Flagged', True, True),
])
def test_install_policy_before_download(status, permission, allowed):
data = run(f'''
status = {'NodeVersionStatus' + status!r}
core.manager_funcs.is_flagged_install_allowed = lambda: {permission!r}
result = asyncio.run(manager.install_by_id('fixture', '2.0.0'))
print(json.dumps({{'ok': result.result, 'message': result.msg,
'terminal': terminal.getvalue(),
'queries': get.call_count, 'downloads': core.manager_downloader.download_url.call_count,
'installed': (nodes / 'fixture/__init__.py').exists()}}))
''')
assert data['ok'] is allowed, data
assert data['installed'] is allowed, data
assert data['downloads'] == int(allowed), data
assert data['queries'] == 1, data
if not allowed:
assert data['message'] == ('This action is not allowed by the current security configuration. '
'See the terminal for details.')
for detail in ('--listen 127.0.0.1', '--listen ::1', '0.0.0.0', '[default]',
'allow_flagged_nodepack_install = true', 'config.ini',
'trusted private network', 'Restart ComfyUI'):
assert detail in data['terminal'], data
else:
assert 'allow_flagged_nodepack_install' not in data['terminal']
@pytest.mark.parametrize('http', [403, 404, 500])
def test_override_does_not_bypass_registry_refusal(http):
data = run(f'''
http = {http}
core.manager_funcs.is_flagged_install_allowed = lambda: True
result = asyncio.run(manager.install_by_id('fixture', '2.0.0'))
print(json.dumps({{'ok': result.result, 'queries': get.call_count,
'downloads': core.manager_downloader.download_url.call_count}}))
''')
assert data == {'ok': False, 'queries': 1, 'downloads': 0}
@pytest.mark.parametrize('operation', ['install', 'lazy', 'instant'])
@pytest.mark.parametrize('return_postinstall', [False, True])
def test_direct_cnr_execution_and_postinstall(operation, return_postinstall):
data = run(f'''
status = 'NodeVersionStatusActive'
operation = {operation!r}
return_postinstall = {return_postinstall!r}
installed = nodes / 'fixture'
if operation != 'install':
installed.mkdir()
(installed / '__init__.py').write_text('original')
(installed / '.tracking').write_text('__init__.py')
manager.active_nodes['fixture'] = ('1.0.0', str(installed))
manager.reserve_cnr_switch = Mock(return_value=True)
if operation == 'install':
result = manager.cnr_install('fixture', instant_execution=True,
no_deps=True, return_postinstall=return_postinstall)
else:
result = manager.cnr_switch_version('fixture', instant_execution=operation == 'instant',
no_deps=True, return_postinstall=return_postinstall)
assert result.result, result.msg
assert result.target == ('2.0.0' if operation == 'instant' else None)
effect = manager.reserve_cnr_switch if operation == 'lazy' else manager.execute_install_script
before = effect.call_count
if return_postinstall:
assert result.postinstall()
if operation == 'lazy':
assert effect.call_args.args[1] == 'https://example.invalid/fixture.zip'
assert effect.call_args.args[-2:] == (True, 'NodeVersionStatusActive')
else:
assert effect.call_args.kwargs == {{'instant_execution': True, 'no_deps': True}}
print(json.dumps({{'queries': get.call_count,
'downloads': core.manager_downloader.download_url.call_count,
'before': before, 'after': effect.call_count,
'content': (installed / '__init__.py').read_text()}}))
''')
assert data == {'queries': 1, 'downloads': int(operation != 'lazy'),
'before': int(not return_postinstall), 'after': 1,
'content': 'original' if operation == 'lazy' else 'VERSION = "2.0.0"'}
@pytest.mark.parametrize('operation', ['install', 'reinstall', 'lazy', 'instant', 'snapshot'])
def test_denial_preserves_installed_version(operation):
data = run(f'''
installed = nodes / 'fixture'
installed.mkdir()
(installed / '__init__.py').write_text('VERSION = "1.0.0"')
(installed / '.tracking').write_text('__init__.py')
manager.active_nodes['fixture'] = ('1.0.0', str(installed))
manager.reserve_cnr_switch = Mock(return_value=True)
operation = {operation!r}
if operation == 'snapshot':
manager.reload = AsyncMock()
manager.get_custom_nodes = AsyncMock(return_value={{}})
core.manager_util.restore_pip_snapshot = Mock()
snapshot = pathlib.Path(tmp) / 'snapshot.json'
snapshot.write_text(json.dumps({{'cnr_custom_nodes': {{'fixture': '2.0.0'}},
'git_custom_nodes': {{}}, 'file_custom_nodes': []}}))
asyncio.run(core.restore_snapshot(str(snapshot)))
elif operation in ('lazy', 'instant'):
result = manager.cnr_switch_version('fixture', '2.0.0', instant_execution=operation == 'instant')
assert not result.result, result.msg
elif operation == 'reinstall':
result = asyncio.run(manager.reinstall_by_id('fixture', '2.0.0'))
assert not result.result, result.msg
else:
result = asyncio.run(manager.install_by_id('fixture', '2.0.0'))
assert not result.result, result.msg
print(json.dumps({{'files': (installed / '__init__.py').read_text(),
'queries': get.call_count, 'downloads': core.manager_downloader.download_url.call_count,
'reservations': manager.reserve_cnr_switch.call_count}}))
''')
assert data == {'files': 'VERSION = "1.0.0"', 'queries': 1, 'downloads': 0, 'reservations': 0}
@pytest.mark.parametrize('listen,allowed', [
('127.0.0.1', True), ('::1', True), ('127.0.0.1,::1', True),
('0.0.0.0', False), ('::', False), ('0.0.0.0,::', False),
('192.168.1.2', False), ('127.0.0.1,0.0.0.0', False), ('', False),
])
def test_all_listener_addresses_must_be_loopback(listen, allowed):
data = run(f'''
print(json.dumps(manager_util.is_cnr_install_allowed('NodeVersionStatusFlagged', False, {listen!r})))
''')
assert data is allowed
@pytest.mark.parametrize('addresses,allowed', [
(['127.0.0.1', '::1'], True), (['127.0.0.1', '192.168.1.2'], False),
([], False), (None, False),
])
def test_hostname_must_resolve_exclusively_to_loopback(addresses, allowed):
data = run(f'''
addresses = {addresses!r}
if addresses is None:
manager_util.socket.getaddrinfo = Mock(side_effect=OSError('unresolvable'))
else:
manager_util.socket.getaddrinfo = Mock(return_value=[
(0, 0, 0, '', (ip, 0)) for ip in addresses])
print(json.dumps(manager_util.is_cnr_install_allowed('NodeVersionStatusFlagged', False, 'host.test')))
''')
assert data is allowed
@pytest.mark.parametrize('state', ['nightly', 'disabled'])
def test_denial_preserves_enabled_state(state):
data = run(f'''
state = {state!r}
installed = nodes / ('fixture' if state == 'nightly' else '.disabled/fixture@1_0_0')
installed.mkdir(parents=True)
(installed / '__init__.py').write_text('original')
if state == 'nightly':
manager.active_nodes['fixture'] = ('nightly', str(installed))
else:
manager.cnr_inactive_nodes['fixture'] = {{'1.0.0': str(installed)}}
before = repr((manager.active_nodes, manager.cnr_inactive_nodes))
result = asyncio.run(manager.install_by_id('fixture', '2.0.0'))
print(json.dumps({{'ok': result.result, 'original': (installed / '__init__.py').read_text(),
'unchanged': before == repr((manager.active_nodes, manager.cnr_inactive_nodes)),
'queries': get.call_count, 'downloads': core.manager_downloader.download_url.call_count}}))
''')
assert data == {'ok': False, 'original': 'original', 'unchanged': True, 'queries': 1, 'downloads': 0}
def test_exact_installed_version_enables_without_registry_lookup():
data = run('''
installed = nodes / '.disabled/fixture@2_0_0'
installed.mkdir(parents=True)
(installed / '__init__.py').write_text('already installed')
manager.cnr_inactive_nodes['fixture'] = {'2.0.0': str(installed)}
result = asyncio.run(manager.install_by_id('fixture', '2.0.0'))
assert manager.active_nodes['fixture'] == ('2.0.0', str(nodes / 'fixture'))
assert asyncio.run(manager.install_by_id('fixture', '2.0.0')).action == 'skip'
print(json.dumps({'ok': result.result, 'old_path': installed.exists(),
'content': (nodes / 'fixture/__init__.py').read_text(), 'queries': get.call_count,
'downloads': core.manager_downloader.download_url.call_count,
'scripts': manager.execute_install_script.call_count}))
''')
assert data == {'ok': True, 'old_path': False, 'content': 'already installed',
'queries': 0, 'downloads': 0, 'scripts': 0}
def test_failed_uninstall_stops_reinstall():
data = run('''
status = 'NodeVersionStatusActive'
manager.unified_uninstall = Mock(return_value=core.ManagedResult('uninstall').fail('cannot remove'))
result = asyncio.run(manager.reinstall_by_id('fixture', '2.0.0'))
print(json.dumps({'ok': result.result, 'message': result.msg, 'queries': get.call_count,
'downloads': core.manager_downloader.download_url.call_count}))
''')
assert data == {'ok': False, 'message': 'cannot remove', 'queries': 1, 'downloads': 0}
@pytest.mark.parametrize('version', ['nightly', 'unknown'])
def test_git_reinstall_replaces_pack_and_reruns_script(version):
data = run(f'''
version = {version!r}
installed = nodes / 'fixture'
installed.mkdir()
(installed / 'original.txt').write_text('original')
repo_url = 'https://example.invalid/fixture'
if version == 'nightly':
manager.active_nodes['fixture'] = ('nightly', str(installed))
else:
manager.unknown_active_nodes['fixture'] = (repo_url, str(installed))
manager.get_custom_nodes = AsyncMock(return_value={{
'fixture': {{'repository': repo_url, 'files': [repo_url]}},
}})
get.side_effect = AssertionError('Git needs no CNR install query')
manager.processed_install.add(str(installed / 'install.py'))
manager.execute_install_script = core.UnifiedManager.execute_install_script.__get__(manager)
core.try_install_script = Mock(return_value=True)
def clone(url, path, **kwargs):
assert url == repo_url
path = pathlib.Path(path)
assert not path.exists()
path.mkdir()
(path / 'replacement.txt').write_text('installed')
(path / 'install.py').write_text('')
assert manager.execute_install_script(url, str(path))
return core.ManagedResult('install-git')
manager.repo_install = Mock(side_effect=clone)
result = asyncio.run(manager.reinstall_by_id('fixture', version))
print(json.dumps({{'ok': result.result, 'original': (installed / 'original.txt').exists(),
'replacement': (installed / 'replacement.txt').read_text(),
'clones': manager.repo_install.call_count, 'scripts': core.try_install_script.call_count}}))
''')
assert data == {'ok': True, 'original': False, 'replacement': 'installed', 'clones': 1, 'scripts': 1}
def test_config_defaults_and_manual_edits_survive_settings_save():
data = _run_child('''
write_ini('[default]\\nsecurity_level = normal\\n')
first = fresh_read()['allow_flagged_nodepack_install']
write_ini('[default]\\nsecurity_level = normal\\nallow_flagged_nodepack_install = true\\n')
manager_core.get_config()['db_mode'] = 'local'
manager_core.write_config()
cached = manager_core.get_config()['allow_flagged_nodepack_install']
fresh = fresh_read()['allow_flagged_nodepack_install']
print(json.dumps([first, cached, fresh]))
''')
assert data == [False, False, True]
@pytest.mark.parametrize('raw,expected', [('TRUE', True), ('false', False), ('garbage', False)])
def test_config_value_roundtrip(raw, expected):
data = _run_child(f'''
write_ini('[default]\\nallow_flagged_nodepack_install = {raw}\\n')
loaded = fresh_read()['allow_flagged_nodepack_install']
manager_core.get_config()['allow_flagged_nodepack_install'] = not loaded
manager_core.write_config()
print(json.dumps([loaded, fresh_read()['allow_flagged_nodepack_install']]))
''')
assert data == [expected, not expected]
+3 -3
View File
@@ -56,7 +56,7 @@ _WANTED_CONSTS = {
_HANDLER_NAMES = {
"install_custom_node_git_url", # S-A
"install_custom_node_pip", # S-B
"_queue_node_install", # S-C
"install_custom_node", # S-C
}
@@ -435,14 +435,14 @@ class BindingProofTest(unittest.TestCase):
for name, flag in (
("install_custom_node_git_url", "allow_git_url_install"),
("install_custom_node_pip", "allow_pip_install"),
("_queue_node_install", "allow_git_url_install"),
("install_custom_node", "allow_git_url_install"),
):
with self.subTest(handler=name):
src = ast.unparse(HANDLERS[name])
self.assertIn("is_dedicated_install_allowed(", src)
self.assertIn(flag, src)
self.assertIn("args.listen", src)
sc_literals = self._ias_literal_calls(HANDLERS["_queue_node_install"])
sc_literals = self._ias_literal_calls(HANDLERS["install_custom_node"])
self.assertIn("middle", sc_literals, "entry gate must stay UNCHANGED")
self.assertIn(None, sc_literals, "a variable-arg retained path must remain")
+5 -4
View File
@@ -299,8 +299,7 @@ class WriteConfigPersistenceTest(unittest.TestCase):
"security_level": cp["default"].get("security_level", "<ABSENT>"),
"allow_git_url_install": cp["default"].get(
"allow_git_url_install", "<ABSENT>"),
"allow_flagged_nodepack_install": cp["default"].get(
"allow_flagged_nodepack_install", "<ABSENT>"),
"key_count": len(cp["default"]),
}))
"""
)
@@ -311,8 +310,10 @@ class WriteConfigPersistenceTest(unittest.TestCase):
"T5: the install flag must bootstrap secure-by-default",
)
self.assertEqual(
"False", payload["allow_flagged_nodepack_install"],
"T5: flagged installs must bootstrap with the override disabled",
18, payload["key_count"],
"T5: bootstrap wrote %d keys; write_config persists 18 "
"(manager_core.py:1685-1704). A change to that count is a scope "
"signal, not a nit." % payload["key_count"],
)
def test_t6_persisted_key_is_not_reclobbered(self):