Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
0c6c974
fix: improve non-admin setup flow with self-healing permissions and a…
sellakumaran Mar 16, 2026
76983b1
feat: batch permissions orchestrator for non-admin setup flow
sellakumaran Mar 17, 2026
fa2c0c7
fix: address PR review comments and align setup summary with batch flow
sellakumaran Mar 17, 2026
9aa34a4
fix: use MSAL/WAM as primary Graph token path to fix cross-user conta…
sellakumaran Mar 18, 2026
e80c293
fix: address Copilot PR review comments
sellakumaran Mar 18, 2026
f8c6908
Improve changelog, auth flows, and admin consent handling
sellakumaran Mar 18, 2026
a270a89
fix: correct user identity for ATG and Graph token acquisition
sellakumaran Mar 18, 2026
b5be53e
fix: update test override signature for CreateBrowserCredential login…
sellakumaran Mar 19, 2026
4820d73
fix: address remaining Copilot PR review comments (#2-5)
sellakumaran Mar 19, 2026
40fe88f
fix: resolve Agent ID Admin setup failures for WAM auth, owner assign…
sellakumaran Mar 19, 2026
1576643
Support login hint for MSAL Graph token acquisition
sellakumaran Mar 19, 2026
f37d1aa
fix: pass login hint to blueprint httpClient token to prevent WAM cro…
sellakumaran Mar 19, 2026
80bf137
fix: address Copilot review comments on token cache, log levels, and …
sellakumaran Mar 19, 2026
3b0aa2f
fix: consent URL Graph-only scopes, SP retry, transitiveMemberOf role…
sellakumaran Mar 19, 2026
8027051
chore: remove docs/plans from version control (internal working docum…
sellakumaran Mar 19, 2026
4cc1c30
feat: add a365 setup admin command for Global Administrator OAuth2 gr…
sellakumaran Mar 20, 2026
6a19be9
feat: add consent URL generation, fix SP retry, and improve setup output
sellakumaran Mar 20, 2026
7430bff
Improve admin consent URLs, retry logic, and testability
sellakumaran Mar 20, 2026
960517a
fix: Copilot review comments, GA role detection, combined consent URL…
sellakumaran Mar 20, 2026
3dc1e2e
perf: eliminate real process/network/delay costs in test paths
sellakumaran Mar 21, 2026
a688b6a
Improve auth reliability, logging, and test rigor
sellakumaran Mar 21, 2026
da6f750
perf: eliminate repeated az CLI subprocess spawns across setup phases
sellakumaran Mar 22, 2026
5b05e37
Refactor infra/client validation: direct ARM/Graph HTTP
sellakumaran Mar 22, 2026
9366a5f
Add --field option to config command and standardize endpoint key
sellakumaran Mar 22, 2026
78a70e0
Handle PR comments
sellakumaran Mar 22, 2026
8af956b
Fix PR comments.
sellakumaran Mar 22, 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
195 changes: 192 additions & 3 deletions .claude/agents/pr-code-reviewer.md

Large diffs are not rendered by default.

30 changes: 26 additions & 4 deletions .claude/skills/review-staged/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
name: review-staged
description: Generate structured code review for staged files (git staged changes) using Claude Code agents. Provides feedback before committing to catch issues early.
allowed-tools: Bash(git:*), Read, Write
allowed-tools: Bash(git:*), Bash(dotnet:*), Bash(cd:*), Read, Write
---

# Review Staged Files Skill
Expand All @@ -27,8 +27,9 @@ Examples:
4. **Analyzes changes** for security, testing, design patterns, and code quality issues
5. **Differentiates contexts**: CLI code vs GitHub Actions code (different standards)
6. **Creates actionable feedback**: Specific refactoring suggestions based on file names and patterns
7. **Generates structured review document** saved to a markdown file
8. **Shows summary** of all issues found organized by severity
7. **Runs the test suite and measures per-test timing** — flags any test taking > 1 second as a performance regression
8. **Generates structured review document** saved to a markdown file
9. **Shows summary** of all issues found organized by severity

## Engineering Review Principles

Expand Down Expand Up @@ -66,6 +67,15 @@ This skill enforces the same principles as the PR review skill:
- **CLI reliability**: CLI code without tests is BLOCKING
- **GitHub Actions tests**: Strongly recommended (HIGH severity) but not blocking
- **Mock external dependencies**: Proper mocking patterns
- **Test performance — measured by running, not just static analysis**: The review ALWAYS runs the full test suite and reports per-test timing. Any test method taking **> 1 second** is flagged as a performance regression (HIGH severity). The finding must include:
- The slow test class and method name(s) with their measured time
- The root cause (cold `AzCliHelper` token cache, missing `WarmAzCliTokenCache` call, real subprocess not mocked, etc.)
- The fix (warmup call pattern, `loginHintResolver` injection, etc.)
- Expected time after fix

If all tests complete in < 1 second each: emit an **INFO — PASS** finding with the total suite time.

**Do not skip the test run.** Static code analysis alone missed the regression in `da6f750`; only measurement catches it reliably.

### Security
- **No hardcoded secrets**: Use environment variables or Azure Key Vault
Expand Down Expand Up @@ -101,7 +111,19 @@ The skill uses **Claude Code directly** for semantic code analysis (same as revi
4. Claude Code gets staged changes: `git diff --staged`
5. Claude Code performs semantic analysis using its own capabilities
6. Claude Code identifies specific issues with line numbers and code references
7. Claude Code writes markdown file to `.codereviews/claude-staged-<timestamp>.md`
7. **Claude Code runs the full test suite with per-test timing:**
```bash
cd src && dotnet test tests.proj --configuration Release --logger "console;verbosity=normal" 2>&1
```
Parse the output for lines matching `[X s]` or `[X,XXX ms]` patterns. Extract test class name, method name, and duration. Flag any test method taking **> 1 second**. Group findings by test class and include the measured times in the review.
8. Claude Code writes markdown file to `.codereviews/claude-staged-<timestamp>.md`

**Test timing output format** (from `dotnet test --logger "console;verbosity=normal"`):
```
Passed SomeTests.Method_Scenario_ExpectedResult [< 1 ms]
Passed OtherTests.Method_Slow [22 s]
```
Any line showing `[X s]` where X ≥ 1 is a slow test. Report all such tests in a dedicated finding.

**Key Advantages**:
- ✅ No API key required - uses Claude Code's existing authentication
Expand Down
15 changes: 14 additions & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
- Focus on quality over quantity of tests
- Add regression tests for bug fixes
- Tests should verify CLI reliability
- **Tests must assert requirements, not implementation** — when a test is changed to match new code behavior (rather than to reflect a changed requirement), that is a red flag. A test that silently tracks whatever the code does provides no regression protection. If a test needs to be updated, explicitly document the requirement the new assertion encodes (use `because:` in FluentAssertions). If you cannot articulate a requirement reason, the test change should be questioned.
- **FluentAssertions `because:` is mandatory for non-obvious assertions** — any assertion on a URL structure, encoding format, security-sensitive behavior, or protocol requirement must include a `because:` clause explaining the invariant being enforced.
- **Dispose IDisposable objects properly**:
- `HttpResponseMessage` objects created in tests must be disposed
- Even in mock/test handlers, follow proper disposal patterns
Expand Down Expand Up @@ -114,7 +116,18 @@
- Check if it's a legacy reference that needs to be updated
- **Files to check**: All `.cs`, `.csx` files in the repository

### Rule 2: Verify Copyright Headers
### Rule 2: Flag Tests Changed to Match Implementation
- **Description**: When a PR or staged change modifies a test assertion to match new code behavior, treat it as a high-priority review flag — not a routine update.
- **The anti-pattern**: A test previously asserted `X`. Code changed, so the test was updated to assert `not X` (or a different value of `X`) without documenting *why the requirement changed*.
- **Why it matters**: Tests that chase implementation provide zero regression protection. They give false confidence — all tests green, but the regression was in the test suite, not just the code. This is how silent regressions reach production.
- **Action**: For every test assertion change in the diff:
1. Ask: "Did the *requirement* change, or just the implementation?"
2. If the requirement changed: the PR must include a comment or `because:` clause stating the new requirement.
3. If only the implementation changed: the test assertion should not need to change. Flag as **HIGH** if a test is weakened (e.g., `Contain` → `NotContain`, `Equal("x")` → `NotBeNull()`).
4. If the assertion is on a security-sensitive, protocol-level, or external-API contract (OAuth URLs, HTTP headers, encoding format): flag as **CRITICAL** — require explicit documented justification.
- **Example of the failure mode** (from project history): Consent URL tests asserted `redirect_uri=` was present. When URL encoding was changed, tests were updated to match. No one asked whether `redirect_uri` was still required by the AAD protocol. The regression (`AADSTS500113`) reached the user before any test caught it.

### Rule 3: Verify Copyright Headers
- **Description**: Ensure all C# files have proper Microsoft copyright headers
- **Action**: If a `.cs` file is missing a copyright header:
- Add the Microsoft copyright header at the top of the file
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Internal working documents
docs/plans/

## A streamlined .gitignore for modern .NET projects
## including temporary files, build results, and
## files generated by popular .NET tools. If you are
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- Server-driven notice system: security advisories and critical upgrade prompts are displayed at startup when a maintainer updates `notices.json`. Notices are suppressed once the user upgrades past the specified `minimumVersion`. Results are cached locally for 4 hours to avoid network calls on every invocation.
- `a365 cleanup azure --dry-run` — preview resources that would be deleted without making any changes or requiring Azure authentication
- `AppServiceAuthRequirementCheck` — validates App Service deployment token before `a365 deploy` begins, catching revoked grants (AADSTS50173) early
- `a365 setup admin` — new command for Global Administrators to complete tenant-wide AllPrincipals OAuth2 permission grants after `a365 setup all` has been run by an Agent ID Admin
### Changed
- `a365 publish` updates manifest IDs, creates `manifest.zip`, and prints concise upload instructions for Microsoft 365 Admin Center (Agents > All agents > Upload custom agent). Interactive prompts only occur in interactive terminals; redirect stdin to suppress them in scripts.

### Fixed
- `a365 cleanup` blueprint deletion now succeeds for Global Administrators even when the blueprint was created by a different user
- `a365 setup all` no longer times out for non-admin users — the CLI immediately surfaces a consent URL to share with an administrator instead of waiting for a browser prompt
- `a365 setup all` requests admin consent once for all resources instead of prompting once per resource
- 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)
Expand Down
7 changes: 6 additions & 1 deletion scripts/cli/install-cli.sh
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,18 @@ if dotnet tool uninstall -g Microsoft.Agents.A365.DevTools.Cli 2>/dev/null; then
sleep 1
else
echo "Could not uninstall existing CLI (may not be installed or locked)."
# Try to clear the tool directory manually if locked
# Try to clear the tool directory and shim manually (handles ghost/orphaned installs)
TOOL_PATH="$HOME/.dotnet/tools/.store/microsoft.agents.a365.devtools.cli"
if [ -d "$TOOL_PATH" ]; then
echo "Attempting to clear locked tool directory..."
rm -rf "$TOOL_PATH" 2>/dev/null || true
sleep 1
fi
# Remove orphaned shim that blocks reinstall even when tool is not registered
SHIM="$HOME/.dotnet/tools/a365"
for ext in "" ".exe"; do
[ -f "${SHIM}${ext}" ] && rm -f "${SHIM}${ext}" 2>/dev/null && echo "Removed orphaned shim: ${SHIM}${ext}" || true
done
fi

# Install with specific version from local source
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,10 +223,15 @@ private static Command CreateDisplaySubcommand(ILogger logger, string configDir)
new[] { "--all", "-a" },
description: "Display both static and generated configuration");

var fieldOption = new Option<string?>(
new[] { "--field", "-f" },
description: "Output the value of a single field (for example: --field messagingEndpoint)");

cmd.AddOption(generatedOption);
cmd.AddOption(allOption);
cmd.AddOption(fieldOption);

cmd.SetHandler(async (bool showGenerated, bool showAll) =>
cmd.SetHandler(async (bool showGenerated, bool showAll, string? field) =>
{
try
{
Expand All @@ -246,6 +251,22 @@ private static Command CreateDisplaySubcommand(ILogger logger, string configDir)
bool displayStatic = !showGenerated || showAll;
bool displayGenerated = showGenerated || showAll;

// --field: output a single value from the selected config and exit
if (!string.IsNullOrWhiteSpace(field))
{
var value = TryGetConfigField(config, field, displayGenerated, displayStatic, logger, displayOptions);
if (value != null)
{
Console.WriteLine(value);
}
else
{
Console.Error.WriteLine($"Field '{field}' not found in configuration.");
ExceptionHandler.ExitWithCleanup(1);
}
return;
}

if (displayStatic)
{
if (showAll)
Expand Down Expand Up @@ -323,8 +344,58 @@ private static Command CreateDisplaySubcommand(ILogger logger, string configDir)
{
logger.LogError(ex, "Failed to display configuration: {Message}", ex.Message);
}
}, generatedOption, allOption);
}, generatedOption, allOption, fieldOption);

return cmd;
}

/// <summary>
/// Looks up a single field by JSON key from config, searching generated config first
/// (when checkGenerated is true) then static config (when checkStatic is true).
/// Returns the string value, or raw JSON text for non-string values, or null if not found.
/// </summary>
internal static string? TryGetConfigField(
Models.Agent365Config config,
string field,
bool checkGenerated,
bool checkStatic,
Microsoft.Extensions.Logging.ILogger logger,
JsonSerializerOptions? serializerOptions = null)
{
var options = serializerOptions ?? new JsonSerializerOptions
{
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};

if (checkGenerated)
{
var generatedConfig = config.GetGeneratedConfigForDisplay(logger);
var generatedJson = JsonSerializer.Serialize(generatedConfig, options);
using var generatedDoc = JsonDocument.Parse(generatedJson);
if (generatedDoc.RootElement.TryGetProperty(field, out var generatedProp) &&
generatedProp.ValueKind != JsonValueKind.Null)
{
return generatedProp.ValueKind == JsonValueKind.String
? generatedProp.GetString()
: generatedProp.GetRawText();
}
}

if (checkStatic)
{
var staticConfig = config.GetStaticConfig();
var staticJson = JsonSerializer.Serialize(staticConfig, options);
using var staticDoc = JsonDocument.Parse(staticJson);
if (staticDoc.RootElement.TryGetProperty(field, out var staticProp) &&
staticProp.ValueKind != JsonValueKind.Null)
{
return staticProp.ValueKind == JsonValueKind.String
? staticProp.GetString()
: staticProp.GetRawText();
}
}

return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,10 @@ private static async Task<bool> CallDiscoverToolServersAsync(IConfigService conf

logger.LogInformation("Environment: {Environment}, Audience: {Audience}", config.Environment, audience);

authToken = await authService.GetAccessTokenAsync(audience);
// Resolve az CLI login hint so WAM targets the correct account instead of
// defaulting to the first cached MSAL account (which may be stale).
var loginHint = await Services.Helpers.AzCliHelper.ResolveLoginHintAsync();
authToken = await authService.GetAccessTokenAsync(audience, userId: loginHint);

if (string.IsNullOrWhiteSpace(authToken))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,13 +311,15 @@ private static async Task AcquireAndDisplayTokenAsync(
logger.LogInformation("");

// Use GetAccessTokenWithScopesAsync for explicit scope control
var loginHint = await AzCliHelper.ResolveLoginHintAsync();
var token = await authService.GetAccessTokenWithScopesAsync(
resourceAppId,
requestedScopes,
tenantId,
forceRefresh,
clientAppId,
useInteractiveBrowser: true);
useInteractiveBrowser: true,
userId: loginHint);

if (string.IsNullOrWhiteSpace(token))
{
Expand Down
13 changes: 10 additions & 3 deletions src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ public static Command CreateCommand(
AgentBlueprintService blueprintService,
BlueprintLookupService blueprintLookupService,
FederatedCredentialService federatedCredentialService,
IClientAppValidator clientAppValidator)
IClientAppValidator clientAppValidator,
IConfirmationProvider confirmationProvider,
ArmApiService? armApiService = null)
{
var command = new Command("setup",
"Set up your Agent 365 environment with granular control over each step\n\n" +
Expand All @@ -51,7 +53,9 @@ public static Command CreateCommand(
" 4. a365 setup permissions bot\n" +
"Or run all steps at once:\n" +
" a365 setup all # Full setup (includes infrastructure)\n" +
" a365 setup all --skip-infrastructure # Skip infrastructure if it already exists");
" a365 setup all --skip-infrastructure # Skip infrastructure if it already exists\n\n" +
"For non-admin users — complete GA-only grants after setup all:\n" +
" a365 setup admin --config-dir \"<path>\" # Run as Global Administrator");

// Add subcommands
command.AddCommand(RequirementsSubcommand.CreateCommand(
Expand All @@ -67,7 +71,10 @@ public static Command CreateCommand(
logger, authValidator, configService, executor, graphApiService, blueprintService));

command.AddCommand(AllSubcommand.CreateCommand(
logger, configService, executor, botConfigurator, authValidator, platformDetector, graphApiService, blueprintService, clientAppValidator, blueprintLookupService, federatedCredentialService));
logger, configService, executor, botConfigurator, authValidator, platformDetector, graphApiService, blueprintService, clientAppValidator, blueprintLookupService, federatedCredentialService, armApiService));

command.AddCommand(AdminSubcommand.CreateCommand(
logger, configService, authValidator, graphApiService, confirmationProvider));

return command;
}
Expand Down
Loading
Loading