comfyui-mcp 0.52.2 → 0.52.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +13 -0
  2. package/dist/comfyui/client.js +35 -8
  3. package/dist/comfyui/client.js.map +1 -1
  4. package/dist/comfyui/fetch.js +44 -0
  5. package/dist/comfyui/fetch.js.map +1 -1
  6. package/dist/comfyui/json-guard.js +47 -3
  7. package/dist/comfyui/json-guard.js.map +1 -1
  8. package/dist/config.js +7 -0
  9. package/dist/config.js.map +1 -1
  10. package/dist/orchestrator/panel-tools.js +1511 -332
  11. package/dist/orchestrator/panel-tools.js.map +1 -1
  12. package/dist/orchestrator/promoted-widget.js +101 -0
  13. package/dist/orchestrator/promoted-widget.js.map +1 -0
  14. package/dist/services/download-cache.js +10 -0
  15. package/dist/services/download-cache.js.map +1 -1
  16. package/dist/services/manager-node-search.js +190 -0
  17. package/dist/services/manager-node-search.js.map +1 -0
  18. package/dist/services/manifest.js +27 -60
  19. package/dist/services/manifest.js.map +1 -1
  20. package/dist/services/mid-command-remedy.js +31 -4
  21. package/dist/services/mid-command-remedy.js.map +1 -1
  22. package/dist/services/model-resolver.js +217 -49
  23. package/dist/services/model-resolver.js.map +1 -1
  24. package/dist/services/node-authoring.js +25 -12
  25. package/dist/services/node-authoring.js.map +1 -1
  26. package/dist/services/process-control.js +230 -3
  27. package/dist/services/process-control.js.map +1 -1
  28. package/dist/services/queue-monitor.js +51 -2
  29. package/dist/services/queue-monitor.js.map +1 -1
  30. package/dist/services/trainer-bootstrap.js +51 -5
  31. package/dist/services/trainer-bootstrap.js.map +1 -1
  32. package/dist/services/ui-bridge.js +89 -17
  33. package/dist/services/ui-bridge.js.map +1 -1
  34. package/dist/services/workspace-env.js +40 -0
  35. package/dist/services/workspace-env.js.map +1 -1
  36. package/dist/tools/node-pack.js +20 -4
  37. package/dist/tools/node-pack.js.map +1 -1
  38. package/dist/tools/process-control.js +1 -1
  39. package/dist/tools/process-control.js.map +1 -1
  40. package/dist/tools/vocabulary.js +350 -0
  41. package/dist/tools/vocabulary.js.map +1 -1
  42. package/dist/utils/origin.js +12 -2
  43. package/dist/utils/origin.js.map +1 -1
  44. package/package.json +1 -1
@@ -13,7 +13,7 @@ import { logger } from "../utils/logger.js";
13
13
  import { downloadWithCache, findResumablePartial, probeRemoteModelPayload } from "./download-cache.js";
14
14
  import { reportDownloadProgress } from "./download-progress.js";
15
15
  import { modelNotFoundMessage } from "./model-root-scope.js";
16
- import { resolveModelsDir, resolveModelsDirWithBases, parseModelsDirFromArgv, hasUnresolvableRelativeModelDirFlag, isLiveAuthoritativeModelsDir, modelsDirNamedByServer, } from "./output-dir.js";
16
+ import { resolveModelsDirWithBases, parseModelsDirFromArgv, parseExtraModelPathsConfigsFromArgvRaw, hasUnresolvableRelativeModelDirFlag, isLiveAuthoritativeModelsDir, modelsDirNamedByServer, } from "./output-dir.js";
17
17
  import { applyDownloadAuth, redactUrlForLogs, } from "./download-auth.js";
