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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,13 @@ Agents that export telemetry through the delegated (OBO) route need `Agent365.Ob

Blueprint agents that export telemetry through the app-only S2S endpoint don't need these permissions, and `a365 setup all` no longer requests them for blueprint agents (#501).

#### 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 (#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
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 @@ -1020,7 +1020,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.
: DefenderPermissionMode.Both);

// Return the full scopesByAudience map alongside the V1-compat mcpScopes so V2
// callers (ApplyConsentUrlsIfNeeded) can route per-server audiences to the bare
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ namespace Microsoft.Agents.A365.DevTools.Cli.Commands.SetupSubcommands;
/// 1. Requirements validation
/// 2. Blueprint creation (shared with DW)
/// 3. Batch permissions on the blueprint (shared with DW pipeline; non-DW spec set:
/// Power Platform API and custom; Observability API is not requested). MAC reads
/// Defender API, Power Platform API, custom, and optionally Observability API). MAC reads
/// from the blueprint, so stamping here gives the same set visibility there.
/// 4. Agent Identity creation via POST /beta/servicePrincipals/Microsoft.Graph.AgentIdentity
/// 5. Agent Identity permission grants (same spec set as step 3) — OBO or S2S
Expand All @@ -36,14 +36,25 @@ internal static class NonDwBlueprintSetupOrchestrator
public static void PrintDryRunPlan(Agent365Config config, ILogger logger, bool isBootstrap = false, string[]? rawArgs = null, bool skipRequirements = false, bool isM365 = false, bool agentRegistrationOnly = false, string? authMode = null, string? messagingEndpointOverride = null, bool skipObservabilityPermissions = false)
{
var sub = new string(' ', SetupHelpers.DryRunValCol);
var selectedAuthMode = authMode ?? config.AuthMode;
var effectiveMode = string.IsNullOrWhiteSpace(selectedAuthMode)
? "obo"
: selectedAuthMode.Trim().ToLowerInvariant();
var defenderPermissionMode = effectiveMode switch
{
"s2s" => DefenderPermissionMode.Application,
"both" => DefenderPermissionMode.Both,
_ => DefenderPermissionMode.Delegated,
};
var observabilityPermissionsEffectivelySkipped =
skipObservabilityPermissions && !SetupHelpers.CustomPermissionsRequestObservability(config);
// Dry-run S2S work comes only from fixed specs today; MCP and custom specs carry delegated scopes.
var fixedSpecsHaveAppRoles = SetupHelpers.GetFixedApiPermissionSpecs(
setInheritable: true,
isM365,
config.Environment,
includeObservability: !skipObservabilityPermissions)
includeObservability: !skipObservabilityPermissions,
defenderPermissionMode)
.Any(s => s.AppRoleScopes is { Length: > 0 });
// --messaging-endpoint flag (if supplied) wins over the init-only config value for the plan.
var plannedEndpoint = !string.IsNullOrWhiteSpace(messagingEndpointOverride)
Expand Down Expand Up @@ -126,20 +137,17 @@ public static void PrintDryRunPlan(Agent365Config config, ILogger logger, bool i
logger.LogInformation(sub + "create managed identity");
}

// 3. Inheritable Permissions — non-DW spec set (Power Platform API and custom; Observability API is
// not requested) stamped on the blueprint via SetInheritablePermissionsAsync so MAC and other
// dependent systems can see them. The same set is applied to the agent identity SP in step 5.
var selectedAuthMode = authMode ?? config.AuthMode;
var effectiveMode = string.IsNullOrWhiteSpace(selectedAuthMode)
? "obo"
: selectedAuthMode.Trim().ToLowerInvariant();
// 3. Inheritable Permissions — the non-DW spec set is stamped on the blueprint so MAC and
// dependent systems can see it. The same set is applied to the agent identity SP in step 5.
logger.LogInformation(SetupHelpers.DryRunRow(3, "Inheritable Permissions") + "configure for {Resources} (Global Administrator required; consent URL printed if absent)",
skipObservabilityPermissions ? "Power Platform API and custom permissions" : "Observability API, Power Platform API, and custom permissions");
skipObservabilityPermissions
? "Defender API, Power Platform API, and custom permissions"
: "Observability API, Defender API, Power Platform API, and custom permissions");
if (observabilityPermissionsEffectivelySkipped)
logger.LogInformation(sub + "Observability API not requested (registered agents export telemetry with an app-only token)");

// 4. Blueprint Permission Grants — per authMode. The consent URL targets the blueprint
// app, and S2S app-role assignments are persisted as grants flowing from the blueprint;
// 4. Blueprint Permission Grants — Defender follows authMode; other permission specs retain
// their configured grant types.
// grouping here keeps all blueprint-side rows (2 Blueprint, 3 Inheritable Permissions,
// 4 Blueprint Permission Grants) contiguous.
if (effectiveMode is "obo")
Expand Down Expand Up @@ -282,6 +290,11 @@ await ctx.ClientAppValidator.GrantConsentForPermissionsAsync(
public static async Task<int> ExecuteAsync(SetupContext ctx)
{
ctx.Results.IsNonDwBlueprintFlow = true;
ctx.Results.EffectiveAuthMode = ctx.IsBothMode
? Models.AuthMode.Both
: ctx.IsS2sMode
? Models.AuthMode.S2s
: Models.AuthMode.Obo;
ctx.Results.ObservabilityPermissionsSkipped = ctx.ObservabilityPermissionsEffectivelySkipped;
ctx.Results.TenantId = ctx.Config.TenantId;
// Bootstrap already printed the "Running..." banner before auth steps; skip here to avoid duplication.
Expand Down Expand Up @@ -465,9 +478,14 @@ internal static async Task ExecuteAgentIdentityAndRegistrationAsync(
// Skipped when --agent-registration-only: identity result flags are pre-set by the caller.
if (!skipIdentityAndPermissions)
{
// Record the auth mode and whether any S2S app role is requested before identity creation,
// so the summary stays accurate when the identity step fails.
ctx.Results.EffectiveAuthMode = ctx.IsBothMode ? Models.AuthMode.Both : ctx.IsS2sMode ? Models.AuthMode.S2s : Models.AuthMode.Obo;
// Keep direct callers of this phase aligned with the top-level orchestrator.
ctx.Results.EffectiveAuthMode = ctx.IsBothMode
? Models.AuthMode.Both
: ctx.IsS2sMode
? Models.AuthMode.S2s
: Models.AuthMode.Obo;
// Record whether the selected auth mode produced application permissions before
// identity creation so the summary stays accurate when that step fails.
if (ctx.IsS2sMode || ctx.IsBothMode)
ctx.Results.NoS2SAppRolesToGrant = !specs.Any(s => s.AppRoleScopes is { Length: > 0 });

Expand Down Expand Up @@ -722,8 +740,8 @@ internal static async Task GrantAgentIdentityS2SPermissionsAsync(
List<ResourcePermissionSpec> specs)
{
var hasS2sSpecs = specs.Any(s => s.AppRoleScopes is { Length: > 0 });
// Blueprint agents no longer request OtelWrite, the only app role setup requested, so this
// step usually has nothing to grant. Record that so the summary does not report a delegated grant.
// Record whether the selected auth mode produced any application permissions so the
// summary can distinguish "no S2S work" from a failed grant.
ctx.Results.NoS2SAppRolesToGrant = !hasS2sSpecs;
if (hasS2sSpecs && AgentIdentityInheritsBlueprintAppRoles(ctx.Results))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ private static Command CreateBotSubcommand(
IBootstrapConfigResolver? resolver = null)
{
var command = new Command("bot",
"Configure Messaging Bot API OAuth2 grants and inheritable permissions\n" +
"Configure Messaging Bot, Observability, Defender, and Power Platform API grants and inheritable permissions\n" +
"Required role: Agent ID Developer; Global Administrator for tenant-wide OAuth2 consent\n" +
"(non-admins receive a unified /v2.0/adminconsent URL to forward to a Global Administrator).\n\n" +
"Prerequisites: Blueprint and MCP permissions (run 'a365 setup permissions mcp' first)\n" +
Expand Down Expand Up @@ -296,7 +296,7 @@ private static Command CreateBotSubcommand(
if (dryRunConfig is null)
{
logger.LogInformation("Dry run: a365 setup permissions bot --dry-run");
logger.LogInformation(" Would configure Messaging Bot API OAuth2 grants and inheritable permissions.");
logger.LogInformation(" Would configure Messaging Bot, Observability, Defender, and Power Platform API grants and inheritable permissions.");
logger.LogInformation("No changes made. Run without --dry-run to execute.");
return;
}
Expand All @@ -306,6 +306,7 @@ private static Command CreateBotSubcommand(
logger.LogInformation(" - Blueprint: {BlueprintId}", dryRunConfig.AgentBlueprintId);
logger.LogInformation(" - Messaging Bot API: {Scope}", ConfigConstants.MessagingBotApiAdminConsentScope);
logger.LogInformation(" - Observability API: {OtelScope} (delegated + application)", ConfigConstants.ObservabilityApiOtelWriteScope);
logger.LogInformation(" - Defender API: {DefenderScope} (delegated + application)", ConfigConstants.DefenderApiRealtimeProtectionScope);
logger.LogInformation(" - Power Platform API: Connectivity.Connections.Read");
logger.LogInformation("No changes made. Run without --dry-run to execute.");
return;
Expand Down Expand Up @@ -848,6 +849,7 @@ internal static async Task RemoveStaleCustomPermissionsAsync(
envAtgAppId,
ConfigConstants.MessagingBotApiAppId,
observabilityAppId,
ConfigConstants.DefenderApiAppId,
PowerPlatformConstants.PowerPlatformApiResourceAppId,
AuthenticationConstants.MicrosoftGraphResourceAppId,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ a365 setup all --authmode both

### Observability permissions

For blueprint agents, `setup all` does not request `Agent365.Observability.OtelWrite` in any auth mode. Registered agents export telemetry with an app-only token through the S2S endpoint, which authorizes them by their agent registration, so no Observability admin consent is needed. Registration is then the agent's only authorization, so a registration failure is reported as an error (exit code 1). OtelWrite was the only app role setup requested. Unless another permission adds an app role, `--authmode s2s` and `both` have nothing to assign for blueprint agents, and the setup summary reports the S2S grant as not required.
For blueprint agents, `setup all` does not request `Agent365.Observability.OtelWrite` in any auth mode. Registered agents export telemetry with an app-only token through the S2S endpoint, which authorizes them by their agent registration, so no Observability admin consent is needed. Registration is then the agent's only authorization, so a registration failure is reported as an error (exit code 1). Defender still contributes the `RealtimeProtection.Evaluate.All` application role in `--authmode s2s` and `both`, so those modes perform an S2S grant even when Observability permissions are omitted.

Agents whose SDK still exports through the delegated (OBO) route need `OtelWrite`; grant it manually (see the CHANGELOG upgrade note). AI Teammate setup is unchanged. Re-running setup does not revoke permissions granted earlier.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@

namespace Microsoft.Agents.A365.DevTools.Cli.Commands.SetupSubcommands;

internal enum DefenderPermissionMode
{
Delegated,
Application,
Both,
}

/// <summary>
/// Describes a single resource whose permissions should be configured on the agent blueprint.
/// Used as input to <see cref="BatchPermissionsOrchestrator.ConfigureAllPermissionsAsync"/>.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,8 @@ internal sealed class SetupContext
/// (Global Administrator's directory role carries the required
/// <c>Application.ReadWrite.All</c>). With this set, missing SPs are excluded from the
/// unified admin-consent URL and recorded on <see cref="SetupResults.MissingSpActions"/>
/// so the setup summary's Action Required block renders them as numbered items, each
/// with the <c>az ad sp create</c> command and a per-SP <c>/v2.0/adminconsent</c> URL.
/// so the setup summary's Action Required block renders them as numbered items with the
/// <c>az ad sp create</c> command and, for delegated specs, a per-SP consent URL.
/// Set explicitly via <c>--skip-sp-provisioning</c> or implicitly when stdin is
/// redirected (CI / coding-agent / pipe scenarios).
/// </summary>
Expand Down Expand Up @@ -179,7 +179,7 @@ public SetupContext(
AgentInstanceOnly = agentInstanceOnly;
IsBootstrap = isBootstrap;
IsM365 = isM365;
AuthMode = string.IsNullOrWhiteSpace(authMode) ? null : authMode.ToLowerInvariant();
AuthMode = string.IsNullOrWhiteSpace(authMode) ? null : authMode.Trim().ToLowerInvariant();
MessagingEndpointOverride = string.IsNullOrWhiteSpace(messagingEndpointOverride) ? null : messagingEndpointOverride.Trim();
SkipSpProvisioning = skipSpProvisioning;
NonInteractive = nonInteractive;
Expand Down
Loading
Loading