Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/DynamoCore/Models/DynamoModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,11 @@ protected DynamoModel(IStartConfiguration config)
NoNetworkMode = config.NoNetworkMode;
HostAnalyticsInfo = config.HostAnalyticsInfo;

// DYN-10745: disabled in test mode since the suite routinely loads packages and
// extensions from directories outside Built-In Packages. Remove with DYN-10739.
LegacyAssistantExtensionGuard.Reset();
LegacyAssistantExtensionGuard.IsEnabled = !IsTestMode;

DebugSettings = new DebugSettings();
if (Logger == null)
{
Expand Down
11 changes: 10 additions & 1 deletion src/DynamoCore/Properties/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/DynamoCore/Properties/Resources.en-US.resx
Original file line number Diff line number Diff line change
Expand Up @@ -755,6 +755,9 @@ Parameter name: {0}</value>
<data name="DuplicatedNewerPackage" xml:space="preserve">
<value>A newer version of the package called {0} version {2} was found at {1} with version {3}. The newer version has been ignored.</value>
</data>
<data name="LegacyAssistantPackageBlocked" xml:space="preserve">
<value>An outdated copy of {0} was found at {1}. Dynamo now includes {0} as a built-in package, so the older copy was not loaded. Delete the folder to complete your upgrade.</value>
</data>
<data name="NoneLinterDescriptorName" xml:space="preserve">
<value>None</value>
</data>
Expand Down
3 changes: 3 additions & 0 deletions src/DynamoCore/Properties/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -757,6 +757,9 @@ Parameter name: {0}</value>
<data name="DuplicatedNewerPackage" xml:space="preserve">
<value>A newer version of the package called {0} version {2} was found at {1} with version {3}. The newer version has been ignored.</value>
</data>
<data name="LegacyAssistantPackageBlocked" xml:space="preserve">
<value>An outdated copy of {0} was found at {1}. Dynamo now includes {0} as a built-in package, so the older copy was not loaded. Delete the folder to complete your upgrade.</value>
</data>
<data name="Autocomplete" xml:space="preserve">
<value>Autocomplete</value>
</data>
Expand Down
1 change: 1 addition & 0 deletions src/DynamoCore/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ Dynamo.Models.DynamoModel.DefaultStartConfiguration.EnableUnTrustedLocationsNoti
Dynamo.Models.DynamoModel.IStartConfiguration.EnableUnTrustedLocationsNotifications.get -> bool
Dynamo.Models.DynamoModel.OpenFileCommand.OpenFileCommand(System.String filePath, System.Boolean forceManualExecutionMode, System.Boolean isTemplate, System.Boolean forceBlockRun) -> void
Dynamo.Models.DynamoModel.InsertFileCommand.InsertFileCommand(System.String filePath, System.Boolean forceManualExecutionMode, System.Boolean forceBlockRun) -> void
static Dynamo.Properties.Resources.LegacyAssistantPackageBlocked.get -> string
172 changes: 172 additions & 0 deletions src/DynamoCore/Utilities/LegacyAssistantExtensionGuard.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dynamo.Core;

namespace Dynamo.Utilities
{
/// <summary>
/// A view extension blocked by <see cref="LegacyAssistantExtensionGuard"/>, recorded so a
/// single startup notification/dialog can be raised for it once the main window exists.
/// </summary>
internal readonly struct BlockedLegacyViewExtension
{
internal string DisplayName { get; }
internal string ManifestPath { get; }
internal string AssemblyPath { get; }

internal BlockedLegacyViewExtension(string displayName, string manifestPath, string assemblyPath)
{
DisplayName = displayName;
ManifestPath = manifestPath;
AssemblyPath = assemblyPath;
}
}

/// <summary>
/// DYN-10745 band-aid. Dynamo 4.2 ships Autodesk Assistant and DynamoMCP as built-in
/// packages for the first time. Older, pre-built-in copies of either extension are still
/// present on some machines (manual alpha installs, or files orphaned by a Revit
/// uninstall) and can silently displace the built-in copy or corrupt assembly resolution
/// order. This guard refuses to load either extension from any location outside Dynamo's
/// Built-In Packages directory.
/// Remove this entire type, and its call sites in PackageLoader.ScanPackageDirectory and
/// ViewExtensionLoader.Load(string), once DYN-10739 lands the permanent architectural fix.
/// </summary>
internal static class LegacyAssistantExtensionGuard
{
internal const string AutodeskAssistantTypeName = "Dynamo.AutodeskAssistant.AutodeskAssistantViewExtension";
internal const string McpViewExtensionTypeName = "Dynamo.MCP.McpViewExtension";

// Autodesk Assistant's package identity churned across DYN-10450
// (AutodeskAssistant -> DynamoAssistant -> back to AutodeskAssistant). Both names are
// treated as the same restricted package so a pre-rename install is still caught.
private static readonly Dictionary<string, string> restrictedPackageDisplayNames =
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
{ "AutodeskAssistant", "Autodesk Assistant" },
{ "DynamoAssistant", "Autodesk Assistant" },
{ "DynamoMCP", "DynamoMCP" }
};

private static readonly Dictionary<string, string> restrictedTypeDisplayNames =
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
{ AutodeskAssistantTypeName, "Autodesk Assistant" },
{ McpViewExtensionTypeName, "DynamoMCP" }
};

// Package blocks record the package's ROOT DIRECTORY: a Dynamo package is a
// self-contained folder, so "delete this folder" is safe and correct advice.
private static readonly HashSet<string> blockedPackageDirectories =
new HashSet<string>(StringComparer.OrdinalIgnoreCase);

// View-extension blocks record the individual manifest/assembly FILE paths, not a
// folder. A legacy install found this way (e.g. files orphaned directly under a
// Revit add-in folder by an uninstall) is not necessarily isolated in its own folder,
// so inferring "delete this folder" from the file's location could tell a user to
// delete something far broader than intended.
private static readonly List<BlockedLegacyViewExtension> blockedViewExtensions =
new List<BlockedLegacyViewExtension>();

/// <summary>
/// Whether the guard is active. Set from DynamoModel startup based on
/// !IsTestMode, so the existing test suite (which routinely loads packages and
/// extensions from directories outside Built-In Packages) is unaffected.
/// </summary>
internal static bool IsEnabled { get; set; }

/// <summary>
/// Looks up the friendly display name for a restricted package name. Returns false
/// for any package name that is not restricted.
/// </summary>
internal static bool TryGetRestrictedPackageDisplayName(string packageName, out string displayName)
{
displayName = null;
return packageName != null && restrictedPackageDisplayNames.TryGetValue(packageName, out displayName);
}

/// <summary>
/// Looks up the friendly display name for a restricted view extension TypeName.
/// Returns false for any TypeName that is not restricted.
/// </summary>
internal static bool TryGetRestrictedViewExtensionDisplayName(string typeName, out string displayName)
{
displayName = null;
return typeName != null && restrictedTypeDisplayNames.TryGetValue(typeName, out displayName);
}

/// <summary>
/// True if the given path is not located under Dynamo's Built-In Packages directory.
/// </summary>
internal static bool IsOutsideBuiltInPackages(string path)
{
if (string.IsNullOrEmpty(path)) return true;

var builtInDirectory = PathManager.BuiltinPackagesDirectory;
if (string.IsNullOrEmpty(builtInDirectory)) return true;

string fullPath;
string fullBuiltInDirectory;
try
{
fullPath = Path.GetFullPath(path);
fullBuiltInDirectory = Path.GetFullPath(builtInDirectory)
.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
}
catch (Exception ex) when (ex is ArgumentException || ex is NotSupportedException || ex is PathTooLongException)
{
return true;
}

// A plain StartsWith would let a sibling like "Built-In PackagesOld" pass as if it
// were under "Built-In Packages" -- require an exact match or a directory-separator
// boundary right after the prefix.
return !fullPath.Equals(fullBuiltInDirectory, StringComparison.OrdinalIgnoreCase) &&
!fullPath.StartsWith(fullBuiltInDirectory + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase);
}

internal static void RecordBlockedPackage(string directory)
{
if (!string.IsNullOrEmpty(directory)) blockedPackageDirectories.Add(directory);
}

internal static void RecordBlockedViewExtension(string displayName, string manifestPath, string assemblyPath)
{
blockedViewExtensions.Add(new BlockedLegacyViewExtension(displayName, manifestPath, assemblyPath));
}

/// <summary>
/// View extensions blocked at that layer only. A block at the package layer already
/// raises its own startup notification via LibraryLoadFailedException, so those
/// packages are intentionally excluded here to avoid a duplicate notification.
/// </summary>
internal static IReadOnlyList<BlockedLegacyViewExtension> BlockedViewExtensions => blockedViewExtensions;

/// <summary>
/// The union of every path blocked by either gate (package folders and view-extension
/// files alike), for the consolidated startup dialog.
/// </summary>
internal static IReadOnlyCollection<string> AllBlockedPaths =>
blockedPackageDirectories
.Concat(blockedViewExtensions.SelectMany(b => new[] { b.ManifestPath, b.AssemblyPath }))
.Where(p => !string.IsNullOrEmpty(p))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();

Check failure on line 156 in src/DynamoCore/Utilities/LegacyAssistantExtensionGuard.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor 'AllBlockedPaths' into a method, properties should not copy collections.

See more on https://sonarcloud.io/project/issues?id=DynamoDS_Dynamo&issues=AZ_ZtzIVnCd0JhALRv5I&open=AZ_ZtzIVnCd0JhALRv5I&pullRequest=17278

internal static bool HasBlockedPaths =>
blockedPackageDirectories.Count > 0 || blockedViewExtensions.Count > 0;

/// <summary>
/// Clears all recorded state. Called once per DynamoModel construction so repeated
/// model instances in the same process (tests, multi-instance hosts) don't leak state
/// from a previous instance.
/// </summary>
internal static void Reset()
{
blockedPackageDirectories.Clear();
blockedViewExtensions.Clear();
}
}
}
19 changes: 18 additions & 1 deletion src/DynamoCoreWpf/Extensions/ViewExtensionLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Reflection;
using System.Xml;
using Dynamo.Logging;
using Dynamo.Utilities;
using DynamoUtilities;