18
18
  export const MODEL_SUBDIRS = [
19
19
  "checkpoints",
@@ -1143,32 +1143,54 @@ category) {
1143
1143
  // "visible" and gives it the qualified note instead. That is the right trade:
1144
1144
  // the note names the file, the root, and the remedy, whereas the false positive
1145
1145
  // is silent and the user discovers it at queue time.
1146
- if (!modelsDirNamedByServer(dest.source))
1146
+ //
1147
+ // One exception (#369, the 0.52.1 reports): an extra model root is vouched for by
1148
+ // the server's OWN command line when argv carries an ABSOLUTE
1149
+ // --extra-model-paths-config — that statement is independent of how the PRIMARY
1150
+ // root was derived, and a download redirected onto such a root
1151
+ // (resolveModelSubfolderWithLiveRoot) must be able to confirm here, or the file
1152
+ // lands exactly where the server reads and is STILL reported unconfirmed. With no
1153
+ // argv-named config the extra roots may come from the auto-loaded config beside
1154
+ // an INFERRED main.py — that inherits the inference and vouches for nothing, so
1155
+ // the early UNKNOWN stands. The unvouched primary still cannot support a
1156
+ // NEGATIVE answer below: unknown stays unknown, never "outside the live roots".
1157
+ const primaryNamed = modelsDirNamedByServer(dest.source);
1158
+ if (!primaryNamed &&
1159
+ !parseExtraModelPathsConfigsFromArgvRaw(dest.snapshot.argv).some((p) => isAbsolute(p))) {
1147
1160
  return { inRoots: undefined };
1161
+ }
1148
1162
  const liveRoot = resolve(dest.modelsDir);
1149
- // A negative answer is only honest when everything it rests on could actually be
1150
- // canonicalized; otherwise say UNKNOWN rather than accuse a correct placement.
1151
- let fullyCanonical = target.ok;
1152
- const primary = await canon(dest.modelsDir);
1153
- fullyCanonical = fullyCanonical && primary.ok;
1154
- if (under(primary))
1155
- return { inRoots: true, liveRoot };
1156
1163
  let extra;
1157
1164
  try {
1158
1165
  extra = await getLiveExtraModelRoots(dest.snapshot);
1159
1166
  }
1160
1167
  catch {
1161
- return { inRoots: undefined, liveRoot };
1168
+ extra = undefined;
1162
1169
  }
1163
1170
  const wantCat = (category ?? "").trim().toLowerCase();
1164
- for (const r of extra.roots) {
1165
- if (wantCat && String(r.category ?? "").trim().toLowerCase() !== wantCat)
1166
- continue;
1167
- const rc = await canon(r.dir);
1168
- fullyCanonical = fullyCanonical && rc.ok;
1169
- if (under(rc))
1171
+ // A negative answer is only honest when everything it rests on could actually be
1172
+ // canonicalized; otherwise say UNKNOWN rather than accuse a correct placement.
1173
+ let fullyCanonical = target.ok;
1174
+ if (primaryNamed) {
1175
+ const primary = await canon(dest.modelsDir);
1176
+ fullyCanonical = fullyCanonical && primary.ok;
1177
+ if (under(primary))
1170
1178
  return { inRoots: true, liveRoot };
1171
1179
  }
1180
+ if (extra?.authoritative) {
1181
+ for (const r of extra.roots) {
1182
+ if (wantCat && String(r.category ?? "").trim().toLowerCase() !== wantCat)
1183
+ continue;
1184
+ const rc = await canon(r.dir);
1185
+ fullyCanonical = fullyCanonical && rc.ok;
1186
+ if (under(rc))
1187
+ return { inRoots: true, liveRoot };
1188
+ }
1189
+ }
1190
+ if (!primaryNamed)
1191
+ return { inRoots: undefined, liveRoot };
1192
+ if (!extra)
1193
+ return { inRoots: undefined, liveRoot };
1172
1194
  // The live roots are known and this path is in none of them.
1173
1195
  if (!extra.authoritative)
1174
1196
  return { inRoots: undefined, liveRoot };
@@ -1568,18 +1590,40 @@ export async function verifyLandedModel(targetPath, targetSubfolder, opts) {
1568
1590
  `registers a category serving this file type, this server cannot load it at all.`,
1569
1591
  };
1570
1592
  }
1593
+ // #369 (0.52.1 recurrence) — RESOLVE THE ROOT **WITH** ITS PROVENANCE.
1594
+ //
1595
+ // This used to call `resolveModelsDir()`, which reports the models dir whatever
1596
+ // its `source` — including `configured-base`, the one value a reachable server
1597
+ // never vouched for. The verdict below then compared the landed path against it
1598
+ // and, finding the file inside, told the user the connected ComfyUI READS that
1599
+ // directory. But that root is the very one this download chose, so the file is
1600
+ // inside it BY CONSTRUCTION: the comparison is true for a correct destination and
1601
+ // for a stale second install alike, and cannot tell them apart. On the reporter's
1602
+ // macOS Comfy Desktop pair (two installs under ~/ComfyUI-Installs, the live one
1603
+ // reached through extra_model_paths) that produced "it is in the right place …
1604
+ // Do NOT move the file" for ~25 GB written where the running server never looks.
1605
+ //
1606
+ // So the SOURCE travels with the path, and only a server-NAMED root licenses the
1607
+ // claim. `modelsDirNamedByServer` — not `isLiveAuthoritativeModelsDir` — because
1608
+ // an `observed-root` is INFERRED from where the interpreter lives and is exactly
1609
+ // the inference that lands on the wrong install (#1562).
1571
1610
  let liveModelsDir;
1611
+ let liveModelsDirNamedByServer = false;
1572
1612
  try {
1573
- liveModelsDir = await resolveModelsDir();
1613
+ const dest = await resolveModelsDirWithBases();
1614
+ liveModelsDir = dest.modelsDir;
1615
+ liveModelsDirNamedByServer = modelsDirNamedByServer(dest.source);
1574
1616
  }
1575
1617
  catch {
1576
1618
  liveModelsDir = undefined;
1619
+ liveModelsDirNamedByServer = false;
1577
1620
  }
1578
1621
  return {
1579
1622
  verifiedPath,
1580
1623
  ...notVisibleVerdict({
1581
1624
  verifiedPath,
1582
1625
  liveModelsDir,
1626
+ liveModelsDirNamedByServer,
1583
1627
  wanted,
1584
1628
  category,
1585
1629
  baseUrl: getComfyUIBaseUrl(),
@@ -1595,14 +1639,33 @@ export async function verifyLandedModel(targetPath, targetSubfolder, opts) {
1595
1639
  /**
1596
1640
  * The verdict for a file that is on disk but absent from the live listing (#1131).
1597
1641
  *
1598
- * "Not listed" has TWO causes and they take OPPOSITE remedies. If the file sits
1599
- * UNDER the very models root the server reads, it is not misplaced: the server
1600
- * simply has not re-read that folder yet. ComfyUI caches its loader option lists
1601
- * and invalidates them on the directory's mtime, so a check this soon after the
1602
- * write routinely races it. Telling that user to "move the file into the running
1603
- * server's models tree" names a directory the file is ALREADY in — a remedy that
1604
- * cannot be followed, which is what the reporter received. Only a file OUTSIDE
1605
- * that root is genuinely in the wrong place.
1642
+ * "Not listed" has THREE causes, and the first two take OPPOSITE remedies:
1643
+ *
1644
+ * INSIDE a root the SERVER NAMED — not misplaced. The server simply has not
1645
+ * re-read that folder yet: ComfyUI caches its loader option lists and
1646
+ * invalidates them on the directory's mtime, so a check this soon after the
1647
+ * write routinely races it. Telling that user to "move the file into the
1648
+ * running server's models tree" names a directory the file is ALREADY in — a
1649
+ * remedy that cannot be followed, which is what #1131's reporter received.
1650
+ *
1651
+ * OUTSIDE that root — genuinely in the wrong place. Move it.
1652
+ *
1653
+ * INSIDE a root NOBODY VOUCHED FOR — unknown, and it must be SAID (#369, the
1654
+ * 0.52.1 recurrence). This third state used to be folded into the first,
1655
+ * because the containment test ran against whatever `resolveModelsDir()`
1656
+ * returned — including a `configured-base`/`observed-root` value the running
1657
+ * server never named. That root is the one the download itself resolved to, so
1658
+ * the landed file is inside it BY CONSTRUCTION and the test answers "inside"
1659
+ * for a correct destination and a stale second install alike. It is the same
1660
+ * shape as #1603: two states, one indistinguishable answer, and no way to say
1661
+ * "unknown". The reporter's macOS Comfy Desktop pair got "so it is in the right
1662
+ * place … Do NOT move the file" for ~25 GB written into an install the running
1663
+ * server (reached through extra_model_paths) never reads.
1664
+ *
1665
+ * The unverified branch is deliberately NOT the move remedy either: refusing to
1666
+ * assert readership is not evidence of misplacement, and #1131's harm was exactly
1667
+ * an unfollowable move instruction. It states both candidates, orders them by
1668
+ * likelihood, and names the ONE check that separates them.
1606
1669
  *
1607
1670
  * The DECISION lives here rather than at the call site so it is covered by the
1608
1671
  * same tests as the wording; a branch chosen upstream and passed in as a boolean
@@ -1611,29 +1674,78 @@ export async function verifyLandedModel(targetPath, targetSubfolder, opts) {
1611
1674
  export function notVisibleVerdict(args) {
1612
1675
  const { verifiedPath, liveModelsDir, wanted, category, baseUrl } = args;
1613
1676
  const insideLexically = liveModelsDir !== undefined && isUnderRoot(verifiedPath, liveModelsDir);
1614
- const insideLiveRoot = insideLexically || args.knownInsideLiveRoots === true;
1677
+ const rootIsServerNamed = args.liveModelsDirNamedByServer === true;
1678
+ // Containment ALONE is not readership. It counts only when the root it is
1679
+ // measured against came from the server (`rootIsServerNamed`), or when the
1680
+ // caller's canonical membership answer — which applies that same narrowing
1681
+ // itself, and additionally covers live extra roots and in-tree junctions —
1682
+ // already said yes.
1683
+ const insideLiveRoot = (insideLexically && rootIsServerNamed) || args.knownInsideLiveRoots === true;
1684
+ // On disk under the root we resolved, with nothing establishing that the server
1685
+ // reads it. Neither remedy is earned; say so, and name the check that decides.
1686
+ const insideUnverifiedRoot = insideLexically && !insideLiveRoot;
1687
+ if (insideUnverifiedRoot) {
1688
+ return {
1689
+ liveVisible: "not-visible",
1690
+ note: `The file IS on disk at ${verifiedPath}, inside ${liveModelsDir} — the models root this ` +
1691
+ `download resolved to. Whether the connected ComfyUI (${baseUrl}) READS that root is ` +
1692
+ `UNCONFIRMED: the running server never named its own models directory, so this root came ` +
1693
+ `from local configuration (COMFYUI_PATH / the saved default workspace) or was inferred ` +
1694
+ `from where the server's interpreter lives — and the file is inside it either way, ` +
1695
+ `because that is the root the download picked. That server does not list "${wanted}" ` +
1696
+ `under "${category}".\n\n` +
1697
+ `TWO very different things produce exactly this, and only one is a problem:\n` +
1698
+ ` 1. a STALE LISTING — ComfyUI caches its loader options and invalidates them on the ` +
1699
+ `directory's mtime, so a check this soon after a write routinely races it; or\n` +
1700
+ ` 2. a DIFFERENT INSTALL — the bytes are in a models tree the running server never ` +
1701
+ `scans, which is #369 and is how multi-GB downloads get reported as landed and are ` +
1702
+ `never seen again.\n\n` +
1703
+ `Do not move it on the strength of this message alone, and do not treat it as placed ` +
1704
+ `either. SEPARATE THE TWO: refresh the node/model definitions — install_comfyui ` +
1705
+ `(action:"refresh_nodes"), or the panel's refresh — then re-check with list_local_models. ` +
1706
+ `If it STILL does not appear, it is case 2: list_local_models (action:"list_paths") ` +
1707
+ `reports the roots the running server actually reads — move the file into the one for ` +
1708
+ `"${category}". To make future downloads verifiable, point COMFYUI_PATH at the install ` +
1709
+ `that is actually running, or launch it with an absolute --base-directory.`,
1710
+ };
1711
+ }
1615
1712
  return {
1616
1713
  // Still not VISIBLE — we did not observe it in the listing, and #369 exists
1617
1714
  // because an unobserved placement must not render as confirmed. The verdict
1618
1715
  // is unchanged; what changes is the explanation and the remedy.
1619
1716
  liveVisible: "not-visible",
1620
1717
  note: insideLiveRoot
1621
- ? (insideLexically
1622
- ? `The file IS on disk at ${verifiedPath}, which is INSIDE the models directory the ` +
1623
- `connected ComfyUI reads (${liveModelsDir}) — so it is in the right place. `
1624
- : `The file IS on disk at ${verifiedPath}, which is inside a models tree the ` +
1625
- `connected ComfyUI reads — reached through a link/junction` +
1626
- `${liveModelsDir ? ` under ${liveModelsDir}` : ""}, so its real path lying ` +
1627
- `outside that directory is the layout working as intended, not a misplacement. `) +
1628
- `That server does not list "${wanted}" under "${category}" YET, which almost always ` +
1629
- `means its cached loader options have not been re-read since the write (ComfyUI ` +
1630
- `invalidates them on the directory's mtime). Do NOT move the file. Refresh the ` +
1631
- `node/model definitions — install_comfyui (action:"refresh_nodes"), or the panel's ` +
1632
- `refresh — or restart ComfyUI, then check list_local_models again.`
1718
+ ? // The lexical phrasing NAMES the directory as one the server reads, so it may
1719
+ // only be used when the SERVER named that directory. Otherwise the membership
1720
+ // answer is what vouched for the placement (an extra root / a junction), and
1721
+ // the wording that does not attribute the root is the honest one.
1722
+ (insideLexically && rootIsServerNamed
1723
+ ? `The file IS on disk at ${verifiedPath}, which is INSIDE the models directory the ` +
1724
+ `connected ComfyUI reads (${liveModelsDir}) — so it is in the right place. `
1725
+ : `The file IS on disk at ${verifiedPath}, which is inside a models tree the ` +
1726
+ `connected ComfyUI reads — reached through a link/junction` +
1727
+ `${liveModelsDir ? ` under ${liveModelsDir}` : ""}, so its real path lying ` +
1728
+ `outside that directory is the layout working as intended, not a misplacement. `) +
1729
+ `That server does not list "${wanted}" under "${category}" YET, which almost always ` +
1730
+ `means its cached loader options have not been re-read since the write (ComfyUI ` +
1731
+ `invalidates them on the directory's mtime). Do NOT move the file. Refresh the ` +
1732
+ `node/model definitions — install_comfyui (action:"refresh_nodes"), or the panel's ` +
1733
+ `refresh — or restart ComfyUI, then check list_local_models again.`
1633
1734
  : `The file IS on disk at ${verifiedPath}, but the connected ComfyUI ` +
1634
1735
  `(${baseUrl}) does NOT list "${wanted}" under "${category}" — it will not be ` +
1635
1736
  `usable in a workflow from there.` +
1636
- (liveModelsDir ? ` The models directory that server reads is ${liveModelsDir}.` : "") +
1737
+ // Same rule as above: name it as WHAT THE SERVER READS only when the server
1738
+ // named it. An unvouched root is still worth printing — it is where the
1739
+ // bytes are — but as the destination this download resolved to, not as a
1740
+ // fact about the server (#369).
1741
+ (liveModelsDir
1742
+ ? rootIsServerNamed
1743
+ ? ` The models directory that server reads is ${liveModelsDir}.`
1744
+ : ` This download resolved its destination root to ${liveModelsDir} from local ` +
1745
+ `configuration — the running server never named its own models directory, so that ` +
1746
+ `is not a confirmed answer for where the file belongs; list_local_models ` +
1747
+ `(action:"list_paths") reports the roots it actually reads.`
1748
+ : "") +
1637
1749
  " Move the file into the running server's models tree (or point COMFYUI_PATH at that install and re-download).",
1638
1750
  };
1639
1751
  }
@@ -1707,28 +1819,84 @@ export async function resolveModelSubfolderWithLiveRoot(targetSubfolder) {
1707
1819
  const { modelsDir, baseDirs, snapshot, source } = await resolveModelsDirWithBases({
1708
1820
  targetCategory: escapes ? "" : categoryOf(normalizedSub),
1709
1821
  });
1710
- const modelsRoot = resolve(modelsDir);
1711
- const targetDir = resolve(modelsRoot, raw);
1822
+ let modelsRoot = resolve(modelsDir);
1823
+ let targetDir = resolve(modelsRoot, raw);
1712
1824
  if (targetDir !== modelsRoot && !targetDir.startsWith(modelsRoot + sep)) {
1713
1825
  throw new ModelError(`Refusing to write outside the models directory: ${raw}`);
1714
1826
  }
1715
- // The root did NOT come from the running server (it could not tell us where it
1716
- // lives, so we fell back to local config). Before writing, check the one thing
1717
- // that can still expose a stale-install destination: the server's own listing for
1718
- // this category. #369 shipped 4.88 GB into a directory the live ComfyUI had never
1719
- // heard of; refusing here costs nothing and saves the transfer.
1720
- await assertDestinationVisibleToLiveServer(modelsRoot, raw, source, snapshot);
1827
+ // #369 (the 0.52.1 reports) — a root the SERVER NAMED beats our best inference.
1828
+ //
1829
+ // When the primary root came from local configuration or from an INFERRED live
1830
+ // root, every guard below can do no better than fail OPEN on an empty tree
1831
+ // (#1147's deliberate trade: an empty listing contradicts nothing), so a stale
1832
+ // second install received the write whenever its evidence was silent — the
1833
+ // 0.52.1 reports had gigabytes land in the non-running install while the
1834
+ // connected Desktop server's models lived in the shared root its
1835
+ // --extra-model-paths-config declares (the very root list_paths shows).
1836
+ //
1837
+ // That config flag is the server's OWN command line naming a root it reads this
1838
+ // category from — a statement, not an inference — so it outranks the unvouched
1839
+ // primary and the download goes THERE instead. The gate is deliberately the
1840
+ // exact evidence: an ABSOLUTE --extra-model-paths-config in argv (a relative
1841
+ // one cannot be located from this process and fails closed, per extra-paths),
1842
+ // an AUTHORITATIVE read of it, and a MODEL category (never custom_nodes — a
1843
+ // download must not become a Python import). Anything less keeps the existing
1844
+ // resolution and its corroboration guards untouched.
1845
+ const serverNamedPrimary = modelsDirNamedByServer(source);
1846
+ let redirectedToExtraRoot = false;
1847
+ if (!serverNamedPrimary && snapshot.reachable && !isRemoteMode()) {
1848
+ const category = categoryOf(normalizedSub).toLowerCase();
1849
+ const argvNamesConfig = parseExtraModelPathsConfigsFromArgvRaw(snapshot.argv).some((p) => isAbsolute(p));
1850
+ if (argvNamesConfig && category && !NON_MODEL_EXTRA_CATEGORIES.has(category)) {
1851
+ const extra = await getLiveExtraModelRoots(snapshot);
1852
+ const namedRoot = extra.authoritative
1853
+ ? extra.roots.find((r) => r.category.trim().toLowerCase() === category)
1854
+ : undefined;
1855
+ if (namedRoot) {
1856
+ const extraRoot = resolve(namedRoot.dir);
1857
+ const redirected = resolve(extraRoot, subfolderRemainder(normalizedSub));
1858
+ if (redirected !== extraRoot && !redirected.startsWith(extraRoot + sep)) {
1859
+ throw new ModelError(`Refusing to write outside the models directory: ${raw}`);
1860
+ }
1861
+ logger.info(`Downloading into the extra model root the connected server named for "${category}" ` +
1862
+ `(${extraRoot}) instead of the unvouched primary root (${modelsRoot}) — the root the ` +
1863
+ `server's --extra-model-paths-config declares is where it actually reads (#369).`);
1864
+ modelsRoot = extraRoot;
1865
+ targetDir = redirected;
1866
+ redirectedToExtraRoot = true;
1867
+ }
1868
+ }
1869
+ }
1870
+ if (!redirectedToExtraRoot) {
1871
+ // The root did NOT come from the running server (it could not tell us where it
1872
+ // lives, so we fell back to local config). Before writing, check the one thing
1873
+ // that can still expose a stale-install destination: the server's own listing for
1874
+ // this category. #369 shipped 4.88 GB into a directory the live ComfyUI had never
1875
+ // heard of; refusing here costs nothing and saves the transfer. (A server-named
1876
+ // primary is not skipped here either — the guard itself decides, because a named
1877
+ // root that does not EXIST locally, e.g. a container-side --models-directory,
1878
+ // still gets the cheap check. Only the REDIRECTED destination is exempt: the
1879
+ // server's own config named it, so disagreeing with our inference is moot.)
1880
+ await assertDestinationVisibleToLiveServer(modelsRoot, raw, source, snapshot);
1881
+ }
1721
1882
  // Path-string containment (above) is not enough: an EXISTING symlink somewhere
1722
1883
  // between modelsRoot and targetDir could redirect the real write OUTSIDE the
1723
1884
  // models dir. Because THIS resolver is the single canonical write-target for
1724
1885
  // every local download (startDownloadJob keying AND downloadModel's write), the
1725
1886
  // guard belongs here — co-located with the resolution the write actually uses —
1726
1887
  // so it can never diverge from a caller's separate pre-validation (e.g.
1727
- // apply_manifest resolving the root a second time; #490 codex review).
1888
+ // apply_manifest resolving the root a second time; #490 codex review). Runs on
1889
+ // the REDIRECTED root too: its code-root veto is what keeps a malicious or
1890
+ // miswritten extra-paths entry from turning this into a write into custom_nodes.
1728
1891
  await assertNoEscapingSymlinkAncestor(modelsRoot, targetDir, raw, baseDirs, snapshot);
1729
1892
  return {
1730
1893
  targetDir,
1731
- liveRootAtResolve: isLiveAuthoritativeModelsDir(source) ? modelsRoot : undefined,
1894
+ // A redirected destination is bound to the EXTRA root, but the writer's
1895
+ // mid-flight swap check compares PRIMARY roots (currentLiveModelsRoot), so
1896
+ // there is no honest value to bind here — leave it unknown, exactly like any
1897
+ // other non-server-named primary. The post-write check still verifies the
1898
+ // landed file against the connected server.
1899
+ liveRootAtResolve: !redirectedToExtraRoot && isLiveAuthoritativeModelsDir(source) ? modelsRoot : undefined,
1732
1900
  };
1733
1901
  }
1734
1902
  /**