Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Agents provisioned before this release need `Agent365.Observability.OtelWrite` g
**Option B — CLI** (`a365 setup admin`) has been removed in this release. Use Option A above, or copy the PowerShell instructions printed in the `a365 setup all` summary output.

### Added
- The CLI now asks the Agent 365 service to register its own service principal the first time it runs against a tenant, removing a manual admin step that previously caused sign-in to fail in newly onboarded tenants. Set `A365_DISABLE_SP_PROVISIONING=true` to opt out.
- Log separator written at the start of each CLI invocation now redacts values for secret-bearing options (e.g. `--idp-client-secret`) so they are not written to the log file in plain text.
- Authentication context (tenant and user) is now logged at the `Information` level whenever the resolved sign-in identity changes, giving operators a clear audit trail in the log file of who the CLI is acting as, without exposing credentials.
- `a365 develop-mcp evaluate` command for evaluating MCP server tool schema quality — runs deterministic and semantic checks (via GitHub Copilot or Claude Code CLIs), computes maturity scoring, and generates an interactive HTML report
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,19 @@ public static class ConfigConstants
/// </summary>
public const string ObservabilityApiOtelWriteScope = "Agent365.Observability.OtelWrite";

/// <summary>
/// Global Power Platform API host. Tenant-scoped Agent 365 provisioning routes are reachable
/// here and are proxied to the tenant's home cluster.
/// </summary>
public const string ProductionProvisioningBaseUrl = "https://api.powerplatform.com";

/// <summary>
/// Route that provisions the Agent 365 CLI service principal in the caller's tenant.
/// Format argument 0 is the tenant ID.
/// </summary>
public const string Agent365CliProvisionPathFormat =
"/maven/tenants/{0}/agent365/servicePrincipals/agent365Cli/provision?api-version=1";

/// <summary>
/// Delegated scope value exposed on the blueprint app registration to enable
/// OBO (On-Behalf-Of) callers to acquire tokens scoped to the agent.
Expand Down Expand Up @@ -176,4 +189,19 @@ public static string GetAgent365ToolsResourceAppId(string environment)

return McpConstants.WorkIQToolsProdAppId;
}

/// <summary>
/// Environment-aware Agent 365 provisioning service base URL.
/// </summary>
public static string GetProvisioningBaseUrl(string? environment)
{
var customEndpoint = Environment.GetEnvironmentVariable(
$"A365_PROVISIONING_ENDPOINT_{environment?.ToUpperInvariant()}")
?? Environment.GetEnvironmentVariable("A365_PROVISIONING_ENDPOINT");

if (!string.IsNullOrWhiteSpace(customEndpoint))
return customEndpoint.TrimEnd('/');

return ProductionProvisioningBaseUrl;
}
}
9 changes: 9 additions & 0 deletions src/Microsoft.Agents.A365.DevTools.Cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,15 @@ private static void ConfigureServices(IServiceCollection services, LogLevel mini
services.AddSingleton<DelegatedConsentService>(); // For AgentApplication.Create permission
services.AddSingleton<ManifestTemplateService>(); // For publish command template extraction

// Provisions the CLI service principal via the Agent 365 service; the CLI is a public
// client and cannot
// provision it itself.
services.AddSingleton<IServicePrincipalProvisioningService>(sp =>
new ServicePrincipalProvisioningService(
sp.GetRequiredService<ILogger<ServicePrincipalProvisioningService>>(),
sp.GetRequiredService<IAuthenticationService>(),
sp.GetRequiredService<IConfigService>()));

// Register ProcessService for cross-platform process launching
services.AddSingleton<IProcessService, ProcessService>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,17 +71,20 @@ internal sealed class BootstrapConfigResolver : IBootstrapConfigResolver
private readonly IConfigService _configService;
private readonly CommandExecutor _executor;
private readonly GraphApiService? _graphApiService;
private readonly IServicePrincipalProvisioningService? _spProvisioningService;
private readonly ILogger<BootstrapConfigResolver> _logger;

public BootstrapConfigResolver(
IConfigService configService,
CommandExecutor executor,
GraphApiService? graphApiService,
ILoggerFactory loggerFactory)
ILoggerFactory loggerFactory,
IServicePrincipalProvisioningService? spProvisioningService = null)
{
_configService = configService;
_executor = executor;
_graphApiService = graphApiService;
_spProvisioningService = spProvisioningService;
_logger = loggerFactory.CreateLogger<BootstrapConfigResolver>();
}

Expand All @@ -92,6 +95,49 @@ public BootstrapConfigResolver(
FileInfo configFile,
bool isCleanupMode = false,
CancellationToken ct = default)
{
var config = await ResolveCoreAsync(agentName, tenantIdFlag, configFile, isCleanupMode, ct);

if (config != null)
{
await EnsureCliServicePrincipalAsync(config.TenantId, ct);
}

return config;
}

/// <summary>
/// The CLI is a public client and cannot provision its own service principal, so the Agent 365
/// service is
/// asked to do it once per tenant per process.
/// </summary>
private async Task EnsureCliServicePrincipalAsync(string? tenantId, CancellationToken ct)
{
if (_spProvisioningService == null)
{
return;
}

try
{
await _spProvisioningService.EnsureProvisionedAsync(tenantId, ct: ct);
}
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
throw;
}
catch (Exception ex)
{
_logger.LogDebug(ex, "Service principal provisioning check did not complete.");
}
}

private async Task<Agent365Config?> ResolveCoreAsync(
string? agentName,
string? tenantIdFlag,
FileInfo configFile,
bool isCleanupMode,
CancellationToken ct)
{
if (!string.IsNullOrWhiteSpace(agentName))
{
Expand Down
Loading
Loading