Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
cbdfdba
fix: device code fallback for macOS browser auth and cross-platform U…
sellakumaran Mar 4, 2026
6c8fe1d
fix: address PR review comments and handle Linux xdg_open_failed error
sellakumaran Mar 4, 2026
4da9b55
fix: auto-fix public client flows, add requirements check to blueprin…
sellakumaran Mar 4, 2026
3272376
fix: eliminate double auth on Linux, improve PS module error handling
sellakumaran Mar 4, 2026
2eae60e
fix: add system requirement checks to all setup commands that use Gra…
sellakumaran Mar 4, 2026
35bd6a0
Skip requirements on dry run; clarify browser fallback
sellakumaran Mar 4, 2026
f10f636
fix: skip requirements on dry run, self-correct missing PS modules in…
sellakumaran Mar 4, 2026
1550c0e
fix: extract real JWT from Graph request headers; unify PS auth scope…
sellakumaran Mar 4, 2026
bae2765
fix: extract last stdout line as JWT token; add clean error for missi…
sellakumaran Mar 4, 2026
5b58ab7
fix: unify PS token cache key in RemoveStale and DeployCommand; updat…
sellakumaran Mar 4, 2026
ed2ace6
fix: improve auth prompt UX and clarify custom permissions message
sellakumaran Mar 5, 2026
4fd11c7
fix: retry blueprint SP lookup on Azure AD propagation delay
sellakumaran Mar 5, 2026
ba0e867
fix: fix admin consent polling via MSAL token with Application.Read.All
sellakumaran Mar 5, 2026
c1e2486
fix: fall back to MSAL when PS Connect-MgGraph fails on any platform
sellakumaran Mar 5, 2026
ac390bf
fix: address PR review comments - disposal, logging, exit handling, d…
sellakumaran Mar 5, 2026
538f309
Merge with main.
sellakumaran Mar 5, 2026
1867f3a
fix: address second round of PR review comments
sellakumaran Mar 5, 2026
64b8c2d
chore: add CHANGELOG, NuGet release notes, and review process updates
sellakumaran Mar 5, 2026
cccd810
fix: address PR review comments - StringComparison, PSGallery, encodi…
sellakumaran Mar 5, 2026
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
1 change: 1 addition & 0 deletions .claude/agents/code-review-manager.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ You are a Senior Code Review Manager with 15+ years of experience leading engine
- Warnings are treated as errors
- IDisposable objects must be properly disposed
- Cross-platform compatibility required (Windows, macOS, Linux)
- `CHANGELOG.md` must be updated in `[Unreleased]` for user-facing changes (features, bug fixes, behavioral changes)

**Your Primary Responsibilities**:

Expand Down
1 change: 1 addition & 0 deletions .claude/agents/code-reviewer.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,5 +249,6 @@ Before completing your review:
3. Are your suggestions backed by specific reasoning?
4. Have you balanced criticism with recognition of good practices?
5. Would following your suggestions result in production-ready code?
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.

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.
7 changes: 6 additions & 1 deletion .claude/agents/pr-code-reviewer.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,12 @@ For each changed file, analyze:
- Path separators
- OS-specific code

7. **Test Coverage Gaps**
7. **CHANGELOG.md Check** (for user-facing changes)
- If the PR adds features, fixes bugs, or changes observable behavior, verify `CHANGELOG.md` has an entry in the `[Unreleased]` section
- Internal refactors, test-only changes, and tooling/CI-only changes do not require a CHANGELOG entry
- Flag as `low` severity if missing from a user-facing PR

8. **Test Coverage Gaps**
- Based on the conditional logic, what specific test scenarios are needed?
- Generate concrete test code examples

Expand Down
1 change: 1 addition & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
### Code Review Mindset
- Be cautious about deleting code; avoid `git restore` without review
- Do not create unnecessary documentation files
- For user-facing changes (features, bug fixes, behavioral changes): verify `CHANGELOG.md` has an entry in the `[Unreleased]` section

---

Expand Down
53 changes: 53 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