namespace Dynamo.Wpf.Extensions
Expand Down Expand Up @@ -44,6 +45,8 @@ internal IViewExtension Load(ViewExtensionDefinition viewExtension)

public IViewExtension Load(string extensionPath)
{
extensionPath = Path.GetFullPath(extensionPath);

var document = new XmlDocument();
document.Load(extensionPath);

Expand All @@ -61,7 +64,7 @@ public IViewExtension Load(string extensionPath)
{
if (item.Name == "AssemblyPath")
{
path = Path.Combine(path, item.InnerText);
path = Path.GetFullPath(Path.Combine(path, item.InnerText));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This one's a bit different from the others below: item.InnerText here comes from an untrusted manifest file, so it could in principle be a rooted path and make Path.Combine discard the manifest's directory. That's fine in this case though — the very next line wraps the result in Path.GetFullPath, and the resolved AssemblyPath is then independently checked against LegacyAssistantExtensionGuard.IsOutsideBuiltInPackages before the assembly is loaded. So whether the manifest's AssemblyPath is relative (joined against the manifest's folder) or rooted (used as-is), the final resolved path is validated the same way. No behavior change needed here.

definition.AssemblyPath = path;
}
else if (item.Name == "TypeName")
Expand All @@ -85,6 +88,20 @@ public IViewExtension Load(string extensionPath)
}
}

// DYN-10745: Dynamo 4.2 ships Autodesk Assistant and DynamoMCP as built-in
// extensions. Refuse to load either from any location outside Built-In Packages,
// before the assembly ever gets loaded. Remove this block once DYN-10739 lands
// the permanent fix.
if (LegacyAssistantExtensionGuard.IsEnabled &&
LegacyAssistantExtensionGuard.TryGetRestrictedViewExtensionDisplayName(definition.TypeName, out var restrictedDisplayName) &&
(LegacyAssistantExtensionGuard.IsOutsideBuiltInPackages(extensionPath) ||
LegacyAssistantExtensionGuard.IsOutsideBuiltInPackages(definition.AssemblyPath)))
{
LegacyAssistantExtensionGuard.RecordBlockedViewExtension(restrictedDisplayName, extensionPath, definition.AssemblyPath);
Log($"Not loading outdated copy of {restrictedDisplayName}. Found at {extensionPath} and {definition.AssemblyPath}");
return null;
}

var extension = Load(definition);
return extension;
}
Expand Down
50 changes: 49 additions & 1 deletion src/DynamoCoreWpf/Properties/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions src/DynamoCoreWpf/Properties/Resources.en-US.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1675,6 +1675,24 @@ Next assemblies were loaded several times:
<data name="PackagePathAutoAddNotificationDetailedDescription" xml:space="preserve">
<value>The import path "{0}" was added to "Node and Package Paths". If you want to update or remove this path, please open "Dynamo &gt; Preferences &gt;Package Manager &gt; Node and Package Paths..."</value>
</data>
<data name="LegacyAssistantExtensionBlockedTitle" xml:space="preserve">
<value>Extension Not Loaded</value>
</data>
<data name="LegacyAssistantExtensionBlockedShortDescription" xml:space="preserve">
<value>An outdated copy of {0} was found and was not loaded.</value>
</data>
<data name="LegacyAssistantExtensionBlockedDetailedDescription" xml:space="preserve">
<value>Dynamo now includes {0} as a built-in extension. Delete the following to complete your upgrade, then restart Dynamo:
{1}</value>
</data>
<data name="LegacyAssistantExtensionsModalTitle" xml:space="preserve">
<value>Outdated Autodesk Assistant / DynamoMCP Files Found</value>
</data>
<data name="LegacyAssistantExtensionsModalMessage" xml:space="preserve">
<value>Dynamo now includes Autodesk Assistant and DynamoMCP as built-in features. Older copies were found outside the built-in location and were not loaded. Delete the following, then restart Dynamo:

