Skip to content
Open
18 changes: 17 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ Agents provisioned before this release need `Agent365.Observability.OtelWrite` g
**Option A — Entra portal** (no config files required):

1. [Entra portal](https://entra.microsoft.com) > **App registrations** > select your **Blueprint** app > **API permissions**
2. **Add a permission** > **APIs my organization uses** > search `9b975845-388f-4429-889e-eab1ef63949c`
2. **Add a permission** > **APIs my organization uses** > search for the Observability app ID for your cloud:
- Commercial: `9b975845-388f-4429-889e-eab1ef63949c`
- GCC Moderate: `2c672ad5-b104-44ed-8069-bb68dd138546`
- GCC High: `009c6bd0-82e4-4466-95b3-4c996521f3d7`
- DoD: `a9e04047-c6a7-430b-a7ae-faf8f8eed1b7`
3. **Delegated permissions** > select `Agent365.Observability.OtelWrite` > **Add permissions**
4. Repeat step 2 > **Application permissions** > select `Agent365.Observability.OtelWrite` > **Add permissions**
5. **Grant admin consent for \<tenant\>** > confirm
Expand Down Expand Up @@ -59,6 +63,18 @@ Agents provisioned before this release need `Agent365.Observability.OtelWrite` g
- `a365 develop get-token --device-code` — forces device code auth for Microsoft Graph scopes the Windows WAM broker rejects (e.g. Exchange `MailboxSettings.ReadWrite`, `ExchangeMessageTrace.Read.All`).

### Fixed
- Setup now requests the required Graph scopes and stops safely when existing blueprint discovery is inconclusive, preventing duplicate blueprints after CLI permission changes.
- GCC Moderate, GCC High, and DoD setup now grant permissions to each cloud's Observability service instead of the commercial service.
- Cloud-specific Agent 365 discover endpoint overrides now also select the messaging endpoint create and delete hosts unless explicit overrides are set.
- Repeated `publish --aiteammate` runs now preserve customized manifest names instead of restoring an overlong blueprint name.
- Repeated `setup blueprint --agent-name` runs now reuse the stored valid client secret instead of creating duplicate credentials.
- `a365 query-entra blueprint-scopes` and `inheritance` now report permission-grant read failures, and `a365 create-instance` now stops safely instead of continuing when existing grants cannot be read.
- `setup requirements` now validates and repairs tenant-owned fallback CLI apps with the administrator bootstrap identity, preventing false "app not found" failures when the first-party CLI app is unavailable.
- `a365 create-instance` now reports invalid custom Graph or authority endpoints as configuration errors instead of aborting with an unhandled exception (#478).
- Cloud-specific Graph, authority, and Agent 365 Tools endpoint overrides now apply consistently across setup, consent, authentication, query, and create-instance flows for sovereign and custom clouds. (#478)
- `a365 query-entra instance-scopes` now reports consent status correctly and fails visibly when permission grants cannot be read (#478).
- `a365 publish` no longer crashes when `manifest.json` has a non-string `name.short` value (#478).
- `setup all --agent-registration-only` now exits non-zero and reports errors when the requested agent registration step fails, while full setup continues to treat registration as best-effort (#478).
- Setup no longer fails to detect the Agent 365 CLI application in tenants where it is not yet provisioned, and reports lookup errors instead of silently switching your configured client app (#489).
- The first-party Agent 365 CLI app now uses device code authentication when Windows Account Manager is unavailable, avoiding unsupported browser-response errors in WSL, macOS, and Linux (#489).
- `setup all --authmode s2s` no longer prints spurious "Action Required" PowerShell steps when the agent identity already inherits its app roles from the blueprint, and now retries the grant automatically before falling back to manual steps (#460).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1337,6 +1337,10 @@ private static void PrintOrphanSummary(
return null;
}

var environment = await SetupHelpers.ResolveBootstrapEnvironmentAsync(
executor, logger, CancellationToken.None);
graphApiService?.ConfigureCloudEndpoints(new Agent365Config { Environment = environment });

// Step 2: Resolve client app ID.
// Prefer a365.config.json when it exists locally and its tenant matches the current tenant.
// Otherwise prefer the first-party service principal, then the named custom-app fallback.
Expand Down Expand Up @@ -1423,6 +1427,9 @@ private static void PrintOrphanSummary(
{
TenantId = tenantId,
ClientAppId = clientAppId ?? string.Empty,
Environment = environment,
GraphBaseUrl = graphApiService?.GraphBaseUrl ?? ConfigConstants.GetGraphBaseUrl(environment),
AuthorityHost = graphApiService?.AuthorityHost ?? ConfigConstants.GetAuthorityHost(environment),
AgentIdentityDisplayName = $"{agentName} Identity",
AgentBlueprintDisplayName = blueprintDisplayName,
AgentDescription = agentName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,11 +168,12 @@ public static Command CreateCommand(ILogger<CreateInstanceCommand> logger, IConf
if (!botApiGrantOk)
logger.LogWarning("Failed to create/update oauth2PermissionGrant for agent identity to Messaging Bot API.");

var observabilityApiAppId = ConfigConstants.GetObservabilityApiAppId(instanceConfig.Environment);
var observabilityApiResourceSpObjectId = await graphApiService.EnsureServicePrincipalForAppIdAsync(
instanceConfig.TenantId,
ConfigConstants.ObservabilityApiAppId)
observabilityApiAppId)
?? throw new InvalidOperationException(
$"Failed to resolve service principal for Observability API (appId {ConfigConstants.ObservabilityApiAppId}).");
$"Failed to resolve service principal for Observability API (appId {observabilityApiAppId}).");

// Grant oauth2PermissionGrants: *agent identity SP* -> Observability API SP
var observabilityApiGrantOk = await graphApiService.CreateOrUpdateOauth2PermissionGrantAsync(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,10 @@ private static async Task<bool> CallDiscoverToolServersAsync(bool skipAuth, ILog
// Resolve az CLI login hint so WAM targets the correct account instead of
// defaulting to the first cached MSAL account (which may be stale).
var loginHint = await Services.Helpers.AzCliHelper.ResolveLoginHintAsync();
authToken = await authService.GetAccessTokenAsync(audience, userId: loginHint);
authToken = await authService.GetAccessTokenAsync(
audience,
userId: loginHint,
authorityHost: ConfigConstants.GetAuthorityHost(environment));

if (string.IsNullOrWhiteSpace(authToken))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using Microsoft.Agents.A365.DevTools.Cli.Constants;
using Microsoft.Agents.A365.DevTools.Cli.Helpers;
using Microsoft.Agents.A365.DevTools.Cli.Models;
using Microsoft.Agents.A365.DevTools.Cli.Services;
using Microsoft.Extensions.Logging;
using System.CommandLine;
Expand Down Expand Up @@ -74,6 +75,10 @@ public static Command CreateCommand(
var setupConfig = File.Exists(configFile.FullName)
? await configService.LoadAsync(configFile.FullName)
: null;
graphApiService.ConfigureCloudEndpoints(setupConfig ?? new Agent365Config
{
Environment = Environment.GetEnvironmentVariable("A365_ENVIRONMENT") ?? "prod"
});

if (setupConfig == null && string.IsNullOrWhiteSpace(appId))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ public static Command CreateCommand(
}

// Determine environment
var environment = setupConfig?.Environment ?? "prod";
var environment = ResolveEnvironment(setupConfig);

// Resolve resource app ID
string resourceAppId;
Expand Down Expand Up @@ -283,7 +283,10 @@ private static async Task<McpServerTokenResult> AcquireTokenAsync(
forceRefresh,
clientAppId,
useInteractiveBrowser: !useDeviceCode,
userId: loginHint);
userId: loginHint,
authorityHost: ConfigConstants.GetAuthorityHost(
ResolveEnvironment(setupConfig),
setupConfig?.AuthorityHost));

if (string.IsNullOrWhiteSpace(token))
{
Expand Down Expand Up @@ -394,7 +397,7 @@ private static async Task AcquireAndDisplayManifestTokensAsync(

logger.LogInformation("");

var tokenAtgAppId = ConfigConstants.GetAgent365ToolsResourceAppId(setupConfig?.Environment ?? "prod");
var tokenAtgAppId = ConfigConstants.GetAgent365ToolsResourceAppId(ResolveEnvironment(setupConfig));
var scopesByAudience = await ManifestHelper.GetScopesByAudienceAsync(manifestPath, resolvedAtgAppId: tokenAtgAppId);
var serverNamesByAudience = await ManifestHelper.GetServerNamesByAudienceAsync(manifestPath, resolvedAtgAppId: tokenAtgAppId);

Expand Down Expand Up @@ -455,6 +458,11 @@ private static string ResolveClientAppId(string? appId, Agent365Config? setupCon
throw new InvalidOperationException("No client application ID specified. Use --app-id or ensure ClientAppId is set in config.");
}

private static string ResolveEnvironment(Agent365Config? setupConfig) =>
setupConfig?.Environment
?? Environment.GetEnvironmentVariable("A365_ENVIRONMENT")
?? "prod";

private static async Task SaveAndReportTokenAsync(
string token,
Agent365Config? setupConfig,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,8 @@ public static Command CreateCommand(

var updatedManifest = await UpdateManifestFileAsync(displayName, blueprintId, manifestPath);
var updatedAgenticUserManifest = await UpdateAgenticUserManifestTemplateFileAsync(blueprintId, agenticUserManifestPath);
var updatedManifestNode = JsonNode.Parse(updatedManifest);
var shortName = GetManifestStringValue(updatedManifestNode?["name"]?["short"]);

if (dryRun)
{
Expand All @@ -238,12 +240,12 @@ public static Command CreateCommand(
logger.LogInformation("Customize before packaging:");
logger.LogInformation(" version - increment for republishing (e.g., 1.0.1), must be higher than previous");

if (string.IsNullOrWhiteSpace(displayName))
if (string.IsNullOrWhiteSpace(shortName))
logger.LogWarning(" name.short - not set; edit manifest.json to provide a short name (30 chars max) before packaging");
else if (displayName.Length > 30)
logger.LogWarning(" name.short - EXCEEDS 30 chars ({Length}), currently: \"{Name}\" -- shorten before packaging", displayName.Length, displayName);
else if (shortName.Length > 30)
logger.LogWarning(" name.short - EXCEEDS 30 chars ({Length}), currently: \"{Name}\" -- shorten before packaging", shortName.Length, shortName);
else
logger.LogInformation(" name.short - 30 chars max, currently: \"{Name}\"", displayName);
logger.LogInformation(" name.short - 30 chars max, currently: \"{Name}\"", shortName);

logger.LogInformation(" name.full - displayed in Microsoft 365");
logger.LogInformation(" description.short - 1-2 sentences");
Expand Down Expand Up @@ -340,8 +342,8 @@ private static async Task<string> UpdateManifestFileAsync(string? displayName, s
node["name"] = nameObj;
}

nameObj["short"] = displayName;
nameObj["full"] = displayName;
SetManifestNameDefault(nameObj, "short", "Your Agent Name", displayName);
SetManifestNameDefault(nameObj, "full", "Your Agent Full Name", displayName);
}

if (node["bots"] is JsonArray bots && bots.Count > 0 && bots[0] is JsonObject botObj)
Expand All @@ -359,6 +361,27 @@ private static async Task<string> UpdateManifestFileAsync(string? displayName, s
return node.ToJsonString(new JsonSerializerOptions { WriteIndented = true });
}

private static void SetManifestNameDefault(
JsonObject name,
string propertyName,
string templateValue,
string displayName)
{
var currentValue = GetManifestStringValue(name[propertyName]);

if (string.IsNullOrWhiteSpace(currentValue) ||
string.Equals(currentValue, templateValue, StringComparison.Ordinal))
{
name[propertyName] = displayName;
}
}

private static string? GetManifestStringValue(JsonNode? node)
=> node is JsonValue valueNode &&
valueNode.TryGetValue<string>(out var value)
? value
: null;

private static async Task<string> UpdateAgenticUserManifestTemplateFileAsync(string blueprintId, string agenticUserManifestPath)
{
var contents = await File.ReadAllTextAsync(agenticUserManifestPath);
Expand Down
Loading
Loading