Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Fix Foundry hosted-agent run and command icon behavior
Conditionally require/provision Foundry project ACR only for hosted-agent publish or explicit registry override, add regression tests for run/publish paths, and align PromptAgent Send Message icon with ChatSparkle.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
  • Loading branch information
2 people authored and davidfowl committed May 29, 2026
commit 042b2355ca7ee1e3ffeea02d8abed5b724730b7e
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,11 @@ private static void ConfigurePublishMode<T>(
var resource = builder.Resource;
var projectResource = project.Resource;

if (!projectResource.HasAnnotationOfType<RequiresHostedAgentRegistryAnnotation>())
{
projectResource.Annotations.Add(new RequiresHostedAgentRegistryAnnotation());
}

ResourceBuilderExtensions.WithComputeEnvironment(builder, project);

// Hosted Agent resource name
Expand Down
79 changes: 44 additions & 35 deletions src/Aspire.Hosting.Foundry/Project/ProjectBuilderExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,12 @@ public static IResourceBuilder<FoundryDeploymentResource> AddModelDeployment(
return builder.ApplicationBuilder.CreateResourceBuilder(builder.Resource.Parent).AddDeployment(name, modelName, modelVersion, format);
}

private static bool RequiresContainerRegistryProvisioning(AzureCognitiveServicesProjectResource project)
{
return project.HasAnnotationOfType<RequiresHostedAgentRegistryAnnotation>()
|| project.HasAnnotationOfType<ContainerRegistryReferenceAnnotation>();
}

internal static void ConfigureInfrastructure(AzureResourceInfrastructure infra)
{
var prefix = infra.AspireResource.Name;
Expand Down Expand Up @@ -411,44 +417,47 @@ internal static void ConfigureInfrastructure(AzureResourceInfrastructure infra)
/*
* Container registry for hosted agents
*
* TODO: only provision if we need to create a Hosted Agent
* Only provision registry dependencies when the project will publish a hosted agent
* or when the user has explicitly supplied a registry override.
*/

AzureProvisioningResource? registry = null;
if (aspireResource.TryGetLastAnnotation<ContainerRegistryReferenceAnnotation>(out var registryReferenceAnnotation) && registryReferenceAnnotation.Registry is AzureProvisioningResource r)
if (RequiresContainerRegistryProvisioning(aspireResource))
{
registry = r;
}
else if (aspireResource.DefaultContainerRegistry is not null)
{
registry = aspireResource.DefaultContainerRegistry;
}
else
{
throw new InvalidOperationException($"No container registry configured for Azure Cognitive Services project resource '{aspireResource.Name}'. A container registry is required to publish and run hosted agents.");
AzureProvisioningResource? registry = null;
if (aspireResource.TryGetLastAnnotation<ContainerRegistryReferenceAnnotation>(out var registryReferenceAnnotation) && registryReferenceAnnotation.Registry is AzureProvisioningResource r)
{
registry = r;
}
else if (aspireResource.DefaultContainerRegistry is not null)
{
registry = aspireResource.DefaultContainerRegistry;
}
else
{
throw new InvalidOperationException($"No container registry configured for Azure Cognitive Services project resource '{aspireResource.Name}'. A container registry is required to publish hosted agents.");
}

var containerRegistry = (ContainerRegistryService)registry.AddAsExistingResource(infra);
infra.Add(containerRegistry);

// Project needs this to pull hosted agent images during hosted-agent deployment.
var pullRa = containerRegistry.CreateRoleAssignment(ContainerRegistryBuiltInRole.AcrPull, RoleManagementPrincipalType.ServicePrincipal, projectPrincipalId);
// There's a bug in the CDK, see https://github.com/Azure/azure-sdk-for-net/issues/47265
pullRa.Name = BicepFunction.CreateGuid(containerRegistry.Id, project.Id, pullRa.RoleDefinitionId);
infra.Add(pullRa);
infra.Add(containerRegistry);
infra.Add(new ProvisioningOutput("AZURE_CONTAINER_REGISTRY_ENDPOINT", typeof(string))
{
Value = containerRegistry.LoginServer
});
infra.Add(new ProvisioningOutput("AZURE_CONTAINER_REGISTRY_NAME", typeof(string))
{
Value = containerRegistry.Name
});
infra.Add(new ProvisioningOutput("AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID", typeof(string))
{
Value = projectPrincipalId
});
}
var containerRegistry = (ContainerRegistryService)registry.AddAsExistingResource(infra);
// Why do we need this?
infra.Add(containerRegistry);

// Project needs this to pull hosted agent images and run them
var pullRa = containerRegistry.CreateRoleAssignment(ContainerRegistryBuiltInRole.AcrPull, RoleManagementPrincipalType.ServicePrincipal, projectPrincipalId);
// There's a bug in the CDK, see https://github.com/Azure/azure-sdk-for-net/issues/47265
pullRa.Name = BicepFunction.CreateGuid(containerRegistry.Id, project.Id, pullRa.RoleDefinitionId);
infra.Add(pullRa);
infra.Add(containerRegistry);
infra.Add(new ProvisioningOutput("AZURE_CONTAINER_REGISTRY_ENDPOINT", typeof(string))
{
Value = containerRegistry.LoginServer
});
infra.Add(new ProvisioningOutput("AZURE_CONTAINER_REGISTRY_NAME", typeof(string))
{
Value = containerRegistry.Name
});
infra.Add(new ProvisioningOutput("AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID", typeof(string))
{
Value = projectPrincipalId
});

// Implicit dependencies for capability hosts
List<ProvisionableResource> capHostDeps = [];
Expand Down
12 changes: 10 additions & 2 deletions src/Aspire.Hosting.Foundry/Project/ProjectResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,9 @@ public AzureCognitiveServicesProjectResource([ResourceName] string name, Action<
Description = $"Prepares Microsoft Foundry project {name} for deployment.",
Action = context =>
{
if (this.HasAnnotationOfType<ContainerRegistryReferenceAnnotation>() &&
DefaultContainerRegistry is not null)
if (DefaultContainerRegistry is not null &&
(this.HasAnnotationOfType<ContainerRegistryReferenceAnnotation>() ||
!this.HasAnnotationOfType<RequiresHostedAgentRegistryAnnotation>()))
{
context.Model.Resources.Remove(DefaultContainerRegistry);
DefaultContainerRegistry = null;
Expand Down Expand Up @@ -248,6 +249,13 @@ public bool TryGetAppIdentityResource([NotNullWhen(true)] out IAppIdentityResour
}
}

/// <summary>
/// Marks a Foundry project as needing container registry provisioning for hosted agent deployment.
/// </summary>
internal sealed class RequiresHostedAgentRegistryAnnotation : IResourceAnnotation
{
}

/// <summary>
/// Configuration for a Microsoft Foundry capability host.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ public static IResourceBuilder<AzurePromptAgentResource> AddPromptAgent(
},
commandOptions: new()
{
IconName = "Agents",
IconName = "ChatSparkle",
IconVariant = IconVariant.Regular,
IsHighlighted = true,
Arguments =
Expand Down
19 changes: 19 additions & 0 deletions tests/Aspire.Hosting.Foundry.Tests/HostedAgentExtensionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#pragma warning disable ASPIRECOMPUTE003 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.

using System.Runtime.CompilerServices;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Utils;
using Microsoft.Extensions.DependencyInjection;
Expand Down Expand Up @@ -186,6 +187,21 @@ public void AsHostedAgent_InRunMode_WithProject_RemovesDefaultContainerRegistryR
Assert.DoesNotContain(builder.Resources, r => r.Name == "my-project-acr");
}

[Fact]
public async Task AsHostedAgent_InRunMode_WithProject_ExecutesBeforeStartHooksWithoutContainerRegistry()
{
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
var project = builder.AddFoundry("account")
.AddProject("my-project");

builder.AddPythonApp("agent", "./app.py", "main:app")
.AsHostedAgent(project);

using var app = builder.Build();

await ExecuteBeforeStartHooksAsync(app, default);
}

[Fact]
public async Task FoundryProject_DefaultRegistryDoesNotAddGlobalRegistryTargets()
{
Expand All @@ -205,4 +221,7 @@ public async Task FoundryProject_DefaultRegistryDoesNotAddGlobalRegistryTargets(
var registryTarget = Assert.Single(registryTargets);
Assert.Same(registry.Resource, registryTarget.Registry);
}

[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "ExecuteBeforeStartHooksAsync")]
private static extern Task ExecuteBeforeStartHooksAsync(DistributedApplication app, CancellationToken cancellationToken);
}
21 changes: 21 additions & 0 deletions tests/Aspire.Hosting.Foundry.Tests/ProjectResourceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,27 @@ public async Task WithAzureContainerRegistry_RemovesDefaultContainerRegistryDuri
Assert.DoesNotContain(defaultRegistry, registries);
}

[Fact]
public async Task AddProject_WithoutHostedAgents_RemovesDefaultContainerRegistryDuringBeforeStart()
{
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);

var project = builder.AddFoundry("account")
.AddProject("my-project");

var defaultRegistry = project.Resource.DefaultContainerRegistry;
Assert.NotNull(defaultRegistry);
Assert.Contains(defaultRegistry, builder.Resources);

using var app = builder.Build();
await ExecuteBeforeStartHooksAsync(app, default);

var model = app.Services.GetRequiredService<DistributedApplicationModel>();
var registries = model.Resources.OfType<AzureContainerRegistryResource>().ToList();
Assert.DoesNotContain(defaultRegistry, registries);
Assert.Null(project.Resource.DefaultContainerRegistry);
}

[Fact]
public void ConnectionStringExpression_HasCorrectFormat()
{
Expand Down
20 changes: 20 additions & 0 deletions tests/Aspire.Hosting.Foundry.Tests/PromptAgentTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,26 @@ public void AddPromptAgent_SetsProjectReference()
Assert.Same(project.Resource, agent.Resource.Project);
}

[Fact]
public void AddPromptAgent_ConfiguresSendMessageCommand()
{
using var builder = TestDistributedApplicationBuilder.Create();
var project = builder.AddFoundry("account")
.AddProject("my-project");
var model = project.AddModelDeployment("gpt41", FoundryModel.OpenAI.Gpt41);

project.AddPromptAgent("my-agent", model);

builder.Build();

var resource = builder.Resources.Single(r => r.Name == "my-agent");
var command = Assert.Single(resource.Annotations.OfType<ResourceCommandAnnotation>());
Assert.Equal("Send Message", command.DisplayName);
Assert.Equal("ChatSparkle", command.IconName);
Assert.Equal(IconVariant.Regular, command.IconVariant);
Assert.True(command.IsHighlighted);
}

[Fact]
public void AddPromptAgent_WithNullName_Throws()
{
Expand Down