{0}</value>
</data>
<data name="PackageSearchStateNoResult" xml:space="preserve">
<value>Search returned no results!</value>
</data>
Expand Down
18 changes: 18 additions & 0 deletions src/DynamoCoreWpf/Properties/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -2357,6 +2357,24 @@ Do you want to install the latest Dynamo update?</value>
<data name="PackagePathAutoAddNotificationDetailedDescription" xml:space="preserve">
<value>The import path "{0}" was added to "Node and Package Paths". If you want to update or remove this path, please open "Dynamo &gt; Preferences &gt;Package Manager &gt; Node and Package Paths..."</value>
</data>
<data name="LegacyAssistantExtensionBlockedTitle" xml:space="preserve">
<value>Extension Not Loaded</value>
</data>
<data name="LegacyAssistantExtensionBlockedShortDescription" xml:space="preserve">
<value>An outdated copy of {0} was found and was not loaded.</value>
</data>
<data name="LegacyAssistantExtensionBlockedDetailedDescription" xml:space="preserve">
<value>Dynamo now includes {0} as a built-in extension. Delete the following to complete your upgrade, then restart Dynamo:
{1}</value>
</data>
<data name="LegacyAssistantExtensionsModalTitle" xml:space="preserve">
<value>Outdated Autodesk Assistant / DynamoMCP Files Found</value>
</data>
<data name="LegacyAssistantExtensionsModalMessage" xml:space="preserve">
<value>Dynamo now includes Autodesk Assistant and DynamoMCP as built-in features. Older copies were found outside the built-in location and were not loaded. Delete the following, then restart Dynamo:

{0}</value>
</data>
<data name="NodeContextMenuIsInput" xml:space="preserve">
<value>Is Input</value>
</data>
Expand Down
Loading
Loading