Skip to content

Commit 6b92e6a

Browse files
sellakumaranclaude
andauthored
fix: device code fallback for macOS browser auth and cross-platform URL opening (#309)
* fix: device code fallback for macOS browser auth and cross-platform URL opening - MsalBrowserCredential: catch PlatformNotSupportedException and fall back to device code flow instead of re-throwing, so macOS/Linux headless environments always get a working auth path - InteractiveGraphAuthService: eagerly acquire token before constructing GraphServiceClient to surface auth failures at construction time rather than silently returning a broken client (Gap 2 from PR #290) - InteractiveGraphAuthService: inject optional credentialFactory for testability - AuthenticationService: replace Console.WriteLine with structured logger calls in device code callback - Add BrowserHelper with cross-platform TryOpenUrl (Windows/macOS/Linux) - Remove duplicated TryOpenBrowser methods from BlueprintSubcommand and A365CreateInstanceRunner; both now call BrowserHelper.TryOpenUrl directly - Add 5 unit tests covering the eager-auth gap and credential caching behavior * fix: address PR review comments and handle Linux xdg_open_failed error - MsalBrowserCredential: also catch MsalClientException(linux_xdg_open_failed) for device code fallback on Linux/WSL (confirmed via WSL Ubuntu 24.04 repro); extract shared AcquireTokenWithDeviceCodeFallbackAsync to avoid duplication - BrowserHelper: fall back to Console.Error when logger is null so the manual URL is never silently lost regardless of caller - BlueprintSubcommand: log consent URL before TryOpenUrl and pass logger so users on headless platforms always see the URL to complete admin consent Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: auto-fix public client flows, add requirements check to blueprint, clean AADSTS7000218 error - ClientAppValidator: add EnsurePublicClientFlowsEnabledAsync (Step 7) that auto-detects and enables 'Allow public client flows' on the app registration via az rest GET/PATCH — required for MSAL device code fallback on macOS/Linux; non-fatal if PATCH fails - BlueprintSubcommand: run config requirements checks (LocationCheck + ClientAppCheck) before blueprint logic, with --skip-requirements opt-out; matches AllSubcommand pattern - MsalBrowserCredential: catch AADSTS7000218/invalid_client before general MsalException to emit clean actionable error message pointing to 'a365 setup requirements' instead of stack trace - Tests: add EnsurePublicClientFlowsEnabledAsync tests (enabled/disabled/patch-fails) and CreateCommand_ShouldHaveSkipRequirementsOption test Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: eliminate double auth on Linux, improve PS module error handling - MsalBrowserCredential: add shared in-memory MSAL token cache for Linux so all MsalBrowserCredential instances within one CLI invocation share the same token cache, eliminating repeated device code prompts during multi-step operations (e.g. blueprint creation + client secret creation) - BrowserHelper: remove exception object from LogWarning so xdg-open failures no longer emit a full stack trace in the output - MicrosoftGraphTokenProvider: detect missing PowerShell module error and log actionable guidance to run 'a365 setup requirements' - SetupHelpers: replace misleading "please sign in when prompted" message with one that mentions missing PS module as a common cause - PowerShellModulesRequirementCheck: auto-install missing modules via Install-Module -Scope CurrentUser -Force -AllowClobber when detected; returns Success if auto-install succeeds, Failure with manual steps if not Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: add system requirement checks to all setup commands that use Graph PS auth setup blueprint was running only config checks (Location + ClientApp), missing PowerShellModulesRequirementCheck entirely. setup permissions mcp/bot/custom and setup permissions copilotstudio had no requirement checks at all. All four now gate on GetSystemRequirementChecks() before executing Graph operations, so missing Microsoft.Graph.Authentication is caught and auto-installed upfront rather than failing mid-run. Closes partial work toward #106. * Skip requirements on dry run; clarify browser fallback Requirements checks are now skipped during dry runs to prevent unintended mutations. Updated `TryOpenUrl` documentation to clarify fallback behavior and logging when browser launch fails. * fix: skip requirements on dry run, self-correct missing PS modules in Graph token acquisition - Skip system requirement checks (including auto-install) when --dry-run is set in PermissionsSubcommand (mcp/bot/custom) and CopilotStudioSubcommand to preserve non-mutating dry-run semantics - MicrosoftGraphTokenProvider now detects a missing/broken Microsoft.Graph module at runtime, auto-installs both required Graph modules, and retries token acquisition once before failing; error message clarifies auto-install was attempted if retry also fails - PowerShellModulesRequirementCheck.ExecutePowerShellCommandAsync returns stderr instead of null on failure so auto-install debug logs are actionable Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: extract real JWT from Graph request headers; unify PS auth scopes to reduce login prompts Microsoft.Graph.Authentication v2+ returns an opaque (non-JWT) token from $ctx.AccessToken that is rejected when used as a Bearer token. Switch to always extracting the token from the Authorization header of a live Invoke-MgGraphRequest call, which always contains the real JWT. Add AgentIdentityBlueprint.UpdateAuthProperties.All to RequiredPermissionGrantScopes so all PowerShell-based Graph operations (OAuth2 grants, service principal lookups, and inheritable permissions) use the same scope set. This ensures Connect-MgGraph authenticates once and the access token is reused from the in-process cache for all downstream calls, reducing device code prompts from 4 to 3 on first run (1 PS + 2 MSAL for different resources). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: extract last stdout line as JWT token; add clean error for missing config - MicrosoftGraphTokenProvider: extract last non-empty stdout line as token. Connect-MgGraph in headless Linux/WSL falls back to device code and writes the 'To sign in...' prompt to stdout, contaminating the full StandardOutput string. Trimming the whole output returns a non-JWT string; taking only the last line (where $token is always written) fixes the JWT validation warning and the resulting SETUP_VALIDATION_FAILED for inheritable permissions. - ConfigFileNotFoundException: new Agent365Exception subclass for missing a365.config.json. ConfigService.LoadAsync now throws this instead of a raw FileNotFoundException, so the global handler displays a clean actionable error ('run a365 config init') instead of a full stack trace. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: unify PS token cache key in RemoveStale and DeployCommand; update test Both RemoveStaleCustomPermissionsAsync (PermissionsSubcommand) and DeployCommand used a hardcoded 2-scope array missing DelegatedPermissionGrant.ReadWrite.All. This produced a different sorted cache key than RequiredPermissionGrantScopes, causing a second Connect-MgGraph device-code prompt during 'setup blueprint' even though a token was already cached from the inheritable permissions step. Switch both sites to AuthenticationConstants.RequiredPermissionGrantScopes so all Graph token acquisitions share one cache entry per CLI invocation. Update Agent365ConfigServiceTests to expect ConfigFileNotFoundException instead of FileNotFoundException, matching the new exception type thrown by ConfigService. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: improve auth prompt UX and clarify custom permissions message MicrosoftGraphTokenProvider: replace misleading "Device Code: False" log with platform-specific guidance so users know what to expect: - Windows: "A browser window will open for authentication..." - Linux/macOS: "A device code prompt will appear below..." This avoids confusion when Connect-MgGraph auto-switches to device code in headless environments even though useDeviceCode=false was requested. PermissionsSubcommand: change "No custom blueprint permissions configured" to "No custom blueprint permissions specified in config. Skipping." to make clear this is about config content, not a system state or error condition. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: retry blueprint SP lookup on Azure AD propagation delay LookupServicePrincipalByAppIdAsync can return null for 10-30s after blueprint creation due to Azure AD eventual consistency. Use the existing RetryHelper (already used for inheritable permissions verification) to retry up to 5 times with 5s base delay / exponential backoff before throwing the 'service principal may not have propagated' error. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: fix admin consent polling via MSAL token with Application.Read.All Root cause: SP lookup and consent grant queries used az CLI token (scopes=null) which lacks Application.Read.All and DelegatedPermissionGrant.ReadWrite.All. Graph silently returned HTTP 200 with empty array, leaving blueprintSpId=null and causing polling to time out after 180s even after consent was granted. Changes: - BlueprintSubcommand: pass RequiredPermissionGrantScopes to SP lookups and CheckConsentExistsAsync; resolve blueprintSpId once at start of consent flow; use new MSAL PollAdminConsentAsync when SP ID is available - AdminConsentHelper: add MSAL PollAdminConsentAsync overload using GraphApiService; add scopes param to CheckConsentExistsAsync; add progress/timeout log messages - BlueprintLookupService: add optional scopes param to GetServicePrincipalByAppIdAsync - GraphApiService: add debug logging for failed GET; fix JsonDocument disposal - CommandExecutor: add optional outputTransform to ExecuteWithStreamingAsync - MicrosoftGraphTokenProvider: reformat PS device code output to MSAL box style - AuthenticationService: reduce token cache log noise (LogInfo -> LogDebug) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: fall back to MSAL when PS Connect-MgGraph fails on any platform When PowerShell Connect-MgGraph fails (NullRef in DeviceCodeCredential on Linux, no TTY, module issues, or any other reason), fall back to MsalBrowserCredential to acquire the Graph token. On Windows this uses WAM; on Linux/macOS it uses device code with silent-first logic. The token is stored in MicrosoftGraphTokenProvider's in-process cache so subsequent calls (inheritable permissions, custom permissions) within the same CLI invocation reuse it without re-prompting. Also adds silent token acquisition attempt before device code in MsalBrowserCredential.AcquireTokenWithDeviceCodeFallbackAsync to reduce unnecessary device code prompts when a cached token exists. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: address PR review comments - disposal, logging, exit handling, doc fixes - ConfigFileNotFoundException: derive from FileNotFoundException so existing catch sites (CleanupCommand, DeployCommand, etc.) continue to work - InteractiveGraphAuthService: remove unused Azure.Identity using; move credential factory resolution inside try/catch for consistent error wrapping - BrowserHelper: include exception object in LogWarning for structured logging - PowerShellModulesRequirementCheck: fix GenerateInstallationInstructions to produce valid PS syntax for multi-module Install-Module calls - PermissionsSubcommand / CopilotStudioSubcommand: replace Environment.Exit(1) with ExceptionHandler.ExitWithCleanup(1) to flush output before exit - MicrosoftGraphTokenProvider: dispose HttpResponseMessage after token extraction - MsalBrowserCredential: use platform-neutral path notation in cache doc comment Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: address second round of PR review comments - ConfigFileNotFoundException: add explicit using System.IO to avoid implicit global usings dependency - SetupHelpers: remove unnecessary ! null-forgiving operator on awaited task (it only asserts the Task itself is non-null, not the result) - BrowserHelper: use ArgumentList instead of Arguments for open/xdg-open to avoid argument parsing issues with special characters in URLs - MsalBrowserCredential: filter cached MSAL account by tenant ID before silent auth to avoid authenticating as wrong identity when multiple accounts are cached; fall through to device code if no tenant match - MicrosoftGraphTokenProvider: reuse IsPowerShellModuleMissingError in ProcessResult to eliminate duplicated string-matching logic Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: add CHANGELOG, NuGet release notes, and review process updates - CHANGELOG.md: Keep a Changelog format covering [Unreleased], 1.1.0, 1.0.0 - Directory.Build.props: PackageReleaseNotes points to CHANGELOG.md (fixes NuGet warning) - src/DEVELOPER.md: update Release Process to describe workflow; add CHANGELOG to PR checklist - .github/copilot-instructions.md: add CHANGELOG reminder to Code Review Mindset - .claude/agents/pr-code-reviewer.md: add CHANGELOG check in Step 2 - .claude/agents/code-reviewer.md: add CHANGELOG check to Self-Verification - .claude/agents/code-review-manager.md: add CHANGELOG to Project Context standards Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: address PR review comments - StringComparison, PSGallery, encoding, docs - MsalBrowserCredential.cs: StringComparison.Ordinal on Contains calls; comment clarifying same-tenant account edge case - InteractiveGraphAuthService.cs: StringComparison.Ordinal on all Contains calls in exception filters - PowerShellModulesRequirementCheck.cs: pin Install-Module to -Repository PSGallery - MicrosoftGraphTokenProvider.cs: pin both Install-Module calls to -Repository PSGallery - GraphApiService.cs: fix U+FFFD encoding artifact in comment - CommandExecutor.cs: document outputPrefix applies only to first line (outputTransform must not return multi-line) - BlueprintLookupService.cs: comment that DisplayName is not populated in this lookup path Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent def9e6e commit 6b92e6a

35 files changed

Lines changed: 1270 additions & 247 deletions

.claude/agents/code-review-manager.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ You are a Senior Code Review Manager with 15+ years of experience leading engine
1515
- Warnings are treated as errors
1616
- IDisposable objects must be properly disposed
1717
- Cross-platform compatibility required (Windows, macOS, Linux)
18+
- `CHANGELOG.md` must be updated in `[Unreleased]` for user-facing changes (features, bug fixes, behavioral changes)
1819

1920
**Your Primary Responsibilities**:
2021

.claude/agents/code-reviewer.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,5 +249,6 @@ Before completing your review:
249249
3. Are your suggestions backed by specific reasoning?
250250
4. Have you balanced criticism with recognition of good practices?
251251
5. Would following your suggestions result in production-ready code?
252+
6. For user-facing changes (features, bug fixes, behavioral changes): has `CHANGELOG.md` been updated in the `[Unreleased]` section? Flag as `low` severity if not.
252253

253254
If you need to see additional context (like related files, configuration, or tests), ask for it explicitly. Your goal is to ensure the code is secure, maintainable, performant, and correctly implements CLI patterns.

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,12 @@ For each changed file, analyze:
144144
- Path separators
145145
- OS-specific code
146146

147-
7. **Test Coverage Gaps**
147+
7. **CHANGELOG.md Check** (for user-facing changes)
148+
- If the PR adds features, fixes bugs, or changes observable behavior, verify `CHANGELOG.md` has an entry in the `[Unreleased]` section
149+
- Internal refactors, test-only changes, and tooling/CI-only changes do not require a CHANGELOG entry
150+
- Flag as `low` severity if missing from a user-facing PR
151+
152+
8. **Test Coverage Gaps**
148153
- Based on the conditional logic, what specific test scenarios are needed?
149154
- Generate concrete test code examples
150155

.github/copilot-instructions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@
100100
### Code Review Mindset
101101
- Be cautious about deleting code; avoid `git restore` without review
102102
- Do not create unnecessary documentation files
103+
- For user-facing changes (features, bug fixes, behavioral changes): verify `CHANGELOG.md` has an entry in the `[Unreleased]` section
103104

104105
---
105106

CHANGELOG.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6+
7+
## [Unreleased]
8+
9+
### Fixed
10+
- macOS/Linux: device code fallback when browser authentication is unavailable (#309)
11+
- Linux: MSAL fallback when PowerShell `Connect-MgGraph` fails in non-TTY environments (#309)
12+
- Admin consent polling no longer times out after 180s — blueprint service principal now resolved with correct MSAL token (#309)
13+
- `ConfigFileNotFoundException` now derives from `FileNotFoundException` so existing catch sites continue to work (#309)
14+
15+
## [1.1.0] - 2026-02
16+
17+
### Added
18+
- Custom blueprint permissions configuration and management — configure any resource's OAuth2 grants and inheritable permissions via `a365.config.json` (#298)
19+
- `setup requirements` subcommand with per-category checks: PowerShell modules, location, client app configuration, Frontier Program enrollment (#293)
20+
- `setup permissions copilotstudio` subcommand for Power Platform `CopilotStudio.Copilots.Invoke` permission (#298)
21+
- Persistent MSAL token cache to reduce repeated WAM login prompts on Windows (#261)
22+
- Auto-detect endpoint name from project settings; globally unique names to prevent accidental collisions (#289)
23+
- `.NET` runtime roll-forward — CLI now works on .NET 9 and later without reinstalling (#276)
24+
- Mock tooling server MCP protocol compliance for Python and Node.js agents (#263)
25+
26+
### Fixed
27+
- Prevent `InternalServerError` loop when `--update-endpoint` fails on create (#304)
28+
- Correct endpoint name derivation for `needsDeployment=false` scenarios (#296)
29+
- Browser auth falls back to device code on macOS when WAM/browser is unavailable (#290)
30+
- `PublishCommand` now returns non-zero exit code on all error paths (#266)
31+
- Azure CLI Graph token cached across publish command Graph API calls (#267)
32+
- PowerShell 5.1 install compatibility and macOS auth testability improvements (#292)
33+
- MOS token cache timezone comparison bug in `TryGetCachedToken` (#278)
34+
- Location config validated before endpoint registration and deletion (#281)
35+
- `CustomClientAppId` correctly set in `BlueprintSubcommand` to fix inheritable permissions (#272)
36+
- Endpoint names trimmed of trailing hyphens to comply with Azure Bot Service naming rules (#257)
37+
- Python projects without `pyproject.toml` handled in `a365 deploy` (#253)
38+
39+
## [1.0.0] - 2025-12
40+
41+
### Added
42+
- `a365 setup blueprint` — creates and configures an Agent Identity Blueprint in Azure AD
43+
- `a365 setup permissions mcp` / `bot` — configures OAuth2 grants and inheritable permissions
44+
- `a365 deploy` — multi-platform deployment (`.NET`, `Node.js`, `Python`) with auto-detection
45+
- `a365 config init` — initialize project configuration
46+
- `a365 cleanup` — remove Azure resources and blueprint configuration
47+
- Interactive browser authentication via MSAL with WAM on Windows
48+
- Microsoft Graph operations using PowerShell `Microsoft.Graph` module
49+
- Admin consent polling with automatic detection
50+
51+
[Unreleased]: https://github.com/microsoft/Agent365-devTools/compare/v1.1.0...HEAD
52+
[1.1.0]: https://github.com/microsoft/Agent365-devTools/compare/v1.0.0...v1.1.0
53+
[1.0.0]: https://github.com/microsoft/Agent365-devTools/releases/tag/v1.0.0

src/DEVELOPER.md

Lines changed: 22 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -833,32 +833,30 @@ Follow Semantic Versioning: `MAJOR.MINOR.PATCH[-PRERELEASE]`
833833

834834
### Create Release
835835

836-
1. Update version in `Microsoft.Agents.A365.DevTools.Cli.csproj`:
837-
```xml
838-
<Version>1.0.0-beta.2</Version>
839-
```
836+
Version is managed automatically by [Nerdbank.GitVersioning](https://github.com/dotnet/Nerdbank.GitVersioning) via `src/version.json`. The NuGet publish process is fully automated through GitHub Actions.
840837

841-
2. Build and pack:
842-
```bash
843-
dotnet clean
844-
dotnet build -c Release
845-
dotnet pack -c Release
846-
```
838+
**Steps to release:**
847839

848-
3. Test locally:
849-
```bash
850-
dotnet tool uninstall -g Microsoft.Agents.A365.DevTools.Cli
851-
dotnet tool install -g Microsoft.Agents.A365.DevTools.Cli \
852-
--add-source ./bin/Release \
853-
--prerelease
854-
```
840+
1. **Update CHANGELOG.md** — move items from `[Unreleased]` to a new version section (e.g., `[1.2.0] - YYYY-MM`). Update the comparison links at the bottom.
855841

856-
4. Publish to NuGet (when ready):
857-
```bash
858-
dotnet nuget push ./bin/Release/Microsoft.Agents.A365.DevTools.Cli.1.0.0-beta.2.nupkg \
859-
--source https://api.nuget.org/v3/index.json \
860-
--api-key YOUR_API_KEY
861-
```
842+
2. **Merge to main** — CI runs automatically: builds, tests, and uploads the NuGet package as a build artifact.
843+
844+
3. **Publish the GitHub release draft** — release-drafter auto-creates a draft release from merged PR titles and labels. Go to [GitHub Releases](https://github.com/microsoft/Agent365-devTools/releases), review the draft, set the correct version tag (e.g., `v1.2.0`), and click **Publish release**.
845+
846+
4. **NuGet publish runs automatically** — the `release.yml` workflow triggers on `release: published` and pushes the package to NuGet.org using the `NUGET_API_KEY` repository secret.
847+
848+
**Test locally before releasing:**
849+
```bash
850+
cd src
851+
dotnet build dirs.proj -c Release
852+
dotnet pack dirs.proj -c Release --output ../NuGetPackages
853+
854+
dotnet tool uninstall -g Microsoft.Agents.A365.DevTools.Cli
855+
dotnet tool install -g Microsoft.Agents.A365.DevTools.Cli \
856+
--add-source ../NuGetPackages \
857+
--prerelease
858+
a365 --version
859+
```
862860

863861
---
864862

@@ -951,6 +949,7 @@ Then run: `source ~/.bashrc` (or `source ~/.zshrc`)
951949
- [ ] No breaking changes (or documented)
952950
- [ ] Error handling implemented
953951
- [ ] Logging added
952+
- [ ] CHANGELOG.md updated in `[Unreleased]` (required for user-facing changes: features, bug fixes, behavioral changes)
954953

955954
---
956955

src/Directory.Build.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
<RepositoryType>git</RepositoryType>
3232
<PackageLicenseExpression>MIT</PackageLicenseExpression>
3333
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
34+
<PackageReleaseNotes>See https://github.com/microsoft/Agent365-devTools/blob/main/CHANGELOG.md for release notes.</PackageReleaseNotes>
3435

3536
<!-- Version is managed by Nerdbank.GitVersioning (nbgv) via version.json -->
3637
<!-- PackageVersion and Version properties are set automatically by nbgv -->

src/Microsoft.Agents.A365.DevTools.Cli/Commands/DeployCommand.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -414,8 +414,9 @@ private static async Task EnsureMcpInheritablePermissionsAsync(
414414

415415
var resourceAppId = ConfigConstants.GetAgent365ToolsResourceAppId(config.Environment);
416416

417-
// Use custom client app auth for inheritable permissions - Azure CLI doesn't support this operation
418-
var requiredPermissions = new[] { "AgentIdentityBlueprint.UpdateAuthProperties.All", "Application.ReadWrite.All" };
417+
// Use custom client app auth for inheritable permissions - Azure CLI doesn't support this operation.
418+
// Use RequiredPermissionGrantScopes so all callers share the same PS token cache key.
419+
var requiredPermissions = AuthenticationConstants.RequiredPermissionGrantScopes;
419420

420421
var (ok, alreadyExists, err) = await blueprintService.SetInheritablePermissionsAsync(
421422
config.TenantId, config.AgentBlueprintId, resourceAppId, scopes, requiredScopes: requiredPermissions, ct);

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

Lines changed: 75 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -154,14 +154,20 @@ public static Command CreateCommand(
154154
"--update-endpoint",
155155
description: "Delete the existing messaging endpoint and register a new one with the specified URL");
156156

157+
var skipRequirementsOption = new Option<bool>(
158+
"--skip-requirements",
159+
description: "Skip requirements validation check\n" +
160+
"Use with caution: setup may fail if prerequisites are not met");
161+
157162
command.AddOption(configOption);
158163
command.AddOption(verboseOption);
159164
command.AddOption(dryRunOption);
160165
command.AddOption(skipEndpointRegistrationOption);
161166
command.AddOption(endpointOnlyOption);
162167
command.AddOption(updateEndpointOption);
168+
command.AddOption(skipRequirementsOption);
163169

164-
command.SetHandler(async (config, verbose, dryRun, skipEndpointRegistration, endpointOnly, updateEndpoint) =>
170+
command.SetHandler(async (config, verbose, dryRun, skipEndpointRegistration, endpointOnly, updateEndpoint, skipRequirements) =>
165171
{
166172
// Generate correlation ID at workflow entry point
167173
var correlationId = HttpClientFactory.GenerateCorrelationId();
@@ -215,6 +221,37 @@ await UpdateEndpointAsync(
215221
return;
216222
}
217223

224+
// Run all requirements checks: system checks (PowerShell modules, Frontier Preview)
225+
// and config checks (Location, ClientApp — includes isFallbackPublicClient auto-fix
226+
// required for device code auth on macOS/Linux/WSL).
227+
// Skip when dryRun is true: ClientAppRequirementCheck can mutate the app registration
228+
// (e.g., set isFallbackPublicClient), which violates dry-run semantics.
229+
if (!skipRequirements && !dryRun)
230+
{
231+
try
232+
{
233+
var requirementsResult = await RequirementsSubcommand.RunRequirementChecksAsync(
234+
RequirementsSubcommand.GetRequirementChecks(clientAppValidator),
235+
setupConfig,
236+
logger,
237+
category: null,
238+
CancellationToken.None);
239+
240+
if (!requirementsResult)
241+
{
242+
logger.LogError("Setup cannot proceed due to the failed requirement checks above. Please fix the issues above and then try again.");
243+
logger.LogError("Use the resolution guidance provided for each failed check.");
244+
ExceptionHandler.ExitWithCleanup(1);
245+
}
246+
}
247+
catch (Exception reqEx)
248+
{
249+
logger.LogError(reqEx, "Requirements check failed with an unexpected error: {Message}", reqEx.Message);
250+
logger.LogError("If you want to bypass requirement validation, rerun this command with the --skip-requirements flag.");
251+
ExceptionHandler.ExitWithCleanup(1);
252+
}
253+
}
254+
218255
if (dryRun)
219256
{
220257
logger.LogInformation("DRY RUN: Create Agent Blueprint");
@@ -281,7 +318,7 @@ await CreateBlueprintImplementationAsync(
281318
correlationId: correlationId
282319
);
283320

284-
}, configOption, verboseOption, dryRunOption, skipEndpointRegistrationOption, endpointOnlyOption, updateEndpointOption);
321+
}, configOption, verboseOption, dryRunOption, skipEndpointRegistrationOption, endpointOnlyOption, updateEndpointOption, skipRequirementsOption);
285322

286323
return command;
287324
}
@@ -773,7 +810,9 @@ public static async Task<bool> EnsureDelegatedConsentWithRetriesAsync(
773810
if (string.IsNullOrWhiteSpace(existingServicePrincipalId))
774811
{
775812
logger.LogDebug("Looking up service principal for blueprint...");
776-
var spLookup = await blueprintLookupService.GetServicePrincipalByAppIdAsync(tenantId, existingAppId, ct);
813+
var spLookup = await blueprintLookupService.GetServicePrincipalByAppIdAsync(
814+
tenantId, existingAppId, ct,
815+
scopes: AuthenticationConstants.RequiredPermissionGrantScopes);
777816

778817
if (spLookup.Found)
779818
{
@@ -1339,29 +1378,35 @@ private static List<string> GetApplicationScopes(Models.Agent365Config setupConf
13391378
var applicationScopes = GetApplicationScopes(setupConfig, logger);
13401379
bool consentAlreadyExists = false;
13411380

1381+
// Resolve blueprint SP object ID once — reused by both pre-check and polling.
1382+
// servicePrincipalId comes from generated config (persisted on previous runs).
1383+
// If absent, look it up using MSAL scopes that include Application.Read.All.
1384+
// Without Application.Read.All the az CLI token causes Graph to return empty results silently.
1385+
var blueprintSpId = servicePrincipalId;
1386+
if (string.IsNullOrWhiteSpace(blueprintSpId))
1387+
{
1388+
logger.LogDebug("Looking up service principal for blueprint...");
1389+
var spLookup = await blueprintLookupService.GetServicePrincipalByAppIdAsync(
1390+
tenantId, appId, ct,
1391+
scopes: AuthenticationConstants.RequiredPermissionGrantScopes);
1392+
blueprintSpId = spLookup.ObjectId;
1393+
}
1394+
13421395
// Only check for existing consent if blueprint already existed
13431396
// New blueprints cannot have consent yet, so skip the verification
13441397
if (alreadyExisted)
13451398
{
13461399
logger.LogInformation("Verifying admin consent for application");
13471400
logger.LogDebug(" - Application scopes: {Scopes}", string.Join(", ", applicationScopes));
13481401

1349-
// Check if consent already exists with required scopes
1350-
var blueprintSpId = servicePrincipalId;
1351-
if (string.IsNullOrWhiteSpace(blueprintSpId))
1352-
{
1353-
logger.LogDebug("Looking up service principal for blueprint to check consent...");
1354-
var spLookup = await blueprintLookupService.GetServicePrincipalByAppIdAsync(tenantId, appId, ct);
1355-
blueprintSpId = spLookup.ObjectId;
1356-
}
1357-
13581402
if (!string.IsNullOrWhiteSpace(blueprintSpId))
13591403
{
1360-
// Get Microsoft Graph service principal ID
1404+
// Get Microsoft Graph service principal ID (needs Application.Read.All)
13611405
var graphSpId = await graphApiService.LookupServicePrincipalByAppIdAsync(
13621406
tenantId,
13631407
AuthenticationConstants.MicrosoftGraphResourceAppId,
1364-
ct);
1408+
ct,
1409+
AuthenticationConstants.RequiredPermissionGrantScopes);
13651410

13661411
if (!string.IsNullOrWhiteSpace(graphSpId))
13671412
{
@@ -1373,7 +1418,8 @@ private static List<string> GetApplicationScopes(Models.Agent365Config setupConf
13731418
graphSpId,
13741419
applicationScopes,
13751420
logger,
1376-
ct);
1421+
ct,
1422+
scopes: AuthenticationConstants.RequiredPermissionGrantScopes);
13771423
}
13781424
}
13791425

@@ -1428,9 +1474,21 @@ await SetupHelpers.EnsureResourcePermissionsAsync(
14281474
logger.LogInformation("Requesting admin consent for application");
14291475
logger.LogInformation(" - Application scopes: {Scopes}", string.Join(", ", applicationScopes));
14301476
logger.LogInformation("Opening browser for Graph API admin consent...");
1431-
TryOpenBrowser(consentUrlGraph);
1477+
logger.LogInformation("If the browser does not open automatically, navigate to this URL to grant consent: {ConsentUrl}", consentUrlGraph);
1478+
BrowserHelper.TryOpenUrl(consentUrlGraph, logger);
14321479

1433-
var consentSuccess = await AdminConsentHelper.PollAdminConsentAsync(executor, logger, appId, "Graph API Scopes", 180, 5, ct);
1480+
bool consentSuccess;
1481+
if (!string.IsNullOrWhiteSpace(blueprintSpId))
1482+
{
1483+
consentSuccess = await AdminConsentHelper.PollAdminConsentAsync(
1484+
graphApiService, logger, tenantId, blueprintSpId,
1485+
"Graph API Scopes", 180, 5, ct);
1486+
}
1487+
else
1488+
{
1489+
logger.LogDebug("Could not resolve blueprint service principal. Falling back to az rest polling.");
1490+
consentSuccess = await AdminConsentHelper.PollAdminConsentAsync(executor, logger, appId, "Graph API Scopes", 180, 5, ct);
1491+
}
14341492

14351493
bool graphInheritablePermissionsConfigured = false;
14361494
string? graphInheritablePermissionsError = null;
@@ -1541,23 +1599,6 @@ private async static Task<GraphServiceClient> GetAuthenticatedGraphClientAsync(I
15411599
}
15421600
}
15431601

1544-
private static void TryOpenBrowser(string url)
1545-
{
1546-
try
1547-
{
1548-
using var p = new System.Diagnostics.Process();
1549-
p.StartInfo = new System.Diagnostics.ProcessStartInfo
1550-
{
1551-
FileName = url,
1552-
UseShellExecute = true
1553-
};
1554-
p.Start();
1555-
}
1556-
catch
1557-
{
1558-
// non-fatal
1559-
}
1560-
}
15611602

15621603
/// <summary>
15631604
/// Creates client secret for Agent Blueprint (Phase 2.5)

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

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT License.
33

44
using Microsoft.Agents.A365.DevTools.Cli.Constants;
5+
using Microsoft.Agents.A365.DevTools.Cli.Exceptions;
56
using Microsoft.Agents.A365.DevTools.Cli.Helpers;
67
using Microsoft.Agents.A365.DevTools.Cli.Models;
78
using Microsoft.Agents.A365.DevTools.Cli.Services;
@@ -63,7 +64,7 @@ public static Command CreateCommand(
6364
if (string.IsNullOrWhiteSpace(setupConfig.AgentBlueprintId))
6465
{
6566
logger.LogError("Blueprint ID not found. Run 'a365 setup blueprint' first.");
66-
Environment.Exit(1);
67+
ExceptionHandler.ExitWithCleanup(1);
6768
}
6869

6970
// Configure GraphApiService with custom client app ID if available
@@ -72,6 +73,20 @@ public static Command CreateCommand(
7273
graphApiService.CustomClientAppId = setupConfig.ClientAppId;
7374
}
7475

76+
// Verify system requirements (PowerShell modules are required for Graph operations).
77+
// Skipped in dry-run: PowerShellModulesRequirementCheck can auto-install modules,
78+
// which would be a side effect in a mode that is supposed to be non-mutating.
79+
if (!dryRun)
80+
{
81+
var systemChecksOk = await RequirementsSubcommand.RunRequirementChecksAsync(
82+
RequirementsSubcommand.GetSystemRequirementChecks(), setupConfig, logger, category: null, CancellationToken.None);
83+
if (!systemChecksOk)
84+
{
85+
logger.LogError("Setup cannot proceed due to failed requirement checks above. Please fix the issues and retry.");
86+
ExceptionHandler.ExitWithCleanup(1);
87+
}
88+
}
89+
7590
if (dryRun)
7691
{
7792
logger.LogInformation("DRY RUN: Configure CopilotStudio Permissions");

0 commit comments

Comments
 (0)