Compare commits

...
567 Commits
Author SHA1 Message Date
JuggernautandGitHub 571233fcc7 Merge 2757728935 into 6ad8a24845 2026-07-05 20:48:35 +09:00
Dr.Lt.Data 6ad8a24845 fix(custom-node-list): §4.7-C-bis #3050 artokun/comfyui-mcp-panel — manual register (CONFLICTING) + node_db/new sync
Python Linting / Run Ruff (push) Waiting to run
2026-07-05 04:39:56 +09:00
Dr.Lt.Data ab8ce7814a update DB 2026-07-05 03:57:17 +09:00
Dr.Lt.Data 99d3e6ddac fix(custom-node-list): §4.2 MERGE_RELOCATE #3022 designloves2/ComfyUI-TJ_NODE_STUDIO_ONE — root relocate + node_db/new sync
- gh pr merge succeeded (MERGEABLE/UNSTABLE); entry landed at root idx 0
- Relocated to canonical position: after last git-clone (idx 5186, new author)
- node_db/new: prepended (top, chronological newest-first)
- Manual verification (post commented_with_response) confirmed:
  * Path Traversal @ nodes.py:576, 1150 FIXED (Path().resolve() + parent check via _safe_resolve_path helper)
  * Command Injection FIXED (subprocess.Popen shell=False + shutil.which)
  * Korean tooltip translated to English
  * Non-English sanity scan CLEAN
