From 0ef2235588dd9273e8435f27668e72e194a04336 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Sun, 22 Feb 2026 03:31:37 +0200 Subject: [PATCH 1/3] feat: multiusers environment variables --- README.md | 2 + glob/manager_server.py | 78 ++++++++++++++++++---- js/comfyui-manager.js | 147 +++++++++++++++++++++++++++++++++++++++-- 3 files changed, 209 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index ea4919a7..360e1088 100644 --- a/README.md +++ b/README.md @@ -330,6 +330,8 @@ The following features can be configured using environment variables: * **COMFYUI_PATH**: The installation path of ComfyUI * **GITHUB_ENDPOINT**: Reverse proxy configuration for environments with limited access to GitHub * **HF_ENDPOINT**: Reverse proxy configuration for environments with limited access to Hugging Face +* **COMFY_MANAGER_PASSKEY**: User password to access ComfyUI-Manager +* **COMFY_MODLES**: Reverse models download location ### Example 1: diff --git a/glob/manager_server.py b/glob/manager_server.py index eff7c032..dfb1f905 100644 --- a/glob/manager_server.py +++ b/glob/manager_server.py @@ -20,6 +20,7 @@ import cm_global import logging import asyncio import queue +import hmac import manager_downloader import manager_migration @@ -312,11 +313,59 @@ def security_403_response(): return web.json_response({"error": "security_level"}, status=403) -def get_model_dir(data, show_log=False): +def get_manager_passkey(): + passkey = os.environ.get("COMFY_MANAGER_PASSKEY") + if passkey is None: + return None + + passkey = passkey.strip() + return passkey if passkey else None + + +@routes.get("/manager/auth") +async def manager_auth_status(request): + return web.json_response({"required": get_manager_passkey() is not None}, status=200) + + +@routes.post("/manager/auth") +async def manager_auth_verify(request): + expected_passkey = get_manager_passkey() + if expected_passkey is None: + return web.Response(status=200) + + try: + payload = await request.json() + provided_passkey = payload.get("password", "") + except Exception: + provided_passkey = await request.text() + + if not isinstance(provided_passkey, str): + return web.Response(status=400, text="Invalid password payload") + + if hmac.compare_digest(provided_passkey, expected_passkey): + return web.Response(status=200) + + return web.Response(status=403, text="Invalid manager passkey") + + +def get_models_base_dir(): + override_path = os.environ.get("COMFY_MODLES") + if not override_path: + # Backward-compatible fallback for the correctly spelled variable. + override_path = os.environ.get("COMFY_MODELS") + + if override_path: + return os.path.abspath(os.path.expandvars(os.path.expanduser(override_path))) + if 'download_model_base' in folder_paths.folder_names_and_paths: - models_base = folder_paths.folder_names_and_paths['download_model_base'][0][0] - else: - models_base = folder_paths.models_dir + return folder_paths.folder_names_and_paths['download_model_base'][0][0] + + return folder_paths.models_dir + + +def get_model_dir(data, show_log=False): + models_base_override = os.environ.get("COMFY_MODLES") or os.environ.get("COMFY_MODELS") + models_base = get_models_base_dir() # NOTE: Validate to prevent path traversal. if any(char in data['filename'] for char in {'/', '\\', ':'}): @@ -355,7 +404,9 @@ def get_model_dir(data, show_log=False): base_model = os.path.join(models_base, data['save_path']) else: model_dir_name = model_dir_name_map.get(data['type'].lower()) - if model_dir_name is not None: + if models_base_override: + base_model = os.path.join(models_base, model_dir_name if model_dir_name is not None else "etc") + elif model_dir_name is not None: base_model = folder_paths.folder_names_and_paths[model_dir_name][0][0] else: base_model = os.path.join(models_base, "etc") @@ -922,6 +973,9 @@ async def fetch_customnode_alternatives(request): def check_model_installed(json_obj): + models_base_override = os.environ.get("COMFY_MODLES") or os.environ.get("COMFY_MODELS") + models_base = get_models_base_dir() + def is_exists(model_dir_name, filename, url): if filename == '': filename = os.path.basename(url) @@ -932,6 +986,10 @@ def check_model_installed(json_obj): if os.path.exists(os.path.join(x, filename)): return True + if models_base_override: + if os.path.exists(os.path.join(models_base, model_dir_name, filename)): + return True + return False model_dir_names = ['checkpoints', 'loras', 'vae', 'text_encoders', 'diffusion_models', 'clip_vision', 'embeddings', @@ -963,14 +1021,8 @@ def check_model_installed(json_obj): item['installed'] = 'True' if 'installed' not in item: - if item['filename'] == '': - filename = os.path.basename(item['url']) - else: - filename = item['filename'] - - fullpath = os.path.join(folder_paths.models_dir, item['save_path'], filename) - - item['installed'] = 'True' if os.path.exists(fullpath) else 'False' + fullpath = get_model_path(item) + item['installed'] = 'True' if fullpath is not None and os.path.exists(fullpath) else 'False' with concurrent.futures.ThreadPoolExecutor(8) as executor: for item in json_obj['models']: diff --git a/js/comfyui-manager.js b/js/comfyui-manager.js index bcf7e9e5..7e48017c 100644 --- a/js/comfyui-manager.js +++ b/js/comfyui-manager.js @@ -239,6 +239,123 @@ var update_policy_combo = null; let share_option = 'all'; var is_updating = false; +let managerAccessVerified = false; + + +function promptManagerPasskey() { + return new Promise((resolve) => { + const overlay = document.createElement("div"); + overlay.style.position = "fixed"; + overlay.style.top = "0"; + overlay.style.left = "0"; + overlay.style.width = "100%"; + overlay.style.height = "100%"; + overlay.style.backgroundColor = "rgba(0, 0, 0, 0.8)"; + overlay.style.display = "flex"; + overlay.style.alignItems = "center"; + overlay.style.justifyContent = "center"; + overlay.style.zIndex = "1102"; + + const dialog = document.createElement("div"); + dialog.style.backgroundColor = "#222"; + dialog.style.color = "#fff"; + dialog.style.padding = "18px"; + dialog.style.borderRadius = "8px"; + dialog.style.width = "min(420px, 90vw)"; + dialog.style.boxSizing = "border-box"; + + const title = document.createElement("h3"); + title.textContent = "Manager Access Required"; + title.style.margin = "0 0 12px 0"; + + const message = document.createElement("div"); + message.textContent = "Enter the manager passkey."; + message.style.marginBottom = "10px"; + + const input = document.createElement("input"); + input.type = "password"; + input.placeholder = "Passkey"; + input.style.width = "100%"; + input.style.padding = "8px"; + input.style.marginBottom = "12px"; + input.style.boxSizing = "border-box"; + + const buttonRow = document.createElement("div"); + buttonRow.style.display = "flex"; + buttonRow.style.justifyContent = "flex-end"; + buttonRow.style.gap = "8px"; + + const cancelButton = document.createElement("button"); + cancelButton.textContent = "Cancel"; + + const confirmButton = document.createElement("button"); + confirmButton.textContent = "Unlock"; + + const close = (value) => { + document.body.removeChild(overlay); + resolve(value); + }; + + cancelButton.onclick = () => close(null); + confirmButton.onclick = () => close(input.value); + input.addEventListener("keydown", (event) => { + if (event.key === "Enter") { + close(input.value); + } + }); + + buttonRow.append(cancelButton, confirmButton); + dialog.append(title, message, input, buttonRow); + overlay.appendChild(dialog); + document.body.appendChild(overlay); + input.focus(); + }); +} + + +async function ensureManagerAccess() { + if (managerAccessVerified) { + return true; + } + + let res; + try { + res = await api.fetchApi("/manager/auth"); + } catch { + // Fallback to legacy behavior when auth endpoint isn't available. + return true; + } + + if (res.status !== 200) { + return true; + } + + const info = await res.json(); + if (!info?.required) { + managerAccessVerified = true; + return true; + } + + while (true) { + const password = await promptManagerPasskey(); + if (password === null) { + return false; + } + + const verifyRes = await api.fetchApi("/manager/auth", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ password }), + }); + + if (verifyRes.status === 200) { + managerAccessVerified = true; + return true; + } + + customAlert("Invalid manager passkey."); + } +} // copied style from https://github.com/pythongosssss/ComfyUI-Custom-Scripts @@ -1487,7 +1604,11 @@ app.registerExtension({ id: "Comfy.Manager.Menu.ToggleVisibility", label: "Toggle Manager Menu Visibility", icon: "mdi mdi-puzzle", - function: () => { + function: async () => { + if (!(await ensureManagerAccess())) { + return; + } + if (!manager_instance) { setManagerInstance(new ManagerMenuDialog()); manager_instance.show(); @@ -1500,7 +1621,11 @@ app.registerExtension({ id: "Comfy.Manager.CustomNodesManager.ToggleVisibility", label: "Toggle Custom Nodes Manager Visibility", icon: "pi pi-server", - function: () => { + function: async () => { + if (!(await ensureManagerAccess())) { + return; + } + if (CustomNodesManager.instance?.isVisible) { CustomNodesManager.instance.close(); return; @@ -1570,7 +1695,11 @@ app.registerExtension({ let cmGroup = new (await import("../../scripts/ui/components/buttonGroup.js")).ComfyButtonGroup( new(await import("../../scripts/ui/components/button.js")).ComfyButton({ icon: "puzzle", - action: () => { + action: async () => { + if (!(await ensureManagerAccess())) { + return; + } + if(!manager_instance) setManagerInstance(new ManagerMenuDialog()); manager_instance.show(); @@ -1581,7 +1710,11 @@ app.registerExtension({ }).element, new(await import("../../scripts/ui/components/button.js")).ComfyButton({ icon: "star", - action: () => { + action: async () => { + if (!(await ensureManagerAccess())) { + return; + } + if(!manager_instance) setManagerInstance(new ManagerMenuDialog()); @@ -1638,7 +1771,11 @@ app.registerExtension({ // old style Manager button const managerButton = document.createElement("button"); managerButton.textContent = "Manager"; - managerButton.onclick = () => { + managerButton.onclick = async () => { + if (!(await ensureManagerAccess())) { + return; + } + if(!manager_instance) setManagerInstance(new ManagerMenuDialog()); manager_instance.show(); From 607d73a7d8f110a7afa734aa430a4004c0006019 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Mon, 23 Feb 2026 05:31:48 +0200 Subject: [PATCH 2/3] extra libs installation dir --- glob/manager_core.py | 16 ++--- glob/manager_util.py | 143 ++++++++++++++++++++++++++++++++++++++++++- prestartup_script.py | 10 ++- 3 files changed, 154 insertions(+), 15 deletions(-) diff --git a/glob/manager_core.py b/glob/manager_core.py index e0b3a6fe..f4a26602 100644 --- a/glob/manager_core.py +++ b/glob/manager_core.py @@ -192,6 +192,8 @@ if comfy_path is None: if comfy_base_path is None: comfy_base_path = comfy_path +manager_util.ensure_comfy_libs_path(os.path.join(comfy_base_path, 'custom_nodes')) + channel_list_template_path = os.path.join(manager_util.comfyui_manager_path, 'channels.list.template') git_script_path = os.path.join(manager_util.comfyui_manager_path, "git_helper.py") @@ -939,9 +941,10 @@ class UnifiedManager: if package_name and not package_name.startswith('#') and package_name not in self.processed_install: self.processed_install.add(package_name) clean_package_name = package_name.split('#')[0].strip() - install_cmd = manager_util.make_pip_cmd(["install", clean_package_name]) + install_cmd = manager_util.make_pip_install_cmd(clean_package_name, get_default_custom_nodes_path()) if clean_package_name != "" and not clean_package_name.startswith('#'): - res = res and try_install_script(url, repo_path, install_cmd, instant_execution=instant_execution) + if install_cmd is not None: + res = res and try_install_script(url, repo_path, install_cmd, instant_execution=instant_execution) pip_fixer.fix_broken() @@ -1993,14 +1996,11 @@ def execute_install_script(url, repo_path, lazy_mode=False, instant_execution=Fa package_name = remap_pip_package(line.strip()) if package_name and not package_name.startswith('#'): - if '--index-url' in package_name: - s = package_name.split('--index-url') - install_cmd = manager_util.make_pip_cmd(["install", s[0].strip(), '--index-url', s[1].strip()]) - else: - install_cmd = manager_util.make_pip_cmd(["install", package_name]) + install_cmd = manager_util.make_pip_install_cmd(package_name, get_default_custom_nodes_path()) if package_name.strip() != "" and not package_name.startswith('#'): - try_install_script(url, repo_path, install_cmd, instant_execution=instant_execution) + if install_cmd is not None: + try_install_script(url, repo_path, install_cmd, instant_execution=instant_execution) pip_fixer.fix_broken() if os.path.exists(install_script_path): diff --git a/glob/manager_util.py b/glob/manager_util.py index fd110b7f..4881e335 100644 --- a/glob/manager_util.py +++ b/glob/manager_util.py @@ -16,6 +16,8 @@ import logging import platform import shlex from functools import lru_cache +from pathlib import Path +from importlib import metadata cache_lock = threading.Lock() @@ -25,6 +27,7 @@ cache_dir = os.path.join(comfyui_manager_path, '.cache') # This path is also up use_uv = False bypass_ssl = False +libs_map = None def add_python_path_to_env(): if platform.system() != "Windows": @@ -35,6 +38,81 @@ def add_python_path_to_env(): os.environ['PATH'] = os.path.dirname(sys.executable)+sep+os.environ['PATH'] +def normalize_package_name(name: str) -> str: + return name.lower().replace('-', '_').strip() + + +def is_comfy_libs_enabled() -> bool: + value = os.environ.get("COMFY_LIBS", "") + return value.lower() == "true" + + +def get_comfy_libs_path(custom_nodes_base_path: str = None) -> str: + if custom_nodes_base_path: + base_dir = os.path.dirname(os.path.abspath(custom_nodes_base_path)) + return os.path.join(base_dir, "libs") + + comfy_base_path = os.environ.get('COMFYUI_FOLDERS_BASE_PATH') or os.environ.get('COMFYUI_PATH') + if comfy_base_path: + return os.path.join(os.path.abspath(comfy_base_path), "libs") + + try: + import folder_paths + custom_nodes_path = folder_paths.get_folder_paths("custom_nodes")[0] + return os.path.join(os.path.dirname(os.path.abspath(custom_nodes_path)), "libs") + except Exception: + # fallback: .../ComfyUI/custom_nodes/ComfyUI-Manager/glob -> .../ComfyUI/libs + return os.path.abspath(os.path.join(comfyui_manager_path, "..", "..", "libs")) + + +def ensure_comfy_libs_path(custom_nodes_base_path: str = None) -> str | None: + if not is_comfy_libs_enabled(): + return None + + libs_path = get_comfy_libs_path(custom_nodes_base_path) + os.makedirs(libs_path, exist_ok=True) + + if libs_path not in sys.path: + sys.path.insert(0, libs_path) + + py_path = os.environ.get("PYTHONPATH", "") + py_parts = py_path.split(os.pathsep) if py_path else [] + if libs_path not in py_parts: + os.environ["PYTHONPATH"] = libs_path if not py_path else libs_path + os.pathsep + py_path + + return libs_path + + +def get_comfy_libs_packages(renew=False, custom_nodes_base_path: str = None): + global libs_map + + if not is_comfy_libs_enabled(): + return {} + + if renew or libs_map is None: + libs_map = {} + libs_path = ensure_comfy_libs_path(custom_nodes_base_path) + if libs_path is None: + return {} + + try: + for dist in metadata.distributions(path=[libs_path]): + name = dist.metadata.get("Name") + version = dist.version + if name and version: + libs_map[normalize_package_name(name)] = version + except Exception: + # fallback: best-effort from dist-info folder names + try: + for item in Path(libs_path).glob("*.dist-info"): + name = item.name.split("-")[0] + libs_map[normalize_package_name(name)] = "0" + except Exception: + pass + + return libs_map + + @lru_cache(maxsize=2) def get_pip_cmd(force_uv=False): """ @@ -94,6 +172,62 @@ def make_pip_cmd(cmd): return base_cmd + cmd +def get_install_target_for_package(requirement: str, custom_nodes_base_path: str = None): + """ + Return a custom libs target path when COMFY_LIBS is enabled and package does not exist + in either site-packages or libs. + """ + if not is_comfy_libs_enabled(): + return None + + try: + parsed = parse_requirement_line(requirement) + pkg_name = normalize_package_name(parsed['package']) if parsed is not None else None + except Exception: + pkg_name = None + + if pkg_name is None: + # Fallback for non-StrictVersion requirement specs. + match = re.match(r'^([A-Za-z0-9_.+-]+)', requirement) + if match is None: + return None + pkg_name = normalize_package_name(match.group(1)) + installed = get_installed_packages().get(pkg_name) + if installed is not None: + return None + + libs_packages = get_comfy_libs_packages(custom_nodes_base_path=custom_nodes_base_path) + if pkg_name in libs_packages: + return None + + return ensure_comfy_libs_path(custom_nodes_base_path) + + +def make_pip_install_cmd(requirement: str, custom_nodes_base_path: str = None): + """ + Build a pip install command from a requirement expression. + Applies --target when COMFY_LIBS=True and the package is not installed. + """ + requirement = requirement.split('#')[0].strip() + if requirement == "" or requirement.startswith("#"): + return None + + install_args = ["install"] + target = get_install_target_for_package(requirement, custom_nodes_base_path=custom_nodes_base_path) + if target is not None: + install_args += ["--target", target] + + if '--index-url' in requirement: + s = requirement.split('--index-url', 1) + package = s[0].strip() + index_url = s[1].strip() + install_args += [package, '--index-url', index_url] + else: + install_args += [requirement] + + return make_pip_cmd(install_args) + + # DON'T USE StrictVersion - cannot handle pre_release version # try: # from distutils.version import StrictVersion @@ -305,12 +439,19 @@ def get_installed_packages(renew=False): logging.error("[ComfyUI-Manager] Failed to retrieve the information of installed pip packages.") return {} + if is_comfy_libs_enabled(): + for name, ver in get_comfy_libs_packages(renew=renew).items(): + if name not in pip_map: + pip_map[name] = ver + return pip_map def clear_pip_cache(): global pip_map + global libs_map pip_map = None + libs_map = None def parse_requirement_line(line): @@ -630,4 +771,4 @@ def restore_pip_snapshot(pips, options): if res != 0: failed.append(x) - print(f"Installation failed for pip packages: {failed}") \ No newline at end of file + print(f"Installation failed for pip packages: {failed}") diff --git a/prestartup_script.py b/prestartup_script.py index dabd39da..d3cad640 100644 --- a/prestartup_script.py +++ b/prestartup_script.py @@ -85,6 +85,7 @@ cm_global.register_api('cm.is_import_failed_extension', is_import_failed_extensi comfyui_manager_path = os.path.abspath(os.path.dirname(__file__)) custom_nodes_base_path = folder_paths.get_folder_paths('custom_nodes')[0] +manager_util.ensure_comfy_libs_path(custom_nodes_base_path) # Check for System User API availability (PR #10966) _has_system_user_api = hasattr(folder_paths, 'get_system_user_directory') @@ -642,13 +643,10 @@ def execute_lazy_install_script(repo_path, executable): package_name = remap_pip_package(line.strip()) package_name = package_name.split('#')[0].strip() if package_name and not is_installed(package_name): - if '--index-url' in package_name: - s = package_name.split('--index-url') - install_cmd = manager_util.make_pip_cmd(["install", s[0].strip(), '--index-url', s[1].strip()]) - else: - install_cmd = manager_util.make_pip_cmd(["install", package_name]) + install_cmd = manager_util.make_pip_install_cmd(package_name, custom_nodes_base_path) - process_wrap(install_cmd, repo_path) + if install_cmd is not None: + process_wrap(install_cmd, repo_path) if os.path.exists(install_script_path) and f'{repo_path}/install.py' not in processed_install: processed_install.add(f'{repo_path}/install.py') From b2a4227b7f0b6ba0a7346fa0085ee1d855f4e50e Mon Sep 17 00:00:00 2001 From: Ahmed Date: Mon, 23 Feb 2026 05:36:36 +0200 Subject: [PATCH 3/3] update readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 360e1088..bbc5b5cc 100644 --- a/README.md +++ b/README.md @@ -332,6 +332,7 @@ The following features can be configured using environment variables: * **HF_ENDPOINT**: Reverse proxy configuration for environments with limited access to Hugging Face * **COMFY_MANAGER_PASSKEY**: User password to access ComfyUI-Manager * **COMFY_MODLES**: Reverse models download location +* **COMFY_LIBS**: Override default libraries installation directory with "libs" folder ### Example 1: