Skip to content

Commit 077b263

Browse files
committed
Fix #460: skip redundant agent-identity S2S grant when role is inherited
When inheritable permissions are configured (allAllowed) and the blueprint SP holds the app roles, the agent identity inherits them automatically — the same basis the OBO/delegated path already relies on. The direct grant on the agent identity SP was therefore redundant and, when the CLI token cannot write app roles, produced a spurious "Action Required" PowerShell block on an otherwise successful Global Admin run. - Skip the per-identity grant (and the PowerShell prompt) when the blueprint grant and inheritance both succeeded; report the inherited grant as Granted. - When a direct grant is still needed (developer / non-inherited path), retry via az rest before falling back to PowerShell, matching the blueprint grant. - Correct stale README/CHANGELOG claim that Phase 2a/2b are always skipped for non-DW agents (untrue since #421). - Add review-time checks for comment-essay and CHANGELOG crispness (pr-code-reviewer #30/#31, review-pr SKILL) and codify both in CLAUDE.md and copilot-instructions. Verified live: setup completes cleanly with no PowerShell block, and query-entra reports the Observability app role granted on the blueprint SP with effective inheritance OK.
1 parent a398cfc commit 077b263

8 files changed

Lines changed: 287 additions & 17 deletions

File tree

.claude/agents/pr-code-reviewer.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1122,6 +1122,26 @@ When a string property represents a discrete set of values (e.g., `"obo"`, `"s2s
11221122
```
11231123
- **Real example (PR #406, Comment 1)**: `isShowSecret = args.Contains("--show-secret")` in `Program.cs` would not skip the Graph preflight when the user passed `--show-secret=true`, accidentally triggering an online call on an offline-only command.
11241124

1125+
### 30. Comment Essay — Multi-Line Rationale Block Where One Line Suffices
1126+
1127+
Comments added in the diff must be crisp. A comment states *why* in one or two lines; it does not retell the change, restate what the code already says, or narrate design history (that belongs in the commit message / PR description).
1128+
1129+
- **Pattern to catch**: any newly-added comment block — `//` inline or `///` XML `<summary>` — running more than ~2 lines of prose rationale; comments that paraphrase the next statement; commit-message-style narration ("Previously we did X, now we do Y because...") in source.
1130+
- **Severity**: `low` — not a runtime defect, but the repo standard. Flag every instance so it does not accumulate.
1131+
- **Check**: for each added comment, ask "does this say *why* in one line?" If it explains mechanism the code already shows, or runs to a paragraph, it is an essay. Trivial mechanical edits (a log-level change, a blank-line separator, a `catch { throw; }`) get **zero or one** line, never a rationale block.
1132+
- **Fix**: cut to a single-line *why*; move the long-form reasoning to the commit message or PR body. Keep an issue/PR reference (`(issue #460)`) but drop the surrounding paragraph.
1133+
- **Real example (issue #460)**: a 7-line `<summary>` and a 6-line inline `// Issue #460: ...` block both restated the inheritance rationale already captured in the commit; each was cut to one line plus the issue reference.
1134+
1135+
### 31. CHANGELOG Entry Not Release-Note-Ready
1136+
1137+
`CHANGELOG.md` `[Unreleased]` feeds straight into the nuget.org release notes, so each entry must be **one crisp sentence about the user-visible change** — readable by a package consumer who has never seen the code.
1138+
1139+
- **Pattern to catch**: a new/edited `CHANGELOG.md` entry that names internal classes/methods (`BatchPermissionsOrchestrator`, `Phase 2a`), explains implementation mechanism (`retries via az rest`, `POST /appRoleAssignments`), restates rationale, or runs to multiple sentences of background.
1140+
- **Severity**: `low`.
1141+
- **Check**: read each CHANGELOG line in the diff. Would a consumer who only runs the CLI understand the *behavior* change from it, with no code knowledge? If it leans on internals or reads like a design note, it fails. Also verify the entry does not contradict another entry in the same `[Unreleased]` block (stale claims left behind by the change).
1142+
- **Fix**: rewrite to the user-facing outcome in one sentence; keep the `(#NNN)` reference. Drop class/method names and mechanism. Correct any sibling entry the change makes stale.
1143+
- **Real example (issue #460)**: a two-sentence entry naming `az rest` / PowerShell internals was cut to one outcome-focused sentence; a stale sibling line ("Phase 2a/2b always skipped for non-DW agents") that the change contradicted was corrected at the same time.
1144+
11251145
## Example Invocation
11261146

11271147
When you receive a request like "Review PR #253", you should:

.claude/skills/review-pr/SKILL.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ This skill enforces the following principles:
6363
- **Function reuse**: Encourages reusing functions across commands
6464
- **No special characters**: Avoids emojis in logs/output (Windows compatibility)
6565
- **Self-documenting code**: Prefers clear code over excessive comments
66+
- **Crisp comments (pr-code-reviewer #30)**: Flags added comments that run past 1-2 lines, restate the code, or narrate design history — a comment says *why* in one line; long-form reasoning belongs in the commit/PR.
67+
- **Release-note-ready CHANGELOG (pr-code-reviewer #31)**: Flags `CHANGELOG.md` entries that name internals, explain mechanism, or run multiple sentences — each entry is one crisp consumer-facing sentence (it ships verbatim to nuget.org release notes).
6668
- **Minimal changes**: Makes only necessary changes to solve the problem
6769

6870
### Testing Standards

.github/copilot-instructions.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,13 @@
100100
- Keep user-facing messages clear and professional
101101
- Follow client-facing help text conventions
102102

103+
### Comments
104+
- Comments are crisp: state *why* in one or two lines, never an essay. A `//` or `///` `<summary>` that runs to a paragraph, restates the code, or narrates the change ("previously X, now Y because...") belongs in the commit message / PR, not in source.
105+
- Keep an issue/PR reference (`(issue #460)`) but drop the surrounding narration. Trivial mechanical edits get zero or one comment line.
106+
107+
### CHANGELOG
108+
- `CHANGELOG.md` `[Unreleased]` ships verbatim to nuget.org release notes. Each entry is one crisp consumer-facing sentenceno class/method names, no implementation mechanism, no multi-sentence rationale. Keep the `(#NNN)` reference; fix any sibling entry the change makes stale.
109+
103110
### Code Review Mindset
104111
- Be cautious about deleting code; avoid `git restore` without review
105112
- Do not create unnecessary documentation files

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ Agents provisioned before this release need `Agent365.Observability.OtelWrite` g
3737
- `obo` (default): principal-scoped delegated grants (`consentType: "Principal"`); no Global Administrator required.
3838
- `s2s`: application role assignments on the agent identity SP; attempted programmatically, falls back to printed PowerShell instructions if the caller lacks Global Administrator.
3939
- `both`: applies both OBO delegated grants and S2S app role assignments.
40-
- Inheritable permissions (Phase 2a) and AllPrincipals grants (Phase 2b) are always skipped for non-DW agents regardless of `authMode`, to avoid requiring a Global Administrator role.
40+
- Non-DW agents stamp inheritable permissions and S2S grants on the blueprint (Global Administrator only); the agent identity inherits them, so a per-identity grant runs only when inheritance is not in force.
4141
- `authMode` can be persisted in `a365.config.json` to apply on every run without the flag.
4242
- `--project-path <path>` option on `develop list-configured`, `develop add-mcp-servers`, and `develop remove-mcp-servers` — specify the manifest location without requiring `a365.config.json`.
4343
- `setup requirements` runs without `a365.config.json` — system checks (PowerShell modules, Frontier enrollment) always run; client app checks run when a config file or Azure CLI session is available.
@@ -58,6 +58,7 @@ Agents provisioned before this release need `Agent365.Observability.OtelWrite` g
5858
- `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`).
5959

6060
### Fixed
61+
- `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).
6162
- `a365 develop get-token` now falls back to device code when the Windows WAM broker rejects Exchange Graph scopes with `ApiContractViolation`, instead of failing with an opaque MSAL error.
6263
- `setup blueprint` now configures the blueprint's inheritable Microsoft Graph permissions even when the signed-in user is not a Global Administrator, no longer aborts with a misleading "Failed to configure inheritable permissions" error when the tenant-wide consent grant cannot be made programmatically, and ends with a setup summary whose Action Required block surfaces the admin-consent URL for non-admins to hand off (#452).
6364
- Messaging endpoint registration and removal now retry on transient network errors instead of failing on a momentary DNS or connection blip.

CLAUDE.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,27 @@ src/Microsoft.Agents.A365.DevTools.Cli/
111111
- All `IDisposable` objects must be disposed (especially `HttpResponseMessage`)
112112
- Cross-platform compatibility required (Windows, macOS, Linux)
113113

114+
### Comments
115+
Comments are crisp: state *why* in one line, not *what* the code already shows. Do not write essays.
116+
- A code comment (`//` or `///` `<summary>`) is **one or two lines**. If you need a paragraph of rationale, it belongs in the commit message or PR description, not in source.
117+
- Keep an issue/PR reference (`(issue #460)`) but drop the surrounding narration.
118+
- Never narrate the change ("previously we did X, now Y because...") — that is commit-message content.
119+
- Trivial mechanical edits (log-level change, blank-line separator, `catch { throw; }`) get zero or one line, never a rationale block.
120+
121+
```csharp
122+
// Bad: essay restating the change and its history
123+
// Issue #460: when inheritable permissions were configured with kind=allAllowed (covering both
124+
// scopes and roles) AND the blueprint SP was granted the app roles, the agent identity inherits
125+
// them automatically — the same basis the OBO branch relies on. A direct grant then only adds a
126+
// duplicate row and a spurious prompt, so we skip it and report the inherited grant as Granted.
127+
128+
// Good: one-line why + reference
129+
// Skip the direct grant when the role is already inherited from the blueprint (issue #460).
130+
```
131+
132+
### CHANGELOG entries
133+
`CHANGELOG.md` `[Unreleased]` ships verbatim to nuget.org release notes. Each entry is **one crisp consumer-facing sentence** about the user-visible change — no class/method names, no implementation mechanism, no multi-sentence rationale. Keep the `(#NNN)` reference. When a change makes a sibling entry stale, fix it in the same edit.
134+
114135
### Input Validation
115136
User-controlled input that reaches file system operations must be validated before use. This applies to CLI arguments, config values read from disk, and any value whose origin is outside this process.
116137

src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/NonDwBlueprintSetupOrchestrator.cs

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -526,7 +526,7 @@ private static async Task ExecuteAgentIdentityAndRegistrationAsync(
526526

527527
// S2S and Both: app role assignments (requires Global Admin; falls back to PowerShell instructions).
528528
if (ctx.IsS2sMode || ctx.IsBothMode)
529-
await GrantOrInstructAgentIdentityAppPermissionsAsync(ctx, specs);
529+
await GrantAgentIdentityS2SPermissionsAsync(ctx, specs);
530530
}
531531
}
532532

@@ -655,10 +655,39 @@ private static async Task ExecuteAgentIdentityAndRegistrationAsync(
655655
}
656656
}
657657

658+
/// <summary>
659+
/// Step 5a (S2S/Both): grants the agent identity's app roles, or skips the grant when they are
660+
/// already inherited from the blueprint (issue #460, see <see cref="AgentIdentityInheritsBlueprintAppRoles"/>).
661+
/// </summary>
662+
internal static async Task GrantAgentIdentityS2SPermissionsAsync(
663+
SetupContext ctx,
664+
List<ResourcePermissionSpec> specs)
665+
{
666+
var hasS2sSpecs = specs.Any(s => s.AppRoleScopes is { Length: > 0 });
667+
if (hasS2sSpecs && AgentIdentityInheritsBlueprintAppRoles(ctx.Results))
668+
{
669+
ctx.Logger.LogDebug("Agent identity inherits S2S app roles from the blueprint; skipping redundant direct grant.");
670+
ctx.Results.AgentIdentityS2SOutcome = Models.GrantOutcome.Granted;
671+
return;
672+
}
673+
674+
await GrantOrInstructAgentIdentityAppPermissionsAsync(ctx, specs);
675+
}
676+
677+
/// <summary>
678+
/// True when the agent identity already holds the blueprint's S2S app roles via inheritance —
679+
/// inheritable permissions configured (allAllowed) and the blueprint SP granted the roles — so a
680+
/// direct grant is redundant (issue #460). Both conditions are required.
681+
/// </summary>
682+
internal static bool AgentIdentityInheritsBlueprintAppRoles(SetupResults results) =>
683+
results.BatchPermissionsPhase2Completed
684+
&& results.BlueprintS2SOutcome == Models.GrantOutcome.Granted;
685+
658686
/// <summary>
659687
/// Attempts to grant app role assignments on the agent identity SP for S2S access.
660-
/// Requires Agent ID Administrator, Application Administrator, or Global Administrator. When the signed-in user lacks
661-
/// one of those roles, prints PowerShell instructions covering only the app permission section.
688+
/// Requires Agent ID Administrator, Application Administrator, or Global Administrator. When the
689+
/// programmatic Graph path fails, retries via <c>az rest</c> against the operator's existing az
690+
/// session before falling back to PowerShell instructions covering only the app permission section.
662691
/// </summary>
663692
internal static async Task GrantOrInstructAgentIdentityAppPermissionsAsync(
664693
SetupContext ctx,
@@ -712,6 +741,20 @@ internal static async Task GrantOrInstructAgentIdentityAppPermissionsAsync(
712741
return;
713742
}
714743

744+
// Issue #460: the Graph path failed (CLI token lacks AppRoleAssignment.ReadWrite.All). Retry
745+
// via az rest before PowerShell — a GA's az token carries it via the directory role. Mirrors
746+
// the blueprint SP fallback in BatchPermissionsOrchestrator.
747+
ctx.Logger.LogDebug("S2S app role assignments on the agent identity could not be completed via the Graph API; falling back to az rest.");
748+
var (attempted, succeeded) = await AzRestS2SRunner.TryRunAsync(
749+
ctx.Executor, agentIdentitySpObjectId, failedSpecs, ctx.Logger, ctx.CancellationToken);
750+
if (attempted && succeeded)
751+
{
752+
using (ctx.Logger.Indent())
753+
ctx.Logger.LogInformation("S2S app role assignments granted to agent identity.");
754+
ctx.Results.AgentIdentityS2SOutcome = Models.GrantOutcome.Granted;
755+
return;
756+
}
757+
715758
// Non-admin fallback: print PowerShell instructions for only the failed resources.
716759
ctx.Results.AgentIdentityS2SOutcome = Models.GrantOutcome.Failed;
717760
ctx.Logger.LogInformation("");

src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ This folder contains the workflow components for the `a365 setup` command. The s
2121
| **SetupHelpers** | `SetupHelpers.cs` | Shared helper methods; `EnsureResourcePermissionsAsync` used by standalone callers and `CopilotStudioSubcommand` |
2222
| **SetupResults** | `SetupResults.cs` | Result models for setup operations |
2323
| **SetupContext** | `SetupContext.cs` | Context bundle threaded through orchestrator steps; exposes `AuthMode`, `IsOboMode`, `IsS2sMode`, `IsBothMode` |
24-
| **NonDwBlueprintSetupOrchestrator** | `NonDwBlueprintSetupOrchestrator.cs` | Blueprint-based non-DW setup flow; skips Phase 2a/2b (inheritable permissions); gates agent identity grants by `authMode` |
24+
| **NonDwBlueprintSetupOrchestrator** | `NonDwBlueprintSetupOrchestrator.cs` | Blueprint-based non-DW setup flow; stamps inheritable permissions + S2S grants on the blueprint, then gates agent identity grants by `authMode` (skipping the per-identity S2S grant when the role is already inherited) |
2525

2626
---
2727

@@ -74,12 +74,12 @@ The `--authmode` option controls how the agent identity service principal is gra
7474
| Value | Behaviour |
7575
|-------|-----------|
7676
| `obo` (default) | Principal-scoped delegated grants (`consentType: "Principal"`) on the agent identity SP — no Global Admin required |
77-
| `s2s` | Application role assignments on the agent identity SP — attempted programmatically; PowerShell instructions printed as fallback if the caller lacks Global Admin |
77+
| `s2s` | Application role assignments — when the blueprint already holds the roles and inheritable permissions are configured (`allAllowed`), the agent identity inherits them and no direct grant is made; otherwise the grant is attempted on the agent identity SP programmatically, then via `az rest`, with PowerShell instructions printed only if both fail |
7878
| `both` | Both OBO delegated grants and S2S app role assignments |
7979

8080
`authMode` may also be persisted in `a365.config.json` so it takes effect on every run without the flag.
8181

82-
Phase 2a (inheritable permissions on the blueprint) and Phase 2b (AllPrincipals grants) are **always skipped** for non-DW agents regardless of `authMode`, to avoid requiring a Global Admin role.
82+
Non-DW agents stamp the same permission spec set on the blueprint (inheritable permissions with `kind=allAllowed`, plus S2S app-role grants on the blueprint SP when the caller is a Global Admin). Because inheritance covers both scopes and roles, the agent identity inherits the blueprint's grants automatically — so the per-identity S2S grant (issue #460) is skipped when the blueprint grant and inheritance both succeeded, and the delegated (OBO) path never issues a per-identity grant at all.
8383

8484
```bash
8585
# Use OBO grants (default)

0 commit comments

Comments
 (0)