### Fixed
- macOS/Linux: device code fallback when browser authentication is unavailable (#309)
- Linux: MSAL fallback when PowerShell `Connect-MgGraph` fails in non-TTY environments (#309)
- Admin consent polling no longer times out after 180s — blueprint service principal now resolved with correct MSAL token (#309)
- `ConfigFileNotFoundException` now derives from `FileNotFoundException` so existing catch sites continue to work (#309)

## [1.1.0] - 2026-02

### Added
- Custom blueprint permissions configuration and management — configure any resource's OAuth2 grants and inheritable permissions via `a365.config.json` (#298)
- `setup requirements` subcommand with per-category checks: PowerShell modules, location, client app configuration, Frontier Program enrollment (#293)
- `setup permissions copilotstudio` subcommand for Power Platform `CopilotStudio.Copilots.Invoke` permission (#298)
- Persistent MSAL token cache to reduce repeated WAM login prompts on Windows (#261)
- Auto-detect endpoint name from project settings; globally unique names to prevent accidental collisions (#289)
- `.NET` runtime roll-forward — CLI now works on .NET 9 and later without reinstalling (#276)
- Mock tooling server MCP protocol compliance for Python and Node.js agents (#263)

### Fixed
- Prevent `InternalServerError` loop when `--update-endpoint` fails on create (#304)
- Correct endpoint name derivation for `needsDeployment=false` scenarios (#296)
- Browser auth falls back to device code on macOS when WAM/browser is unavailable (#290)
- `PublishCommand` now returns non-zero exit code on all error paths (#266)
- Azure CLI Graph token cached across publish command Graph API calls (#267)
- PowerShell 5.1 install compatibility and macOS auth testability improvements (#292)
- MOS token cache timezone comparison bug in `TryGetCachedToken` (#278)
- Location config validated before endpoint registration and deletion (#281)
- `CustomClientAppId` correctly set in `BlueprintSubcommand` to fix inheritable permissions (#272)
- Endpoint names trimmed of trailing hyphens to comply with Azure Bot Service naming rules (#257)
- Python projects without `pyproject.toml` handled in `a365 deploy` (#253)

## [1.0.0] - 2025-12

### Added
- `a365 setup blueprint` — creates and configures an Agent Identity Blueprint in Azure AD
- `a365 setup permissions mcp` / `bot` — configures OAuth2 grants and inheritable permissions
- `a365 deploy` — multi-platform deployment (`.NET`, `Node.js`, `Python`) with auto-detection
- `a365 config init` — initialize project configuration
- `a365 cleanup` — remove Azure resources and blueprint configuration
- Interactive browser authentication via MSAL with WAM on Windows
- Microsoft Graph operations using PowerShell `Microsoft.Graph` module
- Admin consent polling with automatic detection

[Unreleased]: https://github.com/microsoft/Agent365-devTools/compare/v1.1.0...HEAD
[1.1.0]: https://github.com/microsoft/Agent365-devTools/compare/v1.0.0...v1.1.0
[1.0.0]: https://github.com/microsoft/Agent365-devTools/releases/tag/v1.0.0
45 changes: 22 additions & 23 deletions src/DEVELOPER.md
Original file line number Diff line number Diff line change
Expand Up @@ -833,32 +833,30 @@ Follow Semantic Versioning: `MAJOR.MINOR.PATCH[-PRERELEASE]`

### Create Release

1. Update version in `Microsoft.Agents.A365.DevTools.Cli.csproj`:
```xml
<Version>1.0.0-beta.2</Version>
```
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.

2. Build and pack:
```bash
dotnet clean
dotnet build -c Release
dotnet pack -c Release
```
**Steps to release:**

3. Test locally:
```bash
dotnet tool uninstall -g Microsoft.Agents.A365.DevTools.Cli
dotnet tool install -g Microsoft.Agents.A365.DevTools.Cli \
--add-source ./bin/Release \
--prerelease
```
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.

4. Publish to NuGet (when ready):
```bash
dotnet nuget push ./bin/Release/Microsoft.Agents.A365.DevTools.Cli.1.0.0-beta.2.nupkg \
--source https://api.nuget.org/v3/index.json \
--api-key YOUR_API_KEY
```
2. **Merge to main** — CI runs automatically: builds, tests, and uploads the NuGet package as a build artifact.

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**.

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.

**Test locally before releasing:**
```bash
cd src
dotnet build dirs.proj -c Release
dotnet pack dirs.proj -c Release --output ../NuGetPackages

dotnet tool uninstall -g Microsoft.Agents.A365.DevTools.Cli
dotnet tool install -g Microsoft.Agents.A365.DevTools.Cli \
--add-source ../NuGetPackages \
--prerelease
a365 --version
```

---

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

---

Expand Down
1 change: 1 addition & 0 deletions src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
<RepositoryType>git</RepositoryType>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<PackageReleaseNotes>See https://github.com/microsoft/Agent365-devTools/blob/main/CHANGELOG.md for release notes.</PackageReleaseNotes>

<!-- Version is managed by Nerdbank.GitVersioning (nbgv) via version.json -->
<!-- PackageVersion and Version properties are set automatically by nbgv -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -414,8 +414,9 @@ private static async Task EnsureMcpInheritablePermissionsAsync(

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

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

var (ok, alreadyExists, err) = await blueprintService.SetInheritablePermissionsAsync(
config.TenantId, config.AgentBlueprintId, resourceAppId, scopes, requiredScopes: requiredPermissions, ct);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,14 +154,20 @@ public static Command CreateCommand(
"--update-endpoint",
description: "Delete the existing messaging endpoint and register a new one with the specified URL");

var skipRequirementsOption = new Option<bool>(
"--skip-requirements",
description: "Skip requirements validation check\n" +
"Use with caution: setup may fail if prerequisites are not met");

command.AddOption(configOption);
command.AddOption(verboseOption);
command.AddOption(dryRunOption);
command.AddOption(skipEndpointRegistrationOption);
command.AddOption(endpointOnlyOption);
command.AddOption(updateEndpointOption);
command.AddOption(skipRequirementsOption);

command.SetHandler(async (config, verbose, dryRun, skipEndpointRegistration, endpointOnly, updateEndpoint) =>
command.SetHandler(async (config, verbose, dryRun, skipEndpointRegistration, endpointOnly, updateEndpoint, skipRequirements) =>
{
// Generate correlation ID at workflow entry point
var correlationId = HttpClientFactory.GenerateCorrelationId();
Expand Down Expand Up @@ -215,6 +221,37 @@ await UpdateEndpointAsync(
return;
}

// Run all requirements checks: system checks (PowerShell modules, Frontier Preview)
// and config checks (Location, ClientApp — includes isFallbackPublicClient auto-fix
// required for device code auth on macOS/Linux/WSL).
// Skip when dryRun is true: ClientAppRequirementCheck can mutate the app registration
// (e.g., set isFallbackPublicClient), which violates dry-run semantics.
if (!skipRequirements && !dryRun)
{
try
{
var requirementsResult = await RequirementsSubcommand.RunRequirementChecksAsync(
RequirementsSubcommand.GetRequirementChecks(clientAppValidator),
setupConfig,
logger,
category: null,
CancellationToken.None);

if (!requirementsResult)
{
logger.LogError("Setup cannot proceed due to the failed requirement checks above. Please fix the issues above and then try again.");
logger.LogError("Use the resolution guidance provided for each failed check.");
ExceptionHandler.ExitWithCleanup(1);
}
}
catch (Exception reqEx)
{
logger.LogError(reqEx, "Requirements check failed with an unexpected error: {Message}", reqEx.Message);
logger.LogError("If you want to bypass requirement validation, rerun this command with the --skip-requirements flag.");
ExceptionHandler.ExitWithCleanup(1);
}
}

if (dryRun)
{
logger.LogInformation("DRY RUN: Create Agent Blueprint");
Expand Down Expand Up @@ -281,7 +318,7 @@ await CreateBlueprintImplementationAsync(
correlationId: correlationId
);

}, configOption, verboseOption, dryRunOption, skipEndpointRegistrationOption, endpointOnlyOption, updateEndpointOption);
}, configOption, verboseOption, dryRunOption, skipEndpointRegistrationOption, endpointOnlyOption, updateEndpointOption, skipRequirementsOption);

return command;
}
Expand Down Expand Up @@ -773,7 +810,9 @@ public static async Task<bool> EnsureDelegatedConsentWithRetriesAsync(
if (string.IsNullOrWhiteSpace(existingServicePrincipalId))
{
logger.LogDebug("Looking up service principal for blueprint...");
var spLookup = await blueprintLookupService.GetServicePrincipalByAppIdAsync(tenantId, existingAppId, ct);
var spLookup = await blueprintLookupService.GetServicePrincipalByAppIdAsync(
tenantId, existingAppId, ct,
scopes: AuthenticationConstants.RequiredPermissionGrantScopes);

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

// Resolve blueprint SP object ID once — reused by both pre-check and polling.
// servicePrincipalId comes from generated config (persisted on previous runs).
// If absent, look it up using MSAL scopes that include Application.Read.All.
// Without Application.Read.All the az CLI token causes Graph to return empty results silently.
var blueprintSpId = servicePrincipalId;
if (string.IsNullOrWhiteSpace(blueprintSpId))
{
logger.LogDebug("Looking up service principal for blueprint...");
var spLookup = await blueprintLookupService.GetServicePrincipalByAppIdAsync(
tenantId, appId, ct,
scopes: AuthenticationConstants.RequiredPermissionGrantScopes);
blueprintSpId = spLookup.ObjectId;
}

// Only check for existing consent if blueprint already existed
// New blueprints cannot have consent yet, so skip the verification
if (alreadyExisted)
{
logger.LogInformation("Verifying admin consent for application");
logger.LogDebug(" - Application scopes: {Scopes}", string.Join(", ", applicationScopes));

// Check if consent already exists with required scopes
var blueprintSpId = servicePrincipalId;
if (string.IsNullOrWhiteSpace(blueprintSpId))
{
logger.LogDebug("Looking up service principal for blueprint to check consent...");
var spLookup = await blueprintLookupService.GetServicePrincipalByAppIdAsync(tenantId, appId, ct);
blueprintSpId = spLookup.ObjectId;
}

if (!string.IsNullOrWhiteSpace(blueprintSpId))
{
// Get Microsoft Graph service principal ID
// Get Microsoft Graph service principal ID (needs Application.Read.All)
var graphSpId = await graphApiService.LookupServicePrincipalByAppIdAsync(
tenantId,
AuthenticationConstants.MicrosoftGraphResourceAppId,
ct);
ct,
AuthenticationConstants.RequiredPermissionGrantScopes);

if (!string.IsNullOrWhiteSpace(graphSpId))
{
Expand All @@ -1373,7 +1418,8 @@ private static List<string> GetApplicationScopes(Models.Agent365Config setupConf
graphSpId,
applicationScopes,
logger,
ct);
ct,
scopes: AuthenticationConstants.RequiredPermissionGrantScopes);
}
}

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

var consentSuccess = await AdminConsentHelper.PollAdminConsentAsync(executor, logger, appId, "Graph API Scopes", 180, 5, ct);
bool consentSuccess;
if (!string.IsNullOrWhiteSpace(blueprintSpId))
{
consentSuccess = await AdminConsentHelper.PollAdminConsentAsync(
graphApiService, logger, tenantId, blueprintSpId,
"Graph API Scopes", 180, 5, ct);
}
else
{
logger.LogDebug("Could not resolve blueprint service principal. Falling back to az rest polling.");
consentSuccess = await AdminConsentHelper.PollAdminConsentAsync(executor, logger, appId, "Graph API Scopes", 180, 5, ct);
}

bool graphInheritablePermissionsConfigured = false;
string? graphInheritablePermissionsError = null;
Expand Down Expand Up @@ -1541,23 +1599,6 @@ private async static Task<GraphServiceClient> GetAuthenticatedGraphClientAsync(I
}
}

private static void TryOpenBrowser(string url)
{
try
{
using var p = new System.Diagnostics.Process();
p.StartInfo = new System.Diagnostics.ProcessStartInfo
{
FileName = url,
UseShellExecute = true
};
p.Start();
}
catch
{
// non-fatal
}
}

/// <summary>
/// Creates client secret for Agent Blueprint (Phase 2.5)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

using Microsoft.Agents.A365.DevTools.Cli.Constants;
using Microsoft.Agents.A365.DevTools.Cli.Exceptions;
using Microsoft.Agents.A365.DevTools.Cli.Helpers;
using Microsoft.Agents.A365.DevTools.Cli.Models;
using Microsoft.Agents.A365.DevTools.Cli.Services;
Expand Down Expand Up @@ -63,7 +64,7 @@ public static Command CreateCommand(
if (string.IsNullOrWhiteSpace(setupConfig.AgentBlueprintId))
{
logger.LogError("Blueprint ID not found. Run 'a365 setup blueprint' first.");
Environment.Exit(1);
ExceptionHandler.ExitWithCleanup(1);
}

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

// Verify system requirements (PowerShell modules are required for Graph operations).
// Skipped in dry-run: PowerShellModulesRequirementCheck can auto-install modules,
// which would be a side effect in a mode that is supposed to be non-mutating.
if (!dryRun)
{
var systemChecksOk = await RequirementsSubcommand.RunRequirementChecksAsync(
RequirementsSubcommand.GetSystemRequirementChecks(), setupConfig, logger, category: null, CancellationToken.None);
if (!systemChecksOk)
{
logger.LogError("Setup cannot proceed due to failed requirement checks above. Please fix the issues and retry.");
ExceptionHandler.ExitWithCleanup(1);
}
}

if (dryRun)
{
logger.LogInformation("DRY RUN: Configure CopilotStudio Permissions");
Expand Down
Loading
Loading