2026-07-05 03:19:42 +09:00
Dr.Lt.Data 84c889b026 Merge branch 'main' of github.com:Comfy-Org/ComfyUI-Manager 2026-07-05 03:18:32 +09:00
Dr.Lt.Data 966813eebf fix(custom-node-list): §4.7-C-bis #3003 A1-multiply/ComfyUI-LatentSaver — manual register, PR CONFLICTING
- CONFLICTING per gh — added directly via §4.7-C-bis
- Author responded to §4.7-D-code comment (2026-06-30): "changed all UI strings to English" @ dfec799
- Manual verification confirmed Korean tooltips at latent_saver.py:51/:55/:136 fully translated + repo-wide non-ASCII scan clean
- root: new author, appended after last git-clone (idx 5185)
- node_db/new: prepended (top)
2026-07-05 03:18:29 +09:00
Dr.Lt.DataandGitHub c305db27fe Merge pull request #3022 from designloves2/add-comfyui-tj-node-studio-one
Add ComfyUI-TJ_NODE_STUDIO_ONE to custom-node-list
2026-07-05 03:18:07 +09:00
Dr.Lt.Data 62cb417cc1 fix(custom-node-list): §4.5 PATCH_BASE_CLOSE #2966 + #2978 — adopt PR descriptions
- #2966 ShootTheSound/comfyUI-Realtime-Lora: description update (adds Z-Image/Qwen/FLUX Klein/SD 1.5 + layer analyzers, selective block loaders, debiasing)
- #2978 AntiMatterComfy/antimatter-nodes: description update (adds video batch, LM Studio agents, text file appender)
- title/reference/files/install_type unchanged for both
- extension-node-map.json changes from #2978 not applied (Layer 1 out of scope)
2026-07-05 02:47:06 +09:00
Dr.Lt.Data bdab3680c4 fix(custom-node-list): §4.5 PATCH_BASE_CLOSE #3045 kgilper/krea-reference — adopt PR title/desc
- title: comfyui-krea-reference → ComfyUI Krea Reference
- description: PR wording (guide cards, per-image roles, stack encoder details)
- author (Kevin Gilper) / reference / files / install_type unchanged
- root + node_db/new sync
2026-07-05 01:16:09 +09:00
Dr.Lt.Data 293ea3a8c5 fix(custom-node-list): §4.7-C-bis #3042 Archerkattri (HiCache + TRELLIS2-HiCache) — manual register, PR CONFLICTING
- Both entries added adjacent to existing archerkattri author cluster (root idx 5109-5110)
- node_db/new: both prepended (top, chronological newest-first)
- HiCache: previously flagged Chinese tooltip resolved (1:1 code scan PASS, safe pool)
- TRELLIS2: verified_safe + code scan PASS
2026-07-05 01:15:02 +09:00
Dr.Lt.Data 5e6a02590b fix(custom-node-list): §4.2 MERGE_RELOCATE #3034 Stillfront/comfyui-sf-nodes — root relocate + node_db/new sync
- Root: relocated from mid-file (idx 354) to after last git-clone (idx 5180, new author)
- node_db/new: prepended (top, chronological newest-first)
- Verify SAFE per verification_results.json:safe_repos (deep verify manual-confirmed all static flags as false positives)
2026-07-05 01:12:37 +09:00
Dr.Lt.DataandGitHub 0f4df5347c Merge pull request #3034 from ivorstrelarsf/main
Add SF ComfyUI Nodes by Stillfront
2026-07-05 01:11:45 +09:00
Dr.Lt.Data 9d7a3adc8a update DB 2026-07-05 01:10:15 +09:00
Dr.Lt.Data 5cb9c828ad update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-07-04 00:53:33 +09:00
Dr.Lt.Data 28855800cd fix(custom-node-list): §4.2 MERGE_RELOCATE #3036 DutchyDutch/ComfyUI_Local_Wildcards — root add + node_db/new relocate
Python Linting / Run Ruff (push) Waiting to run
- Root: added at index 5182 (after last git-clone, new author)
- node_db/new: relocated from bottom to top (chronological newest-first, §4.2 Step 9), indent 6→4 space normalized
- Merge commit: 87882134 (gh pr merge)
2026-07-03 04:36:49 +09:00
Dr.Lt.DataandGitHub 87882134e4 Merge pull request #3036 from DutchyDutch/add-comfyui-local-wildcards
Add ComfyUI Local Wildcards custom node
2026-07-03 04:34:37 +09:00
Dr.Lt.Data c903f8be4a update DB 2026-07-03 04:20:52 +09:00
Dr.Lt.Data 267449c55d fix(custom-node-list): §4.5 PATCH_BASE_CLOSE #3027 darth-veitcher/comfydv — adopt PR title/desc
- title: Comfy DV → Comfy DV Nodes
- description: PR wording (dynamic string formatting, seed-controlled random, workflow circuit-breaker)
- author/reference/files/install_type/id unchanged
- root only (not in node_db/new)
2026-07-03 04:14:45 +09:00
Dr.Lt.Data 6178d27b01 fix(custom-node-list): §4.5 PATCH_BASE_CLOSE #3029 gh055/ComfyUI-Dynamic-Combo — adopt PR title/desc
- title: Dynamic Text-to-Combo Generator → ComfyUI Dynamic Text-to-Combo Generator
- description: PR wording (Set/Get node setups)
- author/reference/files/install_type unchanged
- root + node_db/new sync
2026-07-03 01:57:15 +09:00
Dr.Lt.Data 4a21c02967 update DB 2026-07-03 01:46:05 +09:00
Dr.Lt.Data 8e06ee6552 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-07-02 04:42:34 +09:00
DutchyDutchandGitHub 19e0bd6592 Add ComfyUI Local Wildcards custom node 2026-06-30 20:02:34 +02:00
Dr.Lt.Data c4d775079b update DB
Python Linting / Run Ruff (push) Waiting to run
2026-07-01 02:51:31 +09:00
Dr.Lt.Data 7e80e39fdb fix(custom-node-list): place ketle-man/comfyui-mask-editor-one entry adjacent to existing ketle-man author block + sync to node_db/new (PR #2922) 2026-07-01 01:59:00 +09:00
Dr.Lt.DataandGitHub 30e055f4f6 Merge pull request #2922 from ketle-man/add-comfyui-mask-editor-one
Add Mask Editor One
2026-07-01 01:57:59 +09:00
Dr.Lt.Data 8183e23858 update DB 2026-07-01 01:54:22 +09:00
Dr.Lt.Data e99a2b7f4c feat(custom-node-list): §4.7-C-bis manual register 4 CONFLICTING PRs (#3032/#3016/#2956/#2935) + sync 5 to node_db/new (PRs #3032/#3016/#3006/#2956/#2935) 2026-07-01 01:47:46 +09:00
Dr.Lt.DataandGitHub ceac3168e4 Merge pull request #3006 from Nekodificador/fix/nkd-preview-tools-rename
Fix NKD Preview Tools entry: repo renamed + dedupe
2026-07-01 01:45:36 +09:00
Dr.Lt.Data c55592b332 update DB 2026-07-01 01:42:00 +09:00
Ivor Strelar afbc217f03 Add SF ComfyUI Nodes by Stillfront 2026-06-30 10:34:49 +02:00
Dr.Lt.Data 94a3ae8abd update DB
Python Linting / Run Ruff (push) Waiting to run
2026-06-30 05:17:52 +09:00
Dr.Lt.Data 66b3266488 fix(custom-node-list): place juangea/bs-comfyui-workflow-manager entry after last git-clone block + sync to node_db/new (PR #3015) 2026-06-30 05:06:51 +09:00
Dr.Lt.DataandGitHub 9fa5b6918d Merge pull request #3015 from juangea/add-bone-studio-workflow-manager
Add Bone-Studio Workflow Manager
2026-06-30 05:05:19 +09:00
Dr.Lt.Data dbe14a93b9 update DB 2026-06-30 05:04:59 +09:00
Dr.Lt.Data 675440b387 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-06-30 04:23:30 +09:00
Dr.Lt.Data dba1b7742b fix(scanner): fail-fast skip on GitHub rate limit (plain urllib3 Retry)
Replace PyGithub's default GithubRetry (which sleeps until rate-limit
reset and retries up to 10x) with a plain urllib3 Retry. A rate-limit
403/429 is no longer in status_forcelist, so it raises
RateLimitExceededException immediately; renew_stat's except-block
catches it and skips. Transient 5xx errors remain retried (D2).

[#1-sub-2]
2026-06-28 23:45:43 +09:00
Dr.Lt.Data c352b16bb1 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-06-28 04:36:55 +09:00
designloves2 a2274577a1 Add ComfyUI-TJ_NODE_STUDIO_ONE to custom node list 2026-06-27 17:05:13 +09:00
Dr.Lt.Data 8da299d978 fix(custom-node-list): §4.3 MERGE_DEDUP #3007 (relocate + remove old base) and #3009 (replace with PR entry + id field)
Python Linting / Run Ruff (push) Has been cancelled
2026-06-27 02:24:52 +09:00
Dr.Lt.DataandGitHub 36aa47f8bf Merge pull request #3007 from monkeykim111/add-comfyui-image-safety-gate
Add ComfyUI-Image-Safety-Gate
2026-06-27 02:22:27 +09:00
Dr.Lt.Data d35a732216 update DB 2026-06-27 02:01:29 +09:00
Dr.Lt.Data fedd2112e0 feat(custom-node-list): §4.7-C-bis manual register 4 CONFLICTING PRs + sync 5 to node_db/new (PRs #3018/#3001/#2995/#2912/#2989) 2026-06-27 01:42:52 +09:00
Dr.Lt.DataandGitHub 6dc448c813 Merge pull request #3018 from kwokkakiu233/main
Add LoRA Txt Loader node
2026-06-27 01:39:02 +09:00
Dr.Lt.Data 673f4ac4f5 update DB 2026-06-26 20:39:39 +09:00
kwokkakiu233 918919c6dc Add LoRA Txt Loader node 2026-06-25 19:58:04 +08:00
Juan GeaandClaude Opus 4.8 77baaecb76 Add Bone-Studio Workflow Manager to custom-node-list.json
A published Comfy Registry node (publisher: bonestudio), same author as the
existing "Bone-Studio Model Manager". Organizes ComfyUI workflows for production
(folder manager + projects, bulk export, optional Git), GPLv3.

Repo: https://github.com/juangea/bs-comfyui-workflow-manager
Registry: https://registry.comfy.org/nodes/bs-comfyui-workflow-manager

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 00:07:14 +02:00
Dr.Lt.Data 049a53f3d5 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-06-25 02:46:23 +09:00
Dr.Lt.Data 8ff2ba8ffe update db
Python Linting / Run Ruff (push) Waiting to run
2026-06-24 03:40:11 +09:00
Dr.Lt.Data b6db1ac854 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-06-22 02:56:24 +09:00
Dr.Lt.DataandGitHub f697116029 Merge pull request #3011 from alexanderar/add-ltx-2.3-models
feat(models): Add LTX-2.3 22B model family.
2026-06-22 02:00:20 +09:00
aartyomov 03861f48c9 feat(models): add LTX-2.3 22B model family
Adds 11 models from Lightricks LTX-2.3:
- 5 checkpoints: dev, distilled 1.1, dev fp8, distilled fp8, dev nvfp4
- 3 LoRAs: distilled lora 1.1, IC-LoRA union control, IC-LoRA motion track
- 3 upscalers: spatial x2 1.1, spatial x1.5, temporal x2
2026-06-21 11:13:53 +00:00
Dr.Lt.Data 579c4f9493 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-06-20 17:07:35 +09:00
Dr.Lt.Data 9e3a5695a7 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-06-20 03:06:51 +09:00
jonathan f92ce59d4b Add ComfyUI-Image-Safety-Gate 2026-06-18 16:22:31 +09:00
Nekodificador d6c6ac2559 Fix NKD Preview Tools entry: repo renamed + dedupe
The repo github.com/Nekodificador/ComfyUI-NKD-Popup-Preview was renamed to
ComfyUI-NKD-Preview-Tools (GitHub redirects old→new). The list still
referenced the old name and had a duplicate entry, so older Manager
installs tracked the orphaned name and stopped seeing updates.

- Point reference/files to the current repo URL
- Update title/description to match the published package
- Remove the duplicate entry

The node is also published on the Comfy Registry as
`comfyui-nkd-preview-tools`.
2026-06-17 17:57:05 +02:00
Dr.Lt.Data 2e93040db5 feat(custom-node-list): §4.7-C-bis manual register 6 CONFLICTING PRs (#2976/#2971/#2964/#2961/#2953/#2941)
Python Linting / Run Ruff (push) Has been cancelled
2026-06-17 02:05:11 +09:00
Dr.Lt.Data 6165515a08 fix(custom-node-list): relocate 3 new-author entries after last git-clone block + sync 6 entries to node_db/new (PRs #2986/#2983/#2990/#2975/#2968) 2026-06-17 02:01:40 +09:00
Dr.Lt.DataandGitHub 0fde8b155b Merge pull request #2968 from sorryhyun/add-anima-pid-easycontrol
add Anima PiD and EasyControl node
2026-06-17 01:59:22 +09:00
Dr.Lt.DataandGitHub 062a164e47 Merge pull request #2975 from rookiestar28/add-longcat-avatar-node
Add ComfyUI LongCat Avatar node
2026-06-17 01:59:16 +09:00
Dr.Lt.DataandGitHub e7ab2f5858 Merge pull request #2983 from elowbe/main
Add comfyui-save-text
2026-06-17 01:59:11 +09:00
Dr.Lt.DataandGitHub 92e3fa5062 Merge pull request #2986 from vantang/add-comfyui-mediahub
Register ComfyUI MediaHub
2026-06-17 01:59:07 +09:00
Dr.Lt.DataandGitHub 90fdb64a67 Merge pull request #2990 from hypnichorse/main
Added Hypnodes to the registry/node list
2026-06-17 01:57:50 +09:00
Dr.Lt.Data b4cdeb2297 update DB 2026-06-17 01:22:35 +09:00
Dr.Lt.Data 3a48b149bb feat(custom-node-list): register 4 SAFE candidates (skbv0/palettedirector, akatz-ai/seamless-tiling, 1girluniversity/scail2-longvideocontext, systms-ai/timeslice-nodes) 2026-06-17 01:15:50 +09:00
Dr.Lt.Data e886d03d31 fix(custom-node-list): archive 2 inaccessible nodes (CrazyDashTool/Combine-video-and-audio-comfyUI, MichaelMaxAgent/comfyui_ML_nodes) 2026-06-17 01:14:40 +09:00
Dr.Lt.Data 73e2f53d2c feat(custom-node-list): register bs-comfyui-model-manager (verified SAFE) 2026-06-17 00:09:15 +09:00
Dr.Lt.Data d6b1e4980a feat(node_db/dev): register bs-comfyui-model-manager (NO NODES, verified SAFE) 2026-06-17 00:07:21 +09:00
Dr.Lt.DataandGitHub e4c5401dd5 feat(security): dedicated install flags decoupled from security_level (#2991)
Python Linting / Run Ruff (push) Waiting to run
* feat(security): add dedicated install flags decoupled from security_level

Gate 'install via git URL' and 'install via pip' with dedicated opt-in
boolean flags (allow_git_url_install / allow_pip_install) in config.ini
[default], fully replacing the security_level term on those surfaces
(REPLACE, not AND — a strict level no longer denies when the flag is on;
a weak level no longer allows when the flag is off).

- glob/manager_server.py: pure predicate is_dedicated_install_allowed
  (flag AND loopback, request-time args.listen); REPLACE gates at
  /customnode/install/git_url and /customnode/install/pip; batch
  unknown-URL arm routes through the same full predicate at the risky
  position (loopback term is load-bearing — the middle entry gate has
  no network-position term; the entry gate itself stays in force);
  unknown-pip in batch stays unconditionally blocked; new
  SECURITY_MESSAGE_FLAG_* denial constants name the responsible flag;
  security_403_response gains flag_token (comfyui_outdated keeps precedence)
- glob/manager_core.py: register both keys (read via get_bool default-false,
  write list, exception fallback); "true"-only truthy; restart-only activation
- js/common.js: 403 dialog copy names the responsible flag at the two
  install call sites
- README.md: security-policy docs for both flags (per-surface scope incl.
  the batch entry-gate qualifier, REPLACE decoupling, loopback bound,
  opt-in config snippet, default-deny + migration note); stale tier lists
  corrected against the actual gates
- CHANGELOG.md: opt-in migration note + accepted residual risk (flags
  bypass the forced-strong outdated-ComfyUI hardening on loopback,
  opt-in only), decoupling claim qualified for the batch entry gate

Tests: unit suite (predicate truth table, REPLACE litmus both directions,
AST binding-proofs against live handlers, subprocess-isolated config
contract) plus a real-server E2E suite that mounts the Manager-under-test
via git worktree (exact-SHA pin, detached) against a real ComfyUI and
exercises both flag surfaces and both arms — deny arms (403 + flag-naming
body/log + no install artifact), git-URL allow arm (real clone), pip allow
arm as a two-phase reservation oracle — with zero-residual self-clean.
Module skips without E2E_COMFYUI_ROOT; unit suite unaffected.

The manager-v4 branch ships the identical policy (shared invariants +
config contract); this tree uses the degraded predicate 'flag AND
loopback' (no personal_cloud-equivalent mode here).

* bump version to v3.41
2026-06-16 03:34:10 +09:00
Dr.Lt.Data 88a7c52410 update DB 2026-06-16 02:15:13 +09:00
hypnichorseandGitHub de560aed84 Fixed typo 2026-06-14 21:39:12 +02:00
hypnichorseandGitHub dc15dcf83c Fixed Array 2026-06-14 21:34:51 +02:00
hypnichorseandGitHub c780c600b5 Added Hypnodes To Node-list 2026-06-14 19:34:20 +02:00
hypnichorseandGitHub eb479ff891 Added Hypnodes to the registry 2026-06-14 19:22:11 +02:00
Dr.Lt.Data 3772432847 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-06-14 23:52:11 +09:00
Van/攻城狮鹿鸣君 86a7d88270 Register ComfyUI MediaHub 2026-06-13 23:22:26 +08:00
Elowbe Software 89809b29d3 Update custom-node-list.json 2026-06-11 19:25:45 -04:00
194bcedcfc fix(graphToPrompt): forward arguments to the wrapped function (#2982)
Python Linting / Run Ruff (push) Has been cancelled
Co-authored-by: Terry Jia <terry@comfy.org>
2026-06-12 07:05:30 +09:00
Dr.Lt.Data d60e603467 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-06-11 01:41:30 +09:00
rookiestar28 6428a1009d Add ComfyUI LongCat Avatar node 2026-06-10 23:13:28 +08:00
Seunghyun Ji c4daf1dffd add Anima PiD and EasyControl node 2026-06-08 19:41:41 +09:00
Dr.Lt.Data 4a1c06b90a update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-06-07 02:51:09 +09:00
Dr.Lt.Data 395bb24427 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-06-05 06:08:54 +09:00
Dr.Lt.Data 25c1cc0921 feat(node_db/dev): register 4 NAME CONFLICT candidates (verified SAFE) 2026-06-05 05:54:28 +09:00
Dr.Lt.Data 629ccba819 fix(custom-node-list:new): sync comfyui-daz-tools entry 2026-06-05 03:47:38 +09:00
Dr.Lt.Data 3d01524ee9 fix(custom-node-list): place comfyui-daz-tools entry after last git-clone block 2026-06-05 03:47:07 +09:00
Dr.Lt.DataandGitHub 81c3c3d62c Merge pull request #2947 from denyazzolin/add-comfyui-daz-tools
Adding ComfyUI-Daz-Tools to Custom Node List
2026-06-05 03:45:42 +09:00
Dr.Lt.Data 1aa6112c57 update DB 2026-06-05 03:44:10 +09:00
Dr.Lt.Data c571cf3ca9 update DB 2026-06-05 00:49:06 +09:00
denyazzolin 4085957308 Add comfyui-daz-tools to custom node list 2026-06-03 21:22:46 -04:00
Dr.Lt.Data 7ddad11d28 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-06-04 03:05:11 +09:00
Dr.Lt.Data 142f4ce365 update DB 2026-06-04 01:59:12 +09:00
Dr.Lt.Data 3ec831a99b update DB
Python Linting / Run Ruff (push) Waiting to run
2026-06-03 07:27:35 +09:00
Dr.Lt.Data 2d373448be update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-06-01 20:16:09 +09:00
Dr.Lt.Data 56a54117ab update DB
Python Linting / Run Ruff (push) Waiting to run
2026-06-01 03:22:14 +09:00
Dr.Lt.Data dfa45a67e0 update DB 2026-06-01 03:16:10 +09:00
Dr.Lt.DataandGitHub 0ccaf67ed9 Merge pull request #2902 from robomello/add-nvml-monitor
Add ComfyUI-NVML-Monitor
2026-06-01 03:08:37 +09:00
Dr.Lt.DataandGitHub 73686b372b Merge pull request #2073 from millerlight/millerlight-patch-1
Millerlight patch 1
2026-06-01 03:08:34 +09:00
Dr.Lt.Data cb46064d25 update DB 2026-06-01 02:34:46 +09:00
Dr.Lt.Data a0f4d401ec update DB 2026-06-01 02:26:10 +09:00
Dr.Lt.Data d6f480c911 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-05-30 03:25:33 +09:00
Dr.Lt.Data 7d611c051e update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-05-28 02:15:37 +09:00
ketle-man 65cf3dbf5c Add Mask Editor One (ketle-man/comfyui-mask-editor-one) 2026-05-27 18:38:07 +09:00
Dr.Lt.Data 66617e061c update DB
Python Linting / Run Ruff (push) Waiting to run
2026-05-27 02:16:39 +09:00
Dr.Lt.Data c72bc4e171 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-05-26 08:56:59 +09:00
Dr.Lt.Data ef470d0119 update DB 2026-05-26 07:50:12 +09:00
Dr.Lt.Data e633858794 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-05-25 08:02:37 +09:00
Dr.Lt.Data 765e64ff1d update DB 2026-05-25 07:51:37 +09:00
Dr.Lt.Data 1bed21771f update DB 2026-05-25 07:41:09 +09:00
Dr.Lt.Data 36144e5608 update DB 2026-05-25 07:31:55 +09:00
Dr.Lt.Data 10f23c1d19 update DB 2026-05-25 07:28:47 +09:00
Dr.Lt.DataandGitHub 5b17ff01bd Merge pull request #2884 from SOLRICKS/remove-legacy-msxyz-listing
Remove legacy MSXYZ node listing
2026-05-25 07:14:26 +09:00
Dr.Lt.Data 8814191992 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-05-24 21:12:10 +09:00
Dr.Lt.DataandGitHub b717647e9c Merge pull request #2898 from Anonymzx/main
Update BangtrixToolkit entry, tag version and description
2026-05-24 21:11:31 +09:00
Dr.Lt.DataandGitHub 0151751ab5 Merge pull request #2892 from AntiMatterComfy/codex/update-antimatter-batch-loader-description
Update AntiMatter Nodes description
2026-05-24 21:11:18 +09:00
Dr.Lt.Data 4bab2faa5e update DB
Python Linting / Run Ruff (push) Waiting to run
2026-05-24 03:29:27 +09:00
robomello a0c4c78e9e Add ComfyUI-NVML-Monitor 2026-05-22 19:51:26 -05:00
Anonymzx bfec5e4ca8 Update BangtrixToolkit entry and description 2026-05-22 00:58:49 +07:00
Dr.Lt.Data bf5c346428 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-05-21 05:53:45 +09:00
AntiMatterComfy eb57f6f6b5 Map AntiMatter batch loader node 2026-05-20 10:37:00 +03:00
AntiMatterComfy 1d4d74f745 Update AntiMatter Nodes description 2026-05-20 10:35:58 +03:00
Dr.Lt.Data 928870f598 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-05-20 08:39:29 +09:00
Dr.Lt.Data 7110ec1eaa update DB 2026-05-20 08:16:23 +09:00
Dr.Lt.Data 2483471541 fix(custom-node-list): dedup antimatter-nodes (#2891), keep PR entry with id field and enriched description 2026-05-20 08:15:42 +09:00
Dr.Lt.DataandGitHub aa747eec52 Merge pull request #2891 from AntiMatterComfy/codex/add-antimatter-nodes
Add AntiMatter Nodes
2026-05-20 08:14:48 +09:00
Dr.Lt.Data da873b5f3b update DB
Python Linting / Run Ruff (push) Waiting to run
2026-05-20 00:35:20 +09:00
AntiMatterComfy ade66eb61d Update AntiMatter Nodes description 2026-05-19 14:29:58 +03:00
AntiMatterComfy 703438251b Add AntiMatter Nodes 2026-05-19 14:26:00 +03:00
Dr.Lt.Data 7cbd62c8bd update DB
Python Linting / Run Ruff (push) Waiting to run
2026-05-19 18:50:44 +09:00
Dr.Lt.Data d0cc0e4fe9 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-05-19 13:01:16 +09:00
Dr.Lt.Data 9ad16c0188 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-05-18 05:39:11 +09:00
Dr.Lt.Data a2c41a2a21 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-05-17 10:10:59 +09:00
Dr.Lt.Data 9690eed4d1 fix(custom-node-list): place ComfyUI-Pixal3D entry after last git-clone block 2026-05-17 10:10:47 +09:00
Dr.Lt.DataandGitHub 5d1d287735 Merge pull request #2874 from dreamrec/add-comfyui-pixal3d
Add ComfyUI-Pixal3D to custom-node-list.json
2026-05-17 10:10:18 +09:00
Dr.Lt.Data 65789a6c9d update DB
Python Linting / Run Ruff (push) Waiting to run
2026-05-17 03:47:43 +09:00
Dr.Lt.Data 871a646fd7 update DB 2026-05-16 22:10:31 +09:00
Dr.Lt.Data 27f81a058e fix(custom-node-list): place PlagueKind-Nodes entry after last git-clone block 2026-05-16 22:10:16 +09:00
Dr.Lt.DataandGitHub af11571dd7 Merge pull request #2875 from PlagueKind/main
Add PlagueKind Nodes to ComfyUI-Manager registry
2026-05-16 22:09:39 +09:00
Dr.Lt.Data f59dd4bdc6 update DB 2026-05-16 21:42:09 +09:00
Dr.Lt.Data 90b87db05c fix(custom-node-list): dedup nicehero/comfyui-conditioning-saver (#2883), keep PR entry with id field and proper-case title 2026-05-16 21:41:10 +09:00
Dr.Lt.DataandGitHub d6a9d6544d Merge pull request #2883 from nicehero/main
Add [comfyui-conditioning-saver](https://github.com/nicehero/comfyui-conditioning-saver) to the custom node list.
2026-05-16 21:40:50 +09:00
Dr.Lt.DataandGitHub 42b028362e Merge pull request #2882 from ketle-man/add-model-and-prompt-from-metadata
Add Model and Prompt from Metadata node
2026-05-16 21:40:17 +09:00
Dr.Lt.Data dcf04578b8 fix(custom-node-list): place ComfyUI-Magos-Nodes entry after last git-clone block 2026-05-16 21:39:55 +09:00
Dr.Lt.DataandGitHub 3eceec5bfa Merge pull request #2878 from MagosDigitalStudio/main
Add ComfyUI-Magos-Nodes
2026-05-16 21:39:19 +09:00
Dr.Lt.Data 2c0d665929 fix(custom-node-list): place BangtrixToolkit entry after last git-clone block 2026-05-16 21:38:56 +09:00
Dr.Lt.DataandGitHub 7fa009e9da Merge pull request #2877 from Anonymzx/main
add BangtrixToolkit to custom nodes list
2026-05-16 21:38:24 +09:00
Dr.Lt.DataandGitHub 7cf155b2fe Merge pull request #2868 from pmarmotte2/register-comfyui-pick-any
Add Comfyui-Pick_Any to custom node list
2026-05-16 21:37:55 +09:00
Dr.Lt.Data 350ff2d044 fix(custom-node-list): place XB_ToolBox entry after last git-clone block 2026-05-16 21:37:31 +09:00
Dr.Lt.DataandGitHub f64b73fc57 Merge pull request #2807 from wjluoxiao/main
Add custom node: XB_ToolBox
2026-05-16 21:36:47 +09:00
Dr.Lt.Data 1958957d36 fix(custom-node-list): place ComfyUI-PuLID-Flux-Chroma entry after last git-clone block 2026-05-16 21:35:49 +09:00
Dr.Lt.DataandGitHub f3236c3031 Merge pull request #1935 from PaoloC68/main
Add ComfyUI-PuLID-Flux-Chroma - First PuLID implementation for Chroma models
2026-05-16 21:32:03 +09:00
Dr.Lt.DataandGitHub fcbad506bb Merge pull request #1913 from flrngel/patch-1
Add ComfyUI_rgbx_xrgb_Wrapper
2026-05-16 21:31:21 +09:00
Dr.Lt.Data 934fd2e091 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-05-16 19:27:39 +09:00
ketle-man 5de92e4f92 Merge upstream/main - resolve conflict 2026-05-16 14:36:37 +09:00
Dr.Lt.Data 3ae1fe6ea6 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-05-16 10:17:57 +09:00
Mustafa SahinandGitHub 19f33695ff chore: remove legacy MSXYZ listing 2026-05-15 20:50:27 +03:00
nicehero 8ea1cf7aa7 add comfyui-conditioning-saver 2026-05-15 13:55:48 +08:00
ketle-manandClaude Sonnet 4.6 936dc09f22 Add Model and Prompt from Metadata (ketle-man)
- Add new entry: model-and-prompt-from-metadata
- Remove duplicate comfyui-2dpose-editor entry (leftover from previous closed PR)
- Add node mappings to extension-node-map.json

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 11:03:50 +09:00
ketle-manandGitHub 5a19558c01 Merge branch 'Comfy-Org:main' into main 2026-05-15 10:58:06 +09:00
MagosDigitalStudioandGitHub 2513b3bff1 Add ComfyUI-Magos-Nodes 2026-05-14 12:58:28 +03:00
Anonymzx 919c07f1e1 add BangtrixToolkit to custom nodes list 2026-05-14 16:20:30 +07:00
PlagueKind 8f646e92a0 Add PlagueKind Nodes 2026-05-14 02:09:09 +02:00
silviu 8d9eeb93f4 Add ComfyUI-Pixal3D — Pixal3D image-to-3D for Windows + RTX 30/40/50
ComfyUI custom node wrapping Tencent's SIGGRAPH 2026 Pixal3D pipeline.
Registers 3 nodes (Pixal3DLoadPipeline, Pixal3DImageToMesh, Pixal3DFreePipeline)
in extension-node-map.json so the Manager's "Install Missing Custom Nodes"
flow can find them.

Repo: https://github.com/dreamrec/ComfyUI-Pixal3D
2026-05-13 22:47:29 +03:00
Dr.Lt.Data 4f7f26da3b update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-05-14 04:21:55 +09:00
ketle-manandGitHub 299976330f Merge branch 'Comfy-Org:main' into main 2026-05-12 13:04:58 +09:00
Dr.Lt.Data 3b2f6fd149 fix(custom-node-list): move locale-pending entries (ComfyUI-VolcEngine, comfyui-image-feeder) from default to dev channel pending English localization
Python Linting / Run Ruff (push) Has been cancelled
2026-05-12 01:20:18 +09:00
Pmarmotte ea3dcde0ca Add Comfyui-Pick_Any custom node 2026-05-11 14:37:29 +02:00
Dr.Lt.Data c2a33d2efc fix(custom-node-list): place ComfyUI Image Feeder entry next to existing ketle-man author block 2026-05-11 20:40:26 +09:00
Dr.Lt.Data d9ac595785 update DB 2026-05-11 20:39:43 +09:00
Dr.Lt.Data b15c12ca4e update DB 2026-05-11 20:39:13 +09:00
Dr.Lt.Data dc0f66f042 update DB 2026-05-11 20:38:32 +09:00
Dr.Lt.Data 3d1210aa52 fix(custom-node-list): place ComfyUI-VolcEngine entry after last git-clone block 2026-05-11 20:37:52 +09:00
Dr.Lt.DataandGitHub d16ba306be Merge pull request #2801 from gzsiang/main
Add ComfyUI-VolcEngine to custom node list
2026-05-11 20:37:12 +09:00
Dr.Lt.Data 1e409cdd11 fix(custom-node-list): place ComfyUI-MaskToTransparent entry next to existing nekodificador author block 2026-05-11 20:36:57 +09:00
Dr.Lt.DataandGitHub 40e27f8eb4 Merge pull request #2837 from Warningning/main
Add ComfyUI-MaskToTransparent node entry
2026-05-11 20:36:09 +09:00
Dr.Lt.Data 6cc7f3106b fix(custom-node-list): place ComfyUI-BodyRatioMapper entry after last git-clone block 2026-05-11 20:35:19 +09:00
Dr.Lt.DataandGitHub e7e36be084 Merge pull request #2864 from wuwukaka/main
Add ComfyUI-BodyRatioMapper custom node
2026-05-11 20:34:28 +09:00
Dr.Lt.Data 6c9dd4be42 fix(custom-node-list): place ComfyUI-DCW entry after last git-clone block 2026-05-11 20:34:18 +09:00
Dr.Lt.DataandGitHub 8f4e7b089b Merge pull request #2830 from namemechan/main
Add: ComfyUI-DCW (SNR-t bias correction)
2026-05-11 20:33:37 +09:00
Dr.Lt.Data 02dcb1dd60 fix(custom-node-list): correct oath-studio files[0] to repo URL and place Claude CLI entry after last git-clone block 2026-05-11 20:32:34 +09:00
Dr.Lt.DataandGitHub c0d5b4e720 Merge pull request #2813 from RandyHaylor/add-comfyui-claude-cli-vision-text-node
Add ComfyUI Claude CLI (Vision+Text) node
2026-05-11 20:30:07 +09:00
Dr.Lt.Data 72db8aed73 fix(custom-node-list): place oath-studio entries after last git-clone block 2026-05-11 20:29:56 +09:00
Dr.Lt.DataandGitHub 0802a88354 Merge pull request #2725 from wes-kay/adding-nodes
Adding nodes
2026-05-11 20:28:12 +09:00
Dr.Lt.Data 2b7aa428b0 update DB 2026-05-11 20:14:16 +09:00
Dr.Lt.Data d61ef2eaba Merge branch 'main' of github.com:Comfy-Org/ComfyUI-Manager 2026-05-11 20:13:18 +09:00
Dr.Lt.Data fcc429e686 update DB 2026-05-11 20:13:11 +09:00
Dr.Lt.DataandGitHub d255e36ab6 Merge pull request #2862 from machinepainting/add-machinepainting-pack
Add MachinePainting Nodes to custom-node-list.json
2026-05-11 20:12:58 +09:00
Dr.Lt.DataandGitHub 3e5c680f38 Merge pull request #2856 from SOLRICKS/main
Update SOLRICKS node listing
2026-05-11 20:07:31 +09:00
Dr.Lt.Data cba745327f update DB 2026-05-11 20:03:01 +09:00
Dr.Lt.Data 8079db221d update DB
Python Linting / Run Ruff (push) Waiting to run
2026-05-11 04:18:53 +09:00
Dr.Lt.Data 5727dc7c55 update DB 2026-05-11 03:59:36 +09:00
Dr.Lt.Data c06ff81821 fix(custom-node-list): restore xss + Image2Halftone entries and place comfymodeldownloader entry after last git-clone block 2026-05-11 03:58:07 +09:00
Dr.Lt.DataandGitHub 10d48c2670 Merge pull request #2866 from thomaskippster/main
Add ComfyUI Model Downloader Bridge to custom-node-list.json
2026-05-11 03:56:43 +09:00
Dr.Lt.Data b824811bc7 fix(custom-node-list): place BizyAirPlus entry after last git-clone block 2026-05-11 03:56:29 +09:00
Dr.Lt.DataandGitHub f9603624a9 Merge pull request #2803 from hahalfx/add-bizyairplus
Update custom-node-list.json for adding new custom-node BizyAirPlus
2026-05-11 03:55:55 +09:00
Dr.Lt.Data cb17d1c848 fix(custom-node-list): place ComfyUI-Styler-Pipeline entry next to existing andreszs author block 2026-05-11 03:55:40 +09:00
Dr.Lt.DataandGitHub 244421458d Merge pull request #2800 from andreszs/add-styler-pipeline
Add ComfyUI-Styler-Pipeline to custom-node-list.json
2026-05-11 03:55:10 +09:00
Dr.Lt.Data 49218fdb57 fix(custom-node-list): place OpenPose-Studio + Ultralytics-Studio entries next to existing andreszs author block 2026-05-11 03:54:53 +09:00
Dr.Lt.DataandGitHub df1505efda Merge pull request #2798 from andreszs/add-openpose-ultralytics-studio
Add ComfyUI-OpenPose-Studio and ComfyUI-Ultralytics-Studio to custom-node-list.json
2026-05-11 03:54:10 +09:00
Dr.Lt.Data 96ae4e0c9f fix(custom-node-list): place ComfyUI-AnimaTool entry next to existing Moeblack author block 2026-05-11 03:53:55 +09:00
Dr.Lt.DataandGitHub f94902966a Merge pull request #2573 from Moeblack/add-comfyui-animatool
Add ComfyUI-AnimaTool
2026-05-11 03:53:07 +09:00
Dr.Lt.Data b4d5d972de update DB 2026-05-11 03:31:37 +09:00
Dr.Lt.Data 3d7f7d2439 fix(custom-node-list): place ComfyUI-MotifVideo2B entry after last git-clone block 2026-05-11 03:27:55 +09:00
Dr.Lt.DataandGitHub 2ae10236b9 Merge pull request #2829 from MotifTechnologies/feat/add-motifvideo2b
Add ComfyUI-MotifVideo2B to custom-node-list.json
2026-05-11 03:27:22 +09:00
Dr.Lt.Data 6ffead7ba9 fix(custom-node-list): place ComfyUI-Pixaroma entry after last git-clone block 2026-05-11 03:27:08 +09:00
Dr.Lt.DataandGitHub 9a17c96da6 Merge pull request #2821 from pixaroma/add-pixaroma
Add ComfyUI-Pixaroma to custom-node-list.json
2026-05-11 03:26:36 +09:00
Dr.Lt.Data 39dfa902d5 fix(custom-node-list): place Kai Civitai LoRA Loader entry after last git-clone block 2026-05-11 03:26:23 +09:00
Dr.Lt.DataandGitHub e4fadee042 Merge pull request #2814 from kenyonxu/main
Add Kai Civitai LoRA Loader to custom node list
2026-05-11 03:25:49 +09:00
Dr.Lt.Data d9f1ff34a7 fix(custom-node-list): place ComfyUI-SBTools entry after last git-clone block 2026-05-11 03:25:34 +09:00
Dr.Lt.DataandGitHub 9aa2ca09b8 Merge pull request #2806 from Amatsukast/add-sbtools
Add ComfyUI-SBTools
2026-05-11 03:25:01 +09:00
Dr.Lt.Data 81605d31db fix(custom-node-list): remove trailing comma after last entry 2026-05-11 03:24:39 +09:00
Dr.Lt.Data bdd6111d9d fix(custom-node-list): place Z-Fuse entry after last git-clone block 2026-05-11 03:24:08 +09:00
Dr.Lt.DataandGitHub a4b6a589d7 Merge pull request #2440 from destinyfaux/main
Z-Fuse
2026-05-11 03:23:30 +09:00
Dr.Lt.Data 611532491f fix(custom-node-list): place ComfyUI-Workflow-Studio entry next to existing ketle-man author block
Python Linting / Run Ruff (push) Waiting to run
2026-05-11 01:33:43 +09:00
Dr.Lt.DataandGitHub ed8baf3e79 Merge pull request #2706 from ketle-man/add-workflow-studio
Add ComfyUI-Workflow-Studio
2026-05-11 01:33:09 +09:00
Dr.Lt.Data 111e4d38f4 fix(custom-node-list): place ComfyUI-PulseOfMotion entry after last git-clone block 2026-05-11 01:32:43 +09:00
Dr.Lt.DataandGitHub e6693e0b9f Merge pull request #2742 from akashzeno/add-pulse-of-motion
Add ComfyUI-PulseOfMotion to custom node list
2026-05-11 01:32:04 +09:00
Dr.Lt.Data 0c8cdf9aa8 update DB 2026-05-11 01:15:02 +09:00
Dr.Lt.Data 8d2d37f1f7 fix(custom-node-list): place Happyhorse-Wrapper + Account-Manager entries next to existing magicwang1111 author block 2026-05-11 01:03:31 +09:00
Dr.Lt.DataandGitHub bc334c7aa5 Merge pull request #2835 from magicwang1111/codex/register-magicwang-nodes
add  ComfyUI-Happyhorse-Wrapper and ComfyUI-Account-Manager
2026-05-11 01:01:56 +09:00
Dr.Lt.Data 33d4b29422 update DB 2026-05-11 00:52:13 +09:00
Dr.Lt.Data 20d352c2b5 update DB 2026-05-11 00:16:15 +09:00
Thomas 117227e2fc Add ComfyUI Model Downloader Bridge to custom-node-list.json 2026-05-09 20:53:50 +02:00
Dr.Lt.Data 7a8fc44f90 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-05-08 19:07:12 +09:00
wuwukasi 49b038bf69 Add ComfyUI-BodyRatioMapper custom node 2026-05-08 14:42:47 +08:00
MachinePainting 1eda1d65ad Add MachinePainting Nodes to custom-node-list.json 2026-05-07 21:18:28 -07:00
pixaroma 10ff7d091c Update ComfyUI-Pixaroma description to current feature set 2026-05-06 06:53:21 +03:00
Mustafa SahinandGitHub 282025ed0e Remove editor settings from PR 2026-05-06 00:56:09 +03:00
Mustafa SahinandGitHub 2bcc052700 Update SOLRICKS node listing 2026-05-06 00:46:04 +03:00
Mustafa SahinandGitHub 30454742b3 Update MSYNTRIX node listing 2026-05-05 14:12:08 +03:00
ketle-manandGitHub 5162e1d601 Merge branch 'Comfy-Org:main' into main 2026-05-02 21:23:26 +09:00
Dr.Lt.Data 8d5c12037f fix(custom-node-list): place DemonAlone-StyleSelector entry next to existing DemonAlone author block
Python Linting / Run Ruff (push) Has been cancelled
2026-05-01 05:01:14 +09:00
Dr.Lt.DataandGitHub d370403e2d Merge pull request #2827 from DemonAlone/Styler
Add DemonAlone-StyleSelector-ComfyUI  to custom-node-list.json
2026-05-01 05:00:39 +09:00
Dr.Lt.Data e67e35795e fix(custom-node-list): place JH-PixelPro entry after last git-clone block 2026-05-01 05:00:29 +09:00
Dr.Lt.DataandGitHub ccf6d98457 Merge pull request #2809 from jetthuangai/add-comfyui-jh-pixelpro
Add ComfyUI-JH-PixelPro
2026-05-01 04:59:51 +09:00
Dr.Lt.Data 113324728e fix(custom-node-list): place Emiewn-Nodes entry after last git-clone block 2026-05-01 04:59:31 +09:00
Dr.Lt.DataandGitHub f2a3d39e26 Merge pull request #2805 from emiewnn/emiewnnodes
Add ComfyUI-Emiewn-Nodes
2026-05-01 04:58:21 +09:00
Dr.Lt.Data ec72d983ad update DB 2026-05-01 04:45:57 +09:00
Dr.Lt.DataandGitHub a3aabfd72c Merge pull request #2811 from Kyreo/main
Add ComfyUI-CharacterPromptBuffer by Kyreo
2026-05-01 04:42:19 +09:00
Dr.Lt.Data 03272b1f70 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-04-30 02:24:55 +09:00
WarningningandGitHub 9686bae08a Add ComfyUI-MaskToTransparent node entry
A simple mask-based transparency tool for ComfyUI. Turns black mask areas into fully transparent pixels while preserving white areas, with edge feathering support. Perfect for object extraction after semantic segmentation with SAM 3.1 models.

 Features
 Automatically makes black mask areas transparent, preserves original image in white areas
 Customizable Gaussian edge feathering to remove jagged edges
 Supports preserving or overwriting original alpha channel
 Fully compatible with RGB & RGBA images
 Ideal for extracting segmented objects after using SAM 3.1 models, removing unwanted background pixels
2026-04-29 15:38:34 +08:00
magicwang1111 9b8f651061 Register magicwang1111 custom nodes 2026-04-28 18:11:01 +08:00
Dr.Lt.Data 66108ccdbc update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-04-27 05:28:49 +09:00
namemechan d8095b527f Add: ComfyUI-DCW (SNR-t bias correction) 2026-04-26 11:55:29 +09:00
Minsu Ha 80efc5dbc0 Add ComfyUI-MotifVideo2B to custom-node-list.json 2026-04-25 11:03:17 +00:00
DemonAloneandGitHub abf9c654b3 AddDemonAlone-StyleSelector-ComfyUI
to custom-node-list.json
2026-04-24 16:24:32 +03:00
pixaroma 5656002524 Add ComfyUI-Pixaroma to custom-node-list.json 2026-04-22 09:48:32 +03:00
pixaroma 484bb2fe4c Add ComfyUI-Pixaroma to custom-node-list.json 2026-04-22 09:47:16 +03:00
wjluoxiaoandGitHub a129cc6321 style: finalize internationalization for title and description 2026-04-22 06:52:05 +08:00
Dr.Lt.DataandGitHub 491f847bbc fix(security): harden CSRF with Content-Type gate and OpenAPI sync (#2819)
Python Linting / Run Ruff (push) Has been cancelled
Defense-in-depth over GET→POST alone: reject the three CORS-safelisted
simple-form Content-Types (x-www-form-urlencoded, multipart/form-data,
text/plain) on 5 no-body POST handlers (snapshot/save,
manager/queue/{reset,start,update_comfyui}, manager/reboot) to block
<form method=POST> CSRF that bypasses method-only gating. Convert 10 pure
state-changing endpoints (fetch_updates, queue/{update_all,reset,start,
update_comfyui}, snapshot/{remove,restore,save}, comfyui_switch_version,
reboot) from GET to POST and split 5 config endpoints
(db_mode/preview_method/channel_url_list/policy/{component,update}) into
GET(read) + POST(write, JSON body). Emit the in_progress + done event pair
from the /manager/queue/install sync-enable fast-path so client UI
finalizes (previously only queue/start's empty worker done fired, leaving
item.restart unset and the Enable button visible after a successful enable).
Harden js/custom-nodes-manager.js completion path: await onQueueCompleted
with try/catch (surfaces silent turbogrid stale-item throws), replace the
{}.length == 0 no-op empty guard, set install_context before queue/install
to avoid a sync-completion race, wrap classList/updateCell in try/catch.
Resynchronize openapi.yaml with the converted routes (method → post, query
params → requestBody JSON schema, sibling post on 5 split endpoints).
Update 31 JS fetchApi call sites across 7 files; add
tests/test_csrf_content_type_helper.py covering 5 Content-Type cases via
aiohttp TestClient.

Reported-by: XlabAI Team of Tencent Xuanwu Lab
CVSS: 8.1 (AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:H)
2026-04-22 05:04:07 +09:00
kenyonxu 57972a5689 Add Kai Civitai LoRA Loader to custom node list
A ComfyUI custom node that extends the built-in LoRA Loader to fetch and
display Civitai metadata including trigger words and preview images.

Features:
- Trigger words output from Civitai metadata
- Preview images with pagination and NSFW blur
- Local caching with 7-day TTL
- Concurrent request deduplication

Repository: https://github.com/kenyonxu/ComfyUI_KaiUtilities
2026-04-21 15:38:32 +08:00
Randy Haylor b71e239f70 Add ComfyUI Claude CLI (Vision+Text) node 2026-04-21 03:01:04 -04:00
Dr.Lt.Data d57c142019 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-04-21 05:22:40 +09:00
gzsiang 5e475f6883 Merge upstream/main into fix-pr2801 — resolve conflicts 2026-04-20 21:43:17 +08:00
Dr.Lt.Data 6a26409bcb update DB
Python Linting / Run Ruff (push) Waiting to run
2026-04-20 02:54:48 +09:00
Kyreo bbc223a76c Add ComfyUI-CharacterPromptBuffer by Kyreo 2026-04-19 19:35:02 +02:00
Kyreo aba857a350 Revert "Add ComfyUI-CharacterPromptBuffer"
This reverts commit 74c2518894.
2026-04-19 19:31:16 +02:00
Kyreo 74c2518894 Add ComfyUI-CharacterPromptBuffer 2026-04-19 19:30:37 +02:00
jetthuangaiandGitHub 98e0098ddd Add ComfyUI-JH-PixelPro 2026-04-18 13:10:08 +07:00
Amatsukast 23da19ea90 Add ComfyUI-SBTools
Adding ComfyUI-SBTools - workflow toolkit with background removal (BiRefNet), prompt/image variable system for batch generation, and chroma key tools.
2026-04-17 21:25:55 +09:00
emiewnnandGitHub ed56212fa0 Add ComfyUI-Emiewn-Nodes 2026-04-17 02:28:20 +02:00
Dr.Lt.Data 22fc850853 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-04-17 03:08:37 +09:00
Andrés ZsögönandGitHub 15fd6dbe37 Add Lora Pipeline to Manager custom node list (#2799) 2026-04-17 03:07:54 +09:00
Dr.Lt.Data f1863994f4 update DB 2026-04-17 02:49:19 +09:00
PasuandGitHub 7c0fa1c581 Added Mayo Nodes (#2784) 2026-04-17 02:48:50 +09:00
Dr.Lt.Data dcb15d4dc9 update DB 2026-04-17 02:48:04 +09:00
诶-阿伟哥andGitHub 6be5491d9e Add ComfyUI-Prompt-Assistant custom node (#2781)
* Add ComfyUI-Prompt-Assistant custom node

* Add ComfyUI-Prompt-Assistant custom node
2026-04-17 02:47:26 +09:00
Dr.Lt.Data 1e8956f42e update DB 2026-04-17 02:46:36 +09:00
OvertliDSGitHubcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>OvertliDS
d7adab215b Add Overtli Studio Suite to custom-node-list.json (#2777)
Agent-Logs-Url: https://github.com/OvertliDS/ComfyUI-Manager/sessions/f43bab28-2dc5-4853-945c-bdb891b85551

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: OvertliDS <188361908+OvertliDS@users.noreply.github.com>
2026-04-17 02:45:47 +09:00
Lei Fengxiang 6a895c7225 Update custom-node-list.json
added custom-node bizyairplus
2026-04-16 16:29:26 +08:00
gzsiang e6d2e947c3 Add ComfyUI-VolcEngine node to registry 2026-04-16 02:29:59 +08:00
Andrés Zsögön 1e8ebfe3db Add Styler Pipeline to Manager custom node list 2026-04-15 12:12:45 -03:00
Andrés Zsögön c30dd182d3 Add OpenPose Studio and Ultralytics Studio to Manager custom node list 2026-04-15 12:01:57 -03:00
Dr.Lt.Data 15ec9a901b update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-04-14 00:36:37 +09:00
rookiestar28andGitHub e9fd8a3078 Add ComfyUI-RookieUI and ComfyUI-OpenClaw entries (#2786) 2026-04-14 00:33:25 +09:00
Dr.Lt.Data 980f8f58af update DB 2026-04-14 00:27:22 +09:00
9b37dcfe42 Add ComfyUI Batch Blend custom node (#2783)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 00:26:34 +09:00
Dr.Lt.Data 2f1d02b688 update DB 2026-04-13 23:25:19 +09:00
knotttttandGitHub 685dca9749 Add ComfyUI TinyPNG node (#2778) 2026-04-13 23:24:39 +09:00
Dr.Lt.Data 03b54019ff update DB 2026-04-13 23:22:01 +09:00
92a47f59cf Register ComfyUI-Nanobanana (#2779)
Co-authored-by: magicwang1111 <magicwang1111@users.noreply.github.com>
2026-04-13 23:20:25 +09:00
Dr.Lt.Data bae0ac11c5 update DB 2026-04-13 23:00:17 +09:00
Juan TreminioandGitHub f016eadd64 Add ComfyUI-ConnectTheDots (#2775) 2026-04-13 22:59:32 +09:00
Dr.Lt.Data 0f13355b08 update DB 2026-04-13 22:58:48 +09:00
magictutandGitHub bf5efd72ff Add Comfyui-Kling-Wrapper to custom node list (#2773) 2026-04-13 22:57:14 +09:00
Dr.Lt.Data 0fc4649d23 update DB 2026-04-13 22:54:30 +09:00
artyclawandGitHub 3842e70fd9 Add artyclaw-comfy nodes (#2765) 2026-04-13 22:53:17 +09:00
Dr.Lt.Data 0952dd1686 update DB 2026-04-13 21:39:00 +09:00
Dr.Lt.Data a7fb42be01 update DB 2026-04-13 21:23:44 +09:00
Guillaume-127andGitHub ee0f446ad0 Add Post Processing PRO (#2774) 2026-04-13 21:22:17 +09:00
Dr.Lt.Data b220733094 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-04-13 12:58:42 +09:00
Dr.Lt.Data 5079365034 update DB 2026-04-13 12:55:57 +09:00
Yang ZhangandGitHub 454f24145e Add ComfyUI-LLM-Prompt-Tagger to custom-node-list.json (#2771) 2026-04-13 12:55:16 +09:00
Dr.Lt.Data 9d838d302f update DB 2026-04-13 12:54:23 +09:00
MSXYZandGitHub 928cc80088 Added MSXYZ custom node (#2770) 2026-04-13 12:53:28 +09:00
Dr.Lt.Data a54ab14c37 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-04-13 12:48:45 +09:00
zaochuan5854andGitHub ba5aca7122 Add ComfyUI-TensorRT-Reforge (#2767) 2026-04-13 12:47:56 +09:00
Dr.Lt.Data 715c6c2426 update DB 2026-04-13 12:46:54 +09:00
e51a83fc8c Add FLUXNATION fused spike attention CUDA kernel for FLUX.1 (#2766)
Co-authored-by: ULT7RA <ULT7RA@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-13 12:46:16 +09:00
wjluoxiaoandGitHub 1fc7b34f4a Add files via upload 2026-04-13 03:35:02 +08:00
wjluoxiaoandGitHub ba33ebe2dc Delete custom-node-list.json 2026-04-13 03:34:43 +08:00
wjluoxiaoandGitHub d98ca19ef5 Add files via upload 2026-04-13 03:32:02 +08:00
MSXYZ a9f7a6167e Added MSXYZ custom node 2026-04-07 11:19:55 +03:00
Dr.Lt.Data bbafbb1290 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-04-06 01:55:26 +09:00
f08075c04d Add CRT nodes to default channel and fix CRT-Nodes reference (#2759)
- Add CRT-HeartMuLa, ComfyUI-OmniVoice_CRT, ComfyUI-QWEN3_TTS
- Fix CRT-Nodes: restore author/title fields, update plugcrypt -> PGCRT

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 01:49:51 +09:00
Jeremie LouvaertandGitHub 246d715838 Add 6 jeremieLouvaert custom node packs (#2747)
Adding the following node packs to the custom node list:

- ComfyUI-Darkroom: 29-node professional color grading & film emulation suite
- ComfyUI-Gemini-Direct: Direct Google Gemini image generation
- ComfyUI-API-Optimizer: API cost tracking and caching
- ComfyUI-Gemini-Conversation-Canvas: Multi-turn conversational image editing
- comfyui-wireless-link-simple: Wireless data transmission nodes
- ComfyUI-Higgsfield-Direct: Direct Higgsfield API integration

All repos are public, have proper __init__.py with NODE_CLASS_MAPPINGS,
and are published on the Comfy Registry under @akurate.
2026-04-06 01:45:55 +09:00
Dr.Lt.Data 2a55999552 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-04-06 01:04:03 +09:00
Atsushi NonakaandGitHub c7da60732e Add ComfyUI-MediaPreview custom node (#2749)
* Update custom-node-list.json

* Add files via upload

* Delete custom-node-list.json

* Rename custom-node-list.json 3.txt to custom-node-list.json

* Delete custom-node-list.json

* Add files via upload

* Delete custom-node-list.json

* Add files via upload
2026-04-06 01:03:01 +09:00
Dr.Lt.Data d0e3336804 update DB 2026-04-06 01:02:26 +09:00
zinigo-creationsandGitHub 49f294bac5 Add Prompt Builder custom node (#2755) 2026-04-06 00:56:43 +09:00
Dr.Lt.Data 347b13b01a update DB 2026-04-06 00:56:07 +09:00
07c0155929 Added DaSiWa Nodes (#2756)
Co-authored-by: Darksidewalker <you@example.com>
2026-04-06 00:55:31 +09:00
InolandandGitHub 7a0beec47e Update custom-node-list.json (#2760) 2026-04-06 00:52:09 +09:00
Dr.Lt.Data 32c5f26838 update DB 2026-04-06 00:46:36 +09:00
Patrick KasteelandGitHub 0b7669a283 Add ComfyUI-Image-to-Prompt-Abacus.AI- custom node entry (kplkasteel) (#2764) 2026-04-06 00:45:02 +09:00
Dr.Lt.Data a1ade259ef update DB 2026-04-06 00:39:20 +09:00
Dr.Lt.Data 3615af1560 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-04-01 02:24:34 +09:00
Dr.Lt.Data 8aca0751d1 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-03-31 08:50:38 +09:00
Dr.Lt.Data c8a50de72c update DB
Python Linting / Run Ruff (push) Waiting to run
2026-03-31 03:37:20 +09:00
Allan.MandGitHub cbf8068515 Add ComfyUI-XPUSYS-Monitor (#2701) 2026-03-31 03:36:25 +09:00
Dr.Lt.Data 0e0b371417 update DB 2026-03-31 01:45:47 +09:00
4669d4bca9 feat: add ComfyUI VRM Pose Editor 3D (#2744)
* Add ComfyUI 2D Pose Editor node

* feat: add ComfyUI VRM Pose Editor 3D

Interactive 3D pose editor node supporting VRM/GLB/GLTF models.
All Three.js dependencies are bundled locally (no CDN required).

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 01:44:48 +09:00
Dr.Lt.Data 59ed61722b update DB 2026-03-31 01:43:48 +09:00
cozdx1andGitHub 8c14349f97 Add ComfyUI-Dynamic-Sigmas (#2743) 2026-03-31 01:42:21 +09:00
Dr.Lt.Data 14352e2c00 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-03-30 04:32:13 +09:00
Dr.Lt.Data 5a4e346f06 update DB 2026-03-30 04:11:38 +09:00
152bd7b4f7 Add Vega Flow V8.9 — Temporal Stabilisation node (#2739)
* Add Vega Flow V8.9 Temporal Stabilisation node

* Add Vega Flow V8.9 Temporal Stabilisation node

---------

Co-authored-by: Vega Flow <vegaflowltd@users.noreply.github.com>
2026-03-30 04:10:44 +09:00
Dr.Lt.Data d966c4ea67 update DB 2026-03-30 04:03:07 +09:00
846fddccb9 Add ComfyUI-MVNT: AI dance choreography from music (#2723)
Adds MVNT (https://github.com/mvnt-app/ComfyUI-MVNT) to the custom
node list. MVNT generates full-body dance motion from audio using a
diffusion model trained with 100+ professional choreographers.

Nodes: MVNT Generate Dance, MVNT Generate Character, MVNT Export Video,
MVNT Preview BVH, MVNT List Styles, MVNT Estimate Cost, MVNT Load Motion.

Published on Comfy Registry as comfyui-mvnt (publisher: mvnt).

Made-with: Cursor

Co-authored-by: Your Name <your.email@example.com>
2026-03-30 04:02:26 +09:00
Dr.Lt.Data d69dc99d84 update DB 2026-03-30 04:01:18 +09:00
StiboandGitHub 12e6f66d92 Add comfyui-nifty-nodes (#2722) 2026-03-30 04:00:16 +09:00
Dr.Lt.Data 1d19a74ca0 update DB 2026-03-30 03:58:07 +09:00
HalfikChanandGitHub 37cac20c4e Add ComfyUI-Prompt-DB (#2720) 2026-03-30 03:57:19 +09:00
Dr.Lt.Data ad9e73837f update DB 2026-03-30 03:55:12 +09:00
wrabitandGitHub 00e81bb353 Add ComfyUI-ImgSlider (#2715)
* Add ComfyUI-ImgSlider node

* Add ComfyUI-ImgSlider node

* Update description
2026-03-30 03:54:34 +09:00
Dr.Lt.Data 3f5eb4dabb update DB 2026-03-30 03:53:25 +09:00
Krishnan RamachandranandGitHub 5462fe7acc Add ComfyUI Pulse MeshAudit custom node for 3D mesh auditing (#2714) 2026-03-30 03:52:24 +09:00
Akash Chowdhury 7427b1e3d7 Add ComfyUI-PulseOfMotion to custom node list 2026-03-30 00:05:34 +05:30
Dr.Lt.Data 8d750aac72 update DB 2026-03-30 03:26:13 +09:00
NRDXandGitHub d85f035662 Add ComfyUI-BFSNodes (#2713) 2026-03-30 03:25:00 +09:00
Dr.Lt.DataandGitHub 2007eab26d fix(security): add litellm supply chain attack detection and improve pip matching (#2731)
Python Linting / Run Ruff (push) Has been cancelled
- Add litellm==1.82.7 and litellm==1.82.8 to blacklist (PYSEC-2026-2)
- Add ultralytics==8.3.42 to blacklist
- Replace substring matching with exact version set matching
- Remove early break to detect multiple malicious packages
2026-03-26 04:17:46 +09:00
Dr.Lt.Data d870273d4b update DB
Python Linting / Run Ruff (push) Waiting to run
2026-03-26 03:42:39 +09:00
Dr.Lt.Data 448c6b2aab update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-03-23 23:23:32 +09:00
jimpi-devandGitHub b30b7eaa25 add workflowuiplugin to custom node list (#2689) 2026-03-23 23:21:56 +09:00
Dr.Lt.Data 9074ab900d update DB
Python Linting / Run Ruff (push) Waiting to run
2026-03-23 02:43:09 +09:00
dogodg3838andGitHub 6e453a697f Upload large custom-node-list.json (#2698) 2026-03-23 02:40:26 +09:00
Dr.Lt.Data 90e05cd4ef update DB 2026-03-23 02:38:42 +09:00
Jean KássioandGitHub 962e2edb44 Added ComfyUI-AceStep_SFT (#2697) 2026-03-23 02:38:03 +09:00
Dr.Lt.Data 0d15786d5b update DB 2026-03-23 02:29:56 +09:00
CownekoandGitHub fe5663a60f Add CWK Prompt Composer node (#2672) 2026-03-23 02:28:56 +09:00
Dr.Lt.Data dd58ac8b99 update DB 2026-03-23 02:23:20 +09:00
t a2aace6a23 Adding comfy vllm, and comfy lora iterator nodes to custom node list 2026-03-21 23:12:30 -06:00
ketle-man 0e9c0baef9 Add ComfyUI-Workflow-Studio to custom node list
Rebase on latest main to resolve merge conflict.
2026-03-21 12:36:19 +09:00
t b770f27099 Adding 'comfy-vllm' custom node, title update 2026-03-20 15:02:05 -06:00
t 96ffcbd0be Adding 'comfy-vllm' custom node 2026-03-20 15:00:16 -06:00
Dr.Lt.Data be5e019ec5 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-03-21 00:46:04 +09:00
ketle-manandGitHub 04e12f4b3b Merge branch 'Comfy-Org:main' into main 2026-03-18 22:13:29 +09:00
Dr.Lt.Data c94236a614 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-03-16 04:38:03 +09:00
Dr.Lt.Data e637943e72 update DB 2026-03-16 04:32:51 +09:00
6d13fd025e Add ComfyUI-ACES-IO by BISAM20 (#2676)
Co-authored-by: BISAM20 <bishoy@bisam20.com>
Co-authored-by: BISAM20 <bisam20@github.com>
2026-03-16 04:31:37 +09:00
Dr.Lt.Data 4787204733 update DB 2026-03-16 04:24:15 +09:00
Phạm HưngandGitHub 7dd05e822f New update node SDVN_Segment_Anything (#2671)
* Update custom-node-list.json | Update description for SDVN_Custom_node

Update description for SDVN_Custom_node

* Update custom-node-list.json
2026-03-16 04:23:10 +09:00
huchukatoandGitHub b1a268501d Add 3 new nodes by huchukato: RIFE-TensorRT-Auto, Upscaler-TensorRT-Auto, HuggingFace (#2670)
- ComfyUI-RIFE-TensorRT-Auto: Ultra fast frame interpolation with automatic TensorRT optimization
- ComfyUI-Upscaler-TensorRT-Auto: 2-4x faster image upscaling with TensorRT
- ComfyUI-HuggingFace: Advanced HuggingFace model downloader with search functionality

All nodes feature automatic installation, enhanced performance, and improved stability over original implementations.
2026-03-16 04:20:34 +09:00
Dr.Lt.Data 0187da4672 update DB 2026-03-16 04:17:30 +09:00
NeflowandGitHub ef27449962 Add booru tagger node. (#2633) 2026-03-16 04:16:11 +09:00
Dr.Lt.Data 03a93ac9ff update DB 2026-03-16 03:04:22 +09:00
Senjin the DragonandGitHub 6ecb39b409 Add ComfyUI Gender Tag Filter node suite by senjinthedragon (#2692)
* Add ComfyUI Gender Tag Filter by senjinthedragon

* Add install_type: git to satisfy manager loader

* Add files array to satisfy json-checker validation

* Update install_type to git-clone for scanner compatibility
2026-03-16 03:03:45 +09:00
Dr.Lt.Data 31e9d281d5 update DB 2026-03-16 03:02:35 +09:00
RimorandGitHub 7198541a34 Add Diana interactive assistant node (#2690) 2026-03-16 03:02:00 +09:00
Dr.Lt.Data e21017ad0b update DB 2026-03-16 03:01:06 +09:00
ketle-manandGitHub 83e07578fd Add ComfyUI 2D Pose Editor node (#2691) 2026-03-16 02:59:45 +09:00
Dr.Lt.Data ad71567e8a update DB 2026-03-16 02:59:29 +09:00
23539a02a0 Add ComfyUI Qwen-VL LoRA node by Dangocan (#2683)
Co-authored-by: Dangocan <dangocan@users.noreply.github.com>
2026-03-16 02:51:57 +09:00
Dr.Lt.Data ac4cf1ce40 update DB 2026-03-16 02:47:37 +09:00
George JiangandGitHub b8d7721e00 Add files via upload (#2679)
feat: add comfyui-save-image-no-meta node
2026-03-16 02:45:57 +09:00
ketle-manandGitHub 23056a0eb8 Add ComfyUI 2D Pose Editor node 2026-03-14 22:45:52 +09:00
Dr.Lt.Data a801227663 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-03-13 12:58:34 +09:00
Dr.Lt.Data 8ffbde9fa8 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-03-10 23:06:07 +09:00
Dr.Lt.Data a87171916d update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-03-09 12:46:48 +09:00
Dr.Lt.Data 008e6ede56 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-03-05 07:57:56 +09:00
Dr.Lt.Data f7d1bba7f0 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-03-05 00:36:56 +09:00
Dr.Lt.Data dc0861b603 update DB 2026-03-05 00:32:55 +09:00
Dr.Lt.Data 7bc06a07fa update DB 2026-03-05 00:31:03 +09:00
zuoyuting214andGitHub 4dd60197c8 Add chanjingAI-ComfyUI (#2645) 2026-03-05 00:30:04 +09:00
Dr.Lt.Data 03d496d80d update DB 2026-03-05 00:17:14 +09:00
Dr.Lt.Data ffc5f03ed0 update DB 2026-03-05 00:10:50 +09:00
Waseem AnjumandGitHub 9d427effcb Add CivitAI Gallery Explorer (#2664) 2026-03-05 00:10:03 +09:00
Dr.Lt.Data f4ba539d62 update DB 2026-03-05 00:08:37 +09:00
Jason HokuandGitHub 65ab25f0d4 Add comfyui-lite-text-to-file-tools (#2642) 2026-03-05 00:07:43 +09:00
Dr.Lt.Data 83edc3d787 update DB 2026-03-05 00:06:28 +09:00
Dr.Lt.Data 001adf19c9 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-03-04 01:33:23 +09:00
Dr.Lt.Data d2459128db update DB
Python Linting / Run Ruff (push) Waiting to run
2026-03-04 00:59:25 +09:00
Dr.Lt.Data 3ad3f0df39 update DB 2026-03-04 00:38:20 +09:00
yinandGitHub 535d288bf1 Add ComfyUI-Lux3D (#2650) 2026-03-04 00:36:55 +09:00
Dr.Lt.Data 05076e56db update DB
Python Linting / Run Ruff (push) Waiting to run
2026-03-03 07:07:46 +09:00
Dr.Lt.Data 6f887b012c update DB 2026-03-03 07:03:17 +09:00
Nichlas SchipperandGitHub eabd5ed9cd Added entry for ComfyUI-OpenImage to custom node list (#2653) 2026-03-03 07:02:29 +09:00
Dr.Lt.Data 6079647938 update DB 2026-03-03 06:59:41 +09:00
Dr.Lt.Data effde1667b update DB 2026-03-03 06:35:14 +09:00
Halil BeycanandGitHub 832344ca20 new node ComfyUI-ImageQualityGate (#2661) 2026-03-03 06:33:11 +09:00
Dr.Lt.Data 25f684c06f update DB 2026-03-03 06:06:14 +09:00
SamSeenandGitHub 5ce422bcfa Update author name and repository URLs for SamSeen custom nodes (#2662) 2026-03-03 06:01:52 +09:00
Dr.Lt.Data 2f66249f51 update DB 2026-03-03 05:56:17 +09:00
Dr.Lt.Data fb90ad9e34 update DB 2026-03-03 05:53:03 +09:00
Rohit MahtoandGitHub 8be1f1494c Add Champdev Custom Nodes (#2654)
* Add Chamdev Custom Nodes

* fix review comments
2026-03-03 05:50:57 +09:00
Dr.Lt.Data 613d2bd648 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-03-02 11:04:04 +09:00
Dr.Lt.Data d7aad369a4 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-02-27 02:12:24 +09:00
Xz3r0andGitHub 2030bb2428 update node description (#2644) 2026-02-27 01:57:40 +09:00
Dr.Lt.Data e4657b2b0f update DB 2026-02-27 01:51:51 +09:00
Pondowner857andGitHub ea3313c429 Add comfyui-workflow-protector to custom node list (#2641) 2026-02-27 01:50:51 +09:00
PxTicksandGitHub 0219872906 Fix handleFile monkeypatch for new frontend signature (#2640)
handleFile signature is updated in ComfyUI frontend [handleFile (file: File, openSource?: WorkflowOpenSource, options?: { deferWarnings?: boolean } )]. Using rest parameters to fix and future-proof.
2026-02-27 01:49:20 +09:00
Dr.Lt.Data 60114405c1 update DB 2026-02-27 01:47:55 +09:00
AnonBOTplandGitHub 00d58a213f Update: Add metadata for Qwen Prompt Expander (#2639)
* Delete custom-node-list.json

* Add files via upload

* Delete custom-node-list.json

* Add files via upload

* Delete custom-node-list.json

* Add files via upload
2026-02-27 01:44:14 +09:00
samy kamkarandGitHub 7fba0abf02 update nvidia urls (#2635) 2026-02-27 01:37:57 +09:00
Dr.Lt.Data c15f4a6117 update DB 2026-02-27 01:37:15 +09:00
TripleHeadedMonkeyandGitHub 1b935e439c Add files via upload (#2634) 2026-02-27 01:36:28 +09:00
Dr.Lt.Data 5df7c9fbae update DB 2026-02-27 01:26:04 +09:00
mr-septemberandGitHub 285bec4f61 Update custom-node-list.json (#2625) 2026-02-27 01:25:09 +09:00
Dr.Lt.Data 971d5a2ad1 update DB 2026-02-27 01:23:36 +09:00
Dr.Lt.Data 96fc7a9bdf update DB 2026-02-27 01:20:06 +09:00
ddeca90708 Add ComfyUI Image Browser by Flibens to custom node list (#2622)
Co-authored-by: Flibens <195953152+Flibens@users.noreply.github.com>
2026-02-27 01:19:22 +09:00
Dr.Lt.Data 299f7d9c74 update DB 2026-02-27 01:14:18 +09:00
Dr.Lt.Data 93e6ff5168 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-02-26 00:26:34 +09:00
Dr.Lt.Data 8ec120e964 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-02-25 12:56:25 +09:00
Dr.Lt.Data 03befe1ac7 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-02-24 08:20:09 +09:00
Dr.Lt.Data c88a9985c1 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-02-24 02:22:17 +09:00
AnonBOTplandGitHub 530f628523 ComfyUI Qwen Prompt Expander (#2620)
* Delete custom-node-list.json

* Add files via upload
2026-02-24 02:20:34 +09:00
Dr.Lt.Data 5919b79752 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-02-23 12:56:09 +09:00
Dr.Lt.Data eb12e015d5 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-02-21 03:49:07 +09:00
Dr.Lt.DataandClaude Sonnet 4.6 3409ad6c3e refactor(scanner): apply KISS/YAGNI/DRY to git error collection
DRY: merge _GIT_ERROR_PATTERNS + _CATEGORY_LABELS into single
_GIT_ERROR_CATEGORIES list of (key, label, regex) 3-tuples.
Adding a category now requires editing one place only.

KISS: replace _GIT_ERROR_COLLECTOR {'lock':..., 'by_category':...}
dict wrapper with two plain module variables _git_error_lock and
_git_errors. Removes string-key access throughout.

YAGNI: remove 200-char message truncation from _record_git_error.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 12:14:17 +09:00
Dr.Lt.DataandClaude Sonnet 4.6 3c24614aeb feat(scanner): collect and report git errors by category
Errors during git clone/pull are now accumulated in a thread-safe
collector and printed as a grouped summary after all operations
complete, instead of being scattered in per-repo log lines.

Error categories: repository_not_found, divergent_branch, auth_failed,
network_error, merge_conflict, permission_denied, other.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 09:27:13 +09:00
Dr.Lt.Data f41365abe9 update DB 2026-02-17 23:40:42 +09:00
huchukatoandGitHub b5defe2a38 Add ComfyUI-TagComplete custom node to list (#2611)
- Tag completion with a1111-sd-webui-tagcomplete style wildcard sub-selection
- Support for CSV files and multiple wildcard sources
- Smart parsing and text overflow handling
- Full compatibility with existing wildcard files
- v2.0.0 with major wildcard workflow improvements
- Category: utility with comprehensive tag and autocomplete features
2026-02-17 23:39:49 +09:00
Dr.Lt.Data ebb354a201 update DB 2026-02-17 10:56:20 +09:00
A神andGitHub b181307098 Add new image processing nodes to custom-node-list (#2472) 2026-02-17 10:54:51 +09:00
Dr.Lt.Data e76457021c update DB 2026-02-17 10:45:38 +09:00
TK3RandGitHub 3cda7f602e added ComfyUI_TK3R_Ext to custom nodes list (#2614) 2026-02-17 10:44:23 +09:00
Dr.Lt.Data d11db7e48c update DB 2026-02-17 10:40:08 +09:00
Benjamin GreggandGitHub 4d22ed9779 Add PBRFusion4 Node (#2609) 2026-02-17 10:39:12 +09:00
DemonAloneandGitHub 51edfa6194 rename repo and fix URL (#2607) 2026-02-17 10:37:40 +09:00
Dr.Lt.Data 5acd638ead update DB 2026-02-17 10:36:45 +09:00
Jacobus MeulenandGitHub fd8f1242e3 Add draggen node to comfy ui manager (#2604) 2026-02-17 10:35:32 +09:00
48acce370a Update va1: add ImageMaskComparer node (#2603)
- Add ImageMaskComparer to extension-node-map.json

- Update description in custom-node-list.json to cover both nodes

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-17 10:31:19 +09:00
Dr.Lt.Data db97557ba9 update DB 2026-02-17 09:54:08 +09:00
b66777aa0c Add comfyui majoor assetsmanager (#2600)
* Add Majoor File Manager node to custom-node-list

Added a new node for Majoor File Manager with relevant details.

* Rename and update Majoor File Manager to Asset Manager

* Fix typos in Majoor Assets Manager entry

* Add 'id' field to Majoor Assets Manager entry

* Update custom-node-list.json

* Add ComfyUI-Majoor-AssetsManager node to custom-node-list.json

* Set ComfyUI-Majoor-AssetsManager node id to majoor-assetsmanager

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
Co-authored-by: Your Name <you@example.com>
2026-02-17 09:52:42 +09:00
Dr.Lt.Data 77377eeddb update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-02-13 12:58:53 +09:00
huchukatoandGitHub bac31f1dc3 Add QwenVL-Mod custom node to list (#2590)
* Add QwenVL-Mod custom node to list

- Enhanced QwenVL node with Flash Attention 2
- WAN 2.2 video generation support
- Free abliterated models
- Comprehensive NSFW support
- Advanced fork with major improvements

* Update QwenVL-Mod to v2.0.7 with latest features

- Updated version from 2.0.4 to 2.0.7
- Enhanced description with Smart Prompt Caching and Fixed Seed Mode
- Updated tags to include smart_caching, prompt_caching, fixed_seed, cinematography
- Revolutionary performance improvements with instant cached responses

* Update QwenVL-Mod to v2.0.8 with latest stable release

- Updated version from 2.0.7 to 2.0.8
- Enhanced description with multilingual support and visual style detection
- Added new tags: multilingual, style_detection, stable
- Includes bug fixes and stability improvements
- All WAN 2.2 presets now support any language with style detection
2026-02-13 12:57:41 +09:00
Dr.Lt.Data ae38dc9239 update DB 2026-02-13 12:55:45 +09:00
Dr.Lt.Data 4f4172bf5e update DB 2026-02-13 12:53:36 +09:00
HmilyandGitHub 8742cc247b feat: add ComfyUI-FitDiTx nodes to the custom node list (#2582) 2026-02-13 12:49:47 +09:00
3c787f1fd2 Add ComfyUI-Model-Downloader custom node (#2578)
* Add ComfyUI-Model-Downloader extension details

* Fix JSON formatting in custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2026-02-13 12:38:44 +09:00
Dr.Lt.Data 438f26a3a2 update DB 2026-02-13 12:33:07 +09:00
sworksteamandGitHub 9ad417cb69 added SEngine (#2570) 2026-02-13 12:31:42 +09:00
Dr.Lt.Data c1869f4d1d update DB
Python Linting / Run Ruff (push) Waiting to run
2026-02-12 12:44:07 +09:00
Dr.Lt.Data 1780916806 update DB 2026-02-12 12:42:07 +09:00
6d8d00e7f3 Add PromptFlow custom node (#2583)
Co-authored-by: Maarten Harms <maartenharms@users.noreply.github.com>
2026-02-12 12:41:19 +09:00
Dr.Lt.Data 6097f88462 update DB 2026-02-12 12:39:53 +09:00
Moser9815andGitHub 40787eface Add ComfyUI-RMAutomation to custom node list (#2580)
Automation nodes for ComfyUI including:
- RM Styles Full/Pipe: Load prompts from JSON with random/increment/decrement modes
- Power LoRA Loader: Advanced LoRA loading with random weight support
- Image fallback, mask gate, latent nodes, and video combine

Repository: https://github.com/Moser9815/ComfyUI-RMAutomation
2026-02-12 12:38:45 +09:00
JuggernautandGitHub 2757728935 Merge branch 'Comfy-Org:main' into main 2026-02-09 16:53:01 +05:30
Dr.Lt.Data 7b3f032e77 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-02-09 12:58:09 +09:00
Dr.Lt.Data e9020377ff update DB 2026-02-09 12:51:00 +09:00
7610b72825 Add Custom Node: ComfyUI-Meld (#2566)
Co-authored-by: HappyOnigiri <253838257+NodeMeld@users.noreply.github.com>
2026-02-09 12:50:22 +09:00
Dr.Lt.Data 76b876f4c8 update DB 2026-02-09 12:49:10 +09:00
4235ca33fe Add ComfyUI Prompt Saver custom node (#2558)
Adds a new node that saves images with formatted prompt reference cards
for easy browsing. Features include:
- Side-by-side thumbnail + prompt display
- JSON syntax highlighting
- Auto font sizing to fit content
- Multiple output formats (full image, reference card, JSON metadata)

Repository: https://github.com/robomello/comfyui-prompt-saver

Co-authored-by: robomello <robomello@users.noreply.github.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-09 12:48:28 +09:00
Dr.Lt.Data e5a10245f4 update DB 2026-02-09 12:46:35 +09:00
BoredCoderandGitHub 90ad0bc926 Add files via upload (#2552) 2026-02-09 12:45:56 +09:00
Dr.Lt.Data 755a6f3c24 update DB 2026-02-09 12:44:59 +09:00
VyacheslavDandGitHub 37027055f7 Update custom-node-list.json (#2542)
dva-qwen-tts
2026-02-09 12:44:00 +09:00
Dr.Lt.Data 08cd0c4e27 update DB 2026-02-09 12:36:58 +09:00
774482cc00 Update custom-node-list.json (#2527)
* Update custom-node-list.json

* Refactor custom-node-list.json to consolidate entries

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2026-02-09 12:35:39 +09:00
JuggernautandGitHub bb46126fbd Merge branch 'Comfy-Org:main' into main 2026-02-06 12:31:48 +05:30
Dr.Lt.Data 4faf4e05e3 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-02-06 12:41:16 +09:00
Brian BlankenshipandGitHub 404dde76c9 Modified name of Pausable Sampler to FreezeFrame for consistency. (#2584) 2026-02-06 12:33:07 +09:00
akawanaandGitHub a58abc29d5 AK Pack nodes description update (#2575) 2026-02-06 12:30:05 +09:00
Dr.Lt.Data 4ea05462e4 update DB 2026-02-06 12:26:26 +09:00
Satrio DewantonoandGitHub 492f7eda6a add reference chain custom node entry (#2568) 2026-02-06 12:25:53 +09:00
Dr.Lt.Data e04224a2bd update DB 2026-02-06 12:23:17 +09:00
971af87529 Add HALO Debug Pack - AMD ROCm bf16 diagnostic tools (#2562)
Adds diagnostic nodes for debugging black image issues caused by
numpy's lack of bfloat16 support on AMD ROCm, older NVIDIA cards,
Apple Silicon, and unified memory systems.

Nodes included:
- HALO VAE Decode/Encode (FP32) - forces fp32 to fix bf16 conversion
- HALO Latent Debug - check sampler output for NaN
- HALO Conditioning Debug - check text encoder for NaN
- HALO Model Debug - inspect model dtype

Repository: https://github.com/bkpaine1/halo_pack

Co-authored-by: bkpaine1 <bkpaine1@github.com>
2026-02-06 12:22:31 +09:00
Dr.Lt.Data 920d70a26a update DB 2026-02-06 12:20:24 +09:00
Nick deLannoyandGitHub 45770ac2cb add comfyui-mobile-frontend (#2560) 2026-02-06 12:19:24 +09:00
Dr.Lt.Data 544ab8328c update DB
Python Linting / Run Ruff (push) Waiting to run
2026-02-05 12:47:06 +09:00
Dr.Lt.Data b06a85e1c2 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-02-05 09:12:41 +09:00
3cad4eb209 Update: description and nodename_pattern for LoraHelper (#2553)
* Update custom-node-list.json

* Update custom-node-list.json

* Modify LoraHelper Chat Nodes entry in custom-node-list

Updated reference URL and install type for LoraHelper Chat Nodes.

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2026-02-05 09:11:55 +09:00
Dr.Lt.Data c5c0749f85 update DB 2026-02-05 09:10:51 +09:00
FearL0rdandGitHub c7711eb574 Add ComfyUI Any-Device Offload custom node (#2551)
Added a new custom node for ComfyUI Any-Device Offload with detailed description and installation type.
2026-02-05 09:09:41 +09:00
Nakano Kenji 794161d81d Add ComfyUI-AnimaTool 2026-02-03 16:33:13 +08:00
Dr.Lt.Data bafe5004ca update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-02-03 07:35:37 +09:00
Dr.Lt.Data 2944e5ef50 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-02-02 12:58:12 +09:00
Dr.Lt.Data df1eaff802 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-01-29 02:22:55 +09:00
Dr.Lt.Data cf5dfbf99d update DB
Python Linting / Run Ruff (push) Waiting to run
2026-01-28 18:29:30 +09:00
Dr.Lt.Data cd0b48de5a update DB
Python Linting / Run Ruff (push) Waiting to run
2026-01-28 01:56:59 +09:00
698ac53b0c Add LoraHelper Nodes (#2520)
* Update custom-node-list.json

* Update custom-node-list.json

* Modify LoraHelper Chat Nodes entry in custom-node-list

Updated reference URL and install type for LoraHelper Chat Nodes.

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2026-01-28 01:56:05 +09:00
Dr.Lt.Data 2a126e0873 update DB 2026-01-28 00:13:42 +09:00
SQandGitHub e3c9fea279 Add ComfyUI Sequential Image Loader (SQ-ImageLoader) (#2549) 2026-01-28 00:12:27 +09:00
Dr.Lt.Data 07a570d1f2 update DB 2026-01-28 00:09:02 +09:00
gmorksandGitHub eec9d9c04f Add ComfyUI-WatchPoint node to custom-node-list (#2541)
Watch Point - Multi-monitor preview node
   
   Features:
   - External monitor preview with zoom/pan
   - Floating in-browser preview
   - Signal Scout for prompt monitoring
   - Stable threading (no crashes)
2026-01-28 00:07:45 +09:00
Dr.Lt.Data 52564c3ad5 update DB 2026-01-28 00:06:40 +09:00
45ce612ac1 Add ComfyUI-Qwen-Canvas node to custom node list (#2547)
* Add ComfyUI-Qwen-Canvas node to custom node list

* Update description for ComfyUI-Qwen-Canvas

* Update description for ComfyUI-Qwen-Canvas

* Add ComfyUI-Qwen-Canvas node to custom-node-list

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2026-01-28 00:05:42 +09:00
Dr.Lt.Data 620d9fd387 update DB 2026-01-28 00:01:40 +09:00
hetimaandGitHub e1c84570ea Update custom-node-list.json (#2545)
add ComfyUI-SingleLinePicker custom node
2026-01-28 00:00:32 +09:00
ArtsticHandGitHub 1bfaaf5d91 Update the main info for my plugin ArtsticH/ComfyUI_EasyKitHT_NodeAlignPro.(Node2.0-based alignment — innovative first support) (#2543)
Python Linting / Run Ruff (push) Waiting to run
2026-01-27 23:32:06 +09:00
Dr.Lt.Data 413c3b3855 update DB 2026-01-27 23:25:15 +09:00
Chen MingyiandGitHub 2eae6faf46 Add ComfyUI DFloat11 Extended custom node (#2528)
Added a new custom node for ComfyUI DFloat11 Extended with installation instructions and a detailed description.
2026-01-27 23:23:50 +09:00
Dr.Lt.Data b320c47e45 update DB 2026-01-27 23:05:54 +09:00
Brian BlankenshipandGitHub d57b0d7c4a Added ComfyUI-Pause to custom_node_list.json (#2536)
* Added ComfyUI-Pause to list

* chore: changed name of the nodepack
2026-01-27 23:04:29 +09:00
Fred N. GarvinandGitHub 4df0628a12 feat: add AutoModelDownloader (fixed diff) (#2495) 2026-01-27 22:50:50 +09:00
Dr.Lt.Data d8e3f531c7 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-01-24 19:04:17 +09:00
Dr.Lt.Data 40829b059a update DB 2026-01-24 18:48:51 +09:00
Dr.Lt.Data 6c48c98900 update DB 2026-01-24 18:42:17 +09:00
934132e922 Add Audio Enhance and Normalize tools (#2537)
* Add Audio Enhance and Normalize tools

* Update ComfyUI-AudioTools node details

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2026-01-24 18:38:15 +09:00
Dr.Lt.Data d2bb0c3b36 update DB 2026-01-24 18:16:01 +09:00
room3devandGitHub f57cc56285 Add ComfyUI-I2VChainHelper node to custom-node-list (#2535)
Added a new node for ComfyUI-I2VChainHelper that assists with image batch processing.
2026-01-24 18:15:13 +09:00
Dr.Lt.Data 63522345cb update DB 2026-01-24 18:14:51 +09:00
wangyff-codeandGitHub 787457bf23 Update custom-node-list.json (#2534)
ADD ComfyUI custom node that converts images into Pixel Art using ONNX models.
2026-01-24 18:12:57 +09:00
Dr.Lt.Data d13837ac5c update DB 2026-01-24 18:09:01 +09:00
loficodingandGitHub c57ee0c6e8 Update custom-node-list.json (#2531)
Added ComfyUI Node HUD extension by loficoding
2026-01-24 18:06:53 +09:00
ArthemyandGitHub 53d924014d Update custom-node-list.json (#2529)
Hey there! I've added a new custom node for Z-image and I'd love to share that too on ComfyUI-Manager! <3
2026-01-24 18:03:47 +09:00
Dr.Lt.Data fcf7581431 update DB 2026-01-24 17:40:20 +09:00
CrateToolsandGitHub 53073a6015 Add Preview Video Monitor Pro (#2499) 2026-01-24 17:38:16 +09:00
Dr.Lt.Data c4fefd4721 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-01-20 23:32:01 +09:00
pixelpainterandGitHub d86f6196d0 Update custom-node-list.json (#2509) 2026-01-20 23:30:34 +09:00
Dr.Lt.Data aa4e7b553f update DB 2026-01-20 22:59:03 +09:00
Dr.Lt.Data 389ca64bd9 update DB 2026-01-20 22:52:42 +09:00
mickmumpitzandGitHub f02e069d93 Update custom-node-list.json (#2521) 2026-01-20 22:45:15 +09:00
LiChunlinandGitHub 8627ac2f4c Comfyui-Image-Concat update to v1.1 (#2519)
1. Add 1 optional IMAGES input port named a0_image.
2. Add 2 new bitmap stitching modes: equal-width vertical stitching (top to bottom), equal-height horizontal stitching (left to right).
3. Add the function of displaying image filenames on the stitched tiles, with customizable text colors (16 colors).
4. Optimized the tile saving mode, including three options: none/save single block/save single image.
5. Simply connect to the b6 port to view the usage help for this node.
6. Standardized the port naming on the node page.
2026-01-20 22:23:51 +09:00
Dr.Lt.Data 656ec027db update DB 2026-01-20 22:20:17 +09:00
room3devandGitHub 83f63001e0 Add ComfyUI-PixelForge node to custom-node-list (#2525)
Adding the new PixelForge custom nodes: Resolution Selector and Resize Image.
2026-01-20 22:14:03 +09:00
Dr.Lt.Data fd07a47db6 update DB 2026-01-20 22:00:46 +09:00
ArthemyandGitHub 701d848fc4 Update custom-node-list.json (#2518) 2026-01-20 21:59:20 +09:00
LiChunlinandGitHub 0aa95ef88f Enhance function for image concatenation tool (#2517)
Updated description to include new features in V1.1.
-  Added 2 new bitmap stitching modes: equal-width vertical stitching (top to bottom), equal-height horizontal stitching (left to right)
-  Optimized the tile saving mode, including three options: none/save single block/save single image
-  Added the function of displaying image filenames on the stitched tiles, with customizable text colors (16 colors)
-  Simply connect to the b6 port to view the usage help for this node
- 🎨 Standardized the port naming on the node page
2026-01-20 21:58:33 +09:00
Dr.Lt.Data e4eb87cc38 update DB 2026-01-20 21:57:54 +09:00
ca8c925fb9 Add Halo-Lipsy: AMD unified memory lip sync node (#2514)
Halo-Lipsy is a native Wav2Lip implementation for ComfyUI that finally works
on AMD APUs with unified memory (Strix Halo, etc.).

Key features:
- No subprocess hacks - runs natively in ComfyUI process
- Face detection on CPU to prevent memory conflicts
- Safe tensor casting for unified memory compatibility
- Sync tuning and edge blending options
- Works on ROCm, AMD APUs, and NVIDIA

Tested on Ryzen AI Max+ 395 (Strix Halo) with 64GB unified memory and ROCm 7.11.

Repository: https://github.com/bkpaine1/Halo-Lipsy

Co-authored-by: bkpaine1 <bkpaine1@users.noreply.github.com>
2026-01-20 21:56:48 +09:00
Dr.Lt.Data 0edd6607ae update DB 2026-01-20 21:55:07 +09:00
Aditya MundhaliaandGitHub 44aa47126e Add ComfyUI-ollama-aditya node (#2498) 2026-01-20 21:54:19 +09:00
Dr.Lt.Data 2f40e125be update DB 2026-01-20 21:53:40 +09:00
Dr.Lt.Data e6a2ae829c update DB 2026-01-20 21:34:04 +09:00
Evgeniy AndriyanoffandGitHub 9390270879 Add ComfyUI Remote Upscale node (#2500) 2026-01-20 21:32:31 +09:00
Dr.Lt.Data 74d1e9d296 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-01-19 19:34:31 +09:00
Dr.Lt.Data 8138954fc6 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-01-19 01:59:09 +09:00
ashish af1c698117 dependency analysis 2026-01-18 16:54:22 +05:30
Dr.Lt.Data e8e0e884f2 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-01-15 22:01:15 +09:00
loz2754andGitHub 379fad3809 Add AUN ComfyUI Nodes to custom node list (#2494) 2026-01-15 21:33:48 +09:00
Dr.Lt.Data bb0ef5bdc3 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-01-15 12:54:22 +09:00
Dr.Lt.Data 9617b0e56e update DB 2026-01-14 01:38:25 +09:00
Dr.Lt.Data f20f8549e6 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-01-14 00:45:38 +09:00
sinanzoo2ndandGitHub 82135ab168 Add ComfyUI-Seed-Wildcard-Pack (#2497) 2026-01-14 00:14:47 +09:00
Dr.Lt.Data 2cb7f021e9 update DB 2026-01-13 18:51:07 +09:00
Dr.Lt.Data 29a59595b9 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-01-13 12:59:33 +09:00
Dr.Lt.Data 58a1051c46 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-01-13 07:32:02 +09:00
Dr.Lt.Data 53bec8be40 update DB 2026-01-13 07:29:47 +09:00
zwaiganiandGitHub 162f25f570 Add ComfyUI-VRAM-watcher node with details (#2493) 2026-01-13 07:28:10 +09:00
Dr.Lt.Data 5d0112f768 update DB
Python Linting / Run Ruff (push) Waiting to run
2026-01-12 01:59:22 +09:00
Jason HokuandGitHub 695d87cc0a Add Ultimate Auto Sampler Suite (#2471) 2026-01-12 01:39:54 +09:00
Dr.Lt.Data f2ba36ec3a update DB 2026-01-12 01:12:58 +09:00
6b83a5a16e Add ComfyUI_FMJ_SaveImageVersions node entry (#2475)
* Add ComfyUI_FMJ_SaveImageVersions node entry

Save images with full metadata (prompt, workflow, software versions, commit)

This repository provides ComfyUI nodes to:
- Save PNG images with embedded metadata (positive / negative prompts and other JSON fields),
- Generate and copy a snapshot describing the environment (commits, versions, GPU, etc.),
- Load an image and restore information from its metadata.

Important: this project runs a local script `snapshot.py` to capture the state of ComfyUI and custom nodes. Only run it in trusted environments.

* Update custom-node-list.json

---------

Co-authored-by: Dr.Lt.Data <128333288+ltdrdata@users.noreply.github.com>
2026-01-12 01:11:35 +09:00
Dr.Lt.Data 10aff14af5 update DB 2026-01-12 01:06:25 +09:00
LiChunlinandGitHub 7634a08073 Add Comfyui-Image-Concat custom node (#2480)
Added a new custom node for ComfyUI that supports image concatenation with advanced features.
2026-01-12 01:05:32 +09:00
Dr.Lt.Data 92cf872f1b update DB 2026-01-12 00:55:47 +09:00
vuhung3990andGitHub c916174499 Add NSFW Guard node to custom-node-list.json (#2489)
Added NSFW Guard node for ComfyUI with automatic detection.
2026-01-12 00:51:05 +09:00
Amit PintzandGitHub 4ad8735c89 Add LTX-2 models. (#2490) 2026-01-12 00:49:45 +09:00
Dr.Lt.Data 2416aa2fc9 update DB
Python Linting / Run Ruff (push) Has been cancelled
2026-01-09 12:53:58 +09:00
Dr.Lt.DataandGitHub b2d6a4eefe Update custom-node-list.json 2025-12-28 08:09:06 +09:00
destinyfauxandGitHub 216e8586bc Update custom-node-list.json
Fixed indentation issue caused in my prior Z-Image Z-Fuse Node.
2025-12-27 12:31:58 -07:00
destinyfauxandGitHub 0cbbe25d89 Update custom-node-list.json
Adding my custom lora nodes for Z-Image. Combines and merges loras non-destructively to enable the usage of many loras without layer saturation.
2025-12-27 12:29:38 -07:00
millerlightandGitHub a471263602 Merge branch 'Comfy-Org:main' into millerlight-patch-1 2025-08-11 16:24:36 +02:00
millerlightandGitHub a062f6f459 Update custom-node-list.json
In v1.0.0 I now strictly enforce a registry-backed whitelist:

    POST /kvtools/peek only serves values for (file_name, key) that exist in kv_registry.json (basename-only, path traversal blocked via abspath().startswith(BASE_DIR)).

    Image serving is scoped to the allowed root (optional extension whitelist), no arbitrary paths.

    UI changes no longer trigger automatic runs.
2025-08-11 04:59:50 +02:00
Paolo Calvi 6d87c3e981 Add ComfyUI-PuLID-Flux-Chroma - First PuLID implementation for Chroma models. 2025-06-16 00:20:47 +02:00
DerrickandGitHub acc11e1bda Add ComfyUI_rgbx_xrgb_Wrapper 2025-06-07 20:55:40 -07:00
39 changed files with 79454 additions and 15799 deletions
+57
View File
@@ -0,0 +1,57 @@
# Changelog
## Unreleased
### Security policy: dedicated install flags (`allow_git_url_install` / `allow_pip_install`)
Two new boolean keys in `config.ini` (`[default]` section), both defaulting to
`false`, now govern the arbitrary-install surfaces:
| Flag | Governs |
|------|---------|
| `allow_git_url_install` | `POST /customnode/install/git_url` and the unknown-git-URL arm of `POST /manager/queue/install` (incl. reinstall delegation) — the entire install transaction, transitive dependency pip installs included. On the batch queue path the flag applies **in addition to** the queue's `security_level` entry gate (see below) |
| `allow_pip_install` | `POST /customnode/install/pip` only |
These surfaces additionally require a **loopback listener** (`--listen` on a
loopback IP such as `127.0.0.1` or `::1` — not a general LAN/private address);
the flags never open a non-loopback deployment. On the two
**direct** endpoints (`POST /customnode/install/git_url` and
`POST /customnode/install/pip`), the flags fully **decouple** the surface
from `security_level`: it no longer has any effect in either direction — a
strict level cannot deny them when the flag is `true`, and a weak level
cannot allow them when the flag is `false`. On the **batch queue path**
(`POST /manager/queue/install`), the flag is **necessary but not
sufficient**: it gates the unknown-git-URL arm at the risky position, while
the queue's normal `security_level` entry gate (`middle`) remains in force —
at `security_level = strong`, batch unknown-URL installs stay denied even
with the flag set to `true`. `security_level` continues to govern every
other gated endpoint unchanged. Only the case-insensitive string `true`
enables a flag; a missing or malformed key reads as `false`.
#### Migration note (no auto-seed)
There is **no automatic migration** from `security_level`. Users who
previously relied on `security_level = weak` (or `normal-`) to use
install-via-git-URL / install-pip must now **opt in explicitly** by adding to
`config.ini`:
```ini
[default]
allow_git_url_install = true
allow_pip_install = true
```
Changes take effect after a **restart** (no hot reload).
#### Residual-risk note — outdated ComfyUI behavior change
On outdated ComfyUI versions (no system-user API), the manager previously
forced `security_level = strong`, which unconditionally denied the
git-URL/pip install surfaces. After this change those surfaces are governed
by the new flags instead: an operator who explicitly sets a flag to `true`
on a **loopback** listener can now perform installs on outdated ComfyUI
where the forced-strong policy previously denied them. This is an accepted,
deliberate trade-off: it requires explicit operator opt-in, remains bounded
to loopback listeners, and the flag-deny path on outdated ComfyUI still
surfaces the `comfyui_outdated` notice. If you operate an outdated ComfyUI
deployment, leave both flags at their default `false` and update ComfyUI.
+63 -3
View File
@@ -384,19 +384,79 @@ When you run the `scan.sh` script:
* all feature is available
* `high` level risky features
* `Install via git url`, `pip install`
* Installation of custom nodes registered not in the `default channel`.
* Fix custom nodes
* Downloading models that are not in `.safetensors` format and not
registered in the `default channel` model list
* NOTE: `Install via git url`, `pip install`, and installation of custom nodes
not registered in the `default channel` are **no longer governed by
`security_level`** — they are governed by the dedicated install flags
described below.
* `middle` level risky features
* Uninstall/Update
* Installation of custom nodes registered in the `default channel`.
* Fix custom nodes
* Restore/Remove Snapshot
* Restart
* `low` level risky features
* Update ComfyUI
### Dedicated install flags: `allow_git_url_install` / `allow_pip_install`
The two arbitrary-install surfaces are governed by dedicated boolean keys in
`config.ini` (`[default]` section), fully **decoupled** from `security_level`:
* `allow_git_url_install`
* governs `Install via Git URL` (`POST /customnode/install/git_url`) **and**
the unknown-git-URL arm of the batch install queue
(`POST /manager/queue/install`, including reinstall delegation) — i.e.
installing any custom node from a git URL that is not registered in the
`default channel` catalog
* on the **batch queue path**, the flag is **necessary but not
sufficient**: the queue's normal `security_level` entry gate (`middle`)
must ALSO pass — at `security_level = strong`, batch unknown-URL
installs stay denied even with the flag set to `true` (only the direct
`Install via Git URL` endpoint is fully independent of `security_level`)
* covers the **entire install transaction** it starts, including the
pack's transitive dependency pip installs
* `allow_pip_install`
* governs **only** the standalone `pip install` feature
(`POST /customnode/install/pip`)
Key properties:
* **Decoupled from `security_level` (replace, not and)** — on the two
**direct endpoints** (`Install via Git URL` and `pip install`),
`security_level` no longer has any effect in either direction: a strict
level cannot deny them when the flag is `true`, and a weak level cannot
allow them when the flag is `false`. (The batch queue path keeps its
`security_level` entry gate in ADDITION to the flag — see the scope bullet
above.) Every other gated feature remains governed by `security_level` as
described above.
* **Loopback only** — the flags take effect **only** when the server listens
on a loopback address (e.g. `--listen 127.0.0.1`). On a non-loopback
listener these surfaces stay denied regardless of the flags; the flags
never widen the exposure of a public deployment.
* **Default deny / explicit opt-in** — both flags default to `false`. Only
the case-insensitive string `true` enables a flag; a missing or malformed
key reads as `false`.
To opt in, edit `config.ini`:
```ini
[default]
allow_git_url_install = true
allow_pip_install = true
```
Changes take effect after a **restart** (no hot reload).
> **Migration note**: there is no automatic migration from `security_level`.
> If you previously relied on `security_level = weak` (or `normal-`) to use
> install-via-git-URL / pip install, you must opt in explicitly with the flags
> above. See `CHANGELOG.md` for details, including a behavior note for
> outdated ComfyUI deployments.
# Disclaimer
+14112 -1282
View File
File diff suppressed because it is too large Load Diff
+18487 -1690
View File
File diff suppressed because it is too large Load Diff
+13485 -7300
View File
File diff suppressed because it is too large Load Diff
+35 -7
View File
@@ -44,7 +44,7 @@ import manager_migration
from node_package import InstalledNodePackage
version_code = [3, 39, 2]
version_code = [3, 41]
version_str = f"V{version_code[0]}.{version_code[1]}" + (f'.{version_code[2]}' if len(version_code) > 2 else '')
@@ -921,7 +921,7 @@ class UnifiedManager:
except:
return version.parse("0.0.0")
def execute_install_script(self, url, repo_path, instant_execution=False, lazy_mode=False, no_deps=False):
def execute_install_script(self, url, repo_path, instant_execution=False, lazy_mode=False, no_deps=False, selected_dependencies=None):
install_script_path = os.path.join(repo_path, "install.py")
requirements_path = os.path.join(repo_path, "requirements.txt")
@@ -933,8 +933,19 @@ class UnifiedManager:
if os.path.exists(requirements_path) and not no_deps:
print("Install: pip packages")
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, manager_files_path)
# Create a set of selected dependency lines for quick lookup
selected_lines = set()
if selected_dependencies:
for dep in selected_dependencies:
selected_lines.add(dep.get('line', '').strip())
lines = manager_util.robust_readlines(requirements_path)
for line in lines:
# Skip if selected_dependencies is provided and this line is not in the selected list
if selected_dependencies is not None and line.strip() not in selected_lines:
continue
package_name = remap_pip_package(line.strip())
if package_name and not package_name.startswith('#') and package_name not in self.processed_install:
self.processed_install.add(package_name)
@@ -1342,7 +1353,7 @@ class UnifiedManager:
return result
def repo_install(self, url: str, repo_path: str, instant_execution=False, no_deps=False, return_postinstall=False):
def repo_install(self, url: str, repo_path: str, instant_execution=False, no_deps=False, return_postinstall=False, selected_dependencies=None):
result = ManagedResult('install-git')
result.append(url)
@@ -1369,7 +1380,7 @@ class UnifiedManager:
repo.close()
def postinstall():
return self.execute_install_script(url, repo_path, instant_execution=instant_execution, no_deps=no_deps)
return self.execute_install_script(url, repo_path, instant_execution=instant_execution, no_deps=no_deps, selected_dependencies=selected_dependencies)
if return_postinstall:
return result.with_postinstall(postinstall)
@@ -1468,7 +1479,7 @@ class UnifiedManager:
else:
return self.cnr_switch_version(node_id, instant_execution=instant_execution, no_deps=no_deps, return_postinstall=return_postinstall).with_ver('cnr')
async def install_by_id(self, node_id: str, version_spec=None, channel=None, mode=None, instant_execution=False, no_deps=False, return_postinstall=False):
async def install_by_id(self, node_id: str, version_spec=None, channel=None, mode=None, instant_execution=False, no_deps=False, return_postinstall=False, selected_dependencies=None):
"""
priority if version_spec == None
1. CNR latest
@@ -1519,7 +1530,7 @@ class UnifiedManager:
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)
res = self.repo_install(repo_url, to_path, instant_execution=instant_execution, no_deps=no_deps, return_postinstall=return_postinstall, selected_dependencies=selected_dependencies)
if res.result:
if version_spec == 'unknown':
self.unknown_active_nodes[node_id] = repo_url, to_path
@@ -1699,6 +1710,8 @@ def write_config():
'always_lazy_install': get_config()['always_lazy_install'],
'network_mode': get_config()['network_mode'],
'db_mode': get_config()['db_mode'],
'allow_git_url_install': get_config()['allow_git_url_install'],
'allow_pip_install': get_config()['allow_pip_install'],
}
# Sanitize all string values to prevent CRLF injection attacks
@@ -1745,6 +1758,8 @@ def read_config():
'network_mode': default_conf.get('network_mode', 'public').lower(),
'security_level': default_conf.get('security_level', 'normal').lower(),
'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),
}
manager_migration.force_security_level_if_needed(result)
return result
@@ -1774,6 +1789,8 @@ def read_config():
'network_mode': 'public', # public | private | offline
'security_level': 'normal', # strong | normal | normal- | weak
'db_mode': 'cache', # local | cache | remote
'allow_git_url_install': False,
'allow_pip_install': False,
}
manager_migration.force_security_level_if_needed(result)
return result
@@ -1968,7 +1985,7 @@ def __win_check_git_pull(path):
process.wait()
def execute_install_script(url, repo_path, lazy_mode=False, instant_execution=False, no_deps=False):
def execute_install_script(url, repo_path, lazy_mode=False, instant_execution=False, no_deps=False, selected_dependencies=None):
# import ipdb; ipdb.set_trace()
install_script_path = os.path.join(repo_path, "install.py")
requirements_path = os.path.join(repo_path, "requirements.txt")
@@ -1980,6 +1997,13 @@ def execute_install_script(url, repo_path, lazy_mode=False, instant_execution=Fa
if os.path.exists(requirements_path) and not no_deps:
print("Install: pip packages")
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, manager_files_path)
# Create a set of selected dependency lines for quick lookup
selected_lines = set()
if selected_dependencies:
for dep in selected_dependencies:
selected_lines.add(dep.get('line', '').strip())
with open(requirements_path, "r") as requirements_file:
for line in requirements_file:
#handle comments
@@ -1990,6 +2014,10 @@ def execute_install_script(url, repo_path, lazy_mode=False, instant_execution=Fa
else:
line = line.split('#')[0].strip()
# Skip if selected_dependencies is provided and this line is not in the selected list
if selected_dependencies is not None and line.strip() not in selected_lines:
continue
package_name = remap_pip_package(line.strip())
if package_name and not package_name.startswith('#'):
+508 -86
View File
@@ -35,6 +35,8 @@ SECURITY_MESSAGE_MIDDLE_OR_BELOW = "ERROR: To use this action, a security_level
SECURITY_MESSAGE_NORMAL_MINUS = "ERROR: To use this feature, you must either set '--listen' to a local IP and set the security level to 'normal-' or lower, or set the security level to 'middle' or 'weak'. Please contact the administrator.\nReference: https://github.com/ltdrdata/ComfyUI-Manager#security-policy"
SECURITY_MESSAGE_GENERAL = "ERROR: This installation is not allowed in this security_level. Please contact the administrator.\nReference: https://github.com/ltdrdata/ComfyUI-Manager#security-policy"
SECURITY_MESSAGE_NORMAL_MINUS_MODEL = "ERROR: Downloading models that are not in '.safetensors' format is only allowed for models registered in the 'default' channel at this security level. If you want to download this model, set the security level to 'normal-' or lower."
SECURITY_MESSAGE_FLAG_GIT_URL = "ERROR: This action requires 'allow_git_url_install = true' in config.ini ([default] section). This setting is independent of security_level. Reference: https://github.com/ltdrdata/ComfyUI-Manager#security-policy"
SECURITY_MESSAGE_FLAG_PIP = "ERROR: This action requires 'allow_pip_install = true' in config.ini ([default] section). This setting is independent of security_level. Reference: https://github.com/ltdrdata/ComfyUI-Manager#security-policy"
routes = PromptServer.instance.routes
@@ -82,6 +84,19 @@ def is_loopback(address):
except ValueError:
return False
def is_dedicated_install_allowed(flag_value: bool, listen_address: str) -> bool:
"""P-direct predicate (adopter-degraded form): flag AND loopback.
Pure helper for the dedicated install flags
(allow_git_url_install / allow_pip_install) — callers pass the
flag value from their own config read and the listener address
from the CLI arguments (request-time evaluation; the import-time
snapshot above is NOT consulted).
"""
return bool(flag_value) and is_loopback(listen_address)
is_local_mode = is_loopback(args.listen)
@@ -305,13 +320,72 @@ import zipfile
import urllib.request
def security_403_response():
"""Return appropriate 403 response based on ComfyUI version."""
def security_403_response(flag_token=None):
"""Return appropriate 403 response based on ComfyUI version.
When `flag_token` is given (dedicated install flag denials), the
body names the responsible flag instead of "security_level". The
`comfyui_outdated` branch stays the FIRST check regardless, and
no-arg callers keep today's body byte-identical.
"""
if not manager_migration.has_system_user_api():
return web.json_response({"error": "comfyui_outdated"}, status=403)
if flag_token is not None:
return web.json_response({"error": flag_token}, status=403)
return web.json_response({"error": "security_level"}, status=403)
# CORS "simple request" Content-Type set per Fetch spec §3.2.3. Browsers send
# <form method=POST> submissions with one of these three MIME types and do NOT
# trigger a CORS preflight, so a malicious cross-origin page can silently POST
# into state-changing endpoints if we only gate on HTTP method. Blocking these
# three Content-Types on no-body mutation endpoints forces any non-same-origin
# POST to use a non-simple Content-Type (e.g. application/json), which triggers
# a preflight that this server rejects by not advertising an Access-Control-
# Allow-Origin response.
_SIMPLE_FORM_CONTENT_TYPES = frozenset({
'application/x-www-form-urlencoded',
'multipart/form-data',
'text/plain',
})
def _reject_simple_form_content_type(request):
"""Reject Content-Types that enable preflight-less <form method=POST> CSRF.
Applied ONLY to POST handlers that do not consume a request body (e.g.,
/snapshot/save, /manager/queue/{reset,start,update_comfyui},
/manager/reboot). These are vulnerable to cross-origin <form method=POST>
attacks because the handler accepts the request without parsing any body —
the attacker needs no ability to forge a valid payload, only to point a
hidden form at the URL.
Handlers that already read a body via ``await request.json()`` are NOT
gated here: a cross-origin <form method=POST> cannot forge a valid JSON
body because the browser refuses to send ``application/json`` without a
CORS preflight, which this server does not answer.
DO NOT add this gate to body-reading handlers — redundant and UX-breaking.
DO NOT remove this gate from no-body handlers — this is the bypass vector.
aiohttp's ``request.content_type`` normalizes the header (lower-cases,
strips parameters), so ``multipart/form-data; boundary=----X`` is compared
as ``multipart/form-data``.
Returns:
web.Response(status=400) when the request has a simple-form
Content-Type that must be rejected. None when the request is allowed
to proceed (no Content-Type, application/json, or any non-simple
Content-Type).
"""
if request.content_type in _SIMPLE_FORM_CONTENT_TYPES:
return web.Response(
status=400,
text='Invalid Content-Type for this endpoint. Use application/json or omit body.',
)
return None
def get_model_dir(data, show_log=False):
if 'download_model_base' in folder_paths.folder_names_and_paths:
models_base = folder_paths.folder_names_and_paths['download_model_base'][0][0]
@@ -443,7 +517,12 @@ async def task_worker():
global tasks_in_progress
async def do_install(item) -> str:
ui_id, node_spec_str, channel, mode, skip_post_install = item
if len(item) == 6:
ui_id, node_spec_str, channel, mode, skip_post_install, selected_dependencies = item
else:
# Backward compatibility
ui_id, node_spec_str, channel, mode, skip_post_install = item
selected_dependencies = []
try:
node_spec = core.unified_manager.resolve_node_spec(node_spec_str)
@@ -452,7 +531,7 @@ async def task_worker():
return f"Cannot resolve install target: '{node_spec_str}'"
node_name, version_spec, is_specified = node_spec
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, selected_dependencies=selected_dependencies)
# discard post install if skip_post_install mode
if res.action not in ['skip', 'enable', 'install-git', 'install-cnr', 'switch-cnr']:
@@ -737,16 +816,19 @@ async def fetch_customnode_mappings(request):
return web.json_response(json_obj, content_type='application/json')
@routes.get("/customnode/fetch_updates")
@routes.post("/customnode/fetch_updates")
async def fetch_updates(request):
try:
if request.rel_url.query["mode"] == "local":
json_data = await request.json()
mode = json_data.get("mode", "default")
if mode == "local":
channel = 'local'
else:
channel = core.get_config()['channel_url']
await core.unified_manager.reload(request.rel_url.query["mode"])
await core.unified_manager.get_custom_nodes(channel, request.rel_url.query["mode"])
await core.unified_manager.reload(mode)
await core.unified_manager.get_custom_nodes(channel, mode)
res = core.unified_manager.fetch_or_pull_git_repo(is_pull=False)
@@ -764,7 +846,7 @@ async def fetch_updates(request):
return web.Response(status=400)
@routes.get("/manager/queue/update_all")
@routes.post("/manager/queue/update_all")
async def update_all(request):
if not is_allowed_security_level('middle'):
logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW)
@@ -774,16 +856,19 @@ async def update_all(request):
is_processing = task_worker_thread is not None and task_worker_thread.is_alive()
if is_processing:
return web.Response(status=401)
await core.save_snapshot_with_postfix('autosave')
if request.rel_url.query["mode"] == "local":
json_data = await request.json()
mode = json_data.get("mode", "default")
if mode == "local":
channel = 'local'
else:
channel = core.get_config()['channel_url']
await core.unified_manager.reload(request.rel_url.query["mode"])
await core.unified_manager.get_custom_nodes(channel, request.rel_url.query["mode"])
await core.unified_manager.reload(mode)
await core.unified_manager.get_custom_nodes(channel, mode)
for k, v in core.unified_manager.active_nodes.items():
if k == 'comfyui-manager':
@@ -1006,14 +1091,15 @@ def get_safe_snapshot_path(target):
return os.path.join(core.manager_snapshot_path, f"{target}.json")
@routes.get("/snapshot/remove")
@routes.post("/snapshot/remove")
async def remove_snapshot(request):
if not is_allowed_security_level('middle'):
logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW)
return security_403_response()
try:
target = request.rel_url.query["target"]
json_data = await request.json()
target = json_data["target"]
path = get_safe_snapshot_path(target)
if path is None:
@@ -1028,14 +1114,15 @@ async def remove_snapshot(request):
return web.Response(status=400)
@routes.get("/snapshot/restore")
@routes.post("/snapshot/restore")
async def restore_snapshot(request):
if not is_allowed_security_level('middle'):
logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW)
return security_403_response()
try:
target = request.rel_url.query["target"]
json_data = await request.json()
target = json_data["target"]
path = get_safe_snapshot_path(target)
if path is None:
@@ -1066,8 +1153,11 @@ async def get_current_snapshot_api(request):
return web.Response(status=400)
@routes.get("/snapshot/save")
@routes.post("/snapshot/save")
async def save_snapshot(request):
resp = _reject_simple_form_content_type(request)
if resp is not None:
return resp
try:
await core.save_snapshot_with_postfix('snapshot')
return web.Response(status=200)
@@ -1228,8 +1318,11 @@ async def reinstall_custom_node(request):
await install_custom_node(request)
@routes.get("/manager/queue/reset")
@routes.post("/manager/queue/reset")
async def reset_queue(request):
resp = _reject_simple_form_content_type(request)
if resp is not None:
return resp
global task_queue
task_queue = queue.Queue()
return web.Response(status=200)
@@ -1270,6 +1363,26 @@ async def install_custom_node(request):
if skip_post_install:
if cnr_id in core.unified_manager.nightly_inactive_nodes or cnr_id in core.unified_manager.cnr_inactive_nodes:
core.unified_manager.unified_enable(cnr_id)
# Mirror the pair of events (in_progress then done) that the async
# task_worker normally emits for queued operations. The in_progress
# event is what sets item.restart=true on the client row so the
# action cell re-renders as "Restart Required"; without it, the
# "Enable" button remains visible after successful enable. The done
# event drives the completion UI (toast, restart indicator, button
# loading clear).
ui_id = json_data.get('ui_id', cnr_id)
PromptServer.instance.send_sync(
"cm-queue-status",
{'status': 'in_progress',
'target': ui_id,
'ui_target': 'nodepack_manager',
'total_count': 1, 'done_count': 0})
PromptServer.instance.send_sync(
"cm-queue-status",
{'status': 'done',
'nodepack_result': {ui_id: 'success'},
'model_result': {},
'total_count': 1, 'done_count': 1})
return web.Response(status=200)
elif selected_version is None:
selected_version = 'latest'
@@ -1299,11 +1412,23 @@ async def install_custom_node(request):
else:
return web.Response(status=404, text=f"Following node pack doesn't provide `nightly` version: ${git_url}")
if not is_allowed_security_level(risky_level):
if risky_level == 'high':
# unknown-URL arm: governed by the dedicated flag predicate
# (flag AND loopback, evaluated at request time). The loopback
# term is load-bearing here — the 'middle' entry gate above has
# no network-position term.
if not is_dedicated_install_allowed(core.get_config()['allow_git_url_install'], args.listen):
logging.error(SECURITY_MESSAGE_FLAG_GIT_URL)
return web.Response(status=404, text="A security error has occurred. Please check the terminal logs")
elif not is_allowed_security_level(risky_level):
# 'block' arm stays an unconditional deny (is_allowed_security_level
# returns False for 'block'); 'middle'/'low' arms unchanged.
logging.error(SECURITY_MESSAGE_GENERAL)
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
# Get selected dependencies if provided
selected_dependencies = json_data.get('selectedDependencies', [])
install_item = json_data.get('ui_id'), node_spec_str, json_data['channel'], json_data['mode'], skip_post_install, selected_dependencies
task_queue.put(("install", install_item))
return web.Response(status=200)
@@ -1311,8 +1436,11 @@ async def install_custom_node(request):
task_worker_thread:threading.Thread = None
@routes.get("/manager/queue/start")
@routes.post("/manager/queue/start")
async def queue_start(request):
resp = _reject_simple_form_content_type(request)
if resp is not None:
return resp
global nodepack_result
global model_result
global task_worker_thread
@@ -1353,11 +1481,21 @@ async def fix_custom_node(request):
@routes.post("/customnode/install/git_url")
async def install_custom_node_git_url(request):
if not is_allowed_security_level('high'):
logging.error(SECURITY_MESSAGE_NORMAL_MINUS)
return security_403_response()
if not is_dedicated_install_allowed(core.get_config()['allow_git_url_install'], args.listen):
logging.error(SECURITY_MESSAGE_FLAG_GIT_URL)
return security_403_response(flag_token='allow_git_url_install')
url = await request.text()
# Read the body as JSON (not raw text): a cross-origin <form method=POST>
# cannot forge an application/json body without a CORS preflight, which this
# server does not answer — same body-handler convention as every other
# request.json() route (see _reject_simple_form_content_type docstring).
# A malformed body or a missing 'url' is a client error (400), not a 500:
# don't let JSONDecodeError/KeyError bubble up as a server fault.
try:
json_data = await request.json()
url = json_data['url']
except (KeyError, ValueError):
return web.Response(status=400, text="Invalid request body: expected JSON object with a 'url' field")
res = await core.gitclone_install(url)
if res.action == 'skip':
@@ -1373,16 +1511,289 @@ async def install_custom_node_git_url(request):
@routes.post("/customnode/install/pip")
async def install_custom_node_pip(request):
if not is_allowed_security_level('high'):
logging.error(SECURITY_MESSAGE_NORMAL_MINUS)
return security_403_response()
if not is_dedicated_install_allowed(core.get_config()['allow_pip_install'], args.listen):
logging.error(SECURITY_MESSAGE_FLAG_PIP)
return security_403_response(flag_token='allow_pip_install')
packages = await request.text()
# JSON body (not raw text) for the same preflight-forcing reason as
# /customnode/install/git_url above; malformed body or missing 'packages'
# is a 400, not a 500.
try:
json_data = await request.json()
packages = json_data['packages']
except (KeyError, ValueError):
return web.Response(status=400, text="Invalid request body: expected JSON object with a 'packages' field")
core.pip_install(packages.split(' '))
return web.Response(status=200)
@routes.post("/customnode/analyze_dependencies")
async def analyze_dependencies(request):
"""
Analyze dependencies for a custom node from git URL.
Fetches requirements.txt, checks installed packages, and returns dependency list with status.
"""
try:
json_data = await request.json()
url = json_data.get('url')
commit_id = json_data.get('commitId')
branch = json_data.get('branch')
if not url:
return web.json_response({'error': 'URL is required'}, status=400)
# Fetch requirements.txt from git repository
requirements_content = await fetch_requirements_from_git(url, commit_id, branch)
if requirements_content is None:
return web.json_response({
'success': True,
'requirements': None,
'dependencies': [],
'noRequirementsFile': True
})
# Parse requirements
dependencies = parse_requirements(requirements_content)
# Get installed packages
installed_packages = manager_util.get_installed_packages()
# Analyze each dependency with subdependencies
analyzed_dependencies = []
for dep_line in dependencies:
if not dep_line.strip() or dep_line.strip().startswith('#'):
continue
# Parse dependency line
parsed = manager_util.parse_requirement_line(dep_line)
if not parsed:
continue
package_name = parsed.get('package')
if not package_name:
# Fallback: extract from line if package is missing
import re
match = re.match(r'^([a-zA-Z0-9_.-]+)', dep_line.strip())
package_name = match.group(1) if match else "Unknown"
version_spec = parsed.get('version')
# Convert version_spec to string if it's a StrictVersion object
if version_spec is not None:
version_spec = str(version_spec)
normalized_name = package_name.lower().replace('-', '_')
# Check if already installed
installed_version = installed_packages.get(normalized_name)
status = 'new'
if installed_version:
status = 'installed'
# Convert version to string if it's not already (handle StrictVersion objects)
current_version_str = str(installed_version) if installed_version else None
# Get subdependencies using pip install --dry-run
# This is optional and failures should not block the main flow
subdependencies = []
# Skip subdependency analysis for already installed packages (not needed)
if status != 'installed':
try:
import subprocess
import sys
# Run pip install --dry-run to get subdependencies
# Some packages like pymeshlab can take longer due to complex dependency resolution
# Use a reasonable timeout - if it times out, we'll continue without subdependencies
result = subprocess.run(
[sys.executable, '-m', 'pip', 'install', '--dry-run', dep_line.strip()],
capture_output=True,
text=True,
timeout=45 # Increased timeout to 45 seconds
)
output = result.stdout + result.stderr
if output:
subdependencies = parse_dry_run_output(output, package_name, installed_packages)
except subprocess.TimeoutExpired:
# Timeout is not critical - continue without subdependencies
logging.debug(f"Subdependency analysis timed out for {package_name} (skipping subdependencies)")
subdependencies = []
except Exception as e:
# Any other error is not critical - continue without subdependencies
logging.debug(f"Failed to analyze subdependencies for {package_name}: {e}")
subdependencies = []
# Add main dependency (always add, even if subdependency analysis failed)
# Ensure all fields are properly set and clean
clean_package_name = str(package_name).strip() if package_name else "Unknown"
# Remove any None/null strings that might have been concatenated
clean_package_name = clean_package_name.replace('None', '').replace('null', '').strip()
if not clean_package_name:
clean_package_name = "Unknown"
analyzed_dependencies.append({
'name': clean_package_name,
'version': str(version_spec) if version_spec else None,
'line': dep_line.strip(),
'status': status,
'currentVersion': current_version_str,
'selected': status != 'installed', # Deselect if already installed
'subdependencies': subdependencies
})
return web.json_response({
'success': True,
'requirements': requirements_content,
'dependencies': analyzed_dependencies,
'noRequirementsFile': False
})
except Exception as e:
logging.error(f"Error analyzing dependencies: {e}")
traceback.print_exc()
return web.json_response({'error': str(e)}, status=500)
def parse_requirements(content):
"""Parse requirements.txt content into list of dependency lines."""
lines = []
for line in content.split('\n'):
line = line.strip()
if line and not line.startswith('#'):
lines.append(line)
return lines
def parse_dry_run_output(output, parent_name, installed_packages):
"""Parse pip install --dry-run output to extract subdependencies."""
import re
subdependencies = []
subdeps_map = {}
lines = output.split('\n')
for line in lines:
line = line.strip()
# Look for "Collecting package==version" lines
if 'Collecting ' in line and 'Using cached' not in line:
# Match: "Collecting package==version" or "Collecting package"
match = re.search(r'Collecting\s+([a-zA-Z0-9_.-]+(?:\[[^\]]+\])?)(?:\s*==\s*([^\s\(]+))?', line)
if match:
dep_name = match.group(1).split('[')[0].strip()
# Clean the name - remove any None/null strings
if dep_name:
dep_name = dep_name.replace('None', '').replace('null', '').strip()
dep_version = match.group(2).strip() if match.group(2) else None
# Clean version too
if dep_version:
dep_version = dep_version.replace('None', '').replace('null', '').strip() or None
# Skip the parent package itself
if dep_name.lower() == parent_name.lower():
continue
# Normalize name
normalized_name = dep_name.lower().replace('-', '_')
# Check if already in map (avoid duplicates)
if normalized_name not in subdeps_map:
# Check if already installed
installed_version = installed_packages.get(normalized_name)
status = 'installed' if installed_version else 'new'
current_version_str = str(installed_version) if installed_version else None
# Ensure name is always a string, not None
if not dep_name:
dep_name = "Unknown"
# Clean the name - remove any None/null strings
clean_dep_name = str(dep_name).strip().replace('None', '').replace('null', '').strip()
if not clean_dep_name:
clean_dep_name = "Unknown"
subdeps_map[normalized_name] = {
'name': clean_dep_name,
'version': str(dep_version) if dep_version else None,
'status': status,
'currentVersion': current_version_str,
'selected': status != 'installed'
}
# Also look for "Would install" lines which have more accurate version info
if 'Would install' in line:
# Match: "Would install package-version"
match = re.search(r'Would install\s+([a-zA-Z0-9_.-]+)-([\d.]+)', line)
if match:
dep_name = match.group(1)
dep_version = match.group(2)
normalized_name = dep_name.lower().replace('-', '_')
if normalized_name in subdeps_map:
# Update with more accurate version
subdeps_map[normalized_name]['version'] = dep_version
# Convert map to list
for normalized_name, dep_info in subdeps_map.items():
subdependencies.append(dep_info)
return subdependencies
async def fetch_requirements_from_git(url, commit_id=None, branch=None):
"""
Fetch requirements.txt from a git repository URL.
Supports GitHub URLs by converting to raw.githubusercontent.com.
"""
try:
# Extract GitHub repo info
if 'github.com' in url:
# Convert to raw GitHub URL
url = url.rstrip('/')
if url.endswith('.git'):
url = url[:-4]
# Extract owner/repo
match = re.search(r'github\.com[:/]([^/]+)/([^/]+)', url)
if not match:
return None
owner = match.group(1)
repo = match.group(2)
# Build raw URL
if commit_id:
raw_url = f"https://raw.githubusercontent.com/{owner}/{repo}/{commit_id}/requirements.txt"
elif branch:
raw_url = f"https://raw.githubusercontent.com/{owner}/{repo}/{branch}/requirements.txt"
else:
raw_url = f"https://raw.githubusercontent.com/{owner}/{repo}/main/requirements.txt"
# Try to fetch using aiohttp
async with aiohttp.ClientSession() as session:
async with session.get(raw_url) as response:
if response.status == 200:
return await response.text()
# Try with master branch if main fails
if 'main' in raw_url:
raw_url = raw_url.replace('/main/', '/master/')
async with session.get(raw_url) as response2:
if response2.status == 200:
return await response2.text()
else:
# For non-GitHub URLs, we'd need to clone temporarily
# For now, return None (can be enhanced later)
logging.warning(f"Non-GitHub URL not fully supported for dependency analysis: {url}")
return None
return None
except Exception as e:
logging.error(f"Error fetching requirements from git: {e}")
return None
@routes.post("/manager/queue/uninstall")
async def uninstall_custom_node(request):
if not is_allowed_security_level('middle'):
@@ -1427,8 +1838,11 @@ async def update_custom_node(request):
return web.Response(status=200)
@routes.get("/manager/queue/update_comfyui")
@routes.post("/manager/queue/update_comfyui")
async def update_comfyui(request):
resp = _reject_simple_form_content_type(request)
if resp is not None:
return resp
is_stable = core.get_config()['update_policy'] != 'nightly-comfyui'
task_queue.put(("update-comfyui", ('comfyui', is_stable)))
return web.Response(status=200)
@@ -1445,11 +1859,12 @@ async def comfyui_versions(request):
return web.Response(status=400)
@routes.get("/comfyui_manager/comfyui_switch_version")
@routes.post("/comfyui_manager/comfyui_switch_version")
async def comfyui_switch_version(request):
try:
if "ver" in request.rel_url.query:
core.switch_comfyui(request.rel_url.query['ver'])
json_data = await request.json()
if "ver" in json_data:
core.switch_comfyui(json_data['ver'])
return web.Response(status=200)
except Exception as e:
@@ -1526,83 +1941,87 @@ async def install_model(request):
@routes.get("/manager/preview_method")
async def preview_method(request):
# Setting change request
if "value" in request.rel_url.query:
# Reject setting change if per-queue preview feature is available
if COMFYUI_HAS_PER_QUEUE_PREVIEW:
return web.Response(text="DISABLED", status=403)
async def get_preview_method(request):
if COMFYUI_HAS_PER_QUEUE_PREVIEW:
return web.Response(text="DISABLED", status=200)
return web.Response(text=core.manager_funcs.get_current_preview_method(), status=200)
# Process normally if not available
set_preview_method(request.rel_url.query['value'])
core.write_config()
return web.Response(status=200)
# Status query request
else:
# Return DISABLED if per-queue preview feature is available
if COMFYUI_HAS_PER_QUEUE_PREVIEW:
return web.Response(text="DISABLED", status=200)
@routes.post("/manager/preview_method")
async def set_preview_method_handler(request):
if COMFYUI_HAS_PER_QUEUE_PREVIEW:
return web.Response(text="DISABLED", status=403)
# Return current value if not available
return web.Response(text=core.manager_funcs.get_current_preview_method(), status=200)
json_data = await request.json()
set_preview_method(json_data['value'])
core.write_config()
return web.Response(status=200)
@routes.get("/manager/db_mode")
async def db_mode(request):
if "value" in request.rel_url.query:
set_db_mode(request.rel_url.query['value'])
core.write_config()
else:
return web.Response(text=core.get_config()['db_mode'], status=200)
async def get_db_mode(request):
return web.Response(text=core.get_config()['db_mode'], status=200)
@routes.post("/manager/db_mode")
async def set_db_mode_handler(request):
json_data = await request.json()
set_db_mode(json_data['value'])
core.write_config()
return web.Response(status=200)
@routes.get("/manager/policy/component")
async def component_policy(request):
if "value" in request.rel_url.query:
set_component_policy(request.rel_url.query['value'])
core.write_config()
else:
return web.Response(text=core.get_config()['component_policy'], status=200)
async def get_component_policy(request):
return web.Response(text=core.get_config()['component_policy'], status=200)
@routes.post("/manager/policy/component")
async def set_component_policy_handler(request):
json_data = await request.json()
set_component_policy(json_data['value'])
core.write_config()
return web.Response(status=200)
@routes.get("/manager/policy/update")
async def update_policy(request):
if "value" in request.rel_url.query:
set_update_policy(request.rel_url.query['value'])
core.write_config()
else:
return web.Response(text=core.get_config()['update_policy'], status=200)
async def get_update_policy(request):
return web.Response(text=core.get_config()['update_policy'], status=200)
@routes.post("/manager/policy/update")
async def set_update_policy_handler(request):
json_data = await request.json()
set_update_policy(json_data['value'])
core.write_config()
return web.Response(status=200)
@routes.get("/manager/channel_url_list")
async def channel_url_list(request):
async def get_channel_url_list(request):
channels = core.get_channel_dict()
if "value" in request.rel_url.query:
channel_url = channels.get(request.rel_url.query['value'])
if channel_url is not None:
core.get_config()['channel_url'] = channel_url
core.write_config()
else:
selected = 'custom'
selected_url = core.get_config()['channel_url']
selected = 'custom'
selected_url = core.get_config()['channel_url']
for name, url in channels.items():
if url == selected_url:
selected = name
break
for name, url in channels.items():
if url == selected_url:
selected = name
break
res = {'selected': selected,
'list': core.get_channel_list()}
return web.json_response(res, status=200)
res = {'selected': selected,
'list': core.get_channel_list()}
return web.json_response(res, status=200)
@routes.post("/manager/channel_url_list")
async def set_channel_url_list(request):
json_data = await request.json()
channels = core.get_channel_dict()
channel_url = channels.get(json_data['value'])
if channel_url is not None:
core.get_config()['channel_url'] = channel_url
core.write_config()
return web.Response(status=200)
@@ -1700,8 +2119,11 @@ async def get_startup_alerts(request):
return web.json_response(alerts)
@routes.get("/manager/reboot")
@routes.post("/manager/reboot")
def restart(self):
resp = _reject_simple_form_content_type(self)
if resp is not None:
return resp
if not is_allowed_security_level('middle'):
logging.error(SECURITY_MESSAGE_MIDDLE_OR_BELOW)
return security_403_response()
+53 -7
View File
@@ -53,6 +53,40 @@ And kill and remove /tmp/ultralytics_runner
The version 8.3.41 to 8.3.42 of the Ultralytics package you installed is compromised. Please uninstall that version and reinstall the latest version.
https://blog.comfy.org/comfyui-statement-on-the-ultralytics-crypto-miner-situation/
""",
"litellm==1.82.7": f"""
Execute following commands:
{sys.executable} -m pip uninstall litellm
The litellm PyPI package versions 1.82.7 and 1.82.8 were compromised via a supply chain attack.
Malicious code harvests SSH keys, environment variables, API keys, cloud credentials, and exfiltrates them to an attacker-controlled server.
Version 1.82.8 also installs a .pth file that executes malware on ANY Python startup, even without importing litellm.
1. Uninstall litellm immediately.
2. Assume all credentials accessible to the litellm environment are compromised.
3. Rotate all API keys, cloud credentials, SSH keys, and database passwords.
4. Check site-packages for unexpected .pth files (e.g. litellm_init.pth) and remove them.
5. Run a full malware scan.
Details: https://github.com/BerriAI/litellm/issues/24518
Advisory: PYSEC-2026-2
""",
"litellm==1.82.8": f"""
Execute following commands:
{sys.executable} -m pip uninstall litellm
The litellm PyPI package versions 1.82.7 and 1.82.8 were compromised via a supply chain attack.
Malicious code harvests SSH keys, environment variables, API keys, cloud credentials, and exfiltrates them to an attacker-controlled server.
Version 1.82.8 also installs a .pth file that executes malware on ANY Python startup, even without importing litellm.
1. Uninstall litellm immediately.
2. Assume all credentials accessible to the litellm environment are compromised.
3. Rotate all API keys, cloud credentials, SSH keys, and database passwords.
4. Check site-packages for unexpected .pth files (e.g. litellm_init.pth) and remove them.
5. Run a full malware scan.
Details: https://github.com/BerriAI/litellm/issues/24518
Advisory: PYSEC-2026-2
"""
}
@@ -60,7 +94,10 @@ https://blog.comfy.org/comfyui-statement-on-the-ultralytics-crypto-miner-situati
pip_blacklist = {
"AppleBotzz": "ComfyUI_LLMVISION",
"ultralytics==8.3.41": "ultralytics==8.3.41"
"ultralytics==8.3.41": "ultralytics==8.3.41",
"ultralytics==8.3.42": "ultralytics==8.3.42",
"litellm==1.82.7": "litellm==1.82.7",
"litellm==1.82.8": "litellm==1.82.8",
}
file_blacklist = {
@@ -93,10 +130,15 @@ https://blog.comfy.org/comfyui-statement-on-the-ultralytics-crypto-miner-situati
print(f"[SECURITY ALERT] custom node '{k}' is dangerous.")
detected.add(v)
installed_pip_set = set(installed_pips.strip().split('\n'))
for k, v in pip_blacklist.items():
if k in installed_pips:
detected.add(v)
break
if '==' in k:
if k in installed_pip_set:
detected.add(v)
else:
if any(line.split('==')[0] == k for line in installed_pip_set):
detected.add(v)
for k, v in file_blacklist.items():
for x in v:
@@ -105,10 +147,14 @@ https://blog.comfy.org/comfyui-statement-on-the-ultralytics-crypto-miner-situati
break
if len(detected) > 0:
for line in installed_pips.split('\n'):
for line in installed_pip_set:
for k, v in pip_blacklist.items():
if k in line:
print(f"[SECURITY ALERT] '{line}' is dangerous.")
if '==' in k:
if line == k:
print(f"[SECURITY ALERT] '{line}' is dangerous.")
else:
if line.split('==')[0] == k:
print(f"[SECURITY ALERT] '{line}' is dangerous.")
print("\n########################################################################")
print(" Malware has been detected, forcibly terminating ComfyUI execution.")
+1 -1
View File
@@ -52,7 +52,7 @@ async function tryInstallCustomNode(event) {
}
}
let response = await api.fetchApi("/manager/reboot");
let response = await api.fetchApi("/manager/reboot", { method: 'POST' });
if(response.status == 403) {
await handle403Response(response);
return false;
+14 -14
View File
@@ -470,12 +470,12 @@ async function updateComfyUI() {
set_inprogress_mode();
const response = await api.fetchApi('/manager/queue/update_comfyui');
const response = await api.fetchApi('/manager/queue/update_comfyui', { method: 'POST' });
showTerminal();
is_updating = true;
await api.fetchApi('/manager/queue/start');
await api.fetchApi('/manager/queue/start', { method: 'POST' });
}
function showVersionSelectorDialog(versions, current, onSelect) {
@@ -625,14 +625,14 @@ async function switchComfyUI() {
showVersionSelectorDialog(versions, obj.current, async (selected_version) => {
if(selected_version == 'nightly') {
update_policy_combo.value = 'nightly-comfyui';
api.fetchApi('/manager/policy/update?value=nightly-comfyui');
api.fetchApi('/manager/policy/update', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ value: 'nightly-comfyui' }) });
}
else {
update_policy_combo.value = 'stable-comfyui';
api.fetchApi('/manager/policy/update?value=stable-comfyui');
api.fetchApi('/manager/policy/update', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ value: 'stable-comfyui' }) });
}
let response = await api.fetchApi(`/comfyui_manager/comfyui_switch_version?ver=${selected_version}`, { cache: "no-store" });
let response = await api.fetchApi('/comfyui_manager/comfyui_switch_version', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ver: selected_version }), cache: "no-store" });
if (response.status == 200) {
infoToast(`ComfyUI version is switched to ${selected_version}`);
}
@@ -769,10 +769,10 @@ async function updateAll(update_comfyui) {
if(update_comfyui) {
update_all_button.innerText = "Updating ComfyUI...";
await api.fetchApi('/manager/queue/update_comfyui');
await api.fetchApi('/manager/queue/update_comfyui', { method: 'POST' });
}
const response = await api.fetchApi(`/manager/queue/update_all?mode=${mode}`);
const response = await api.fetchApi('/manager/queue/update_all', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ mode: mode }) });
if (response.status == 403) {
await handle403Response(response);
@@ -784,7 +784,7 @@ async function updateAll(update_comfyui) {
}
else if(response.status == 200) {
is_updating = true;
await api.fetchApi('/manager/queue/start');
await api.fetchApi('/manager/queue/start', { method: 'POST' });
}
}
@@ -813,7 +813,7 @@ function restartOrStop() {
rebootAPI();
}
else {
api.fetchApi('/manager/queue/reset');
api.fetchApi('/manager/queue/reset', { method: 'POST' });
infoToast('Cancel', 'Remaining tasks will stop after completing the current task.');
}
}
@@ -967,7 +967,7 @@ class ManagerMenuDialog extends ComfyDialog {
.then(data => { this.datasrc_combo.value = data; });
this.datasrc_combo.addEventListener('change', function (event) {
api.fetchApi(`/manager/db_mode?value=${event.target.value}`);
api.fetchApi('/manager/db_mode', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ value: event.target.value }) });
});
const dbRetrievalSetttingItem = createSettingsCombo("DB", this.datasrc_combo);
@@ -1043,7 +1043,7 @@ class ManagerMenuDialog extends ComfyDialog {
}
// Normal operation
api.fetchApi(`/manager/preview_method?value=${event.target.value}`)
api.fetchApi('/manager/preview_method', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ value: event.target.value }) })
.then(response => {
if (response.status === 403) {
// Feature transitioned to native
@@ -1087,7 +1087,7 @@ class ManagerMenuDialog extends ComfyDialog {
}
channel_combo.addEventListener('change', function (event) {
api.fetchApi(`/manager/channel_url_list?value=${event.target.value}`);
api.fetchApi('/manager/channel_url_list', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ value: event.target.value }) });
});
channel_combo.value = data.selected;
@@ -1152,7 +1152,7 @@ class ManagerMenuDialog extends ComfyDialog {
});
component_policy_combo.addEventListener('change', function (event) {
api.fetchApi(`/manager/policy/component?value=${event.target.value}`);
api.fetchApi('/manager/policy/component', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ value: event.target.value }) });
set_component_policy(event.target.value);
});
@@ -1171,7 +1171,7 @@ class ManagerMenuDialog extends ComfyDialog {
});
update_policy_combo.addEventListener('change', function (event) {
api.fetchApi(`/manager/policy/update?value=${event.target.value}`);
api.fetchApi('/manager/policy/update', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ value: event.target.value }) });
});
const updateSetttingItem = createSettingsCombo("Update", update_policy_combo);
+10 -6
View File
@@ -185,7 +185,7 @@ export async function rebootAPI() {
const isConfirmed = await customConfirm("Are you sure you'd like to reboot the server?");
if (isConfirmed) {
try {
const response = await api.fetchApi("/manager/reboot");
const response = await api.fetchApi("/manager/reboot", { method: 'POST' });
if (response.status == 403) {
await handle403Response(response);
return false;
@@ -223,16 +223,19 @@ function isValidURL(url) {
}
export async function install_pip(packages) {
if(packages.includes('&'))
if(packages.includes('&')) {
app.ui.dialog.show(`Invalid PIP package enumeration: '${packages}'`);
return;
}
const res = await api.fetchApi("/customnode/install/pip", {
method: "POST",
body: packages,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ packages: packages }),
});
if(res.status == 403) {
await handle403Response(res);
await handle403Response(res, "To use this feature, set 'allow_pip_install = true' in config.ini ([default] section), then restart ComfyUI (the config is read once at startup). This setting is independent of security_level.");
return;
}
@@ -263,11 +266,12 @@ export async function install_via_git_url(url, manager_dialog) {
const res = await api.fetchApi("/customnode/install/git_url", {
method: "POST",
body: url,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: url }),
});
if(res.status == 403) {
await handle403Response(res);
await handle403Response(res, "To use this feature, set 'allow_git_url_install = true' in config.ini ([default] section), then restart ComfyUI (the config is read once at startup). This setting is independent of security_level.");
return;
}
+4 -4
View File
@@ -678,7 +678,7 @@ export class ComponentBuilderDialog extends ComfyDialog {
let orig_handleFile = app.handleFile;
async function handleFile(file) {
async function handleFile(file, ...args) {
if (file.name?.endsWith(".json") || file.name?.endsWith(".pack")) {
const reader = new FileReader();
reader.onload = async () => {
@@ -694,7 +694,7 @@ async function handleFile(file) {
await handle_import_components(jsonContent);
}
else {
orig_handleFile.call(app, file);
orig_handleFile.call(app, file, ...args);
}
};
reader.readAsText(file);
@@ -702,7 +702,7 @@ async function handleFile(file) {
return;
}
orig_handleFile.call(app, file);
orig_handleFile.call(app, file, ...args);
}
app.handleFile = handleFile;
@@ -780,7 +780,7 @@ export function set_component_policy(v) {
let graphToPrompt = app.graphToPrompt;
app.graphToPrompt = async function () {
let p = await graphToPrompt.call(app);
let p = await graphToPrompt.apply(app, arguments);
try {
let groupNodes = p.workflow.extra?.groupNodes;
if(groupNodes) {
+645 -19
View File
@@ -67,6 +67,7 @@ export class CustomNodesManager {
this.filter = '';
this.keywords = '';
this.restartMap = {};
this.analyzeDependenciesBeforeInstall = false; // Default: false
this.init();
@@ -77,6 +78,36 @@ export class CustomNodesManager {
}
init() {
// Create checkbox for dependency analysis
const analyzeDepsCheckbox = $el("input", {
type: "checkbox",
id: "cn-analyze-deps-checkbox",
checked: this.analyzeDependenciesBeforeInstall,
onchange: (e) => {
this.analyzeDependenciesBeforeInstall = e.target.checked;
},
style: {
marginRight: "6px",
cursor: "pointer"
}
});
const analyzeDepsLabel = $el("label", {
for: "cn-analyze-deps-checkbox",
style: {
display: "flex",
alignItems: "center",
cursor: "pointer",
color: "#fff",
fontSize: "12px",
marginRight: "10px",
whiteSpace: "nowrap"
}
}, [
analyzeDepsCheckbox,
$el("span", { textContent: "Analyse dependencies before node installation" })
]);
const header = $el("div.cn-manager-header.px-2", {}, [
// $el("label", {}, [
// $el("span", { textContent: "Filter" }),
@@ -84,6 +115,7 @@ export class CustomNodesManager {
// ]),
createSettingsCombo("Filter", $el("select.cn-manager-filter")),
$el("input.cn-manager-keywords.p-inputtext.p-component", { type: "search", placeholder: "Search" }),
analyzeDepsLabel,
$el("div.cn-manager-status"),
$el("div.cn-flex-auto"),
$el("div.cn-manager-channel")
@@ -105,6 +137,421 @@ export class CustomNodesManager {
this.initGrid();
}
showDependencySelectorDialog(dependencies, onSelect) {
const dialog = new ComfyDialog();
dialog.element.style.zIndex = 1100;
dialog.element.style.width = "900px";
dialog.element.style.maxHeight = "80vh";
dialog.element.style.padding = "0";
dialog.element.style.backgroundColor = "#2a2a2a";
dialog.element.style.border = "1px solid #3a3a3a";
dialog.element.style.borderRadius = "8px";
dialog.element.style.boxSizing = "border-box";
dialog.element.style.overflow = "hidden";
const contentStyle = {
width: "100%",
display: "flex",
flexDirection: "column",
padding: "20px",
boxSizing: "border-box",
gap: "15px"
};
// Create scrollable table container with sticky header
const tableContainer = $el("div", {
style: {
maxHeight: "500px",
overflowY: "auto",
border: "1px solid #4a4a4a",
borderRadius: "4px",
backgroundColor: "#1a1a1a",
position: "relative"
}
});
// Create table
const table = $el("table", {
style: {
width: "100%",
borderCollapse: "separate",
borderSpacing: "0",
fontSize: "14px"
}
});
// Create table header with sticky positioning
const thead = $el("thead", {
style: {
position: "sticky",
top: "0",
zIndex: "10",
backgroundColor: "#2a2a2a",
boxShadow: "0 2px 4px rgba(0,0,0,0.3)"
}
}, [
$el("tr", {
style: {
backgroundColor: "#2a2a2a",
borderBottom: "2px solid #4a4a4a"
}
}, [
$el("th", {
textContent: "",
style: {
padding: "10px",
textAlign: "left",
width: "40px",
color: "#fff"
}
}),
$el("th", {
textContent: "Dependency Name",
style: {
padding: "10px",
textAlign: "left",
color: "#fff",
fontWeight: "bold"
}
}),
$el("th", {
textContent: "Current Version",
style: {
padding: "10px",
textAlign: "left",
color: "#fff",
fontWeight: "bold"
}
}),
$el("th", {
textContent: "Incoming Version",
style: {
padding: "10px",
textAlign: "left",
color: "#fff",
fontWeight: "bold"
}
})
])
]);
// Create table body
const tbody = $el("tbody", {});
// Create table rows for each dependency and its subdependencies
let rowIndex = 0;
dependencies.forEach((dep) => {
// Ensure name is not null/undefined and clean it
let depName = dep.name;
if (!depName || depName === 'null' || depName === 'None') {
// Fallback: extract from line
if (dep.line) {
depName = dep.line.split(/[=<>!~]/)[0].trim();
} else {
depName = "Unknown";
}
}
// Remove any "null" suffix that might have been appended
depName = String(depName).replace(/null$/i, '').trim();
const isInstalled = dep.status === 'installed';
const incomingVersion = dep.version || "NA";
const currentVersion = dep.currentVersion || "NA";
// Main dependency row
const row = $el("tr", {
style: {
backgroundColor: rowIndex % 2 === 0 ? "#1a1a1a" : "#222222",
borderBottom: "1px solid #3a3a3a"
}
}, [
$el("td", {
style: {
padding: "10px",
textAlign: "center"
}
}, [
$el("input", {
type: "checkbox",
checked: dep.selected,
onchange: (e) => {
dep.selected = e.target.checked;
},
style: {
cursor: "pointer",
width: "18px",
height: "18px"
}
})
]),
$el("td", {
style: {
padding: "10px",
color: isInstalled ? "#888" : "#fff"
}
}, [
$el("span", {
textContent: depName,
style: {
fontWeight: "500",
marginRight: isInstalled ? "8px" : "0"
}
}),
isInstalled ? $el("span", {
textContent: "Installed",
style: {
display: "inline-block",
backgroundColor: "#2a4a2a",
color: "#4a9",
padding: "2px 6px",
borderRadius: "3px",
fontSize: "10px",
fontWeight: "bold",
border: "1px solid #4a9"
}
}) : ''
]),
$el("td", {
textContent: currentVersion,
style: {
padding: "10px",
color: isInstalled ? "#4a9" : "#aaa",
fontFamily: "monospace"
}
}),
$el("td", {
textContent: incomingVersion,
style: {
padding: "10px",
color: "#fff",
fontFamily: "monospace"
}
})
]);
tbody.appendChild(row);
rowIndex++;
// Add subdependencies as indented rows
if(dep.subdependencies && dep.subdependencies.length > 0) {
dep.subdependencies.forEach((subdep) => {
// Ensure subdependency name is not null/undefined and clean it
let subdepName = subdep.name;
if (!subdepName || subdepName === 'null' || subdepName === 'None') {
subdepName = "Unknown";
}
// Remove any "null" suffix that might have been appended
subdepName = String(subdepName).replace(/null$/i, '').trim();
const subIsInstalled = subdep.status === 'installed';
const subIncomingVersion = subdep.version || "NA";
const subCurrentVersion = subdep.currentVersion || "NA";
const subRow = $el("tr", {
style: {
backgroundColor: rowIndex % 2 === 0 ? "#1a1a1a" : "#222222",
borderBottom: "1px solid #3a3a3a"
}
}, [
$el("td", {
style: {
padding: "10px",
textAlign: "center"
}
}, [
$el("input", {
type: "checkbox",
checked: subdep.selected,
onchange: (e) => {
subdep.selected = e.target.checked;
},
style: {
cursor: "pointer",
width: "18px",
height: "18px"
}
})
]),
$el("td", {
style: {
padding: "10px 10px 10px 30px",
color: subIsInstalled ? "#888" : "#aaa",
fontSize: "13px"
}
}, [
$el("span", {
textContent: "└─ " + subdepName,
style: {
fontWeight: "400",
marginRight: subIsInstalled ? "8px" : "0"
}
}),
subIsInstalled ? $el("span", {
textContent: "Installed",
style: {
display: "inline-block",
backgroundColor: "#2a4a2a",
color: "#4a9",
padding: "2px 6px",
borderRadius: "3px",
fontSize: "10px",
fontWeight: "bold",
border: "1px solid #4a9"
}
}) : ''
]),
$el("td", {
textContent: subCurrentVersion,
style: {
padding: "10px",
color: subIsInstalled ? "#4a9" : "#666",
fontFamily: "monospace",
fontSize: "13px"
}
}),
$el("td", {
textContent: subIncomingVersion,
style: {
padding: "10px",
color: "#aaa",
fontFamily: "monospace",
fontSize: "13px"
}
})
]);
tbody.appendChild(subRow);
rowIndex++;
});
}
});
table.appendChild(thead);
table.appendChild(tbody);
tableContainer.appendChild(table);
const content = $el("div", {
style: contentStyle
}, [
$el("h3", {
textContent: "Select Dependencies to Install",
style: {
color: "#ffffff",
backgroundColor: "#1a1a1a",
padding: "10px 15px",
margin: "0 0 10px 0",
width: "100%",
textAlign: "center",
borderRadius: "4px",
boxSizing: "border-box"
}
}),
$el("div", {
textContent: `${dependencies.filter(d => d.status === 'installed').length} already installed, ${dependencies.filter(d => d.status !== 'installed').length} to install`,
style: {
color: "#aaa",
fontSize: "12px",
marginBottom: "5px"
}
}),
tableContainer,
$el("div", {
style: {
display: "flex",
justifyContent: "space-between",
width: "100%",
gap: "10px",
marginTop: "10px"
}
}, [
$el("button", {
textContent: "Cancel",
onclick: () => {
onSelect(null); // Pass null to indicate cancellation
dialog.close();
},
style: {
flex: "1",
padding: "8px",
backgroundColor: "#4a4a4a",
color: "#ffffff",
border: "none",
borderRadius: "4px",
cursor: "pointer"
}
}),
$el("button", {
textContent: "Select All",
onclick: () => {
dependencies.forEach(dep => {
if (dep.status !== 'installed') {
dep.selected = true;
}
// Also select subdependencies
if(dep.subdependencies) {
dep.subdependencies.forEach(subdep => {
if(subdep.status !== 'installed') {
subdep.selected = true;
}
});
}
});
// Update checkboxes in the table
const checkboxes = tableContainer.querySelectorAll('input[type="checkbox"]');
checkboxes.forEach((checkbox) => {
if(!checkbox.disabled) {
checkbox.checked = true;
}
});
},
style: {
padding: "8px 15px",
backgroundColor: "#4a6a4a",
color: "#ffffff",
border: "none",
borderRadius: "4px",
cursor: "pointer"
}
}),
$el("button", {
textContent: "Install Selected",
onclick: () => {
// Collect all selected dependencies (main + subdependencies)
const selected = [];
dependencies.forEach(d => {
if(d.selected) {
selected.push(d);
}
// Also include selected subdependencies
if(d.subdependencies) {
d.subdependencies.forEach(subdep => {
if(subdep.selected) {
selected.push(subdep);
}
});
}
});
onSelect(selected);
dialog.close();
},
style: {
flex: "1",
padding: "8px",
backgroundColor: "#4CAF50",
color: "#ffffff",
border: "none",
borderRadius: "4px",
cursor: "pointer"
}
}),
])
]);
console.log('[Dependency Dialog] Showing dialog with', dependencies.length, 'dependencies');
dialog.show(content);
console.log('[Dependency Dialog] Dialog shown');
}
showVersionSelectorDialog(versions, onSelect) {
const dialog = new ComfyDialog();
dialog.element.style.zIndex = 1100;
@@ -462,7 +909,7 @@ export class CustomNodesManager {
".cn-manager-stop": {
click: () => {
api.fetchApi('/manager/queue/reset');
api.fetchApi('/manager/queue/reset', { method: 'POST' });
infoToast('Cancel', 'Remaining tasks will stop after completing the current task.');
}
},
@@ -1470,15 +1917,116 @@ export class CustomNodesManager {
}
}
// For install mode, analyze dependencies BEFORE starting installation
let selectedDependencies = [];
let dependencyDialogShown = false; // Track if dialog was shown
if(mode === "install" && this.analyzeDependenciesBeforeInstall) {
// Analyze dependencies for all items first (only if checkbox is enabled)
for (const hash of list) {
const item = this.grid.getRowItemBy("hash", hash);
if (!item) {
console.log('[Dependency Analysis] Item not found for hash:', hash);
continue;
}
const data = item.originalData;
console.log('[Dependency Analysis] Item data:', {
title: item.title,
files: data.files,
repository: data.repository,
hasFiles: !!data.files,
filesLength: data.files ? data.files.length : 0
});
// Try multiple ways to get the git URL
let gitUrl = null;
if(data.files && data.files.length > 0) {
gitUrl = data.files[0];
} else if(data.repository) {
gitUrl = data.repository;
}
if(gitUrl && (gitUrl.includes('github.com') || gitUrl.includes('.git'))) {
try {
this.showStatus(`Analyzing dependencies for ${item.title}...`);
console.log('[Dependency Analysis] Fetching dependencies for:', gitUrl);
const analyzeRes = await api.fetchApi('/customnode/analyze_dependencies', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url: gitUrl,
commitId: data.commit_id,
branch: data.branch
})
});
console.log('[Dependency Analysis] Response status:', analyzeRes.status);
if(analyzeRes.status === 200) {
const analyzeData = await analyzeRes.json();
console.log('[Dependency Analysis] Response data:', {
success: analyzeData.success,
hasDependencies: !!analyzeData.dependencies,
dependenciesCount: analyzeData.dependencies ? analyzeData.dependencies.length : 0,
noRequirementsFile: analyzeData.noRequirementsFile
});
if(analyzeData.success && analyzeData.dependencies && analyzeData.dependencies.length > 0) {
console.log('[Dependency Analysis] Showing dialog with', analyzeData.dependencies.length, 'dependencies');
dependencyDialogShown = true;
// Show dependency selection dialog and wait for user
const userSelection = await new Promise((resolve) => {
this.showDependencySelectorDialog(analyzeData.dependencies, (selected) => {
console.log('[Dependency Analysis] User selected:', selected);
resolve(selected);
});
});
// If user cancelled (null), stop installation
if(userSelection === null) {
console.log('[Dependency Analysis] User cancelled installation');
this.showStatus("Installation cancelled");
return;
}
selectedDependencies = userSelection || [];
console.log('[Dependency Analysis] Selected dependencies:', selectedDependencies.length);
} else if(analyzeData.noRequirementsFile) {
console.log('[Dependency Analysis] No requirements.txt file found');
} else {
console.log('[Dependency Analysis] No dependencies to show');
}
} else {
const errorText = await analyzeRes.text();
console.error('[Dependency Analysis] API error:', analyzeRes.status, errorText);
}
} catch(e) {
console.error('[Dependency Analysis] Exception:', e);
// Continue with installation even if dependency analysis fails
}
} else {
console.log('[Dependency Analysis] Not a GitHub URL or no URL found:', gitUrl);
}
}
}
target.classList.add("cn-btn-loading");
this.showError("");
let needRestart = false;
let errorMsg = "";
await api.fetchApi('/manager/queue/reset');
await api.fetchApi('/manager/queue/reset', { method: 'POST' });
// Set install_context BEFORE per-item queue enqueue calls so that any
// server-side synchronous completion (e.g., sync enable of an inactive
// node) that emits cm-queue-status before we return here still finds
// install_context populated in onQueueCompleted. target_items is shared
// by reference so further pushes below remain visible.
let target_items = [];
this.install_context = {btn: btn, targets: target_items};
for (const hash of list) {
const item = this.grid.getRowItemBy("hash", hash);
@@ -1505,6 +2053,46 @@ export class CustomNodesManager {
data.mode = this.mode;
data.ui_id = hash;
// Add selected dependencies to data (including subdependencies)
// Only install selected dependencies - respect user's selection
const allSelected = [];
if(selectedDependencies.length > 0) {
selectedDependencies.forEach(d => {
// Add main dependency if selected
if(d.selected) {
allSelected.push({
name: d.name,
version: d.version,
line: d.line
});
}
// Add selected subdependencies
if(d.subdependencies) {
d.subdependencies.forEach(subdep => {
if(subdep.selected) {
allSelected.push({
name: subdep.name,
version: subdep.version,
line: `${subdep.name}${subdep.version ? '==' + subdep.version : ''}`
});
}
});
}
});
}
// Set selectedDependencies:
// - If dialog was shown: always set (even if empty) to respect user's selection
// - If dialog was not shown: don't set (install all dependencies - original behavior)
if(dependencyDialogShown) {
// User saw the dialog, respect their selection (even if empty)
data.selectedDependencies = allSelected;
} else if(allSelected.length > 0) {
// Dialog wasn't shown but we have selections (shouldn't happen, but just in case)
data.selectedDependencies = allSelected;
}
// If dialog wasn't shown and no selections, don't set selectedDependencies
// This means backend will install all dependencies (original behavior)
let install_mode = mode;
if(mode == 'switch') {
install_mode = 'install';
@@ -1550,8 +2138,6 @@ export class CustomNodesManager {
}
}
this.install_context = {btn: btn, targets: target_items};
if(errorMsg) {
this.showError(errorMsg);
show_message("[Installation Errors]\n"+errorMsg);
@@ -1563,7 +2149,7 @@ export class CustomNodesManager {
}
}
else {
await api.fetchApi('/manager/queue/start');
await api.fetchApi('/manager/queue/start', { method: 'POST' });
this.showStop();
showTerminal();
}
@@ -1576,6 +2162,10 @@ export class CustomNodesManager {
const item = self.grid.getRowItemBy("hash", hash);
if (!item) {
return;
}
item.restart = true;
self.restartMap[item.hash] = true;
self.grid.updateCell(item, "action");
@@ -1583,45 +2173,81 @@ export class CustomNodesManager {
}
else if(event.detail.status == 'done') {
self.hideStop();
self.onQueueCompleted(event.detail);
// Await + error logging so any unhandled rejection surfaces to the
// console instead of silently swallowing completion finalization
// (root cause of disable/enable button staying loading with no toast).
try {
await self.onQueueCompleted(event.detail);
} catch (e) {
console.error("[ComfyUI-Manager] onQueueCompleted failed:", e);
}
}
}
async onQueueCompleted(info) {
// `nodepack_result` is a dict serialized from a Python dict, not an array.
// `dict.length` is `undefined` and `undefined == 0` is `false`, so the
// previous `result.length == 0` guard was a no-op; switch to a correct
// empty-check that also tolerates null/undefined.
let result = info.nodepack_result;
if(result.length == 0) {
if (!result || Object.keys(result).length === 0) {
return;
}
let self = CustomNodesManager.instance;
if(!self.install_context) {
if (!self || !self.install_context) {
return;
}
const { target, label, mode } = self.install_context.btn;
target.classList.remove("cn-btn-loading");
const targets = self.install_context.targets || [];
// Compute errorMsg upfront so the downstream user-visible finalization
// (showRestart / showMessage / infoToast) fires regardless of whether
// any DOM-touching step below throws.
let errorMsg = "";
for(let hash in result){
for (let hash in result) {
let v = result[hash];
if(v != 'success' && v != 'skip')
errorMsg += v+'\n';
if (v != 'success' && v != 'skip') {
errorMsg += v + '\n';
}
}
for(let k in self.install_context.targets) {
let item = self.install_context.targets[k];
self.grid.updateCell(item, "action");
// Defensive: `target` may be a detached DOM node (the in_progress
// handler's updateCell can re-render the row and replace the button
// element). classList.remove on a detached node is a no-op, but we
// still guard in case target was torn down entirely.
try {
if (target && target.classList) {
target.classList.remove("cn-btn-loading");
}
} catch (e) {
console.warn("[ComfyUI-Manager] Failed to clear button loading state:", e);
}
// Defensive: grid.updateCell can throw if the item was removed or the
// grid was re-rendered between in_progress and done. Do NOT let this
// loop abort the completion finalization below — that was the observed
// failure mode for disable/enable (no toast, no "restart required"
// message).
try {
for (let k in targets) {
let item = targets[k];
if (item) {
self.grid.updateCell(item, "action");
}
}
} catch (e) {
console.warn("[ComfyUI-Manager] Failed to refresh target cells after queue completion:", e);
}
if (errorMsg) {
self.showError(errorMsg);
show_message("Installation Error:\n"+errorMsg);
show_message("Installation Error:\n" + errorMsg);
} else {
self.showStatus(`${label} ${result.length} custom node(s) successfully`);
self.showStatus(`${label} ${Object.keys(result).length} custom node(s) successfully`);
}
self.showRestart();
+3 -3
View File
@@ -170,7 +170,7 @@ export class ModelManager {
".cmm-manager-stop": {
click: () => {
api.fetchApi('/manager/queue/reset');
api.fetchApi('/manager/queue/reset', { method: 'POST' });
infoToast('Cancel', 'Remaining tasks will stop after completing the current task.');
}
},
@@ -444,7 +444,7 @@ export class ModelManager {
let needRefresh = false;
let errorMsg = "";
await api.fetchApi('/manager/queue/reset');
await api.fetchApi('/manager/queue/reset', { method: 'POST' });
let target_items = [];
@@ -503,7 +503,7 @@ export class ModelManager {
}
}
else {
await api.fetchApi('/manager/queue/start');
await api.fetchApi('/manager/queue/start', { method: 'POST' });
this.showStop();
showTerminal();
}
+3 -3
View File
@@ -9,7 +9,7 @@ loadCss("./snapshot.css");
async function restore_snapshot(target) {
if(SnapshotManager.instance) {
try {
const response = await api.fetchApi(`/snapshot/restore?target=${target}`, { cache: "no-store" });
const response = await api.fetchApi('/snapshot/restore', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ target: target }), cache: "no-store" });
if(response.status == 403) {
await handle403Response(response);
@@ -37,7 +37,7 @@ async function restore_snapshot(target) {
async function remove_snapshot(target) {
if(SnapshotManager.instance) {
try {
const response = await api.fetchApi(`/snapshot/remove?target=${target}`, { cache: "no-store" });
const response = await api.fetchApi('/snapshot/remove', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ target: target }), cache: "no-store" });
if(response.status == 403) {
await handle403Response(response);
@@ -63,7 +63,7 @@ async function remove_snapshot(target) {
async function save_current_snapshot() {
try {
const response = await api.fetchApi('/snapshot/save', { cache: "no-store" });
const response = await api.fetchApi('/snapshot/save', { method: 'POST', cache: "no-store" });
app.ui.dialog.close();
return true;
}
+319
View File
@@ -5180,6 +5180,325 @@
"size": "25.75GB"
},
{
"name": "LTX-2 19B Dev FP8",
"type": "checkpoint",
"base": "LTX-2",
"save_path": "checkpoints/LTX-2",
"description": "LTX-2 19B Dev FP8 model.",
"reference": "https://huggingface.co/Lightricks/LTX-2",
"filename": "ltx-2-19b-dev-fp8.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2/resolve/main/ltx-2-19b-dev-fp8.safetensors",
"size": "27.1GB"
},
{
"name": "LTX-2 19B Distilled FP8",
"type": "checkpoint",
"base": "LTX-2",
"save_path": "checkpoints/LTX-2",
"description": "LTX-2 19B Distilled FP8 model.",
"reference": "https://huggingface.co/Lightricks/LTX-2",
"filename": "ltx-2-19b-distilled-fp8.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2/resolve/main/ltx-2-19b-distilled-fp8.safetensors",
"size": "27.1GB"
},
{
"name": "LTX-2 19B Dev",
"type": "checkpoint",
"base": "LTX-2",
"save_path": "checkpoints/LTX-2",
"description": "LTX-2 19B Dev model.",
"reference": "https://huggingface.co/Lightricks/LTX-2",
"filename": "ltx-2-19b-dev.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2/resolve/main/ltx-2-19b-dev.safetensors",
"size": "43.3GB"
},
{
"name": "LTX-2 19B Distilled",
"type": "checkpoint",
"base": "LTX-2",
"save_path": "checkpoints/LTX-2",
"description": "LTX-2 19B Distilled model.",
"reference": "https://huggingface.co/Lightricks/LTX-2",
"filename": "ltx-2-19b-distilled.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2/resolve/main/ltx-2-19b-distilled.safetensors",
"size": "43.3GB"
},
{
"name": "LTX-2 Spatial Upscaler",
"type": "upscale",
"base": "upscale",
"save_path": "default",
"description": "Spatial upscaler model for LTX-2. This model enhances the spatial resolution of generated videos.",
"reference": "https://huggingface.co/Lightricks/LTX-2",
"filename": "ltx-2-spatial-upscaler-x2-1.0.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2/resolve/main/ltx-2-spatial-upscaler-x2-1.0.safetensors",
"size": "996MB"
},
{
"name": "LTX-2 Temporal Upscaler",
"type": "upscale",
"base": "upscale",
"save_path": "default",
"description": "Temporal upscaler model for LTX-2. This model enhances the temporal resolution of generated videos.",
"reference": "https://huggingface.co/Lightricks/LTX-2",
"filename": "ltx-2-temporal-upscaler-x2-1.0.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2/resolve/main/ltx-2-temporal-upscaler-x2-1.0.safetensors",
"size": "262MB"
},
{
"name": "LTX-2 19B Distilled LoRA",
"type": "lora",
"base": "LTX-2",
"save_path": "loras",
"description": "A LoRA adapter that transforms the standard LTX-2 19B model into a distilled version when loaded.",
"reference": "https://huggingface.co/Lightricks/LTX-2",
"filename": "ltx-2-19b-distilled-lora-384.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2/resolve/main/ltx-2-19b-distilled-lora-384.safetensors",
"size": "7.67GB"
},
{
"name": "LTX-2 19B IC LoRA - Canny Control",
"type": "lora",
"base": "LTX-2",
"save_path": "loras/LTX-2",
"description": "LoRA for canny control on LTX-2 19B IC model. Intended for advanced edge control and guided generation.",
"reference": "https://huggingface.co/Lightricks/LTX-2-19b-IC-LoRA-Canny-Control",
"filename": "ltx-2-19b-ic-lora-canny-control.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2-19b-IC-LoRA-Canny-Control/resolve/main/ltx-2-19b-ic-lora-canny-control.safetensors",
"size": "654MB"
},
{
"name": "LTX-2 19B IC LoRA - Depth Control",
"type": "lora",
"base": "LTX-2",
"save_path": "loras/LTX-2",
"description": "LoRA for depth control on LTX-2 19B IC model. Adds depth-aware generation guidance.",
"reference": "https://huggingface.co/Lightricks/LTX-2-19b-IC-LoRA-Depth-Control",
"filename": "ltx-2-19b-ic-lora-depth-control.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2-19b-IC-LoRA-Depth-Control/resolve/main/ltx-2-19b-ic-lora-depth-control.safetensors",
"size": "654MB"
},
{
"name": "LTX-2 19B IC LoRA - Detailer",
"type": "lora",
"base": "LTX-2",
"save_path": "loras/LTX-2",
"description": "LoRA detailer for LTX-2 19B IC. Improves fine details and sharpness in generated outputs.",
"reference": "https://huggingface.co/Lightricks/LTX-2-19b-IC-LoRA-Detailer",
"filename": "ltx-2-19b-ic-lora-detailer.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2-19b-IC-LoRA-Detailer/resolve/main/ltx-2-19b-ic-lora-detailer.safetensors",
"size": "2.62GB"
},
{
"name": "LTX-2 19B IC LoRA - Pose Control",
"type": "lora",
"base": "LTX-2",
"save_path": "loras/LTX-2",
"description": "LoRA for pose control on LTX-2 19B IC model. Enables pose-guided image/video generation.",
"reference": "https://huggingface.co/Lightricks/LTX-2-19b-IC-LoRA-Pose-Control",
"filename": "ltx-2-19b-ic-lora-pose-control.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2-19b-IC-LoRA-Pose-Control/resolve/main/ltx-2-19b-ic-lora-pose-control.safetensors",
"size": "654MB"
},
{
"name": "LTX-2 19B LoRA - Camera Control Dolly In",
"type": "lora",
"base": "LTX-2",
"save_path": "loras/LTX-2",
"description": "LoRA for dolly-in camera control with LTX-2 19B. Simulates camera moving closer to subject.",
"reference": "https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Dolly-In",
"filename": "ltx-2-19b-lora-camera-control-dolly-in.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Dolly-In/resolve/main/ltx-2-19b-lora-camera-control-dolly-in.safetensors",
"size": "327MB"
},
{
"name": "LTX-2 19B LoRA - Camera Control Dolly Left",
"type": "lora",
"base": "LTX-2",
"save_path": "loras/LTX-2",
"description": "LoRA for dolly-left camera control with LTX-2 19B. Simulates camera moving left.",
"reference": "https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Dolly-Left",
"filename": "ltx-2-19b-lora-camera-control-dolly-left.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Dolly-Left/resolve/main/ltx-2-19b-lora-camera-control-dolly-left.safetensors",
"size": "327MB"
},
{
"name": "LTX-2 19B LoRA - Camera Control Dolly Out",
"type": "lora",
"base": "LTX-2",
"save_path": "loras/LTX-2",
"description": "LoRA for dolly-out camera control with LTX-2 19B. Simulates camera moving away from subject.",
"reference": "https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Dolly-Out",
"filename": "ltx-2-19b-lora-camera-control-dolly-out.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Dolly-Out/resolve/main/ltx-2-19b-lora-camera-control-dolly-out.safetensors",
"size": "327MB"
},
{
"name": "LTX-2 19B LoRA - Camera Control Dolly Right",
"type": "lora",
"base": "LTX-2",
"save_path": "loras/LTX-2",
"description": "LoRA for dolly-right camera control with LTX-2 19B. Simulates camera moving right.",
"reference": "https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Dolly-Right",
"filename": "ltx-2-19b-lora-camera-control-dolly-right.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Dolly-Right/resolve/main/ltx-2-19b-lora-camera-control-dolly-right.safetensors",
"size": "327MB"
},
{
"name": "LTX-2 19B LoRA - Camera Control Jib Down",
"type": "lora",
"base": "LTX-2",
"save_path": "loras/LTX-2",
"description": "LoRA for jib-down camera control with LTX-2 19B. Simulates vertical camera movement downwards.",
"reference": "https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Jib-Down",
"filename": "ltx-2-19b-lora-camera-control-jib-down.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Jib-Down/resolve/main/ltx-2-19b-lora-camera-control-jib-down.safetensors",
"size": "2.21GB"
},
{
"name": "LTX-2 19B LoRA - Camera Control Jib Up",
"type": "lora",
"base": "LTX-2",
"save_path": "loras/LTX-2",
"description": "LoRA for jib-up camera control with LTX-2 19B. Simulates vertical camera movement upwards.",
"reference": "https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Jib-Up",
"filename": "ltx-2-19b-lora-camera-control-jib-up.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Jib-Up/resolve/main/ltx-2-19b-lora-camera-control-jib-up.safetensors",
"size": "2.21GB"
},
{
"name": "LTX-2 19B LoRA - Camera Control Static",
"type": "lora",
"base": "LTX-2",
"save_path": "loras/LTX-2",
"description": "LoRA for static camera control with LTX-2 19B. Simulates stationary/static camera view.",
"reference": "https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Static",
"filename": "ltx-2-19b-lora-camera-control-static.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Static/resolve/main/ltx-2-19b-lora-camera-control-static.safetensors",
"size": "2.21GB"
},
{
"name": "LTX-2.3 22B Dev",
"type": "checkpoint",
"base": "LTX-2.3",
"save_path": "checkpoints/LTX-2.3",
"description": "LTX-2.3 22B development model.",
"reference": "https://huggingface.co/Lightricks/LTX-2.3",
"filename": "ltx-2.3-22b-dev.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2.3/resolve/main/ltx-2.3-22b-dev.safetensors",
"size": "42.98GB"
},
{
"name": "LTX-2.3 22B Distilled 1.1",
"type": "checkpoint",
"base": "LTX-2.3",
"save_path": "checkpoints/LTX-2.3",
"description": "LTX-2.3 22B distilled model v1.1.",
"reference": "https://huggingface.co/Lightricks/LTX-2.3",
"filename": "ltx-2.3-22b-distilled-1.1.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2.3/resolve/main/ltx-2.3-22b-distilled-1.1.safetensors",
"size": "42.98GB"
},
{
"name": "LTX-2.3 22B Dev FP8",
"type": "checkpoint",
"base": "LTX-2.3",
"save_path": "checkpoints/LTX-2.3",
"description": "LTX-2.3 22B Dev FP8 model.",
"reference": "https://huggingface.co/Lightricks/LTX-2.3-fp8",
"filename": "ltx-2.3-22b-dev-fp8.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2.3-fp8/resolve/main/ltx-2.3-22b-dev-fp8.safetensors",
"size": "27.14GB"
},
{
"name": "LTX-2.3 22B Distilled FP8",
"type": "checkpoint",
"base": "LTX-2.3",
"save_path": "checkpoints/LTX-2.3",
"description": "LTX-2.3 22B Distilled FP8 model.",
"reference": "https://huggingface.co/Lightricks/LTX-2.3-fp8",
"filename": "ltx-2.3-22b-distilled-fp8.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2.3-fp8/resolve/main/ltx-2.3-22b-distilled-fp8.safetensors",
"size": "27.50GB"
},
{
"name": "LTX-2.3 22B Dev NVFP4",
"type": "checkpoint",
"base": "LTX-2.3",
"save_path": "checkpoints/LTX-2.3",
"description": "LTX-2.3 22B Dev NVFP4 model.",
"reference": "https://huggingface.co/Lightricks/LTX-2.3-nvfp4",
"filename": "ltx-2.3-22b-dev-nvfp4.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2.3-nvfp4/resolve/main/ltx-2.3-22b-dev-nvfp4.safetensors",
"size": "20.21GB"
},
{
"name": "LTX-2.3 22B Distilled LoRA 1.1",
"type": "lora",
"base": "LTX-2.3",
"save_path": "loras/ltxv/ltx2",
"description": "A LoRA adapter that transforms the standard LTX-2.3 22B model into a distilled version when loaded.",
"reference": "https://huggingface.co/Lightricks/LTX-2.3",
"filename": "ltx-2.3-22b-distilled-lora-384-1.1.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2.3/resolve/main/ltx-2.3-22b-distilled-lora-384-1.1.safetensors",
"size": "7.08GB"
},
{
"name": "LTX-2.3 22B IC-LoRA Union Control",
"type": "lora",
"base": "LTX-2.3",
"save_path": "loras/ltxv/ltx2",
"description": "In-Context LoRA (IC LoRA) for unified multi-condition control (canny, depth, pose) guided video generation.",
"reference": "https://huggingface.co/Lightricks/LTX-2.3-22b-IC-LoRA-Union-Control",
"filename": "ltx-2.3-22b-ic-lora-union-control-ref0.5.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2.3-22b-IC-LoRA-Union-Control/resolve/main/ltx-2.3-22b-ic-lora-union-control-ref0.5.safetensors",
"size": "0.61GB"
},
{
"name": "LTX-2.3 22B IC-LoRA Motion Track Control",
"type": "lora",
"base": "LTX-2.3",
"save_path": "loras/ltxv/ltx2",
"description": "In-Context LoRA (IC LoRA) for motion track guided video generation.",
"reference": "https://huggingface.co/Lightricks/LTX-2.3-22b-IC-LoRA-Motion-Track-Control",
"filename": "ltx-2.3-22b-ic-lora-motion-track-control-ref0.5.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2.3-22b-IC-LoRA-Motion-Track-Control/resolve/main/ltx-2.3-22b-ic-lora-motion-track-control-ref0.5.safetensors",
"size": "0.30GB"
},
{
"name": "LTX-2.3 Spatial Upscaler x2 1.1",
"type": "upscale",
"base": "upscale",
"save_path": "latent_upscale_models",
"description": "Spatial upscaler model for LTX-2.3. This model enhances the spatial resolution of generated videos by 2x.",
"reference": "https://huggingface.co/Lightricks/LTX-2.3",
"filename": "ltx-2.3-spatial-upscaler-x2-1.1.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2.3/resolve/main/ltx-2.3-spatial-upscaler-x2-1.1.safetensors",
"size": "0.93GB"
},
{
"name": "LTX-2.3 Spatial Upscaler x1.5",
"type": "upscale",
"base": "upscale",
"save_path": "latent_upscale_models",
"description": "Spatial upscaler model for LTX-2.3. This model enhances the spatial resolution of generated videos by 1.5x.",
"reference": "https://huggingface.co/Lightricks/LTX-2.3",
"filename": "ltx-2.3-spatial-upscaler-x1.5-1.0.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2.3/resolve/main/ltx-2.3-spatial-upscaler-x1.5-1.0.safetensors",
"size": "1.02GB"
},
{
"name": "LTX-2.3 Temporal Upscaler x2",
"type": "upscale",
"base": "upscale",
"save_path": "latent_upscale_models",
"description": "Temporal upscaler model for LTX-2.3. This model enhances the temporal resolution of generated videos.",
"reference": "https://huggingface.co/Lightricks/LTX-2.3",
"filename": "ltx-2.3-temporal-upscaler-x2-1.0.safetensors",
"url": "https://huggingface.co/Lightricks/LTX-2.3/resolve/main/ltx-2.3-temporal-upscaler-x2-1.0.safetensors",
"size": "0.24GB"
},
{
"name": "LTX-Video Spatial Upscaler v0.9.7",
"type": "upscale",
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+2519 -1619
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+11 -1
View File
@@ -371,6 +371,16 @@
],
"install_type": "git-clone",
"description": "Complete ComfyUI development toolkit with 8 professional nodes including VAE tools, universal type testing, and comprehensive debugging infrastructure."
}
},
{
"author": "ganlvtech",
"title": "ComfyUI-CustomModelPatcher",
"reference": "https://github.com/ganlvtech/ComfyUI-CustomModelPatcher",
"files": [
"https://github.com/ganlvtech/ComfyUI-CustomModelPatcher"
],
"install_type": "git-clone",
"description": "Demonstrates GPU memory management techniques for external models like onnxruntime and InsightFace in ComfyUI by pre-allocating VRAM. (Description by CC)"
}
]
}
+188 -29
View File
@@ -191,11 +191,20 @@ paths:
description: Mapping of node packages to node classes
/customnode/fetch_updates:
get:
post:
summary: Check for updates
description: Fetches updates for custom nodes
parameters:
- $ref: '#/components/parameters/modeParam'
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
mode:
type: string
enum: [local, remote, default]
description: Source mode (e.g., "local", "remote")
responses:
'200':
description: No updates available
@@ -423,13 +432,22 @@ paths:
# Queue Management Endpoints
/manager/queue/update_all:
get:
post:
summary: Update all custom nodes
description: Queues update operations for all installed custom nodes
security:
- securityLevel: []
parameters:
- $ref: '#/components/parameters/modeParam'
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
mode:
type: string
enum: [local, remote, default]
description: Source mode (e.g., "local", "remote")
responses:
'200':
description: Update queued successfully
@@ -439,7 +457,7 @@ paths:
description: Security policy violation
/manager/queue/reset:
get:
post:
summary: Reset queue
description: Resets the operation queue
responses:
@@ -479,7 +497,7 @@ paths:
description: Target node not found or security issue
/manager/queue/start:
get:
post:
summary: Start queue processing
description: Starts processing the operation queue
responses:
@@ -575,7 +593,7 @@ paths:
description: Disable operation queued successfully
/manager/queue/update_comfyui:
get:
post:
summary: Update ComfyUI
description: Queues an update operation for ComfyUI itself
responses:
@@ -621,13 +639,22 @@ paths:
$ref: '#/components/schemas/SnapshotItem'
/snapshot/remove:
get:
post:
summary: Remove snapshot
description: Removes a specified snapshot
security:
- securityLevel: []
parameters:
- $ref: '#/components/parameters/targetParam'
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [target]
properties:
target:
type: string
description: Target identifier
responses:
'200':
description: Snapshot removed successfully
@@ -637,13 +664,22 @@ paths:
description: Security policy violation
/snapshot/restore:
get:
post:
summary: Restore snapshot
description: Restores a specified snapshot
security:
- securityLevel: []
parameters:
- $ref: '#/components/parameters/targetParam'
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [target]
properties:
target:
type: string
description: Target identifier
responses:
'200':
description: Snapshot restoration scheduled
@@ -667,7 +703,7 @@ paths:
description: Error creating snapshot
/snapshot/save:
get:
post:
summary: Save snapshot
description: Saves the current system state as a new snapshot
responses:
@@ -699,15 +735,19 @@ paths:
description: Error retrieving versions
/comfyui_manager/comfyui_switch_version:
get:
post:
summary: Switch ComfyUI version
description: Switches to a specified ComfyUI version
parameters:
- name: ver
in: query
description: Target version
schema:
type: string
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
ver:
type: string
description: Target version
responses:
'200':
description: Version switch successful
@@ -715,7 +755,7 @@ paths:
description: Error switching version
/manager/reboot:
get:
post:
summary: Reboot ComfyUI
description: Restarts the ComfyUI server
security:
@@ -746,7 +786,32 @@ paths:
text/plain:
schema:
type: string
post:
summary: Set preview method
description: Sets the latent preview method (write-only; use GET to read)
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [value]
properties:
value:
type: string
enum: [auto, latent2rgb, taesd, none]
description: New preview method
responses:
'200':
description: Setting updated
content:
text/plain:
schema:
type: string
'400':
description: Invalid value
/manager/db_mode:
get:
summary: Get or set database mode
@@ -766,7 +831,32 @@ paths:
text/plain:
schema:
type: string
post:
summary: Set database mode
description: Sets the database mode (write-only; use GET to read)
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [value]
properties:
value:
type: string
enum: [channel, local, remote]
description: New database mode
responses:
'200':
description: Setting updated
content:
text/plain:
schema:
type: string
'400':
description: Invalid value
/manager/policy/component:
get:
summary: Get or set component policy
@@ -785,7 +875,29 @@ paths:
text/plain:
schema:
type: string
post:
summary: Set component policy
description: Sets the component policy (write-only; use GET to read)
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [value]
properties:
value:
type: string
description: New component policy
responses:
'200':
description: Setting updated
content:
text/plain:
schema:
type: string
/manager/policy/update:
get:
summary: Get or set update policy
@@ -805,7 +917,32 @@ paths:
text/plain:
schema:
type: string
post:
summary: Set update policy
description: Sets the update policy (write-only; use GET to read)
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [value]
properties:
value:
type: string
enum: [stable, nightly, nightly-comfyui]
description: New update policy
responses:
'200':
description: Setting updated
content:
text/plain:
schema:
type: string
'400':
description: Invalid value
/manager/channel_url_list:
get:
summary: Get or set channel URL
@@ -836,7 +973,29 @@ paths:
type: string
url:
type: string
post:
summary: Set channel URL
description: Sets the channel URL for custom node sources (write-only; use GET to read current + list)
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [value]
properties:
value:
type: string
description: New channel name
responses:
'200':
description: Setting updated
content:
text/plain:
schema:
type: string
# Component Management Endpoints
/manager/component/save:
post:
+3 -2
View File
@@ -1,13 +1,14 @@
[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.39.2"
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"]
[project.urls]
Repository = "https://github.com/ltdrdata/ComfyUI-Manager"
# Used by Comfy Registry https://comfyregistry.org
# Used by Comfy Registry https://registry.comfy.org
[tool.comfy]
PublisherId = "drltdata"
+101 -3
View File
@@ -2,6 +2,8 @@ import ast
import re
import os
import json
import threading
from collections import defaultdict
from git import Repo
import concurrent
import datetime
@@ -15,12 +17,13 @@ builtin_nodes = set()
import sys
from urllib.parse import urlparse
from urllib3.util.retry import Retry
from github import Github, Auth
from pathlib import Path
from typing import Set, Dict, Optional
# Scanner version for cache invalidation
SCANNER_VERSION = "2.0.12" # Add dict comprehension + export list detection
SCANNER_VERSION = "2.0.13" # Add fallback for dynamic v3 node_id
# Cache for extract_nodes and extract_nodes_enhanced results
_extract_nodes_cache: Dict[str, Set[str]] = {}
@@ -195,6 +198,82 @@ g = None
parse_cnt = 0
# Thread-safe git error state
_git_error_lock = threading.Lock()
_git_errors: defaultdict = defaultdict(list) # category -> list[{'repo': str, 'op': str, 'msg': str}]
# Ordered categories: (key, display label, compiled regex). First match wins.
# Single source of truth — add new categories here only.
_GIT_ERROR_CATEGORIES = [
('repository_not_found', 'Repository Not Found', re.compile(
r'repository\s+not\s+found|does\s+not\s+exist|\b404\b|remote:\s*repository\s+not\s+found',
re.IGNORECASE
)),
('divergent_branch', 'Divergent Branch', re.compile(
r'divergent\s+branches|need\s+to\s+specify\s+how\s+to\s+reconcile\s+divergent\s+branches',
re.IGNORECASE
)),
('auth_failed', 'Authentication Failed', re.compile(
r'authentication\s+failed|could\s+not\s+read\s+username|invalid\s+username|invalid\s+password|auth\s+failed',
re.IGNORECASE
)),
('network_error', 'Network Error', re.compile(
r'could\s+not\s+resolve\s+host|connection\s+refused|timed?\s*out|failed\s+to\s+connect|'
r'network\s+is\s+unreachable|temporary\s+failure\s+in\s+name\s+resolution',
re.IGNORECASE
)),
('merge_conflict', 'Merge Conflict', re.compile(
r'merge\s+conflict|\bCONFLICT\b|automatic\s+merge\s+failed',
re.IGNORECASE
)),
('permission_denied', 'Permission Denied', re.compile(
r'permission\s+denied|access\s+denied|operation\s+not\s+permitted|publickey',
re.IGNORECASE
)),
]
def _categorize_git_error(error_str: str) -> str:
"""Classify a git error string into a category. First match wins."""
for category, _label, pattern in _GIT_ERROR_CATEGORIES:
if pattern.search(error_str):
return category
return 'other'
def _record_git_error(repo_name: str, op: str, error: Exception) -> None:
"""Record a git error in the thread-safe collector."""
category = _categorize_git_error(str(error))
with _git_error_lock:
_git_errors[category].append({'repo': repo_name, 'op': op, 'msg': str(error)})
def _report_git_errors() -> None:
"""Print a grouped summary of git errors by category."""
if not _git_errors:
return
total = sum(len(v) for v in _git_errors.values())
print(f"\n{'='*60}")
print(f"Git Operation Errors Summary: {total} failure(s)")
print(f"{'='*60}")
for category, label, _pattern in _GIT_ERROR_CATEGORIES:
entries = _git_errors.get(category, [])
if not entries:
continue
print(f"\n[{label}] ({len(entries)} repo(s))")
for entry in entries:
print(f"{entry['repo']} ({entry['op']}): {entry['msg']}")
other_entries = _git_errors.get('other', [])
if other_entries:
print(f"\n[Other] ({len(other_entries)} repo(s))")
for entry in other_entries:
print(f"{entry['repo']} ({entry['op']}): {entry['msg']}")
print(f"{'='*60}\n")
def extract_nodes(code_text):
global parse_cnt
@@ -936,6 +1015,9 @@ def extract_v3_nodes(code_text):
node_id = extract_node_id_from_schema(node)
if node_id:
nodes.add(node_id)
else:
# Fallback: use class name when node_id is dynamic/empty
nodes.add(node.name)
return nodes
@@ -1157,7 +1239,7 @@ def clone_or_pull_git_repository(git_url):
repo_name = git_url.split("/")[-1]
if repo_name.endswith(".git"):
repo_name = repo_name[:-4]
repo_dir = os.path.join(temp_dir, repo_name)
if os.path.exists(repo_dir):
@@ -1169,12 +1251,14 @@ def clone_or_pull_git_repository(git_url):
print(f"Pulling {repo_name}...")
except Exception as e:
print(f"Failed to pull '{repo_name}': {e}")
_record_git_error(repo_name, 'pull', e)
else:
try:
Repo.clone_from(git_url, repo_dir, recursive=True)
print(f"Cloning {repo_name}...")
except Exception as e:
print(f"Failed to clone '{repo_name}': {e}")
_record_git_error(repo_name, 'clone', e)
def update_custom_nodes(scan_only_mode=False, url_list_file=None):
@@ -1325,11 +1409,18 @@ def update_custom_nodes(scan_only_mode=False, url_list_file=None):
if not skip_stat_update:
process_git_stats(git_url_titles_preemptions)
# Reset error collector before this run
with _git_error_lock:
_git_errors.clear()
# Git clone/pull for all repositories
with concurrent.futures.ThreadPoolExecutor(11) as executor:
for url, title, preemptions, node_pattern in git_url_titles_preemptions:
executor.submit(process_git_url_title, url, title, preemptions, node_pattern)
# Report any git errors grouped by category (after all workers complete)
_report_git_errors()
# .py file download (skip in scan-only mode - only process git repos)
if not scan_only_mode:
py_url_titles_and_pattern = get_py_urls_from_json('custom-node-list.json')
@@ -1529,7 +1620,14 @@ if __name__ == "__main__":
if not skip_stat_update:
auth = Auth.Token(os.environ.get('GITHUB_TOKEN'))
g = Github(auth=auth)
# Use a plain urllib3 Retry (NOT PyGithub's default GithubRetry) so that
# a GitHub rate-limit response (403/429) raises RateLimitExceededException
# IMMEDIATELY instead of sleeping until the rate-limit reset and retrying
# up to 10x. The except-block in renew_stat() then catches it and skips
# (returns None). 403/429 are intentionally NOT in status_forcelist, so
# they fail fast; only transient 5xx errors are retried.
g = Github(auth=auth, retry=Retry(total=2, backoff_factor=0.5,
status_forcelist=[500, 502, 503, 504]))
else:
g = None
+1 -1
View File
@@ -4,7 +4,7 @@ git clone https://github.com/ltdrdata/ComfyUI-Manager comfyui-manager
cd ..
python -m venv venv
source venv/bin/activate
python -m pip install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu121
python -m pip install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu130
python -m pip install -r requirements.txt
python -m pip install -r custom_nodes/comfyui-manager/requirements.txt
cd ..
+1 -1
View File
@@ -4,7 +4,7 @@ git clone https://github.com/ltdrdata/ComfyUI-Manager comfyui-manager
cd ..
python -m venv venv
call venv/Scripts/activate
python -m pip install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu121
python -m pip install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu130
python -m pip install -r requirements.txt
python -m pip install -r custom_nodes/comfyui-manager/requirements.txt
cd ..
+41
View File
@@ -0,0 +1,41 @@
"""Test-runner guard for the GOAL #32 tests/ modules.
WHY THIS FILE EXISTS (collection hazard, not test logic):
The repo root contains ``__init__.py`` the ComfyUI plugin entrypoint
which at import time appends ``glob/`` to sys.path and imports
``manager_server`` (which needs ``folder_paths`` / ``comfy.cli_args`` /
a constructed ``PromptServer``). pytest 8 collects any ancestor
directory that carries an ``__init__.py`` as a ``Package`` node and
IMPORTS that ``__init__.py`` during test setup (observed module name:
``__init__``). Outside a live ComfyUI process that import can never
succeed, so EVERY test under tests/ errors at setup including the
pre-existing tests/test_csrf_content_type_helper.py whenever pytest's
rootdir ends up at or above the repo root (e.g. running inside a git
worktree nested under the parent checkout).
The guard below pre-seeds ``sys.modules`` with an inert stub whose
``__file__`` matches the real path, so pytest's
``import_path(<repo-root>/__init__.py)`` resolves to the stub without
executing the plugin entrypoint. Conftest files load before the setup
phase, so the stub is always in place in time. This does NOT touch
production code and does NOT alter what the tests import themselves
(they use AST-extraction / subprocess isolation per the
tests/test_csrf_content_type_helper.py precedent ``glob/`` is never
added to the runner's sys.path).
"""
import sys
import types
from pathlib import Path
_REPO_ROOT = Path(__file__).resolve().parent.parent
_ROOT_INIT = _REPO_ROOT / "__init__.py"
if _ROOT_INIT.exists() and "__init__" not in sys.modules:
_stub = types.ModuleType("__init__")
_stub.__file__ = str(_ROOT_INIT)
_stub.__doc__ = (
"Inert stand-in for the ComfyUI-Manager plugin entrypoint; "
"see tests/conftest.py for rationale."
)
sys.modules["__init__"] = _stub
+169
View File
@@ -0,0 +1,169 @@
#!/usr/bin/env bash
# setup_e2e_env.sh — E2E environment builder for GOAL #60 (T1, spec §2).
#
# Builds the DISPOSABLE test ComfyUI root used by
# tests/e2e/test_e2e_install_flags.py. ENV BUILD ONLY: donor steps 4-5
# (editable pip install of the Manager + custom_nodes symlink) are
# deliberately DROPPED — the Manager is mounted via `git worktree add`
# by the `mount_worktree` session fixture in the test module, which is
# the SOLE owner of mount create/reuse/teardown (spec §2 T1
# single-ownership rule). This script never touches the Manager repo.
#
# Idempotent: re-run is a no-op when the marker + key artifacts exist
# (E2E-SC-01).
#
# Input env vars:
# E2E_COMFYUI_ROOT — target directory (default: mktemp -d)
# COMFYUI_BRANCH — ComfyUI clone ref (default: master; Q-2)
# PYTHON — python executable for version probe (default: python3)
#
# Output (last line of stdout):
# E2E_COMFYUI_ROOT=/path/to/environment
#
# Exit: 0=success, 1=failure
set -euo pipefail
COMFYUI_REPO="https://github.com/comfyanonymous/ComfyUI.git"
PYTORCH_CPU_INDEX="https://download.pytorch.org/whl/cpu"
# Minimal seed config. use_uv=false: the venv is seeded with pip
# (`uv venv --seed`), so the Manager's make_pip_cmd resolves to
# `<venv-python> -m pip` and the suite's pip-uninstall hygiene helpers
# work without uv on PATH at server runtime. The install flags are NOT
# seeded here — stage_flags.sh stages them per launch identity (SC-06).
CONFIG_INI_CONTENT="[default]
file_logging = false
use_uv = false"
log() { echo "[setup_e2e] $*"; }
err() { echo "[setup_e2e] ERROR: $*" >&2; }
die() { err "$@"; exit 1; }
validate_prerequisites() {
local py="${PYTHON:-python3}"
local missing=()
command -v git >/dev/null 2>&1 || missing+=("git")
command -v uv >/dev/null 2>&1 || missing+=("uv")
command -v "$py" >/dev/null 2>&1 || missing+=("$py")
if [[ ${#missing[@]} -gt 0 ]]; then
die "Missing prerequisites: ${missing[*]}"
fi
local py_version major minor
py_version=$("$py" -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')")
major="${py_version%%.*}"
minor="${py_version##*.}"
if [[ "$major" -lt 3 ]] || { [[ "$major" -eq 3 ]] && [[ "$minor" -lt 9 ]]; }; then
die "Python 3.9+ required, found $py_version"
fi
log "Prerequisites OK (python=$py_version)"
}
check_already_setup() {
local root="$1"
if [[ -f "$root/.e2e_setup_complete" ]] \
&& [[ -d "$root/comfyui" ]] \
&& [[ -d "$root/venv" ]] \
&& [[ -f "$root/comfyui/user/__manager/config.ini" ]]; then
log "Environment already set up at $root (marker exists). Skipping. (E2E-SC-01 idempotence)"
echo "E2E_COMFYUI_ROOT=$root"
exit 0
fi
}
verify_setup() {
local root="$1"
local venv_py="$root/venv/bin/python"
local errors=0
log "Running verification checks..."
[[ -f "$root/comfyui/main.py" ]] || { err "Verification FAIL: comfyui/main.py not found"; ((errors+=1)); }
[[ -x "$venv_py" ]] || { err "Verification FAIL: venv python not executable"; ((errors+=1)); }
[[ -f "$root/comfyui/user/__manager/config.ini" ]] || { err "Verification FAIL: config.ini not found"; ((errors+=1)); }
# venv must carry pip (uv venv --seed) — the suite's hygiene helpers
# and the Manager's reservation-consuming boot both call `-m pip`.
if ! "$venv_py" -m pip --version >/dev/null 2>&1; then
err "Verification FAIL: venv pip not available"
((errors+=1))
fi
# comfy is a local package inside the ComfyUI checkout
if ! PYTHONPATH="$root/comfyui" "$venv_py" -c "import comfy" 2>/dev/null; then
err "Verification FAIL: 'import comfy' failed"
((errors+=1))
fi
# [D2] half-check at setup time: the Manager must NOT be pip-installed
# into this venv (the worktree mount is the only delivery mechanism).
if "$venv_py" -m pip show comfyui-manager >/dev/null 2>&1 \
|| "$venv_py" -m pip show ComfyUI-Manager >/dev/null 2>&1; then
err "Verification FAIL: a pip-installed Manager exists in the venv (wrong layout for [D2])"
((errors+=1))
fi
if [[ "$errors" -gt 0 ]]; then
die "Verification failed with $errors error(s)"
fi
log "Verification OK: all checks passed"
}
# ===== Main =====
validate_prerequisites
PYTHON="${PYTHON:-python3}"
COMFYUI_BRANCH="${COMFYUI_BRANCH:-master}"
CREATED_BY_US=false
if [[ -z "${E2E_COMFYUI_ROOT:-}" ]]; then
E2E_COMFYUI_ROOT="$(mktemp -d -t e2e_comfyui_XXXXXX)"
CREATED_BY_US=true
log "Created E2E_COMFYUI_ROOT=$E2E_COMFYUI_ROOT"
else
mkdir -p "$E2E_COMFYUI_ROOT"
log "Using E2E_COMFYUI_ROOT=$E2E_COMFYUI_ROOT"
fi
check_already_setup "$E2E_COMFYUI_ROOT"
cleanup_on_failure() {
local exit_code=$?
if [[ "$exit_code" -ne 0 ]] && [[ "$CREATED_BY_US" == "true" ]]; then
err "Setup failed. Cleaning up $E2E_COMFYUI_ROOT"
rm -rf "$E2E_COMFYUI_ROOT"
fi
}
trap cleanup_on_failure EXIT
log "Step 1/5: Cloning ComfyUI (branch=$COMFYUI_BRANCH)..."
if [[ -d "$E2E_COMFYUI_ROOT/comfyui/.git" ]]; then
log " ComfyUI already cloned, skipping"
else
git clone --depth=1 --branch "$COMFYUI_BRANCH" "$COMFYUI_REPO" "$E2E_COMFYUI_ROOT/comfyui"
fi
log "Step 2/5: Creating virtual environment (seeded with pip)..."
if [[ -d "$E2E_COMFYUI_ROOT/venv" ]]; then
log " venv already exists, skipping"
else
uv venv --seed "$E2E_COMFYUI_ROOT/venv"
fi
VENV_PY="$E2E_COMFYUI_ROOT/venv/bin/python"
log "Step 3/5: Installing ComfyUI dependencies (CPU-only torch index)..."
uv pip install \
--python "$VENV_PY" \
-r "$E2E_COMFYUI_ROOT/comfyui/requirements.txt" \
--extra-index-url "$PYTORCH_CPU_INDEX"
log "Step 4/5: Writing seed config.ini + HOME isolation dirs..."
mkdir -p "$E2E_COMFYUI_ROOT/comfyui/user/__manager"
echo "$CONFIG_INI_CONTENT" > "$E2E_COMFYUI_ROOT/comfyui/user/__manager/config.ini"
mkdir -p "$E2E_COMFYUI_ROOT/home/.config"
mkdir -p "$E2E_COMFYUI_ROOT/home/.local/share"
mkdir -p "$E2E_COMFYUI_ROOT/logs"
mkdir -p "$E2E_COMFYUI_ROOT/comfyui/custom_nodes"
log "Step 5/5: Verifying setup..."
verify_setup "$E2E_COMFYUI_ROOT"
# Marker written ONLY after verification passes (E2E-SC-01)
date -Iseconds > "$E2E_COMFYUI_ROOT/.e2e_setup_complete"
trap - EXIT
log "Setup complete."
echo "E2E_COMFYUI_ROOT=$E2E_COMFYUI_ROOT"
+82
View File
@@ -0,0 +1,82 @@
#!/usr/bin/env bash
# stage_flags.sh — Per-launch-identity config staging (T4, spec §1.4).
#
# Analog of the donor's start_comfyui_strict.sh config patching, split
# out as a pure staging script (launch is a separate step; the pytest
# fixture owns restore+delete of the backup at teardown — donor
# symmetry, spec §1.4).
#
# Modes (arg $1):
# deny — REMOVE both flag keys (L-D: flags ABSENT also live-proves
# "missing key reads false")
# allow — set allow_git_url_install = true AND allow_pip_install = true
# (L-A / L-P)
#
# Backup: config.ini.before-flags, created ONLY if not already present
# (crashed-run-safe — preserves the true baseline across crashed runs).
# Restore + DELETE of the backup happens in the pytest fixture teardown,
# NOT here (E2E-SC-06/07).
#
# Input env vars:
# E2E_COMFYUI_ROOT — (required)
#
# Exit: 0=staged, 1=failure
set -euo pipefail
MODE="${1:-}"
log() { echo "[stage_flags] $*"; }
err() { echo "[stage_flags] ERROR: $*" >&2; }
die() { err "$@"; exit 1; }
[[ -n "${E2E_COMFYUI_ROOT:-}" ]] || die "E2E_COMFYUI_ROOT is not set"
[[ "$MODE" == "deny" || "$MODE" == "allow" ]] || die "usage: stage_flags.sh deny|allow"
CONFIG="$E2E_COMFYUI_ROOT/comfyui/user/__manager/config.ini"
BACKUP="$CONFIG.before-flags"
[[ -f "$CONFIG" ]] || die "config not found at $CONFIG (run setup_e2e_env.sh first)"
# Backup ONLY if absent (crashed-run-safe; SC-06)
if [[ ! -f "$BACKUP" ]]; then
cp "$CONFIG" "$BACKUP"
log "Backed up original config to $BACKUP"
else
log "Backup already present at $BACKUP (preserving original baseline)"
fi
stage_key() {
local key="$1" value="$2"
if grep -qE "^${key}\s*=" "$CONFIG"; then
sed -i -E "s|^${key}\s*=.*|${key} = ${value}|" "$CONFIG"
else
# The append-after target MUST exist, otherwise the sed below is a
# silent no-op and the flag never lands (false-PASS for the allow arm).
grep -qE "^\[default\]" "$CONFIG" \
|| die "no [default] section in $CONFIG — cannot stage '${key}' (would silently no-op)"
sed -i -E "/^\[default\]/a ${key} = ${value}" "$CONFIG"
fi
}
remove_key() {
local key="$1"
sed -i -E "/^${key}\s*=/d" "$CONFIG"
}
case "$MODE" in
deny)
remove_key "allow_git_url_install"
remove_key "allow_pip_install"
log "Staged DENY config (both flags ABSENT — missing key reads false)"
;;
allow)
stage_key "allow_git_url_install" "true"
stage_key "allow_pip_install" "true"
log "Staged ALLOW config (both flags = true)"
;;
esac
# The staged value takes effect ONLY on the NEXT launch (restart-only
# cached_config — by construction, no hot-reload assumption; SC-06).
log "Staged config at $CONFIG (effective on next launch)"
+142
View File
@@ -0,0 +1,142 @@
#!/usr/bin/env bash
# start_comfyui.sh — Foreground-blocking ComfyUI launcher (T2, spec §1.3).
#
# Starts ComfyUI in the background and blocks until the server answers
# GET /system_stats (or timeout). Deltas vs the donor script (binding,
# spec §1.3):
# - NO --enable-manager: the Manager is a custom-node-style plugin
# activated by ComfyUI's custom_nodes scan of the worktree mount.
# - NO COMFYUI_MANAGER_SKIP_MANAGER_REQUIREMENTS: that belt does not
# exist in this repo (Q-6 verified); watchdog row E2E-SC-42 covers
# the residual.
# - --listen is ALWAYS passed explicitly (LISTEN env): the predicate
# under test is request-time `flag AND is_loopback(args.listen)` —
# the listener value is LOAD-BEARING.
# - Per-launch log isolation (E2E-SC-04, MANDATORY): each launch
# identity writes a FRESH log file comfyui.<port>.<launch-id>.log,
# so a deny-copy substring from L-D is unfindable by L-A/R-A log
# assertions (stale-substring false-PASS class).
# - Readiness = poll GET /system_stats == 200; child exit code 0
# during the wait = Manager-triggered restart -> KEEP polling;
# non-zero exit -> fail fast with log tail.
#
# Input env vars:
# E2E_COMFYUI_ROOT — (required) root from setup_e2e_env.sh
# PORT — listen port (default: 8189)
# TIMEOUT — max seconds to readiness (default: 120)
# LISTEN — listener address (default: 127.0.0.1)
# LAUNCH_ID — launch identity tag for the log file (default: default)
#
# Output (last line on success):
# COMFYUI_PID=<pid> PORT=<port> LOG_FILE=<path>
#
# Exit: 0=ready, 1=timeout/failure
set -euo pipefail
PORT="${PORT:-8189}"
TIMEOUT="${TIMEOUT:-120}"
LISTEN="${LISTEN:-127.0.0.1}"
LAUNCH_ID="${LAUNCH_ID:-default}"
log() { echo "[start_comfyui] $*"; }
err() { echo "[start_comfyui] ERROR: $*" >&2; }
die() { err "$@"; exit 1; }
[[ -n "${E2E_COMFYUI_ROOT:-}" ]] || die "E2E_COMFYUI_ROOT is not set"
[[ -d "$E2E_COMFYUI_ROOT/comfyui" ]] || die "ComfyUI not found at $E2E_COMFYUI_ROOT/comfyui"
[[ -x "$E2E_COMFYUI_ROOT/venv/bin/python" ]] || die "venv python not found"
[[ -f "$E2E_COMFYUI_ROOT/.e2e_setup_complete" ]] || die "Setup marker not found. Run setup_e2e_env.sh first."
PY="$E2E_COMFYUI_ROOT/venv/bin/python"
COMFY_DIR="$E2E_COMFYUI_ROOT/comfyui"
LOG_DIR="$E2E_COMFYUI_ROOT/logs"
LOG_FILE="$LOG_DIR/comfyui.${PORT}.${LAUNCH_ID}.log"
# Port-namespaced PID file (donor WI-CC incident: shared PID file caused
# a cross-port kill).
PID_FILE="$LOG_DIR/comfyui.${PORT}.pid"
mkdir -p "$LOG_DIR"
# --- Pre-launch port clear ---
if ss -tlnp 2>/dev/null | grep -q ":${PORT}\b"; then
log "Port $PORT is in use. Attempting to stop existing process..."
if [[ -f "$PID_FILE" ]]; then
OLD_PID="$(cat "$PID_FILE")"
if kill -0 "$OLD_PID" 2>/dev/null; then
kill "$OLD_PID" 2>/dev/null || true
sleep 2
fi
fi
if ss -tlnp 2>/dev/null | grep -q ":${PORT}\b"; then
pkill -f "main\\.py.*--port $PORT" 2>/dev/null || true
sleep 2
fi
if ss -tlnp 2>/dev/null | grep -q ":${PORT}\b"; then
die "Port $PORT is still in use after cleanup attempt"
fi
log "Port $PORT cleared."
fi
# --- Launch (FRESH per-launch log file) ---
log "Starting ComfyUI on port $PORT (listen=$LISTEN, launch_id=$LAUNCH_ID)..."
: > "$LOG_FILE"
PYTHONUNBUFFERED=1 \
HOME="$E2E_COMFYUI_ROOT/home" \
nohup "$PY" -u "$COMFY_DIR/main.py" \
--cpu \
--port "$PORT" \
--listen "$LISTEN" \
> "$LOG_FILE" 2>&1 &
COMFYUI_PID=$!
echo "$COMFYUI_PID" > "$PID_FILE"
log "ComfyUI PID=$COMFYUI_PID, log=$LOG_FILE"
# --- Block until ready: poll /system_stats (restart-tolerant) ---
log "Waiting up to ${TIMEOUT}s for ComfyUI readiness (GET /system_stats)..."
DEADLINE=$(( $(date +%s) + TIMEOUT ))
READY=0
while [[ "$(date +%s)" -lt "$DEADLINE" ]]; do
if curl -sf --max-time 2 "http://127.0.0.1:${PORT}/system_stats" >/dev/null 2>&1; then
READY=1
break
fi
# Child exit handling: exit 0 = Manager-triggered restart -> keep
# polling (a restarted process will bind the port); non-zero -> fail fast.
if ! kill -0 "$COMFYUI_PID" 2>/dev/null; then
if wait "$COMFYUI_PID" 2>/dev/null; then
: # exit 0 — restart class, keep polling
else
RC=$?
err "ComfyUI exited with code $RC. Last 30 lines of log:"
tail -n 30 "$LOG_FILE" >&2
rm -f "$PID_FILE"
exit 1
fi
fi
sleep 1
done
if [[ "$READY" -ne 1 ]]; then
err "Timeout (${TIMEOUT}s) waiting for ComfyUI. Last 30 lines of log:"
tail -n 30 "$LOG_FILE" >&2
kill "$COMFYUI_PID" 2>/dev/null || true
rm -f "$PID_FILE"
exit 1
fi
# A Manager-triggered restart (child exit 0 above) leaves COMFYUI_PID pointing
# at the dead original child while a FRESH process now owns the port. Re-resolve
# the live listener PID so PID_FILE (consumed by stop_comfyui.sh) targets the
# process that must actually be killed at teardown.
LISTENER_PID="$(ss -tlnp 2>/dev/null | grep ":${PORT}\b" | grep -oE 'pid=[0-9]+' | head -n1 | cut -d= -f2)"
if [[ -n "$LISTENER_PID" && "$LISTENER_PID" != "$COMFYUI_PID" ]]; then
log "Listener PID $LISTENER_PID differs from launched PID $COMFYUI_PID (restart). Updating PID file."
COMFYUI_PID="$LISTENER_PID"
echo "$COMFYUI_PID" > "$PID_FILE"
fi
log "ComfyUI is ready."
echo "COMFYUI_PID=$COMFYUI_PID PORT=$PORT LOG_FILE=$LOG_FILE"
+101
View File
@@ -0,0 +1,101 @@
#!/usr/bin/env bash
# stop_comfyui.sh — Graceful ComfyUI shutdown (T3, spec §1.3 stop contract).
#
# Donor mirror: SIGTERM -> 10s grace -> SIGKILL -> port-pattern pkill
# fallback -> port-free verify (incl. the legacy-PID-file warning from
# the donor WI-CC cross-port-kill incident).
#
# Input env vars:
# E2E_COMFYUI_ROOT — (required) path to the E2E environment
# PORT — ComfyUI port (default: 8189)
#
# Exit: 0=stopped, 1=failed
set -euo pipefail
PORT="${PORT:-8189}"
GRACE_PERIOD=10
log() { echo "[stop_comfyui] $*"; }
err() { echo "[stop_comfyui] ERROR: $*" >&2; }
die() { err "$@"; exit 1; }
[[ -n "${E2E_COMFYUI_ROOT:-}" ]] || die "E2E_COMFYUI_ROOT is not set"
# Ownership guard: only signal a PID whose cmdline references THIS E2E root.
# On shared runners a bare "main.py --port N" pattern (or a reused PID) could
# otherwise match an unrelated process; the launcher invokes
# "$E2E_COMFYUI_ROOT/venv/bin/python $E2E_COMFYUI_ROOT/comfyui/main.py", so the
# root path is always present in our process's cmdline.
belongs_to_root() {
local pid="$1"
[[ -n "$pid" && -r "/proc/$pid/cmdline" ]] || return 1
tr '\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null | grep -qF "$E2E_COMFYUI_ROOT"
}
# PIDs currently listening on PORT (deduped).
listener_pids() {
ss -tlnp 2>/dev/null | grep ":${PORT}\b" | grep -oE 'pid=[0-9]+' | cut -d= -f2 | sort -u
}
PID_FILE="$E2E_COMFYUI_ROOT/logs/comfyui.${PORT}.pid"
LEGACY_PID_FILE="$E2E_COMFYUI_ROOT/logs/comfyui.pid"
if [[ -f "$LEGACY_PID_FILE" ]] && [[ ! -f "$PID_FILE" ]]; then
log "WARN: found legacy unported PID file $LEGACY_PID_FILE but no ${PID_FILE}. Cross-port risk — ignoring legacy file."
fi
COMFYUI_PID=""
if [[ -f "$PID_FILE" ]]; then
COMFYUI_PID="$(cat "$PID_FILE")"
log "Read PID=$COMFYUI_PID from $PID_FILE"
fi
if [[ -n "$COMFYUI_PID" ]] && kill -0 "$COMFYUI_PID" 2>/dev/null; then
if belongs_to_root "$COMFYUI_PID"; then
log "Sending SIGTERM to PID $COMFYUI_PID..."
kill "$COMFYUI_PID" 2>/dev/null || true
elapsed=0
while kill -0 "$COMFYUI_PID" 2>/dev/null && [[ "$elapsed" -lt "$GRACE_PERIOD" ]]; do
sleep 1
elapsed=$((elapsed + 1))
done
if kill -0 "$COMFYUI_PID" 2>/dev/null; then
log "Process still alive after ${GRACE_PERIOD}s. Sending SIGKILL..."
kill -9 "$COMFYUI_PID" 2>/dev/null || true
sleep 1
fi
else
log "WARN: PID $COMFYUI_PID does NOT belong to $E2E_COMFYUI_ROOT (reused/stale PID). Refusing to kill it."
fi
fi
# Fallback: kill the port listener(s) (covers Manager-restarted processes whose
# PID differs from the recorded one) — but ONLY those verified to belong to this
# E2E root, never a broad pattern pkill that could hit unrelated CI processes.
if ss -tlnp 2>/dev/null | grep -q ":${PORT}\b"; then
log "Port $PORT still in use. Killing verified-own listener(s)..."
for pid in $(listener_pids); do
if belongs_to_root "$pid"; then
log "Sending SIGTERM to listener PID $pid..."
kill "$pid" 2>/dev/null || true
else
log "WARN: PID $pid on port $PORT is not part of $E2E_COMFYUI_ROOT — leaving it alone."
fi
done
sleep 2
for pid in $(listener_pids); do
if belongs_to_root "$pid"; then
log "Listener PID $pid still alive. Sending SIGKILL..."
kill -9 "$pid" 2>/dev/null || true
fi
done
sleep 1
fi
rm -f "$PID_FILE"
if ss -tlnp 2>/dev/null | grep -q ":${PORT}\b"; then
die "Port $PORT is still in use after shutdown"
fi
log "ComfyUI stopped."
+752
View File
@@ -0,0 +1,752 @@
"""GOAL #60 — Real-server E2E for the dedicated install flags (worktree-mounted Manager).
Boots a REAL ComfyUI server from a disposable test root
(`E2E_COMFYUI_ROOT`, built by tests/e2e/scripts/setup_e2e_env.sh) with
the Manager mounted via `git worktree add --detach` (NEVER pip-installed
[D2]) and exercises both dedicated-flag surfaces over live HTTP.
Usage:
bash tests/e2e/scripts/setup_e2e_env.sh # once (E2E-SC-01)
E2E_COMFYUI_ROOT=/path/to/root pytest tests/e2e/test_e2e_install_flags.py -v
Per-row map (goal60-scenarios.md, 24 rows spec §3 BINDING):
SC-01 setup_e2e_env.sh (pre-suite script; idempotent build + marker not a pytest test)
SC-02 mount_worktree fixture create path (SHA pin, .git-file, no-pip, printed SHA)
SC-03 mount_worktree fixture reuse path + path-prefix scoping invariant
SC-04 _start_server via start_comfyui.sh (readiness poll, restart tolerance,
per-launch log comfyui.<port>.<launch-id>.log)
SC-05 test_00_smoke_manager_version in EVERY server-up class (+abort guard)
SC-06 _stage via stage_flags.sh (backup-if-absent; restart-only by construction)
SC-07 class fixture finalizers (stop + port-free + config restore + backup DELETE)
+ mount_worktree finalizer (unmount + prune + absence assert)
SC-10 TestDenyArms.test_01_sa_deny SC-11 TestDenyArms.test_02_sb_deny
SC-12 TestAllowArms.test_01_git_url_allow
SC-13 TestAllowArms.test_02_pip_allow_reserved (anti-false-PASS VERBATIM)
SC-14 TestAllowArms.test_03_restart_consumes_reservation (R-A through the holder)
SC-20 _pre_guards in both class fixtures (before any request)
SC-21 TestAllowArms.test_04_clone_residual_cleanup (+ installed-index cross-check)
SC-22 TestAllowArms.test_05_pip_residual_uninstall
SC-23 _reservation_guard UNCONDITIONAL fixture-teardown guard (failure path)
SC-24 TestZeroResidual.test_99_zero_residual_sweep (unmount half lives in the
mount_worktree finalizer it cannot be asserted from inside the session)
SC-30 module-level pytestmark (env unset -> all SKIP; unit suite unaffected)
SC-31 module-level pytestmark (marker absent -> all SKIP)
SC-32 needs_network marker on fixture-dependent (allow-arm / public) rows
SC-33 collection safety by construction: stdlib + pytest + requests
(via pytest.importorskip) ONLY no glob/ imports, no server imports,
HTTP only at test time
SC-40 TestPublicListener (opt-in E2E_PUBLIC_LISTEN=1; L-P @ 0.0.0.0)
SC-41 batch S-C/S-C' E2E — DEFERRED (Q-5; spec FREEZE item 3; recorded here)
SC-42 TestAllowArms.test_06_requirements_watchdog (L-A launch log)
Fixture-lifecycle ownership (spec §3 BINDING block):
- every class server fixture DECLARES mount_worktree (mount-before-launch);
- process handle lives in a MUTABLE ServerHolder owned by the fixture;
teardown stops the CURRENT holder content (whatever launch identity is live);
- SC-14 restarts THROUGH the holder (stop L-A -> launch R-A -> replace handle);
- stop-before-next-class is structural (pytest class-fixture scoping);
- `requests` is imported via pytest.importorskip (absence degrades to SKIP).
T6 note: no tests/e2e/conftest.py all fixtures are single-module, so the
optional T6 file is not demanded (spec §2 T6 condition not met).
"""
from __future__ import annotations
import configparser
import os
import shutil
import socket
import subprocess
import sys
import time
import uuid
from pathlib import Path
import pytest
# ---------------------------------------------------------------------------
# Skip gates (E2E-SC-30/31) — BEFORE anything env-dependent
# ---------------------------------------------------------------------------
E2E_ROOT = os.environ.get("E2E_COMFYUI_ROOT", "")
_MARKER_OK = bool(E2E_ROOT) and os.path.isfile(os.path.join(E2E_ROOT, ".e2e_setup_complete"))
pytestmark = pytest.mark.skipif(
not _MARKER_OK,
reason="E2E_COMFYUI_ROOT not set or E2E environment not ready (.e2e_setup_complete missing)",
)
# requests: test-extra — absence degrades to SKIP, never a collection error
# (spec §3 binding block item 5; [D4]).
requests = pytest.importorskip("requests")
# ---------------------------------------------------------------------------
# Constants / paths
# ---------------------------------------------------------------------------
PORT = int(os.environ.get("PORT", "8189"))
TIMEOUT = int(os.environ.get("TIMEOUT", "120"))
BASE_URL = f"http://127.0.0.1:{PORT}"
THIS_DIR = Path(__file__).resolve().parent
SCRIPTS_DIR = THIS_DIR / "scripts"
MANAGER_REPO = THIS_DIR.parents[1] # repo root of the checkout running the suite
ROOT = Path(E2E_ROOT) if E2E_ROOT else Path(".")
COMFY_DIR = ROOT / "comfyui"
CN_DIR = COMFY_DIR / "custom_nodes"
MOUNT = CN_DIR / "comfyui-manager"
CFG = COMFY_DIR / "user" / "__manager" / "config.ini"
CFG_BACKUP = Path(str(CFG) + ".before-flags")
SCRIPTS_FILE = COMFY_DIR / "user" / "__manager" / "startup-scripts" / "install-scripts.txt"
LOGS_DIR = ROOT / "logs"
VENV_PY = ROOT / "venv" / "bin" / "python"
# Owned fixtures ONLY (goal60-scenarios.md Conventions; [D3])
NODEPACK_URL = "https://github.com/ltdrdata/nodepack-test1-do-not-install"
PACK_NAME = "nodepack-test1-do-not-install"
# pip stimulus uses the git+ scheme: pip/uv require it for VCS URLs — a
# plain GitHub repo URL serves HTML and cannot install (verified by probe;
# spec amendment requested via leader pushback 2026-06-08; the SC-13/14
# oracle itself is encoded VERBATIM).
PIP_URL = "git+https://github.com/ltdrdata/pip-test1-do-not-install"
PIP_PKG = "pip-test1-do-not-install"
PIP_IMPORT = "pip_test1_do_not_install"
PIP_MARKER = "pip-test1-do-not-install:ok"
# Amendment A2 (live-run finding, leader-approved): the S-A nodepack fixture
# is deliberately NOT zero-dep — it pins python-slugify==8.0.4 in its
# requirements as the invariant-4 ride-along proof vehicle. The SC-42
# watchdog therefore allowlists exactly that requirement, and the
# transitive-dep residual class is swept at allow-class teardown + SC-24.
# (The S-B pip fixture IS zero-dep as documented.)
NODEPACK_PINNED_REQ = "python-slugify==8.0.4"
TRANSITIVE_DEPS = ("python-slugify", "text-unidecode")
POLL_TIMEOUT = 60
POLL_INTERVAL = 1.0
# Distinctive substrings of the flag-naming denial constants @ d45c8e6b
DENY_COPY_GIT = "'allow_git_url_install = true' in config.ini"
DENY_COPY_PIP = "'allow_pip_install = true' in config.ini"
# Old security_level-attributing copy (must NOT appear on flag denials)
OLD_COPY_GENERAL = "is not allowed in this security_level"
OLD_COPY_NORMAL_MINUS = "set the security level to"
# ---------------------------------------------------------------------------
# Network probe (E2E-SC-32) — evaluated ONLY when the env gate is open, so
# collection without the env performs no network IO (SC-33).
# ---------------------------------------------------------------------------
def _network_available() -> bool:
try:
with socket.create_connection(("github.com", 443), timeout=5):
return True
except OSError:
return False
_NETWORK = _network_available() if _MARKER_OK else False
needs_network = pytest.mark.skipif(
not _NETWORK,
reason="github.com unreachable — network-dependent fixture row skipped (E2E-SC-32)",
)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _run(cmd, check=False, timeout=180, env=None, cwd=None):
return subprocess.run(
cmd, capture_output=True, text=True, timeout=timeout, check=check,
env=env, cwd=cwd,
)
def _script(name: str) -> str:
return str(SCRIPTS_DIR / name)
def _script_env(**extra) -> dict:
env = {**os.environ, "E2E_COMFYUI_ROOT": str(ROOT), "PORT": str(PORT),
"TIMEOUT": str(TIMEOUT)}
env.update({k: str(v) for k, v in extra.items()})
return env
def _pack_dir(name: str = PACK_NAME) -> Path:
return CN_DIR / name
def _pack_exists(name: str = PACK_NAME) -> bool:
return _pack_dir(name).is_dir()
def _remove_pack(name: str = PACK_NAME) -> None:
"""Donor _remove_pack pattern: rmtree 3-retry + rename-to-.trash_ fallback."""
path = _pack_dir(name)
if path.is_symlink():
path.unlink()
return
if not path.is_dir():
return
for attempt in range(3):
try:
shutil.rmtree(path)
return
except OSError:
if attempt < 2:
time.sleep(1)
trash = CN_DIR / f".trash_{uuid.uuid4().hex[:8]}"
try:
os.rename(path, trash)
shutil.rmtree(trash, ignore_errors=True)
except OSError:
shutil.rmtree(path, ignore_errors=True)
def _wait_for(predicate, timeout=POLL_TIMEOUT, interval=POLL_INTERVAL) -> bool:
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
if predicate():
return True
time.sleep(interval)
return False
def _pip_import_rc() -> int:
return _run([str(VENV_PY), "-c", f"import {PIP_IMPORT}"]).returncode
def _pip_marker_rc() -> "subprocess.CompletedProcess":
return _run([
str(VENV_PY), "-c",
f"import {PIP_IMPORT} as m; assert m.MARKER == '{PIP_MARKER}'",
])
def _pip_uninstall() -> "subprocess.CompletedProcess":
return _run([str(VENV_PY), "-m", "pip", "uninstall", "-y", PIP_PKG])
def _scripts_clean() -> bool:
"""True when the reservation file is absent OR carries no pip-test1 line."""
if not SCRIPTS_FILE.exists():
return True
return "pip-test1" not in SCRIPTS_FILE.read_text(errors="ignore")
def _reservation_guard() -> None:
"""E2E-SC-23 — UNCONDITIONAL teardown guard for the unconsumed-reservation
leak class: a leaked line would pip-install on ANY next boot of this root."""
if SCRIPTS_FILE.exists() and "pip-test1" in SCRIPTS_FILE.read_text(errors="ignore"):
SCRIPTS_FILE.unlink()
assert _scripts_clean(), "reservation guard failed to clear pip-test1 line (SC-23)"
def _restore_config() -> None:
"""E2E-SC-07: restore from backup, then DELETE the backup and assert absence
(a surviving stale backup would silently restore an outdated config at a
FUTURE run's teardown via the create-only-if-absent rule)."""
if CFG_BACKUP.exists():
shutil.copyfile(CFG_BACKUP, CFG)
CFG_BACKUP.unlink()
assert not CFG_BACKUP.exists(), "config backup must be DELETED after restore (SC-07/24)"
def _port_free() -> bool:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
s.settimeout(1)
return s.connect_ex(("127.0.0.1", PORT)) != 0
finally:
s.close()
def _pre_guards() -> None:
"""E2E-SC-20 — before EACH arm's matrix rows; assert all three."""
_remove_pack(PACK_NAME)
assert not _pack_exists(PACK_NAME), (
f"pre-guard: failed to clean {PACK_NAME} (file locks?)"
)
_pip_uninstall() # ignore rc: not-installed is fine
assert _pip_import_rc() != 0, "pre-guard: pip fixture importable before test"
_reservation_guard()
assert _scripts_clean(), "pre-guard: stale pip-test1 reservation present"
# ---------------------------------------------------------------------------
# Server lifecycle (E2E-SC-04 + spec §3 binding holder contract)
# ---------------------------------------------------------------------------
class ServerHolder:
"""Mutable process-handle holder owned by the class server fixture.
The holder always points at the CURRENT launch identity; SC-14 replaces
its content when it restarts through it, so class teardown stops
whatever is live no orphan."""
def __init__(self):
self.launch_id: str | None = None
self.log_path: Path | None = None
self.live = False
self.smoke_ok = False
def _stage(mode: str) -> None:
r = _run(["bash", _script("stage_flags.sh"), mode], env=_script_env(), check=False)
assert r.returncode == 0, f"stage_flags.sh {mode} failed:\n{r.stdout}\n{r.stderr}"
assert CFG_BACKUP.exists(), "backup must exist after staging (SC-06)"
def _start_server(holder: ServerHolder, launch_id: str, listen: str = "127.0.0.1") -> None:
r = _run(
["bash", _script("start_comfyui.sh")],
env=_script_env(LISTEN=listen, LAUNCH_ID=launch_id),
timeout=TIMEOUT + 90,
)
assert r.returncode == 0, (
f"start_comfyui.sh failed for launch {launch_id}:\n{r.stdout}\n{r.stderr}"
)
holder.launch_id = launch_id
holder.log_path = LOGS_DIR / f"comfyui.{PORT}.{launch_id}.log"
holder.live = True
assert holder.log_path.is_file(), "per-launch log file missing (SC-04)"
def _stop_server(holder: ServerHolder) -> None:
if not holder.live:
return
r = _run(["bash", _script("stop_comfyui.sh")], env=_script_env(), timeout=120)
assert r.returncode == 0, f"stop_comfyui.sh failed:\n{r.stdout}\n{r.stderr}"
holder.live = False
assert _port_free(), "port still bound after stop (SC-07)"
def _launch_log(holder: ServerHolder) -> str:
assert holder.log_path is not None and holder.log_path.is_file()
return holder.log_path.read_text(errors="ignore")
def _named_log(launch_id: str) -> str:
p = LOGS_DIR / f"comfyui.{PORT}.{launch_id}.log"
assert p.is_file(), f"launch log for {launch_id} missing"
return p.read_text(errors="ignore")
def _require_smoke(holder: ServerHolder) -> None:
"""SC-05 abort semantics: matrix rows refuse to run after a smoke failure
so a mount/activation problem cannot produce misleading 404 results."""
if not holder.smoke_ok:
pytest.fail(
"aborting matrix row: smoke (GET /manager/version) has not passed "
"for this launch — mount/activation problem or Q-2 bundled-manager "
"collision (E2E-SC-05)"
)
def _smoke(holder: ServerHolder) -> None:
r = requests.get(f"{BASE_URL}/manager/version", timeout=10)
assert r.status_code == 200, (
f"smoke FAILED: GET /manager/version -> {r.status_code}; the "
f"worktree-mounted plugin did not register its routes (E2E-SC-05). "
f"Log tail:\n{_launch_log(holder)[-2000:]}"
)
assert r.text.strip(), "smoke: /manager/version body empty"
holder.smoke_ok = True
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture(scope="session")
def mount_worktree():
"""E2E-SC-02/03 — SOLE owner of mount create / reuse-verify / teardown."""
ref = os.environ.get("E2E_MANAGER_REF", "HEAD")
r = _run(["git", "-C", str(MANAGER_REPO), "rev-parse", f"{ref}^{{commit}}"], check=True)
sha = r.stdout.strip()
# Scoping invariant (SC-03): the mount path is {ROOT}-prefixed and is
# NEVER under the members' .claude/worktrees tree. Every mount/teardown
# command below references ONLY this path.
mount = MOUNT.resolve()
assert ".claude/worktrees" not in str(mount).replace(os.sep, "/"), (
"mount path must never live under member worktrees (SC-03 scoping)"
)
assert str(mount).startswith(str(ROOT.resolve())), (
"mount path must be {ROOT}-prefixed (SC-03 scoping)"
)
porcelain = _run(["git", "-C", str(MANAGER_REPO), "worktree", "list", "--porcelain"]).stdout
if f"worktree {mount}" in porcelain:
# Reuse path (SC-03)
head = _run(["git", "-C", str(mount), "rev-parse", "HEAD"]).stdout.strip()
if head != sha:
_run(["git", "-C", str(mount), "checkout", "--detach", sha], check=True)
else:
# Create path (SC-02)
_run(["git", "-C", str(MANAGER_REPO), "worktree", "add", "--detach",
str(mount), sha], check=True)
# From here a worktree exists at `mount`. Any failure between now and the
# yield (the verify asserts below) must STILL run teardown — otherwise a
# failed setup leaks an orphaned worktree into the next session (review
# follow-up). Hence the try/finally wraps verify + yield, not just yield.
try:
# Verify (every session)
head = _run(["git", "-C", str(mount), "rev-parse", "HEAD"]).stdout.strip()
assert head == sha, f"mount HEAD {head} != expected {sha} (SC-02)"
print(f"[mount_worktree] Manager mounted at {mount} @ SHA {sha}") # [D2] traceability
assert (mount / ".git").is_file(), (
".git in the mount must be a FILE (gitdir pointer) — worktree layout (SC-02)"
)
# [D2] other half: no pip-installed Manager in the venv; per MM §2.2 no
# assertion anywhere relies on the mounted Manager's OWN version/remote
# self-report (.git-file degradation accepted by design — spec R5).
for dist in ("comfyui-manager", "ComfyUI-Manager"):
rc = _run([str(VENV_PY), "-m", "pip", "show", dist]).returncode
assert rc != 0, f"pip-installed Manager '{dist}' found in venv — violates [D2]"
yield {"path": mount, "sha": sha}
finally:
if os.environ.get("E2E_KEEP_MOUNT"):
print(f"[mount_worktree] E2E_KEEP_MOUNT set — keeping {mount}")
else:
# Exception-safe: prune + absence asserts run even when remove fails
# (review iter-2 — crash residue must still be surfaced honestly).
try:
_run(["git", "-C", str(MANAGER_REPO), "worktree", "remove", "--force", str(mount)],
check=True)
finally:
_run(["git", "-C", str(MANAGER_REPO), "worktree", "prune"])
porcelain = _run(["git", "-C", str(MANAGER_REPO), "worktree", "list", "--porcelain"]).stdout
assert f"worktree {mount}" not in porcelain, "mount still listed after remove (SC-07)"
assert not mount.exists(), "mount dir still present after remove (SC-07)"
@pytest.fixture(scope="class")
def deny_server(mount_worktree):
"""L-D: both flags ABSENT (live 'missing key reads false'), loopback."""
_pre_guards() # SC-20
_stage("deny") # SC-06
holder = ServerHolder()
_start_server(holder, "L-D") # SC-04
yield holder
# Exception-safe teardown chain (review iter-2 must-fix): a failing
# stop is exactly the crashed-run shape SC-23 exists for — the guard
# and the config restore MUST run regardless.
try:
_stop_server(holder) # SC-07 (current handle, whatever is live)
finally:
try:
_reservation_guard() # SC-23 — UNCONDITIONAL
finally:
_restore_config() # SC-07: restore + DELETE backup
@pytest.fixture(scope="class")
def allow_server(mount_worktree):
"""L-A: both flags true, loopback. SC-14 mutates the holder to R-A."""
_pre_guards() # SC-20 (re-guards before the allow arm)
_stage("allow") # SC-06
holder = ServerHolder()
_start_server(holder, "L-A")
yield holder
# Exception-safe teardown chain (review iter-2 must-fix): every
# residual guard runs even when the stop (or an earlier sweep step)
# raises — SC-23 is contractually UNCONDITIONAL (R3 leak class).
try:
_stop_server(holder) # stops the CURRENT identity (L-A or R-A)
finally:
try:
_remove_pack(PACK_NAME) # defensive re-sweep (primary assert is SC-21)
_pip_uninstall() # defensive (primary assert is SC-22)
# Amendment A2: sweep the S-A fixture's transitive-dep residual
# class (python-slugify + text-unidecode ride the git
# transaction; verified NOT in ComfyUI's own requirements).
_run([str(VENV_PY), "-m", "pip", "uninstall", "-y", *TRANSITIVE_DEPS])
finally:
try:
_reservation_guard() # SC-23 — UNCONDITIONAL (failure path cover)
finally:
_restore_config()
@pytest.fixture(scope="class")
def public_server(mount_worktree):
"""L-P (opt-in, Q-7): both flags true, 0.0.0.0 listener."""
_pre_guards()
_stage("allow")
holder = ServerHolder()
_start_server(holder, "L-P", listen="0.0.0.0")
yield holder
# Exception-safe teardown chain (review iter-2 must-fix).
try:
_stop_server(holder)
finally:
try:
_reservation_guard()
finally:
_restore_config()
# ---------------------------------------------------------------------------
# Classes — definition order IS execution order (deny first on the fresh env)
# ---------------------------------------------------------------------------
class TestDenyArms:
"""L-D launch: SC-05 smoke, SC-10, SC-11 (deny rows are offline-safe —
denial happens before any network access)."""
def test_00_smoke_manager_version(self, deny_server):
_smoke(deny_server) # SC-05
def test_01_sa_deny(self, deny_server):
"""E2E-SC-10: S-A deny — 403 + exact flag token + no artifact + honest log."""
_require_smoke(deny_server)
r = requests.post(f"{BASE_URL}/customnode/install/git_url",
json={"url": NODEPACK_URL}, timeout=30)
assert r.status_code == 403
assert r.json() == {"error": "allow_git_url_install"}, (
f"deny body must carry the flag token, got {r.text!r}"
)
assert not _pack_exists(PACK_NAME), "clone artifact created on DENY (SC-10)"
log = _launch_log(deny_server)
assert DENY_COPY_GIT in log, "flag-naming denial copy missing from L-D log"
assert OLD_COPY_GENERAL not in log and OLD_COPY_NORMAL_MINUS not in log, (
"denial attributed to security_level — honest-copy violation (SC-10)"
)
def test_02_sb_deny(self, deny_server):
"""E2E-SC-11: S-B deny — 403 + flag token + no reservation + not importable."""
_require_smoke(deny_server)
r = requests.post(f"{BASE_URL}/customnode/install/pip",
json={"packages": PIP_URL}, timeout=30)
assert r.status_code == 403
assert r.json() == {"error": "allow_pip_install"}
assert _scripts_clean(), "reservation recorded on DENY (SC-11)"
assert _pip_import_rc() != 0, "pip fixture importable after DENY (SC-11)"
log = _launch_log(deny_server)
assert DENY_COPY_PIP in log, "flag-naming denial copy missing from L-D log"
class TestAllowArms:
"""L-A launch + R-A restart. ORDERED methods (donor sequential-class
precedent): SC-12 -> SC-13 -> SC-14 -> SC-21 -> SC-22 -> SC-42."""
def test_00_smoke_manager_version(self, allow_server):
_smoke(allow_server) # SC-05 (re-smoke on the new launch)
@needs_network
def test_01_git_url_allow(self, allow_server):
"""E2E-SC-12: S-A allow — 200 + real clone + clone-target proof."""
_require_smoke(allow_server)
r = requests.post(f"{BASE_URL}/customnode/install/git_url",
json={"url": NODEPACK_URL}, timeout=120)
assert r.status_code == 200, f"S-A allow expected 200, got {r.status_code}: {r.text!r}"
assert _wait_for(lambda: _pack_exists(PACK_NAME)), (
f"{PACK_NAME} not cloned within {POLL_TIMEOUT}s (SC-12)"
)
git_dir = _pack_dir() / ".git"
assert git_dir.is_dir(), "no .git DIRECTORY — not a real clone (SC-12)"
# Donor clone-target proof: .git/config [remote "origin"] url matches
# the requested URL modulo the .git suffix.
cp = configparser.ConfigParser()
cp.read(git_dir / "config")
section = 'remote "origin"'
assert section in cp, f'[{section}] missing from .git/config: {cp.sections()!r}'
remote_url = cp[section].get("url", "").rstrip("/")
expected = NODEPACK_URL.rstrip("/")
assert remote_url in (expected, expected + ".git"), (
f"clone targeted the WRONG repo: {remote_url!r} != {expected!r} (SC-12)"
)
@needs_network
def test_02_pip_allow_reserved(self, allow_server):
"""E2E-SC-13 (VERBATIM anti-false-PASS oracle): 200 = RESERVED, NOT
INSTALLED. Asserting import success here would be the exact false-PASS
the MM correction exists to prevent."""
_require_smoke(allow_server)
r = requests.post(f"{BASE_URL}/customnode/install/pip",
json={"packages": PIP_URL}, timeout=30)
assert r.status_code == 200, f"S-B allow expected 200, got {r.status_code}: {r.text!r}"
assert SCRIPTS_FILE.is_file(), "no reservation file after S-B allow (SC-13)"
content = SCRIPTS_FILE.read_text(errors="ignore")
reserved_lines = [
ln for ln in content.splitlines()
if "'#FORCE'" in ln and PIP_PKG in ln
]
assert reserved_lines, (
f"no reservation line with '#FORCE' + {PIP_PKG!r} in {SCRIPTS_FILE}:\n{content}"
)
# MANDATORY: the package is NOT installed at this point.
assert _pip_import_rc() != 0, (
"pip fixture importable right after the 200 — reservation semantics "
"violated, or a previous run leaked state (SC-13 anti-false-PASS)"
)
@needs_network
def test_03_restart_consumes_reservation(self, allow_server):
"""E2E-SC-14: R-A restart THROUGH the holder; the consuming boot
executes + removes the reservation; MARKER import proves field-level."""
_require_smoke(allow_server)
assert SCRIPTS_FILE.is_file(), "precondition: reservation must exist (SC-13 first)"
# Restart THROUGH the holder (spec §3 binding item 3): stop the live
# L-A process, relaunch as R-A with the SAME staged config, replace
# the handle — class teardown then stops R-A.
_stop_server(allow_server)
_start_server(allow_server, "R-A")
_smoke(allow_server)
# Field-level positive proof (not just exit code):
marker = _pip_marker_rc()
assert marker.returncode == 0, (
f"MARKER import failed after the consuming restart (SC-14):\n"
f"{marker.stderr}\nR-A log tail:\n{_named_log('R-A')[-3000:]}"
)
assert not SCRIPTS_FILE.exists(), (
"install-scripts.txt NOT removed by the consuming boot (SC-14 self-clean)"
)
ra_log = _named_log("R-A")
assert "## ComfyUI-Manager: EXECUTE =>" in ra_log and PIP_PKG in ra_log, (
"R-A log lacks the startup-script execution block (SC-14)"
)
assert "Startup script completed." in ra_log, (
"R-A log lacks the startup-script completion line (SC-14)"
)
@needs_network
def test_04_clone_residual_cleanup(self, allow_server):
"""E2E-SC-21: clone-dir hygiene; FS-absence primary + installed-index
cross-check while the server is still up (defensive, donor pattern)."""
_remove_pack(PACK_NAME)
assert not _pack_exists(PACK_NAME), "clone dir still present (SC-21 primary)"
try:
r = requests.get(f"{BASE_URL}/customnode/installed", timeout=15)
if r.status_code == 200:
installed = r.json()
assert PACK_NAME not in installed, (
f"{PACK_NAME} still in /customnode/installed after removal (SC-21)"
)
for key, pkg in installed.items():
if isinstance(pkg, dict):
assert pkg.get("cnr_id") != PACK_NAME and pkg.get("aux_id") != PACK_NAME, (
f"installed entry {key!r} still references {PACK_NAME!r} (SC-21)"
)
except (ValueError, requests.RequestException):
# Spec SC-21: if the response schema proves awkward, FS-absence
# alone satisfies this row.
pass
@needs_network
def test_05_pip_residual_uninstall(self, allow_server):
"""E2E-SC-22: S-B residual class 1 (venv package)."""
r = _pip_uninstall()
assert r.returncode == 0, f"pip uninstall failed (SC-22):\n{r.stdout}\n{r.stderr}"
assert _pip_import_rc() != 0, "pip fixture importable after uninstall (SC-22)"
@needs_network
def test_06_requirements_watchdog(self, allow_server):
"""E2E-SC-42 (Q-6 watchdog, amendment A2): every management-script
EXECUTE in the L-A launch log must be attributable to the owned
fixture's OWN pinned requirements (python-slugify==8.0.4 — the
nodepack fixture's deliberate invariant-4 ride-along requirement).
Any other EXECUTE (e.g. a Manager-requirements install the Q-6
risk this row guards) FAILS the watchdog.
The allowlisted line doubles as LIVE proof of the invariant-4
ride-along class: a dependency pip install executed inside the
git-URL transaction without consulting allow_pip_install."""
la_log = _named_log("L-A")
banner = "## ComfyUI-Manager: EXECUTE =>"
idx = 0
execs = []
while True:
idx = la_log.find(banner, idx)
if idx < 0:
break
execs.append(la_log[idx: idx + 600])
idx += len(banner)
# Non-vacuity (review iter-2 / A2 positive half): the ride-along
# MUST have happened — exactly ONE management-script execution,
# the fixture's single pinned requirement.
assert len(execs) == 1, (
f"expected exactly 1 management-script execution during L-A "
f"(the fixture's pinned requirement ride-along), found "
f"{len(execs)} (SC-42 / A2)"
)
window = execs[0]
assert NODEPACK_PINNED_REQ in window, (
"unexpected management-script execution during L-A — not "
"attributable to the fixture's pinned requirement "
f"({NODEPACK_PINNED_REQ}) (SC-42 watchdog):\n{window}"
)
# Line-level shape: it must be a pip-install command, not an
# arbitrary script that merely mentions the requirement string.
assert "'pip'" in window and "'install'" in window, (
f"EXECUTE block is not a pip-install command (SC-42):\n{window}"
)
@pytest.mark.skipif(
not os.environ.get("E2E_PUBLIC_LISTEN"),
reason="public-listener row is opt-in (E2E_PUBLIC_LISTEN=1) — Q-7 default-off",
)
class TestPublicListener:
"""E2E-SC-40 (opt-in): flags=true + 0.0.0.0 -> still 403 on both surfaces.
Live proof of invariant 2 (predicate = flag AND loopback at REQUEST time)."""
def test_00_smoke_manager_version(self, public_server):
_smoke(public_server)
def test_01_sa_public_deny(self, public_server):
_require_smoke(public_server)
r = requests.post(f"{BASE_URL}/customnode/install/git_url",
json={"url": NODEPACK_URL}, timeout=30)
assert r.status_code == 403
assert r.json() == {"error": "allow_git_url_install"}
assert not _pack_exists(PACK_NAME)
def test_02_sb_public_deny(self, public_server):
_require_smoke(public_server)
r = requests.post(f"{BASE_URL}/customnode/install/pip",
json={"packages": PIP_URL}, timeout=30)
assert r.status_code == 403
assert r.json() == {"error": "allow_pip_install"}
assert _scripts_clean()
class TestZeroResidual:
"""E2E-SC-24: the complete [D3] residual inventory in one assertion block.
Runs AFTER the server classes (their class-scoped fixtures have finalized:
server stopped, config restored, backup deleted). The unmount half of the
inventory is asserted by the mount_worktree finalizer itself it cannot
be asserted from inside the session while the mount is still live."""
def test_99_zero_residual_sweep(self, mount_worktree):
# custom_nodes clean (incl. .trash_ fallback leftovers)
assert not _pack_exists(PACK_NAME), "nodepack residue in custom_nodes (SC-24)"
leftovers = [p.name for p in CN_DIR.iterdir()
if p.name.startswith((".trash_", PACK_NAME))]
assert not leftovers, f"residual entries in custom_nodes: {leftovers} (SC-24)"
# venv clean
assert _pip_import_rc() != 0, "pip fixture still importable (SC-24)"
# Amendment A2: transitive-dep residual class swept
for dep in TRANSITIVE_DEPS:
rc = _run([str(VENV_PY), "-m", "pip", "show", dep]).returncode
assert rc != 0, f"transitive dep {dep!r} survived the sweep (SC-24 / A2)"
# reservation clean
assert _scripts_clean(), "pip-test1 reservation residue (SC-24)"
# config restored + backup DELETED
assert CFG.is_file(), "config.ini missing after restore (SC-24)"
cfg_text = CFG.read_text(errors="ignore")
assert "allow_git_url_install" not in cfg_text, "staged flag leaked into restored config (SC-24)"
assert "allow_pip_install" not in cfg_text, "staged flag leaked into restored config (SC-24)"
assert not CFG_BACKUP.exists(), "stale config backup survived (SC-24 / peer R2)"
# port free
assert _port_free(), f"port {PORT} still bound (SC-24)"
if __name__ == "__main__":
sys.exit(pytest.main([__file__, "-v"]))
+121
View File
@@ -0,0 +1,121 @@
"""AC verification for wi-001 (B2): Content-Type rejection helper.
Validates _reject_simple_form_content_type against the 5-item curl matrix
from the WI's acceptance criteria using aiohttp test client. The test
builds a minimal aiohttp app that mirrors the helper's wiring into a
no-body POST handler, so we exercise the real request.content_type
parsing path rather than a mock.
AC matrix:
form-url 400
multipart 400
text/plain 400
no-CT 200
application/json 200
"""
import asyncio
import unittest
from pathlib import Path
from aiohttp import web
from aiohttp.test_utils import TestClient, TestServer
# Parse the helper from manager_server.py without importing it, to avoid
# pulling in the full ComfyUI/PromptServer stack. Note: we intentionally do
# NOT add the `glob/` directory to sys.path — the dir name would shadow
# Python's stdlib `glob` module and break pytest collection.
REPO_ROOT = Path(__file__).resolve().parent.parent
def _load_helper():
"""Parse manager_server.py and execute only the helper definition."""
import ast
source = (REPO_ROOT / "glob" / "manager_server.py").read_text()
tree = ast.parse(source)
wanted = {"_SIMPLE_FORM_CONTENT_TYPES", "_reject_simple_form_content_type"}
nodes = []
for node in tree.body:
if isinstance(node, ast.Assign):
for target in node.targets:
if isinstance(target, ast.Name) and target.id in wanted:
nodes.append(node)
elif isinstance(node, ast.FunctionDef) and node.name in wanted:
nodes.append(node)
module = ast.Module(body=nodes, type_ignores=[])
ns = {"web": web, "frozenset": frozenset}
exec(compile(module, "manager_server_helpers", "exec"), ns)
return ns["_reject_simple_form_content_type"]
_reject_simple_form_content_type = _load_helper()
async def _handler(request):
resp = _reject_simple_form_content_type(request)
if resp is not None:
return resp
return web.Response(status=200)
class ContentTypeRejectionTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.loop = asyncio.new_event_loop()
asyncio.set_event_loop(cls.loop)
app = web.Application()
app.router.add_post("/noop", _handler)
cls.server = TestServer(app, loop=cls.loop)
cls.client = TestClient(cls.server, loop=cls.loop)
cls.loop.run_until_complete(cls.client.start_server())
@classmethod
def tearDownClass(cls):
cls.loop.run_until_complete(cls.client.close())
cls.loop.close()
def _post(self, headers):
async def go():
return await self.client.post("/noop", headers=headers, data=b"")
return self.loop.run_until_complete(go())
def test_form_urlencoded_rejected(self):
r = self._post({"Content-Type": "application/x-www-form-urlencoded"})
self.assertEqual(r.status, 400)
def test_multipart_form_data_rejected(self):
# aiohttp requires a boundary for multipart; helper should still reject
# based on the primary mimetype.
r = self._post({"Content-Type": "multipart/form-data; boundary=xyz"})
self.assertEqual(r.status, 400)
def test_text_plain_rejected(self):
r = self._post({"Content-Type": "text/plain"})
self.assertEqual(r.status, 400)
def test_no_content_type_allowed(self):
# Explicitly strip Content-Type: aiohttp client may add a default,
# so we use a raw request to ensure absence is tested.
async def go():
import aiohttp
async with aiohttp.ClientSession() as session:
async with session.post(
self.client.make_url("/noop"),
data=None,
skip_auto_headers=["Content-Type"],
) as resp:
return resp.status
status = self.loop.run_until_complete(go())
self.assertEqual(status, 200)
def test_application_json_allowed(self):
r = self._post({"Content-Type": "application/json"})
self.assertEqual(r.status, 200)
if __name__ == "__main__":
unittest.main(verbosity=2)
+153
View File
@@ -0,0 +1,153 @@
"""Unit tests for the dedicated-install-flag predicate.
Covers `is_dedicated_install_allowed(flag_value, listen_address)` in
glob/manager_server.py:
- Truth table: allowed iff flag is true AND the listener is loopback.
- REPLACE-by-construction: the 2-arg signature has no security_level /
network_mode parameter and the body references no config machinery,
so security_level cannot influence the outcome in either direction.
- Cross-flag isolation: a single flag_value input cannot consult the
other flag.
- Request-time evaluation: the body must not read the import-time
`is_local_mode` snapshot (callers pass args.listen per request).
Harness: glob/manager_server.py is not importable under the test runner
(`from comfy.cli_args import args`, PromptServer), so we AST-parse the
file and exec only the wanted pure defs `glob/` is never added to
sys.path (the dir name shadows the stdlib `glob`).
"""
import ast
import inspect
import unittest
from pathlib import Path
from typing import Any
REPO_ROOT = Path(__file__).resolve().parent.parent
MANAGER_SERVER_PATH = REPO_ROOT / "glob" / "manager_server.py"
_WANTED = {"is_loopback", "is_dedicated_install_allowed"}
def _load_predicates():
"""Parse manager_server.py; exec only the wanted pure function defs."""
source = MANAGER_SERVER_PATH.read_text()
tree = ast.parse(source)
nodes = []
node_by_name = {}
for node in tree.body:
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name in _WANTED:
nodes.append(node)
node_by_name[node.name] = node
missing = _WANTED - node_by_name.keys()
assert not missing, f"expected pure defs missing from manager_server.py: {missing}"
module = ast.Module(body=nodes, type_ignores=[])
ns: dict = {"bool": bool}
exec(compile(module, "manager_server_predicates", "exec"), ns)
return ns, node_by_name
_NS, _NODES = _load_predicates()
IS_LOOPBACK: Any = _NS["is_loopback"]
PREDICATE: Any = _NS["is_dedicated_install_allowed"]
PREDICATE_NODE = _NODES["is_dedicated_install_allowed"]
class IsLoopbackBehaviorTest(unittest.TestCase):
"""Pins the loopback term the predicate composes."""
def test_ipv4_loopback(self):
self.assertTrue(IS_LOOPBACK("127.0.0.1"))
def test_public_address(self):
self.assertFalse(IS_LOOPBACK("0.0.0.0"))
def test_ipv6_loopback(self):
self.assertTrue(IS_LOOPBACK("::1"))
def test_invalid_address_reads_false(self):
# Non-IP strings deny-by-default (ValueError path).
self.assertFalse(IS_LOOPBACK("localhost"))
self.assertFalse(IS_LOOPBACK(""))
class DedicatedInstallPredicateTest(unittest.TestCase):
"""P-direct truth table + REPLACE-by-construction."""
def test_truth_table(self):
"""allowed iff flag AND loopback."""
cases = [
# (flag_value, listen_address, expected)
(True, "127.0.0.1", True),
(False, "127.0.0.1", False),
(True, "0.0.0.0", False),
(False, "0.0.0.0", False),
(True, "::1", True),
(True, "not-an-ip", False), # invalid listen -> deny
]
for flag_value, listen, expected in cases:
with self.subTest(flag=flag_value, listen=listen):
result = PREDICATE(flag_value, listen)
self.assertIsInstance(result, bool)
self.assertEqual(result, expected)
def test_falsy_flag_values_deny(self):
"""Secure-by-default: any falsy flag never allows."""
for falsy in (False, None, 0, ""):
with self.subTest(flag=falsy):
self.assertFalse(PREDICATE(falsy, "127.0.0.1"))
def test_signature_has_no_security_level(self):
"""Exactly (flag_value, listen_address) — no security_level term."""
params = list(inspect.signature(PREDICATE).parameters)
self.assertEqual(params, ["flag_value", "listen_address"])
for name in params:
self.assertNotIn("security", name)
self.assertNotIn("network_mode", name)
def test_body_free_of_config_machinery(self):
"""Body references no security_level plumbing, config reader, or the
import-time `is_local_mode` snapshot (request-time evaluation)."""
forbidden = {
"is_allowed_security_level",
"security_level",
"get_config",
"core",
"is_local_mode",
"network_mode",
"args",
}
seen = set()
for node in ast.walk(PREDICATE_NODE):
if isinstance(node, ast.Name):
seen.add(node.id)
elif isinstance(node, ast.Attribute):
seen.add(node.attr)
elif isinstance(node, ast.Constant) and isinstance(node.value, str):
seen.add(node.value)
self.assertEqual(
seen & forbidden, set(),
"predicate body must stay config-import-free",
)
def test_cross_flag_isolation_by_construction(self):
"""A single flag_value input cannot consult the other flag."""
seen_strings = {
node.value
for node in ast.walk(PREDICATE_NODE)
if isinstance(node, ast.Constant) and isinstance(node.value, str)
}
self.assertNotIn("allow_git_url_install", seen_strings)
self.assertNotIn("allow_pip_install", seen_strings)
self.assertTrue(PREDICATE(True, "127.0.0.1"))
self.assertFalse(PREDICATE(False, "127.0.0.1"))
def test_purity_deterministic(self):
"""Pure predicate — repeat calls identical."""
for _ in range(3):
self.assertTrue(PREDICATE(True, "127.0.0.1"))
self.assertFalse(PREDICATE(True, "0.0.0.0"))
if __name__ == "__main__":
unittest.main(verbosity=2)
+242
View File
@@ -0,0 +1,242 @@
"""Config-contract tests for the dedicated install flags.
Drives the real glob/manager_core config reader/writer through a
subprocess-isolated harness and pins: missing keys read False
(secure-by-default), only case-insensitive "true" is truthy, write
round-trips losslessly, edits need a restart (cached_config), the
exception-fallback path supplies False, no auto-migration seeds the
flags from a legacy security_level, and the get_bool missing->False
quirk the flags rely on stays frozen.
Harness: the child process injects a stub `folder_paths` (routing
import-time side effects into a tmpdir, and making has_system_user_api()
True so force_security_level_if_needed does not force 'strong'), prepends
`glob/` to ITS OWN sys.path (shadowing of stdlib `glob` confined to the
child), points manager_core.manager_config_path at a tmp config.ini,
resets cached_config, runs the scenario, and prints one JSON line for the
parent to assert.
"""
import json
import subprocess
import sys
import textwrap
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
_CHILD_PREAMBLE = textwrap.dedent(
"""
import sys, types, tempfile, os, json
tmp = tempfile.mkdtemp(prefix="cm_flags_cfg_")
stub = types.ModuleType("folder_paths")
stub.get_user_directory = lambda: tmp
stub.get_system_user_directory = lambda *a, **k: os.path.join(tmp, "sysuser")
sys.modules["folder_paths"] = stub
sys.path.insert(0, {glob_path!r})
import manager_core
CONFIG_PATH = os.path.join(tmp, "config.ini")
manager_core.manager_config_path = CONFIG_PATH
manager_core.cached_config = None
def write_ini(text):
with open(CONFIG_PATH, "w") as f:
f.write(text)
def fresh_read():
manager_core.cached_config = None
return manager_core.get_config()
def flag_view(cfg):
return {{
"git": cfg.get("allow_git_url_install", "<ABSENT>"),
"pip": cfg.get("allow_pip_install", "<ABSENT>"),
}}
"""
)
def _run_child(body):
"""Run a scenario body in the isolated child; return its JSON payload."""
script = _CHILD_PREAMBLE.format(glob_path=str(REPO_ROOT / "glob")) + textwrap.dedent(body)
proc = subprocess.run(
[sys.executable, "-c", script],
capture_output=True,
text=True,
timeout=180,
cwd=str(REPO_ROOT),
)
if proc.returncode != 0:
raise AssertionError(
"config-harness child failed (rc=%d). stderr tail:\n%s"
% (proc.returncode, "\n".join(proc.stderr.strip().splitlines()[-8:]))
)
lines = proc.stdout.strip().splitlines()
if not lines:
raise AssertionError(
"config-harness child exited 0 but produced no stdout. stderr tail:\n%s"
% "\n".join(proc.stderr.strip().splitlines()[-8:])
)
last_line = lines[-1]
try:
return json.loads(last_line)
except json.JSONDecodeError as e:
raise AssertionError(
"config-harness child emitted a non-JSON last line: %r\nfull stdout:\n%s"
% (last_line, proc.stdout)
) from e
class InstallFlagsConfigContractTest(unittest.TestCase):
def test_sc17_missing_keys_read_false(self):
"""Both keys absent from config.ini -> both flags read False
(secure-by-default)."""
payload = _run_child(
"""
write_ini("[default]\\nsecurity_level = normal\\n")
print(json.dumps(flag_view(fresh_read())))
"""
)
self.assertIs(payload["git"], False)
self.assertIs(payload["pip"], False)
def test_sc18_malformed_and_case_matrix(self):
"""Only case-insensitive "true" is truthy; malformed -> False."""
payload = _run_child(
"""
out = {}
for raw in ["1", "yes", "TRUE", "true ", "true"]:
write_ini("[default]\\nallow_git_url_install = %s\\nallow_pip_install = %s\\n" % (raw, raw))
cfg = fresh_read()
out[raw] = flag_view(cfg)
print(json.dumps(out))
"""
)
expected = {
"1": False, # malformed: numeric truthiness NOT honored
"yes": False, # malformed: yes/no NOT honored
"TRUE": True, # case-insensitive read (:1724)
"true ": True, # configparser strips surrounding whitespace
"true": True,
}
for raw, want in expected.items():
with self.subTest(value=raw):
self.assertIs(payload[raw]["git"], want)
self.assertIs(payload[raw]["pip"], want)
def test_sc19_write_round_trip(self):
"""write_config persists str(bool); round-trip is lossless."""
payload = _run_child(
"""
write_ini("[default]\\nsecurity_level = normal\\n")
cfg = fresh_read()
cfg["allow_git_url_install"] = True
cfg["allow_pip_install"] = False
manager_core.write_config()
raw = open(CONFIG_PATH).read()
reread = flag_view(fresh_read())
print(json.dumps({
"raw_has_git_true": "allow_git_url_install = True" in raw,
"raw_has_pip_false": "allow_pip_install = False" in raw,
"reread": reread,
}))
"""
)
self.assertTrue(
payload["raw_has_git_true"],
"write_config must persist allow_git_url_install = True in [default]",
)
self.assertTrue(
payload["raw_has_pip_false"],
"write_config must persist allow_pip_install = False in [default]",
)
self.assertIs(payload["reread"]["git"], True)
self.assertIs(payload["reread"]["pip"], False)
def test_sc20_restart_only_activation(self):
"""Editing config.ini without restart has NO effect (cache wins);
a reset (== restart) picks up the change."""
payload = _run_child(
"""
write_ini("[default]\\nallow_git_url_install = false\\n")
first = manager_core.get_config() # populates cached_config
before_edit = flag_view(first)
write_ini("[default]\\nallow_git_url_install = true\\n")
cached = flag_view(manager_core.get_config()) # NO reset: cache must win
after_restart = flag_view(fresh_read()) # reset == restart
print(json.dumps({
"before_edit": before_edit,
"cached_after_edit": cached,
"after_restart": after_restart,
}))
"""
)
self.assertIs(payload["before_edit"]["git"], False)
self.assertIs(
payload["cached_after_edit"]["git"],
False,
"cached_config must NOT hot-reload the edited flag",
)
self.assertIs(payload["after_restart"]["git"], True)
def test_sc21_exception_fallback_supplies_false(self):
"""Corrupted config.ini -> exception-fallback dict supplies flags False."""
payload = _run_child(
"""
# No [default] section header -> read_config raises inside try,
# lands in the exception-fallback dict.
write_ini("allow_git_url_install = true\\ngarbage without section\\n")
cfg = fresh_read()
print(json.dumps({
"flags": flag_view(cfg),
"fallback_marker_file_logging": cfg.get("file_logging"),
}))
"""
)
# file_logging True proves the FALLBACK dict was used (the parse
# path would yield False for a missing file_logging key).
self.assertIs(
payload["fallback_marker_file_logging"],
True,
"corrupted ini must route through the exception-fallback dict",
)
self.assertIs(payload["flags"]["git"], False)
self.assertIs(payload["flags"]["pip"], False)
def test_sc28_no_auto_migration_from_weak(self):
"""Legacy `security_level=weak` does NOT seed the flags (no auto-migration)."""
payload = _run_child(
"""
write_ini("[default]\\nsecurity_level = weak\\n")
cfg = fresh_read()
print(json.dumps({
"flags": flag_view(cfg),
"security_level": cfg.get("security_level"),
}))
"""
)
self.assertEqual(payload["security_level"], "weak")
self.assertIs(payload["flags"]["git"], False, "no auto-seed from weak")
self.assertIs(payload["flags"]["pip"], False, "no auto-seed from weak")
def test_sc42_get_bool_quirk_guard(self):
"""get_bool ignores its default param: missing `file_logging` reads
False despite a True default. The flags rely on this missing->False
quirk; this guard pins it."""
payload = _run_child(
"""
write_ini("[default]\\nsecurity_level = normal\\n")
cfg = fresh_read()
print(json.dumps({"file_logging": cfg.get("file_logging", "<ABSENT>")}))
"""
)
self.assertIs(
payload["file_logging"],
False,
"get_bool quirk changed: missing key no longer reads False — "
"new flags rely on missing->False",
)
if __name__ == "__main__":
unittest.main(verbosity=2)
+461
View File
@@ -0,0 +1,461 @@
"""Handler-gate tests for the dedicated install flags.
Three layers, each covering what the others can't:
1. SCInstallGateMirrorTest a slim mirror of the batch-install handler
(`install_custom_node`, S-C) wired to the REAL extracted gate
primitives. Covers the handler *composition* that the pure predicate
test cannot: risky-level routing, the load-bearing public canary
(the entry gate has no network term, so the deny must come from the
predicate's loopback term), block-arm unconditionality, the
security_level entry gate, and the CNR/middle false-pass guards.
2. DenialConstantsTest content of the flag denial constants and the
`security_403_response` precedence, asserted directly (no server).
3. BindingProofTest AST proof that the REAL handlers (S-A/S-B/S-C) are
wired to the predicate and that the old `is_allowed_security_level('high')`
gate is gone from S-A/S-B (closes the mirror-vs-real gap).
The direct S-A/S-B allow/deny behavior is covered by the binding proof
(wiring) plus the real-server E2E suite (behavior); the mirror here
focuses on S-C, whose multi-arm branching is the genuine logic risk.
Harness: glob/manager_server.py is not importable under the runner
(`from comfy.cli_args import args`, PromptServer), so we AST-extract the
gate primitives and exec them into a stub namespace `glob/` is never
added to sys.path (the dir name shadows stdlib glob).
"""
import ast
import asyncio
import contextlib
import inspect
import json
import logging
import unittest
from pathlib import Path
from typing import Any
from aiohttp import web
from aiohttp.test_utils import TestClient, TestServer
REPO_ROOT = Path(__file__).resolve().parent.parent
MANAGER_SERVER_PATH = REPO_ROOT / "glob" / "manager_server.py"
_WANTED_FUNCS = {
"is_loopback",
"is_dedicated_install_allowed",
"is_allowed_security_level",
"security_403_response",
}
_WANTED_CONSTS = {
"SECURITY_MESSAGE_MIDDLE_OR_BELOW",
"SECURITY_MESSAGE_NORMAL_MINUS",
"SECURITY_MESSAGE_GENERAL",
"SECURITY_MESSAGE_FLAG_GIT_URL",
"SECURITY_MESSAGE_FLAG_PIP",
}
_HANDLER_NAMES = {
"install_custom_node_git_url", # S-A
"install_custom_node_pip", # S-B
"install_custom_node", # S-C
}
class _MigrationStub:
def __init__(self):
self.system_user_api = True
def has_system_user_api(self):
return self.system_user_api
class _CoreStub:
"""Stand-in for `core` consulted by is_allowed_security_level."""
def __init__(self):
self.security_level = "normal"
def get_config(self):
return {"security_level": self.security_level}
def _load_surfaces():
source = MANAGER_SERVER_PATH.read_text()
tree = ast.parse(source)
exec_nodes = []
handler_nodes = {}
for node in tree.body:
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
if node.name in _WANTED_FUNCS:
node.decorator_list = [] # exec needs no aiohttp routing context
exec_nodes.append(node)
if node.name in _HANDLER_NAMES:
handler_nodes[node.name] = node
elif isinstance(node, ast.Assign):
for target in node.targets:
if isinstance(target, ast.Name) and target.id in _WANTED_CONSTS:
exec_nodes.append(node)
module = ast.Module(body=exec_nodes, type_ignores=[])
ns: dict = {
"web": web,
"bool": bool,
"manager_migration": _MigrationStub(),
"core": _CoreStub(),
"is_local_mode": True,
}
exec(compile(module, "manager_server_gate_surfaces", "exec"), ns)
# Feature is implemented — these must resolve, else the extraction or
# the production code regressed.
for name in _WANTED_FUNCS | _WANTED_CONSTS:
assert ns.get(name) is not None, "missing gate primitive: %s" % name
for name in _HANDLER_NAMES:
assert name in handler_nodes, "missing handler: %s" % name
return ns, handler_nodes
NS, HANDLERS = _load_surfaces()
PREDICATE: Any = NS["is_dedicated_install_allowed"]
IS_LOOPBACK: Any = NS["is_loopback"]
IAS: Any = NS["is_allowed_security_level"]
SEC_403: Any = NS["security_403_response"]
CATALOG_URL = "https://github.com/catalog/listed-node"
UNKNOWN_URL = "https://github.com/x/not-in-catalog"
CATALOG_PIP = "torch"
UNKNOWN_PIP = "definitely-not-in-catalog-pkg"
def _body_unknown(files=None, pip=None):
"""version=='unknown' ingestion arm."""
return {
"version": "unknown",
"selected_version": "unknown",
"files": files or [UNKNOWN_URL],
"pip": pip or [],
"channel": "default",
"mode": "cache",
"ui_id": "test-row",
}
def _body_cnr_latest():
"""non-nightly CNR arm — risky='low' set statically."""
return {
"version": "1.0.0",
"selected_version": "latest",
"id": "catalog-cnr-pack",
"channel": "default",
"mode": "cache",
"ui_id": "test-row",
}
class _TrackingFlags(dict):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.reads = []
def __getitem__(self, key):
self.reads.append(key)
return super().__getitem__(key)
class GateEnv:
"""Injectable per-row environment for the mirror app."""
def __init__(self, git=False, pip=False, listen="127.0.0.1", security_level="normal"):
self.flags = _TrackingFlags(
{"allow_git_url_install": git, "allow_pip_install": pip}
)
self.listen = listen
self.security_level = security_level
self.task_queue = []
self.risky_calls = 0
self.catalog_urls = {CATALOG_URL}
self.catalog_pips = {CATALOG_PIP}
def get_risky_level(self, files, pip_packages):
"""Mirror of get_risky_level: URL check precedes pip check."""
self.risky_calls += 1
for x in files or []:
if x not in self.catalog_urls:
return "high"
for p in pip_packages or []:
if p not in self.catalog_pips:
return "block"
return "middle"
_SC_DENY_TEXT = "A security error has occurred. Please check the terminal logs"
def _apply_env(env):
"""Retained gates use the is_local_mode snapshot + config stub."""
NS["is_local_mode"] = IS_LOOPBACK(env.listen)
NS["core"].security_level = env.security_level
def _make_sc_install(env):
"""Slim mirror of install_custom_node (S-C) in its post-change gate
shape: security_level entry gate, then risky-level routing where the
'high' (unknown-URL) arm goes through the dedicated predicate and the
retained arms keep is_allowed_security_level."""
async def sc_install(request):
# ENTRY gate — UNCHANGED (security_level-governed)
if not IAS("middle"):
logging.error(NS["SECURITY_MESSAGE_MIDDLE_OR_BELOW"])
return web.Response(status=403, text=_SC_DENY_TEXT)
json_data = await request.json()
risky_level = None
git_url = None
selected_version = json_data.get("selected_version")
if json_data["version"] != "unknown" and selected_version != "unknown":
if selected_version != "nightly":
risky_level = "low" # static — get_risky_level NOT called
else:
git_url = [json_data.get("repository")]
else:
git_url = json_data.get("files")
if risky_level is None:
risky_level = env.get_risky_level(git_url, json_data.get("pip", []))
if risky_level == "high":
# unknown-URL arm -> dedicated predicate (flag AND loopback)
if not PREDICATE(env.flags["allow_git_url_install"], env.listen):
logging.error(NS["SECURITY_MESSAGE_FLAG_GIT_URL"])
return web.Response(status=404, text=_SC_DENY_TEXT)
elif not IAS(risky_level):
# 'block' is always False -> unconditional deny; 'middle'/'low'
# retained UNCHANGED.
logging.error(NS["SECURITY_MESSAGE_GENERAL"])
return web.Response(status=404, text=_SC_DENY_TEXT)
env.task_queue.append(("install", json_data.get("ui_id")))
return web.Response(status=200)
app = web.Application()
app.router.add_post("/manager/queue/install", sc_install)
return app
class _LogCapture(logging.Handler):
def __init__(self):
super().__init__()
self.messages = []
def emit(self, record):
self.messages.append(record.getMessage())
@contextlib.contextmanager
def _capture_logs():
handler = _LogCapture()
root = logging.getLogger()
old_level = root.level
root.addHandler(handler)
root.setLevel(logging.DEBUG)
try:
yield handler.messages
finally:
root.removeHandler(handler)
root.setLevel(old_level)
class SCInstallGateMirrorTest(unittest.TestCase):
"""Batch-install (S-C) gate composition via a slim handler mirror."""
def setUp(self):
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
def tearDown(self):
self.loop.close()
def _post(self, env, body):
_apply_env(env)
async def go():
server = TestServer(_make_sc_install(env))
client = TestClient(server)
await client.start_server()
try:
resp = await client.post("/manager/queue/install", json=body)
return resp.status, await resp.text()
finally:
await client.close()
return self.loop.run_until_complete(go())
def _installs(self, env):
return [item for item in env.task_queue if item[0] == "install"]
def _has(self, logs, const_name):
return any(NS[const_name] in m for m in logs)
def test_high_arm_allow(self):
env = GateEnv(git=True, listen="127.0.0.1")
with _capture_logs() as logs:
status, _ = self._post(env, _body_unknown())
self.assertEqual(status, 200)
self.assertEqual(len(self._installs(env)), 1)
self.assertFalse(self._has(logs, "SECURITY_MESSAGE_FLAG_GIT_URL"))
def test_high_arm_flag_deny(self):
env = GateEnv(git=False, listen="127.0.0.1")
with _capture_logs() as logs:
status, text = self._post(env, _body_unknown())
self.assertEqual(status, 404) # risky-position deny shape kept
self.assertIn("A security error has occurred", text)
self.assertEqual(self._installs(env), [])
self.assertTrue(self._has(logs, "SECURITY_MESSAGE_FLAG_GIT_URL"))
self.assertFalse(self._has(logs, "SECURITY_MESSAGE_NORMAL_MINUS"))
def test_load_bearing_public_canary(self):
"""Entry gate passes on a public listener; the deny MUST come from
the predicate's loopback term (the 'middle' set has no network
term). 404 (risky deny), not 403 (entry deny)."""
env = GateEnv(git=True, listen="0.0.0.0", security_level="normal")
with _capture_logs() as logs:
status, _ = self._post(env, _body_unknown())
self.assertEqual(status, 404)
self.assertEqual(self._installs(env), [])
self.assertFalse(
self._has(logs, "SECURITY_MESSAGE_MIDDLE_OR_BELOW"),
"entry gate must PASS here — deny must come from the predicate",
)
self.assertTrue(self._has(logs, "SECURITY_MESSAGE_FLAG_GIT_URL"))
def test_unknown_pip_block_unconditional(self):
"""Unknown-pip 'block' stays unconditional regardless of both flags
(catalog URL + non-catalog pip; URL check precedes pip check)."""
env = GateEnv(git=True, pip=True, listen="127.0.0.1")
body = _body_unknown(files=[CATALOG_URL], pip=[UNKNOWN_PIP])
with _capture_logs() as logs:
status, _ = self._post(env, body)
self.assertEqual(status, 404)
self.assertEqual(self._installs(env), [])
self.assertEqual(env.risky_calls, 1)
self.assertTrue(self._has(logs, "SECURITY_MESSAGE_GENERAL"))
self.assertEqual(env.flags.reads, [], "block arm must not consult the flags")
def test_entry_gate_strong_denies_despite_flags(self):
"""The security_level entry gate stays in force; flags do NOT bypass it."""
env = GateEnv(git=True, pip=True, listen="127.0.0.1", security_level="strong")
with _capture_logs() as logs:
status, _ = self._post(env, _body_unknown())
self.assertEqual(status, 403)
self.assertTrue(self._has(logs, "SECURITY_MESSAGE_MIDDLE_OR_BELOW"))
self.assertEqual(self._installs(env), [])
self.assertEqual(env.flags.reads, [], "entry deny must not consult the flags")
def test_cnr_latest_arm_never_consults_flags(self):
"""non-nightly CNR sets risky='low' statically — get_risky_level and
the flags are never consulted (false-pass guard)."""
env = GateEnv(git=False, pip=False, listen="127.0.0.1")
status, _ = self._post(env, _body_cnr_latest())
self.assertEqual(status, 200)
self.assertEqual(len(self._installs(env)), 1)
self.assertEqual(env.risky_calls, 0)
self.assertEqual(env.flags.reads, [], "flags must NOT be consulted on the CNR arm")
def test_middle_arm_retained(self):
"""all-catalog body -> risky='middle'; consults security_level
(UNCHANGED), not the flags."""
env = GateEnv(git=False, pip=False, listen="127.0.0.1")
body = _body_unknown(files=[CATALOG_URL], pip=[CATALOG_PIP])
status, _ = self._post(env, body)
self.assertEqual(status, 200)
self.assertEqual(len(self._installs(env)), 1)
self.assertEqual(env.risky_calls, 1)
self.assertEqual(env.flags.reads, [], "flags must NOT be consulted on the middle arm")
class DenialConstantsTest(unittest.TestCase):
"""Denial-copy honesty + security_403_response precedence (no server)."""
def _assert_honest_copy(self, const, flag_name):
self.assertIn(flag_name, const, "constant must name the responsible flag")
self.assertIn("config.ini", const, "constant must name config.ini")
for cause_phrasing in (
"is not allowed in this security_level",
"set the security level",
"a security_level of",
"security level configuration",
):
self.assertNotIn(cause_phrasing, const)
self.assertNotEqual(const, NS["SECURITY_MESSAGE_NORMAL_MINUS"])
self.assertNotEqual(const, NS["SECURITY_MESSAGE_GENERAL"])
def test_flag_constants_content(self):
self._assert_honest_copy(NS["SECURITY_MESSAGE_FLAG_GIT_URL"], "allow_git_url_install")
self._assert_honest_copy(NS["SECURITY_MESSAGE_FLAG_PIP"], "allow_pip_install")
def test_security_403_precedence(self):
"""outdated branch FIRST; flag_token names the flag; no-arg callers
stay byte-identical."""
self.assertIn("flag_token", inspect.signature(SEC_403).parameters)
NS["manager_migration"].system_user_api = False
try:
resp = SEC_403(flag_token="allow_git_url_install")
self.assertEqual(
json.loads(resp.text), {"error": "comfyui_outdated"},
"comfyui_outdated must take PRECEDENCE over flag_token",
)
finally:
NS["manager_migration"].system_user_api = True
resp = SEC_403(flag_token="allow_git_url_install")
self.assertEqual(json.loads(resp.text), {"error": "allow_git_url_install"})
resp = SEC_403()
self.assertEqual(
json.loads(resp.text), {"error": "security_level"},
"no-arg callers must stay byte-identical",
)
class BindingProofTest(unittest.TestCase):
"""AST proof that the REAL handlers are wired to the predicate (closes
the mirror-vs-real gap)."""
@staticmethod
def _ias_literal_calls(node):
out = []
for sub in ast.walk(node):
if (
isinstance(sub, ast.Call)
and isinstance(sub.func, ast.Name)
and sub.func.id == "is_allowed_security_level"
and sub.args
):
arg = sub.args[0]
out.append(arg.value if isinstance(arg, ast.Constant) else None)
return out
def test_handlers_bind_predicate(self):
"""S-A, S-B, S-C all gate via is_dedicated_install_allowed with the
right flag + args.listen (request-time); S-C keeps the 'middle'
entry gate and a variable-arg retained is_allowed_security_level path."""
for name, flag in (
("install_custom_node_git_url", "allow_git_url_install"),
("install_custom_node_pip", "allow_pip_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["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")
def test_replace_no_high_literal_at_sa_sb(self):
"""REPLACE proof: no is_allowed_security_level('high') remains at
S-A / S-B (the flag fully replaced the old security_level gate)."""
for name in ("install_custom_node_git_url", "install_custom_node_pip"):
with self.subTest(handler=name):
self.assertNotIn(
"high", self._ias_literal_calls(HANDLERS[name]),
"%s still gates via is_allowed_security_level('high')" % name,
)
if __name__ == "__main__":
unittest.main(verbosity=2)
+136
View File
@@ -0,0 +1,136 @@
"""Structural (grep/AST) guards for the dedicated install flags.
Cheap source-level guards that complement the behavioral tests:
- Frontend 403 copy: both install surfaces in js/common.js name their
responsible flag, and the generic fallback copy stays unchanged.
- No new HTTP install surface is added.
- cm-cli stays an ungated local operator tool.
- The migration module never references the flags (no auto-seed
explicit opt-in only).
Harness: read/grep + AST over glob/*.py, cm-cli.py and js/*.js. No
imports of `glob/` modules (the dir name shadows stdlib glob).
"""
import re
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
MANAGER_SERVER_PATH = REPO_ROOT / "glob" / "manager_server.py"
MANAGER_MIGRATION_PATH = REPO_ROOT / "glob" / "manager_migration.py"
CM_CLI_PATH = REPO_ROOT / "cm-cli.py"
JS_COMMON_PATH = REPO_ROOT / "js" / "common.js"
GENERIC_403_COPY = "This action is not allowed with this security level configuration."
FLAG_TOKENS = ("allow_git_url_install", "allow_pip_install")
def _js_function_block(source, func_name):
"""Slice an `export async function <name>` block (up to the next
export or EOF)."""
start = source.find("export async function %s" % func_name)
if start < 0:
raise AssertionError("function %s not found in js source" % func_name)
next_export = source.find("export ", start + 1)
return source[start: next_export if next_export > 0 else len(source)]
def _handle403_call_args(source):
"""All handle403Response(...) CALL argument strings (def/import lines
excluded)."""
calls = []
for match in re.finditer(r"handle403Response\s*\(([^()]*(?:\([^()]*\)[^()]*)*)\)", source):
line_start = source.rfind("\n", 0, match.start()) + 1
line = source[line_start: source.find("\n", match.start())]
if "function handle403Response" in line or line.lstrip().startswith("import"):
continue
calls.append(match.group(1).strip())
return calls
class JsCopyStructuralTest(unittest.TestCase):
"""Frontend honest-copy contract."""
@classmethod
def setUpClass(cls):
cls.common_src = JS_COMMON_PATH.read_text()
def test_surface_messages_name_their_flag(self):
"""Both install 403 branches pass a flag-naming defaultMessage."""
for func, flag in (
("install_via_git_url", "allow_git_url_install"),
("install_pip", "allow_pip_install"),
):
with self.subTest(func=func):
block = _js_function_block(self.common_src, func)
two_arg_calls = [a for a in _handle403_call_args(block) if "," in a]
self.assertTrue(
two_arg_calls,
"%s must call handle403Response with a defaultMessage" % func,
)
self.assertIn(flag, block)
self.assertIn("config.ini", block)
def test_generic_fallback_and_frozen_callers_unchanged(self):
"""The generic fallback copy stays (exactly its two occurrences in
handle403Response), and no other handle403Response caller across
js/ gains a defaultMessage."""
self.assertEqual(self.common_src.count(GENERIC_403_COPY), 2)
surface_blocks = "".join(
_js_function_block(self.common_src, name)
for name in ("install_pip", "install_via_git_url")
)
allowed_two_arg = {a for a in _handle403_call_args(surface_blocks) if "," in a}
for js_file in sorted((REPO_ROOT / "js").glob("*.js")):
source = js_file.read_text()
for args in _handle403_call_args(source):
if "," in args:
self.assertIn(
args, allowed_two_arg,
"frozen handle403Response caller in %s gained a "
"defaultMessage: handle403Response(%s)" % (js_file.name, args),
)
class StructuralSecurityGuardsTest(unittest.TestCase):
"""Source-level guards against scope bleed."""
def test_no_new_install_route_surface(self):
"""No new HTTP surface for git-URL/pip install."""
source = MANAGER_SERVER_PATH.read_text()
routes = set(re.findall(r"@routes\.post\(\"([^\"]+)\"\)", source))
expected_surfaces = {
"/customnode/install/git_url",
"/customnode/install/pip",
"/manager/queue/install",
"/manager/queue/reinstall",
}
self.assertTrue(expected_surfaces.issubset(routes))
install_like = {r for r in routes if "install" in r}
self.assertEqual(
install_like,
expected_surfaces
| {"/manager/queue/uninstall", "/manager/queue/install_model"},
"install-like route set drifted — no new install surface allowed",
)
def test_cm_cli_ungated(self):
"""cm-cli stays a local operator tool — no gate, no flag lookup."""
source = CM_CLI_PATH.read_text()
for token in FLAG_TOKENS + ("is_allowed_security_level", "is_dedicated_install_allowed"):
self.assertNotIn(token, source, "cm-cli.py must stay ungated")
def test_no_autoseed_in_migration(self):
"""The migration module never references the flags (explicit
opt-in only no auto-seed from security_level)."""
source = MANAGER_MIGRATION_PATH.read_text()
for token in FLAG_TOKENS:
self.assertNotIn(
token, source,
"manager_migration.py must not seed/translate the new flags",
)
if __name__ == "__main__":
unittest.main(verbosity=2)