Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@ public static Command CreateCommand(
using (var content = new StringContent(titlePayload, System.Text.Encoding.UTF8, "application/json"))
{
titlesResp = await http.PostAsync(titlesUrl, content);
}
}
}
catch (HttpRequestException ex)
{
Expand Down Expand Up @@ -439,8 +439,8 @@ public static Command CreateCommand(
HttpResponseMessage allowResp;
try
{
using var content = new StringContent(allowedPayload, System.Text.Encoding.UTF8, "application/json");
allowResp = await http.PostAsync(allowUrl, content);
using var content = new StringContent(allowedPayload, System.Text.Encoding.UTF8, "application/json");
allowResp = await http.PostAsync(allowUrl, content);
}
catch (HttpRequestException ex)
{
Expand Down Expand Up @@ -478,7 +478,6 @@ public static Command CreateCommand(
return;
}

// Use native C# service for Graph operations
logger.LogInformation("Executing Graph API operations (native C# implementation)...");
logger.LogInformation("TenantId: {TenantId}, BlueprintId: {BlueprintId}", tenantId, blueprintId);

Expand Down
46 changes: 11 additions & 35 deletions src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -324,25 +324,6 @@ await ProjectSettingsSyncHelper.ExecuteAsync(
return command;
}

/// <summary>
/// Convert Agent365Config to DeploymentConfiguration
/// </summary>
private static DeploymentConfiguration ConvertToDeploymentConfig(Agent365Config config)
{
return new DeploymentConfiguration
{
ResourceGroup = config.ResourceGroup,
AppName = config.WebAppName,
ProjectPath = config.DeploymentProjectPath,
DeploymentZip = "app.zip",
BuildConfiguration = "Release",
PublishOptions = new PublishOptions
{
SelfContained = false,
OutputPath = "publish"
}
};
}

/// <summary>
/// Display verification URLs and next steps after successful setup
Expand Down Expand Up @@ -439,8 +420,18 @@ private static async Task RegisterBlueprintMessagingEndpointAsync(
throw new InvalidOperationException("Messaging endpoint provider registration failed");
}

// Generate endpoint name with Azure Bot Service constraints (4-42 chars)
var baseEndpointName = $"{setupConfig.WebAppName}-endpoint";
var endpointName = baseEndpointName.Length > 42
? baseEndpointName.Substring(0, 42)
: baseEndpointName;
if (endpointName.Length < 4)
{
logger.LogError("Bot endpoint name '{EndpointName}' is too short (must be at least 4 characters)", endpointName);
throw new InvalidOperationException($"Bot endpoint name '{endpointName}' is too short (must be at least 4 characters)");
}

// Register messaging endpoint using agent blueprint identity and deployed web app URL
var endpointName = $"{setupConfig.WebAppName}-endpoint";
var messagingEndpoint = $"https://{setupConfig.WebAppName}.azurewebsites.net/api/messages";

logger.LogInformation(" - Registering blueprint messaging endpoint");
Expand Down Expand Up @@ -484,21 +475,6 @@ private static async Task RegisterBlueprintMessagingEndpointAsync(
}
}

/// <summary>
/// Get well-known resource names for common Microsoft services
/// </summary>
private static string GetWellKnownResourceName(string? resourceAppId)
{
return resourceAppId switch
{
"00000003-0000-0000-c000-000000000000" => "Microsoft Graph",
"00000002-0000-0000-c000-000000000000" => "Azure Active Directory Graph",
"797f4846-ba00-4fd7-ba43-dac1f8f63013" => "Azure Service Management",
"00000001-0000-0000-c000-000000000000" => "Azure ESTS Service",
_ => $"Unknown Resource ({resourceAppId})"
};
}

private static async Task EnsureMcpOauth2PermissionGrantsAsync(
GraphApiService graph,
Agent365Config cfg,
Expand Down
11 changes: 11 additions & 0 deletions src/Microsoft.Agents.A365.DevTools.Cli/Constants/ErrorCodes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
namespace Microsoft.Agents.A365.DevTools.Cli.Constants
{
public static class ErrorCodes
{
public const string AzureAuthFailed = "AZURE_AUTH_FAILED";
public const string PythonNotFound = "PYTHON_NOT_FOUND";
public const string DeploymentAppFailed = "DEPLOYMENT_APP_FAILED";
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.Agents.A365.DevTools.Cli.Constants;

namespace Microsoft.Agents.A365.DevTools.Cli.Exceptions;

/// <summary>
Expand All @@ -11,7 +13,7 @@ public class AzureAuthenticationException : Agent365Exception
{
public AzureAuthenticationException(string reason)
: base(
errorCode: "AZURE_AUTH_FAILED",
errorCode: ErrorCodes.AzureAuthFailed,
issueDescription: "Azure CLI authentication failed",
errorDetails: new List<string> { reason },
mitigationSteps: new List<string>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.Agents.A365.DevTools.Cli.Constants;

namespace Microsoft.Agents.A365.DevTools.Cli.Exceptions;

/// <summary>
/// Exception thrown during app deployment.
/// </summary>
public class DeployAppException : Agent365Exception
{
private const string DeployAppIssueDescription = "App Deployment failed";

public DeployAppException(string reason)
: base(
errorCode: ErrorCodes.DeploymentAppFailed,
issueDescription: DeployAppIssueDescription,
errorDetails: new List<string> { reason },
mitigationSteps: new List<string>
{
"Please review the logs and retry the deployment",
})
{
}

public override int ExitCode => 1; // General deployment error
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.Agents.A365.DevTools.Cli.Constants;

namespace Microsoft.Agents.A365.DevTools.Cli.Exceptions;

/// <summary>
/// Exception thrown when Python Locator fails to find a valid Python installation.
/// </summary>
public class PythonLocatorException : Agent365Exception
{
private const string PythonLocatorIssueDescription = "Python Locator failed";

public PythonLocatorException(string reason)
: base(
errorCode: ErrorCodes.PythonNotFound,
issueDescription: PythonLocatorIssueDescription,
errorDetails: new List<string> { reason },
mitigationSteps: new List<string>
{
"Python not found. Please install Python from https://www.python.org/. If you have already installed it, please include it in path",
"Ensure pip is installed with Python",
})
{
}

public override int ExitCode => 2; // Configuration error
}
5 changes: 0 additions & 5 deletions src/Microsoft.Agents.A365.DevTools.Cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,10 @@ static async Task<int> Main(string[] args)
{
// Unexpected error - this is a BUG, show full stack trace
Log.Fatal(ex, "Application terminated unexpectedly");
Console.ForegroundColor = ConsoleColor.Red;
Console.Error.WriteLine();
Console.Error.WriteLine("Unexpected error occurred. This may be a bug in the CLI.");
Console.Error.WriteLine("Please report this issue at: https://github.com/microsoft/Agent365-devTools/issues");
Console.Error.WriteLine();
Console.ResetColor();
return 1;
}
finally
Expand All @@ -131,9 +129,6 @@ static async Task<int> Main(string[] args)
/// </summary>
private static void HandleAgent365Exception(Exceptions.Agent365Exception ex)
{
// Set console color based on error severity
Console.ForegroundColor = ConsoleColor.Red;

// Display formatted error message
Console.Error.Write(ex.GetFormattedMessage());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ namespace Microsoft.Agents.A365.DevTools.Cli.Services;
/// <summary>
/// C# implementation fully equivalent to a365-createinstance.ps1.
/// Supports all phases: Identity/User creation and License assignment.
/// Native C# implementation - no PowerShell script dependencies.
/// MCP permissions are configured via inheritable permissions during setup phase.
/// </summary>
public sealed class A365CreateInstanceRunner
Expand Down Expand Up @@ -187,12 +186,12 @@ string GetConfig(string name) =>
await SaveInstanceAsync(generatedConfigPath, instance, cancellationToken);
_logger.LogInformation("Core inputs mapped and instance seed saved to {Path}", generatedConfigPath);

// ========================================================================
// Phase 1: Agent Identity + Agent User Creation (Native C# Implementation)
// ========================================================================
// ==============================================
// Phase 1: Agent Identity + Agent User Creation
// ==============================================
if (step == "identity" || step == "all")
{
_logger.LogInformation("Phase 1: Creating Agent Identity and Agent User (Native C# Implementation)");
_logger.LogInformation("Phase 1: Creating Agent Identity and Agent User");

var agentIdentityDisplayName = GetConfig("agentIdentityDisplayName");
var agentUserDisplayName = GetConfig("agentUserDisplayName");
Expand Down Expand Up @@ -415,7 +414,7 @@ string GetConfig(string name) =>
// ============================
if (step == "licenses" || step == "all")
{
_logger.LogInformation("Phase 2: License assignment (Native C# Implementation)");
_logger.LogInformation("Phase 2: License assignment");

if (instance.TryGetPropertyValue("AgenticUserId", out var userIdNode))
{
Expand All @@ -440,10 +439,6 @@ string GetConfig(string name) =>
return true;
}

// ========================================================================
// Native C# Implementation Methods (Replace PowerShell Scripts)
// ========================================================================

/// <summary>
/// Create Agent Identity using Microsoft Graph API
/// Replaces createAgenticUser.ps1 (identity creation part)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public sealed class A365SetupRunner
private const string ConnectivityResourceAppId = "0ddb742a-e7dc-4899-a31e-80e797ec7144"; // Connectivity
private const string InheritablePermissionsResourceAppIdId = "00000003-0000-0ff1-ce00-000000000000";
private const string MicrosoftGraphCommandLineToolsAppId = "14d82eec-204b-4c2f-b7e8-296a70dab67e"; // Microsoft Graph Command Line Tools
private const string DocumentationMessage = "See documentation at https://aka.ms/agent365/setup for more information.";

public A365SetupRunner(
ILogger<A365SetupRunner> logger,
Expand Down Expand Up @@ -496,8 +497,7 @@ public async Task<bool> RunAsync(string configPath, string generatedConfigPath,
}

/// <summary>
/// Create Agent Blueprint using Microsoft Graph API (native C# implementation)
/// Replaces createAgentBlueprint.ps1
/// Create Agent Blueprint using Microsoft Graph API
///
/// IMPORTANT: This requires interactive authentication with Application.ReadWrite.All permission.
/// Uses the same authentication flow as Connect-MgGraph in PowerShell.
Expand Down Expand Up @@ -893,7 +893,7 @@ private async Task<bool> CreateFederatedIdentityCredentialAsync(
lastError = error;

// Check if it's a propagation issue (resource not found)
if ((error.Contains("Request_ResourceNotFound") || error.Contains("does not exist")) && attempt < maxRetries)
if ((error.Contains("Request_ResourceNotFound") || error.Contains("does not exist")) && attempt < maxRetries)
{
var delayMs = initialDelayMs * (int)Math.Pow(2, attempt - 1); // Exponential backoff
_logger.LogWarning("Application object not yet propagated (attempt {Attempt}/{MaxRetries}). Retrying in {Delay}ms...",
Expand Down Expand Up @@ -1060,7 +1060,6 @@ private async Task ConfigureMcpServerPermissionsAsync(

/// <summary>
/// Create a client secret for the Agent Blueprint using Microsoft Graph API.
/// Native C# implementation - no PowerShell dependencies.
/// The secret is encrypted using DPAPI on Windows before storage.
/// </summary>
private async Task CreateBlueprintClientSecretAsync(
Expand Down Expand Up @@ -1317,14 +1316,31 @@ private async Task ConfigureInheritablePermissionsAsync(
{
_logger.LogError(ex, "Failed to get authenticated Graph client.");
_logger.LogWarning("Authentication failed, skipping inheritable permissions configuration.");
_logger.LogWarning("");
_logger.LogWarning("MANUAL CONFIGURATION REQUIRED:");
_logger.LogWarning(" You need to configure inheritable permissions manually in Azure Portal.");
_logger.LogWarning(" {DocumentationMessage}", DocumentationMessage);
_logger.LogWarning("");

generatedConfig["inheritanceConfigured"] = false;
generatedConfig["inheritanceConfigError"] = "Authentication failed: " + ex.Message;
return;
}

var graphToken = await GetTokenFromGraphClient(graphClient, tenantId);
if (string.IsNullOrWhiteSpace(graphToken))
{
_logger.LogError("Failed to acquire Graph API access token");
throw new InvalidOperationException("Cannot update inheritable permissions without Graph API token");
_logger.LogWarning("Skipping inheritable permissions configuration");
_logger.LogWarning("");
_logger.LogWarning("MANUAL CONFIGURATION REQUIRED:");
_logger.LogWarning(" You need to configure inheritable permissions manually in Azure Portal.");
_logger.LogWarning(" {DocumentationMessage}", DocumentationMessage);
_logger.LogWarning("");

generatedConfig["inheritanceConfigured"] = false;
generatedConfig["inheritanceConfigError"] = "Failed to acquire Graph API access token";
return;
}

// Read scopes from a365.config.json
Expand Down Expand Up @@ -1390,10 +1406,39 @@ private async Task ConfigureInheritablePermissionsAsync(
}
else
{
_logger.LogError("Failed to configure Graph inheritable permissions: {Status} - {Error}",
graphResponse.StatusCode, error);
generatedConfig["inheritanceConfigured"] = false;
generatedConfig["graphInheritanceError"] = error;
// Check if this is an authorization error
bool isAuthorizationError =
error.Contains("Authorization_RequestDenied", StringComparison.OrdinalIgnoreCase) ||
error.Contains("Insufficient privileges", StringComparison.OrdinalIgnoreCase) ||
graphResponse.StatusCode == System.Net.HttpStatusCode.Forbidden ||
graphResponse.StatusCode == System.Net.HttpStatusCode.Unauthorized;

if (isAuthorizationError)
{
_logger.LogError("Failed to configure Graph inheritable permissions: {Status} - {Error}",
graphResponse.StatusCode, error);
_logger.LogError("");
_logger.LogError("=== INSUFFICIENT PERMISSIONS DETECTED ===");
_logger.LogError("");
_logger.LogError("The current user account does not have sufficient privileges to configure inheritable permissions.");
_logger.LogError("");
foreach (var scope in inheritableScopes)
{
_logger.LogError(" - {Scope}", scope);
}
_logger.LogError(" 5. Click 'Grant admin consent'");
_logger.LogError("");

generatedConfig["inheritanceConfigured"] = false;
generatedConfig["graphInheritanceError"] = error;
}
else
{
_logger.LogError("Failed to configure Graph inheritable permissions: {Status} - {Error}",
graphResponse.StatusCode, error);
generatedConfig["inheritanceConfigured"] = false;
generatedConfig["graphInheritanceError"] = error;
}
}
}
else
Expand Down Expand Up @@ -1445,9 +1490,26 @@ private async Task ConfigureInheritablePermissionsAsync(
}
else
{
_logger.LogError("Failed to configure Connectivity inheritable permissions: {Status} - {Error}",
connectivityResponse.StatusCode, error);
generatedConfig["connectivityInheritanceError"] = error;
// Check if this is an authorization error
bool isAuthorizationError =
error.Contains("Authorization_RequestDenied", StringComparison.OrdinalIgnoreCase) ||
error.Contains("Insufficient privileges", StringComparison.OrdinalIgnoreCase) ||
connectivityResponse.StatusCode == System.Net.HttpStatusCode.Forbidden ||
connectivityResponse.StatusCode == System.Net.HttpStatusCode.Unauthorized;

if (isAuthorizationError)
{
_logger.LogError("Failed to configure Connectivity inheritable permissions: {Status} - {Error}",
connectivityResponse.StatusCode, error);
_logger.LogError("See the troubleshooting steps above for resolving permission issues.");
generatedConfig["connectivityInheritanceError"] = error;
}
else
{
_logger.LogError("Failed to configure Connectivity inheritable permissions: {Status} - {Error}",
connectivityResponse.StatusCode, error);
generatedConfig["connectivityInheritanceError"] = error;
}
}
}
else
Expand Down
Loading
Loading