Skip to content

Commit 1cfadfb

Browse files
sellakumaranclaude
andauthored
fix: fall back to device code when browser auth fails on macOS (#290)
* fix: fall back to device code when browser auth fails on macOS MsalBrowserCredential.GetTokenAsync now catches PlatformNotSupportedException (thrown by MSAL on macOS 15.x) and wraps it as MsalAuthenticationFailedException so it is properly classified rather than appearing as an unexpected error. AuthenticateInteractivelyAsync detects this platform exception at the token acquisition site and automatically falls back to device code flow, allowing a365 setup to complete on macOS without manual workarounds. Extracts CreateDeviceCodeCredential helper to eliminate the duplicated DeviceCodeCredential setup between the normal device code path and the fallback. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Improve device code auth logging and error handling Moved device code authentication info logs to calling sites so they're only shown when relevant. Changed platform-not-supported log level in MsalBrowserCredential from Error to Warning to better reflect recoverable conditions. * Refactor CLI install: delegate to Bash script Replaced PowerShell install logic with a Bash script (install-cli.sh) that handles cleaning, building, packing, and installing the Agent 365 CLI from a local NuGet package. PowerShell now locates bash and invokes the script, unifying installation across platforms and simplifying maintenance. --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent b82a51f commit 1cfadfb

4 files changed

Lines changed: 203 additions & 140 deletions

File tree

scripts/cli/install-cli.ps1

Lines changed: 34 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -1,124 +1,42 @@
11
# install-cli.ps1
2-
# This script installs the Agent 365 CLI from a local NuGet package in the publish folder.
3-
# Usage: Run this script from the root of the extracted package (where publish/ exists)
2+
# This script installs the Agent 365 CLI from a local NuGet package.
3+
# Delegates to install-cli.sh (requires bash - Git Bash on Windows, system bash on macOS/Linux)
44

5-
# Get the repository root directory (two levels up from scripts/cli/)
6-
$repoRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
7-
$projectPath = Join-Path $repoRoot 'src\Microsoft.Agents.A365.DevTools.Cli\Microsoft.Agents.A365.DevTools.Cli.csproj'
8-
9-
# Verify the project file exists
10-
if (-not (Test-Path $projectPath)) {
11-
Write-Error "ERROR: Project file not found at $projectPath"
12-
exit 1
13-
}
14-
15-
$outputDir = Join-Path $PSScriptRoot 'nupkg'
16-
if (-not (Test-Path $outputDir)) {
17-
New-Item -ItemType Directory -Path $outputDir | Out-Null
18-
}
19-
20-
# Clean old packages to ensure fresh build
21-
Write-Host "Cleaning old packages from $outputDir..."
22-
Get-ChildItem -Path $outputDir -Filter '*.nupkg' | Remove-Item -Force
23-
24-
# Clear NuGet package cache to avoid version conflicts
25-
Write-Host "Clearing NuGet package cache..."
26-
Remove-Item ~/.nuget/packages/microsoft.agents.a365.devtools.cli -Recurse -Force -ErrorAction SilentlyContinue
27-
# Also clear the dotnet tools cache
28-
Remove-Item ~/.dotnet/toolResolverCache -Recurse -Force -ErrorAction SilentlyContinue
29-
Write-Host "Package cache cleared"
30-
31-
# Force clean by removing bin/obj folders
32-
Write-Host "Force cleaning bin and obj folders..."
33-
$projectDir = Split-Path $projectPath -Parent
34-
$binPath = Join-Path $projectDir "bin"
35-
$objPath = Join-Path $projectDir "obj"
36-
Write-Host " Removing: $binPath"
37-
Remove-Item $binPath -Recurse -Force -ErrorAction SilentlyContinue
38-
Write-Host " Removing: $objPath"
39-
Remove-Item $objPath -Recurse -Force -ErrorAction SilentlyContinue
40-
Write-Host "Folders cleaned"
41-
42-
# Clean the project to ensure fresh build
43-
Write-Host "Cleaning project..."
44-
dotnet clean $projectPath -c Release
45-
46-
# Build the project first to ensure NuGet restore and build outputs exist
47-
Write-Host "Building CLI tool (Release configuration)..."
48-
dotnet build $projectPath -c Release
49-
if ($LASTEXITCODE -ne 0) {
50-
Write-Error "ERROR: dotnet build failed. Check output above for details."
51-
exit 1
52-
}
53-
Write-Host "Packing CLI tool to $outputDir (Release configuration)..."
54-
# Remove --no-build to ensure pack rebuilds if needed
55-
dotnet pack $projectPath -c Release -o $outputDir -p:IncludeSymbols=false -p:TreatWarningsAsErrors=false
56-
if ($LASTEXITCODE -ne 0) {
57-
Write-Error "ERROR: dotnet pack failed. Check output above for details."
58-
exit 1
59-
}
60-
61-
# Find the generated .nupkg
62-
$nupkg = Get-ChildItem -Path $outputDir -Filter 'Microsoft.Agents.A365.DevTools.Cli*.nupkg' | Select-Object -First 1
63-
if (-not $nupkg) {
64-
Write-Error "ERROR: NuGet package not found in $outputDir."
5+
$shScript = Join-Path $PSScriptRoot "install-cli.sh"
6+
if (-not (Test-Path $shScript)) {
7+
Write-Error "ERROR: install-cli.sh not found at $shScript"
658
exit 1
669
}
6710

68-
Write-Host "Installing Agent 365 CLI from local package: $($nupkg.Name)"
69-
70-
# Kill any running a365 processes to release file locks
71-
Write-Host "Checking for running a365 processes..."
72-
$processes = Get-Process -Name "a365" -ErrorAction SilentlyContinue
73-
if ($processes) {
74-
Write-Host "Stopping running a365 processes..." -ForegroundColor Yellow
75-
$processes | Stop-Process -Force -ErrorAction SilentlyContinue
76-
Start-Sleep -Seconds 1
77-
}
78-
79-
# Uninstall any existing global CLI tool (force to handle version conflicts)
80-
Write-Host "Uninstalling existing CLI tool..."
81-
dotnet tool uninstall -g Microsoft.Agents.A365.DevTools.Cli 2>&1
82-
if ($LASTEXITCODE -eq 0) {
83-
Write-Host "Existing CLI uninstalled successfully." -ForegroundColor Green
84-
# Give the system a moment to release file locks
85-
Start-Sleep -Seconds 1
86-
} else {
87-
Write-Host "Could not uninstall existing CLI (may not be installed or locked)." -ForegroundColor Yellow
88-
# Try to clear the tool directory manually if locked
89-
$toolPath = Join-Path $env:USERPROFILE ".dotnet\tools\.store\microsoft.agents.a365.devtools.cli"
90-
if (Test-Path $toolPath) {
91-
Write-Host "Attempting to clear locked tool directory..." -ForegroundColor Yellow
92-
Remove-Item $toolPath -Recurse -Force -ErrorAction SilentlyContinue
93-
Start-Sleep -Seconds 1
11+
# Find bash: Git Bash on Windows (ships with Git for Windows), system bash elsewhere
12+
$bash = $null
13+
if ($IsWindows) {
14+
# Locate bash.exe from git's install directory (Git for Windows always ships bash alongside git)
15+
$gitExe = Get-Command git -ErrorAction SilentlyContinue
16+
if ($gitExe) {
17+
$gitDir = Split-Path $gitExe.Source -Parent
18+
$candidate = Join-Path $gitDir "bash.exe"
19+
if (Test-Path $candidate) {
20+
$bash = $candidate
21+
}
9422
}
23+
if (-not $bash) {
24+
# Fallback: check well-known Git for Windows install paths
25+
foreach ($path in @(
26+
"$env:ProgramFiles\Git\bin\bash.exe",
27+
"${env:ProgramFiles(x86)}\Git\bin\bash.exe",
28+
"$env:LocalAppData\Programs\Git\bin\bash.exe"
29+
)) {
30+
if (Test-Path $path) { $bash = $path; break }
31+
}
32+
}
33+
if (-not $bash) {
34+
Write-Error "ERROR: bash.exe not found. Install Git for Windows from https://git-scm.com/download/win"
35+
exit 1
36+
}
37+
} else {
38+
$bash = "bash"
9539
}
9640

97-
# Install with specific version from local source
98-
Write-Host "Installing CLI tool..."
99-
$version = $nupkg.Name -replace 'Microsoft\.Agents\.A365\.DevTools\.Cli\.(.*)\.nupkg','$1'
100-
Write-Host "Version: $version" -ForegroundColor Cyan
101-
102-
# Try update first (which forces reinstall), fall back to install if not already installed
103-
Write-Host "Attempting to update tool..."
104-
dotnet tool update -g Microsoft.Agents.A365.DevTools.Cli --add-source $outputDir --version $version 2>&1 | Out-Null
105-
if ($LASTEXITCODE -ne 0) {
106-
Write-Host "Update failed, attempting fresh install..."
107-
dotnet tool install -g Microsoft.Agents.A365.DevTools.Cli --add-source $outputDir --version $version
108-
}
109-
if ($LASTEXITCODE -ne 0) {
110-
Write-Error "ERROR: CLI installation failed. Check output above for details."
111-
exit 1
112-
}
113-
114-
Write-Host "Agent 365 CLI installed successfully." -ForegroundColor Green
115-
Write-Host ""
116-
Write-Host "Verifying installation..."
117-
$installedVersion = dotnet tool list -g | Select-String "microsoft.agents.a365.devtools.cli"
118-
if ($installedVersion) {
119-
Write-Host "Installed: $installedVersion" -ForegroundColor Cyan
120-
Write-Host ""
121-
Write-Host "IMPORTANT: If you have the CLI running in another terminal, close it and reopen to pick up the new version." -ForegroundColor Yellow
122-
} else {
123-
Write-Warning "Could not verify installation. Try running 'a365 --help' to test."
124-
}
41+
& $bash $shScript
42+
exit $LASTEXITCODE

scripts/cli/install-cli.sh

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
#!/usr/bin/env bash
2+
# install-cli.sh
3+
# This script installs the Agent 365 CLI from a local NuGet package.
4+
# Usage: Run this script from the repo root, or directly from scripts/cli/
5+
6+
# Get the repository root directory (two levels up from scripts/cli/)
7+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8+
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
9+
PROJECT_PATH="$REPO_ROOT/src/Microsoft.Agents.A365.DevTools.Cli/Microsoft.Agents.A365.DevTools.Cli.csproj"
10+
11+
# Verify the project file exists
12+
if [ ! -f "$PROJECT_PATH" ]; then
13+
echo "ERROR: Project file not found at $PROJECT_PATH" >&2
14+
exit 1
15+
fi
16+
17+
OUTPUT_DIR="$SCRIPT_DIR/nupkg"
18+
mkdir -p "$OUTPUT_DIR"
19+
20+
# Clean old packages to ensure fresh build
21+
echo "Cleaning old packages from $OUTPUT_DIR..."
22+
rm -f "$OUTPUT_DIR"/*.nupkg
23+
24+
# Clear NuGet package cache to avoid version conflicts
25+
echo "Clearing NuGet package cache..."
26+
rm -rf ~/.nuget/packages/microsoft.agents.a365.devtools.cli
27+
# Also clear the dotnet tools cache
28+
rm -rf ~/.dotnet/toolResolverCache
29+
echo "Package cache cleared"
30+
31+
# Force clean by removing bin/obj folders
32+
echo "Force cleaning bin and obj folders..."
33+
PROJECT_DIR="$(dirname "$PROJECT_PATH")"
34+
echo " Removing: $PROJECT_DIR/bin"
35+
rm -rf "$PROJECT_DIR/bin"
36+
echo " Removing: $PROJECT_DIR/obj"
37+
rm -rf "$PROJECT_DIR/obj"
38+
echo "Folders cleaned"
39+
40+
# Clean the project to ensure fresh build
41+
echo "Cleaning project..."
42+
dotnet clean "$PROJECT_PATH" -c Release
43+
44+
# Build the project first to ensure NuGet restore and build outputs exist
45+
echo "Building CLI tool (Release configuration)..."
46+
dotnet build "$PROJECT_PATH" -c Release
47+
if [ $? -ne 0 ]; then
48+
echo "ERROR: dotnet build failed. Check output above for details." >&2
49+
exit 1
50+
fi
51+
52+
echo "Packing CLI tool to $OUTPUT_DIR (Release configuration)..."
53+
dotnet pack "$PROJECT_PATH" -c Release -o "$OUTPUT_DIR" -p:IncludeSymbols=false -p:TreatWarningsAsErrors=false
54+
if [ $? -ne 0 ]; then
55+
echo "ERROR: dotnet pack failed. Check output above for details." >&2
56+
exit 1
57+
fi
58+
59+
# Find the generated .nupkg
60+
NUPKG=$(find "$OUTPUT_DIR" -name 'Microsoft.Agents.A365.DevTools.Cli*.nupkg' | head -1)
61+
if [ -z "$NUPKG" ]; then
62+
echo "ERROR: NuGet package not found in $OUTPUT_DIR." >&2
63+
exit 1
64+
fi
65+
66+
echo "Installing Agent 365 CLI from local package: $(basename "$NUPKG")"
67+
68+
# Kill any running a365 processes to release file locks
69+
echo "Checking for running a365 processes..."
70+
if pgrep -x "a365" > /dev/null 2>&1; then
71+
echo "Stopping running a365 processes..."
72+
pkill -x "a365" 2>/dev/null || true
73+
sleep 1
74+
fi
75+
76+
# Uninstall any existing global CLI tool (force to handle version conflicts)
77+
echo "Uninstalling existing CLI tool..."
78+
if dotnet tool uninstall -g Microsoft.Agents.A365.DevTools.Cli 2>/dev/null; then
79+
echo "Existing CLI uninstalled successfully."
80+
# Give the system a moment to release file locks
81+
sleep 1
82+
else
83+
echo "Could not uninstall existing CLI (may not be installed or locked)."
84+
# Try to clear the tool directory manually if locked
85+
TOOL_PATH="$HOME/.dotnet/tools/.store/microsoft.agents.a365.devtools.cli"
86+
if [ -d "$TOOL_PATH" ]; then
87+
echo "Attempting to clear locked tool directory..."
88+
rm -rf "$TOOL_PATH" 2>/dev/null || true
89+
sleep 1
90+
fi
91+
fi
92+
93+
# Install with specific version from local source
94+
echo "Installing CLI tool..."
95+
VERSION=$(basename "$NUPKG" | sed 's/Microsoft\.Agents\.A365\.DevTools\.Cli\.\(.*\)\.nupkg/\1/')
96+
echo "Version: $VERSION"
97+
98+
# Try update first (which forces reinstall), fall back to install if not already installed
99+
echo "Attempting to update tool..."
100+
if ! dotnet tool update -g Microsoft.Agents.A365.DevTools.Cli --add-source "$OUTPUT_DIR" --version "$VERSION" > /dev/null 2>&1; then
101+
echo "Update failed, attempting fresh install..."
102+
dotnet tool install -g Microsoft.Agents.A365.DevTools.Cli --add-source "$OUTPUT_DIR" --version "$VERSION"
103+
fi
104+
if [ $? -ne 0 ]; then
105+
echo "ERROR: CLI installation failed. Check output above for details." >&2
106+
exit 1
107+
fi
108+
109+
echo "Agent 365 CLI installed successfully."
110+
echo ""
111+
echo "Verifying installation..."
112+
INSTALLED=$(dotnet tool list -g | grep -i "microsoft.agents.a365.devtools.cli" || true)
113+
if [ -n "$INSTALLED" ]; then
114+
echo "Installed: $INSTALLED"
115+
echo ""
116+
echo "IMPORTANT: If you have the CLI running in another terminal, close it and reopen to pick up the new version."
117+
else
118+
echo "WARNING: Could not verify installation. Try running 'a365 --help' to test."
119+
fi

src/Microsoft.Agents.A365.DevTools.Cli/Services/AuthenticationService.cs

Lines changed: 45 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -231,33 +231,23 @@ private async Task<TokenInfo> AuthenticateInteractivelyAsync(
231231
// Device code flow - works in all environments including SSH/remote sessions
232232
_logger.LogInformation("Using device code authentication...");
233233
_logger.LogInformation("Please sign in with your Microsoft account");
234-
235-
credential = new DeviceCodeCredential(new DeviceCodeCredentialOptions
236-
{
237-
TenantId = effectiveTenantId,
238-
ClientId = effectiveClientId,
239-
AuthorityHost = AzureAuthorityHosts.AzurePublicCloud,
240-
TokenCachePersistenceOptions = new TokenCachePersistenceOptions
241-
{
242-
Name = AuthenticationConstants.ApplicationName
243-
},
244-
DeviceCodeCallback = (code, cancellation) =>
245-
{
246-
Console.WriteLine();
247-
Console.WriteLine("==========================================================================");
248-
Console.WriteLine($"To sign in, use a web browser to open the page:");
249-
Console.WriteLine($" {code.VerificationUri}");
250-
Console.WriteLine();
251-
Console.WriteLine($"And enter the code: {code.UserCode}");
252-
Console.WriteLine("==========================================================================");
253-
Console.WriteLine();
254-
return Task.CompletedTask;
255-
}
256-
});
234+
credential = CreateDeviceCodeCredential(effectiveTenantId, effectiveClientId);
257235
}
258236

259237
var tokenRequestContext = new TokenRequestContext(scopes);
260-
var tokenResult = await credential.GetTokenAsync(tokenRequestContext, default);
238+
AccessToken tokenResult;
239+
try
240+
{
241+
tokenResult = await credential.GetTokenAsync(tokenRequestContext, default);
242+
}
243+
catch (MsalAuthenticationFailedException ex) when (useInteractiveBrowser && ex.InnerException is PlatformNotSupportedException)
244+
{
245+
_logger.LogWarning("Browser authentication is not supported on this platform, falling back to device code flow...");
246+
_logger.LogInformation("Using device code authentication...");
247+
_logger.LogInformation("Please sign in with your Microsoft account");
248+
var deviceCodeCredential = CreateDeviceCodeCredential(effectiveTenantId, effectiveClientId);
249+
tokenResult = await deviceCodeCredential.GetTokenAsync(tokenRequestContext, default);
250+
}
261251

262252
_logger.LogInformation("Authentication successful!");
263253

@@ -523,6 +513,37 @@ public bool ValidateScopesForResource(string resourceUrl, string? manifestPath =
523513
}
524514
}
525515

516+
/// <summary>
517+
/// Creates a DeviceCodeCredential configured for interactive device code authentication.
518+
/// This flow works in all environments including SSH, remote sessions, and platforms where
519+
/// browser-based authentication is unavailable.
520+
/// </summary>
521+
private DeviceCodeCredential CreateDeviceCodeCredential(string tenantId, string clientId)
522+
{
523+
return new DeviceCodeCredential(new DeviceCodeCredentialOptions
524+
{
525+
TenantId = tenantId,
526+
ClientId = clientId,
527+
AuthorityHost = AzureAuthorityHosts.AzurePublicCloud,
528+
TokenCachePersistenceOptions = new TokenCachePersistenceOptions
529+
{
530+
Name = AuthenticationConstants.ApplicationName
531+
},
532+
DeviceCodeCallback = (code, cancellation) =>
533+
{
534+
Console.WriteLine();
535+
Console.WriteLine("==========================================================================");
536+
Console.WriteLine($"To sign in, use a web browser to open the page:");
537+
Console.WriteLine($" {code.VerificationUri}");
538+
Console.WriteLine();
539+
Console.WriteLine($"And enter the code: {code.UserCode}");
540+
Console.WriteLine("==========================================================================");
541+
Console.WriteLine();
542+
return Task.CompletedTask;
543+
}
544+
});
545+
}
546+
526547
/// <summary>
527548
/// Clears cached authentication token(s)
528549
/// </summary>

src/Microsoft.Agents.A365.DevTools.Cli/Services/MsalBrowserCredential.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,11 @@ public override async ValueTask<AccessToken> GetTokenAsync(
313313
_logger?.LogDebug("Successfully acquired token via interactive authentication.");
314314
return new AccessToken(interactiveResult.AccessToken, interactiveResult.ExpiresOn);
315315
}
316+
catch (PlatformNotSupportedException ex)
317+
{
318+
_logger?.LogWarning("Browser authentication is not supported on this platform: {Message}", ex.Message);
319+
throw new MsalAuthenticationFailedException($"Browser authentication is not supported on this platform ({ex.Message})", ex);
320+
}
316321
catch (MsalException ex)
317322
{
318323
_logger?.LogError(ex, "MSAL authentication failed: {Message}", ex.Message);

0 commit comments

Comments
 (0)