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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

**Option B — CLI**: `a365 setup admin` has been removed in this release. For blueprints that lack the inheritable-permission entry, including those created after this release, run `a365 setup permissions custom --resource-app-id <Observability app ID for your cloud, from step 2> --scopes Agent365.Observability.OtelWrite` to stamp it and request consent for the delegated permission. The CLI does not grant the application permission; use Option A for it.

#### Existing agents: grant Defender API permissions

Agents provisioned before this release should have a Global Administrator re-run `a365 setup all --authmode <mode>` for blueprint agents (delegated for `obo`, application for `s2s`, or both for `both`) or `a365 setup all` without `--authmode` for AI Teammates (both permission types) to stamp Defender inheritance and grant `RealtimeProtection.Evaluate.All` (#485).

### Added

- `a365 setup all` now grants the Defender API `RealtimeProtection.Evaluate.All` permission according to `--authmode` for blueprint agents (delegated for `obo`, application for `s2s`, both for `both`) and as both delegated and application for AI Teammates (#485).
- `a365 develop-mcp grant-agents-access --agent-blueprint-id <GUID> --mcp-server-name <NAME>` reports which agent instances of a blueprint are missing the permission to call a BYO MCP server and prompts you to select which ones to grant it to, requiring administrator consent on the first run in a tenant (#500).
- When more than one Entra application shares the MCP server's name, `a365 develop-mcp grant-agents-access` now lists them all and asks which one to use instead of failing (#500).
- `a365 develop-mcp grant-agents-access --help` now lists Microsoft's first-party agent blueprint names and IDs, and the same list is printed when `--agent-blueprint-id` is missing or not a GUID, so you can find the ID without looking it up elsewhere (#500).
Expand Down Expand Up @@ -74,6 +80,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- `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 all` now completes application permission grants after provisioning a missing resource service principal and lists manual provisioning before dependent permission grants (#485).
- Standalone `setup permissions` commands now list missing service principal provisioning and resource-specific consent before dependent permission grants (#485).
- Successful delegated consent recovery now persists verified grants and removes obsolete provisioning instructions while preserving pending application-role work (#485).
- AI Teammate and blueprint `--authmode both` setup summaries now report both application and delegated permissions without treating pending or unverified consent as granted (#485).
- `a365 create-instance` now reports an ambiguous government-cloud environment as a configuration error with guidance to select a specific cloud (#478).
- Setup now warns before replacing a stale stored blueprint ID with the sole application matching the configured display name (#478).
- Messaging endpoint create and delete overrides now reject non-HTTPS URLs and URLs containing user information, query strings, or fragments (#478).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -670,6 +670,7 @@ private static Command CreateInstanceScopesSubcommand(
null or "" => null,
AuthenticationConstants.MicrosoftGraphResourceAppId => "Microsoft Graph",
ConfigConstants.MessagingBotApiAppId => "Messaging Bot API",
ConfigConstants.DefenderApiAppId => "Defender API",
PowerPlatformConstants.PowerPlatformApiResourceAppId => "Power Platform API",
"00000002-0000-0000-c000-000000000000" => "Azure Active Directory Graph",
"797f4846-ba00-4fd7-ba43-dac1f8f63013" => "Azure Service Management",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public static Command CreateCommand(
" a365 setup requirements Validate prerequisites\n" +
" a365 setup blueprint Create the agent blueprint (Entra app)\n" +
" a365 setup permissions mcp MCP server permissions (when using MCP tools)\n" +
" a365 setup permissions bot Bot API + Power Platform\n" +
" a365 setup permissions bot Bot API + Defender + Power Platform\n" +
" a365 setup permissions custom Inline custom resource permission (--resource-app-id, --scopes)\n" +
" a365 setup permissions copilotstudio Copilot Studio agent permissions\n\n" +
"Roles required:\n" +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1022,7 +1022,14 @@ await PermissionsSubcommand.RemoveStaleCustomPermissionsAsync(
// names so V2 audiences read as e.g. "mcp_MailTools" rather than "Agent 365 Tools".
var specs = await SetupHelpers.BuildConfiguredPermissionSpecsAsync(
ctx.Config, setInheritable: true, isM365: ctx.IsM365, scopesByAudience, serverNamesByAudience,
includeObservability: !ctx.SkipObservabilityPermissions);
includeObservability: !ctx.SkipObservabilityPermissions,
defenderPermissionMode: ctx.Results.IsNonDwBlueprintFlow
? ctx.IsS2sMode
? DefenderPermissionMode.Application
: ctx.IsBothMode
? DefenderPermissionMode.Both
: DefenderPermissionMode.Delegated
Comment thread
slreznit marked this conversation as resolved.
Resolved
: DefenderPermissionMode.Both);
Comment thread
slreznit marked this conversation as resolved.
Resolved

// Return the full scopesByAudience map alongside the V1-compat mcpScopes so V2
// callers (ApplyConsentUrlsIfNeeded) can route per-server audiences to the bare
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ internal static partial class AzRestS2SRunner
IReadOnlyList<ResourcePermissionSpec> specs,
ILogger logger,
CancellationToken ct,
string graphBaseUrl = GraphApiConstants.BaseUrl)
string graphBaseUrl = GraphApiConstants.BaseUrl,
IReadOnlyDictionary<string, string>? resourceSpObjectIds = null)
{
if (!GuidPattern().IsMatch(blueprintSpObjectId))
{
Expand All @@ -77,6 +78,12 @@ internal static partial class AzRestS2SRunner
logger.LogWarning("az rest S2S runner: spec '{ResourceName}' has invalid ResourceAppId - skipping.", spec.ResourceName);
return (false, false);
}
if (resourceSpObjectIds?.TryGetValue(spec.ResourceAppId, out var knownSpId) == true
&& !GuidPattern().IsMatch(knownSpId))
{
logger.LogWarning("az rest S2S runner: spec '{ResourceName}' has invalid resource SP id - skipping.", spec.ResourceName);
return (false, false);
}
foreach (var role in spec.AppRoleScopes!)
{
if (!SafeRolePattern().IsMatch(role))
Expand Down Expand Up @@ -110,7 +117,9 @@ internal static partial class AzRestS2SRunner
ct.ThrowIfCancellationRequested();
try
{
var ok = await AssignOneAsync(executor, blueprintSpObjectId, spec, existingAssignments, baseUrl, logger, ct);
string? resourceSpObjectId = null;
resourceSpObjectIds?.TryGetValue(spec.ResourceAppId, out resourceSpObjectId);
var ok = await AssignOneAsync(executor, blueprintSpObjectId, spec, existingAssignments, baseUrl, logger, ct, resourceSpObjectId);
if (!ok) allOk = false;
}
catch (OperationCanceledException)
Expand Down Expand Up @@ -140,11 +149,15 @@ private static async Task<bool> AssignOneAsync(
HashSet<(string ResourceId, string AppRoleId)> existingAssignments,
string graphBaseUrl,
ILogger logger,
CancellationToken ct)
CancellationToken ct,
string? knownResourceSpObjectId)
{
var resourcePath = knownResourceSpObjectId is not null
? $"/v1.0/servicePrincipals/{knownResourceSpObjectId}?$select=id,appRoles"
: $"/v1.0/servicePrincipals?$filter=appId eq '{spec.ResourceAppId}'&$select=id,appRoles";
var spResult = await executor.ExecuteAsync(
"az",
$"rest --method GET --url \"{graphBaseUrl}/v1.0/servicePrincipals?$filter=appId eq '{spec.ResourceAppId}'&$select=id,appRoles\"",
$"rest --method GET --url \"{graphBaseUrl}{resourcePath}\"",
captureOutput: true,
suppressErrorLogging: true,
cancellationToken: ct);
Expand Down Expand Up @@ -320,10 +333,15 @@ internal static (string? SpId, Dictionary<string, string> AppRolesByValue) TryEx
try
{
using var doc = JsonDocument.Parse(azStandardOutput);
if (!doc.RootElement.TryGetProperty("value", out var value)) return (null, rolesByValue);
if (value.GetArrayLength() == 0) return (null, rolesByValue);

var first = value[0];
if (doc.RootElement.ValueKind != JsonValueKind.Object) return (null, rolesByValue);
var first = doc.RootElement;
if (first.TryGetProperty("value", out var value))
{
if (value.ValueKind != JsonValueKind.Array || value.GetArrayLength() == 0)
return (null, rolesByValue);
first = value[0];
}
if (first.ValueKind != JsonValueKind.Object) return (null, rolesByValue);
string? spId = first.TryGetProperty("id", out var idEl) && idEl.ValueKind == JsonValueKind.String
? idEl.GetString()
: null;
Expand Down
Loading
Loading