Skip to content
Open
Show file tree
Hide file tree
Changes from all 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