Skip to content
Closed
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
- `a365 network gsa enable|disable|status` — turns Global Secure Access on or off for the tenant's Agent 365 environment. Global Secure Access is a per-environment Power Platform setting, and the Agent 365 managed environment's id is not published, so the platform resolves that environment and applies the change on your behalf. `NotConfigured` is reported distinctly from `Disabled`, because a tenant that has never set the value has not turned it off. Requires Global Administrator or Power Platform Administrator. See [docs/commands/network-gsa.md](docs/commands/network-gsa.md).
- `a365 network vnet link|unlink|status` — links an Azure virtual network to Agent 365 through a Power Platform NetworkInjection enterprise policy. Replaces `Enable-SubnetInjection` from the `Microsoft.PowerPlatform.EnterprisePolicies` module, which cannot be used because it requires the id of the Agent 365 managed environment and that id is not published. The CLI reads the policy's `systemId` from Azure with your existing `az login` and the platform performs the link against the environment it resolves for your tenant. Requires Global Administrator or Power Platform Administrator. See [docs/commands/network.md](docs/commands/network.md).
- Setup and bootstrap now use Microsoft's first-party Agent 365 CLI application when it is present in your tenant, validating it without changing Microsoft's app registration, and fall back to a tenant-owned "Agent 365 CLI" app when it is not (#489).
- 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.
Expand Down
3 changes: 3 additions & 0 deletions docs/commands/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ There is reference documentation for each command.
| [network vnet link](network.md#link) | Link a NetworkInjection enterprise policy to your Agent 365 environment. |
| [network vnet unlink](network.md#unlink) | Remove the virtual network link from your Agent 365 environment. |
| [network vnet status](network.md#status) | Show whether a virtual network policy is linked to your Agent 365 environment. |
| [network gsa enable](network-gsa.md#enable-and-disable) | Turn Global Secure Access on for your Agent 365 environment. |
| [network gsa disable](network-gsa.md#enable-and-disable) | Turn Global Secure Access off for your Agent 365 environment. |
| [network gsa status](network-gsa.md#status) | Show whether Global Secure Access is on for your Agent 365 environment. |
| [publish](https://learn.microsoft.com/microsoft-agent-365/developer/reference/cli/publish) | Update manifest.json ID values and publish the package. Configure federated identity and app role assignments. |
| [query-entra](https://learn.microsoft.com/microsoft-agent-365/developer/reference/cli/query-entra) | Query Microsoft Entra ID for agent information including scopes, permissions, and consent status. |
| [query-entra blueprint-scopes](https://learn.microsoft.com/microsoft-agent-365/developer/reference/cli/query-entra#query-entra-blueprint-scopes) | List configured scopes and consent status for the agent blueprint. |
Expand Down
88 changes: 88 additions & 0 deletions docs/commands/network-gsa.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# `a365 network gsa`

Turns **Global Secure Access** on or off for the tenant's Agent 365 Power Platform environment,
without needing the id of that environment.

## Why this command exists

Global Secure Access is a per-environment Power Platform setting. Agent 365 provisions a managed
environment for the tenant and does not publish its id, so the setting cannot be reached through
the Power Platform admin surfaces that take an environment id. These subcommands ask the Agent 365
platform to apply the change against the environment it resolves for your tenant.

## Prerequisites

- **Global Administrator** or **Power Platform Administrator** in the tenant. The platform rejects
anyone else.
- An `az login` to the tenant you intend to configure.
- Public cloud only. Sovereign clouds are not supported.

Nothing is read from Azure — unlike `a365 network vnet` — but the `az login` still matters: it is
what selects the tenant. The commands authenticate against the tenant and account of your current
`az account show`, so `az login --tenant <id>` is how you choose which tenant to configure when you
have more than one. Without an explicit tenant the Windows broker silently returns whichever
account Windows prefers, which would apply a tenant-wide setting to the wrong tenant. If the
account you are signed into cannot be matched, the command fails rather than falling back.

## Subcommands

| Command | Description |
| --- | --- |
| `a365 network gsa enable` | Turn Global Secure Access on. |
| `a365 network gsa disable` | Turn Global Secure Access off. |
| `a365 network gsa status` | Show whether Global Secure Access is on. |

### `enable` and `disable`

```bash
a365 network gsa enable [--wait]
a365 network gsa disable [--wait]
```

| Option | Description |
| --- | --- |
| `--wait` | Keep polling until the change appears on the environment, instead of returning while it is still being applied. |

Requesting the value the environment already holds is a no-op and succeeds.

### `status`

```bash
a365 network gsa status
```

There is no operation handle to pass. Power Platform applies the change asynchronously but issues
no operation id for it, so the CLI reports progress by re-reading the setting rather than by
polling a handle.

## Statuses and exit codes

| Status | Meaning |
| --- | --- |
| `Enabled` | Global Secure Access is on. |
| `Disabled` | Global Secure Access is off. |
| `NotConfigured` | The tenant has never set the value. This is **not** the same as `Disabled`. |

A change that has been accepted but has not yet surfaced is reported as still being applied, with
the status still showing the value it has not yet displaced.

Exit code is `1` on any request error, and `0` otherwise — including a change that is still being
applied, which is a legitimate outcome when `--wait` is not passed.

## Typical flow

```bash
a365 network gsa enable --wait
a365 network gsa status
```

## Troubleshooting

| Symptom | Cause |
| --- | --- |
| `403` from the platform | Caller is not a Global or Power Platform Administrator, or the CLI app lacks consent for the `AgentTools.Gsa.*` scopes. |
| `409`, reporting a governing policy | A Power Platform policy owns this setting. Change it through that policy; the environment-level value is ignored while the policy applies. |
| `404`, reporting no environment | The tenant has no Agent 365 environment yet. |
| Status stays `NotConfigured` after `disable` | Read it again — the change is applied asynchronously and `--wait` is the way to block on it. |
| `Could not determine your Azure tenant` | No usable `az login`. Run `az login --tenant <id>` for the tenant you want to configure. |
| Sign-in prompt names the wrong account | The tenant comes from `az account show`. Run `az account set` / `az login --tenant <id>` to point at the intended tenant, then retry. |
133 changes: 132 additions & 1 deletion src/Microsoft.Agents.A365.DevTools.Cli/Commands/NetworkCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ public static class NetworkCommand
public static Command CreateCommand(
ILogger logger,
IVNetLinkService vnetLinkService,
IAzureCliService azureCliService)
IAzureCliService azureCliService,
IGsaService gsaService)
{
var networkCommand = new Command("network", "Configure tenant networking for Agent 365");

Expand All @@ -40,7 +41,17 @@ public static Command CreateCommand(
vnetCommand.AddCommand(CreateUnlinkSubcommand(logger, vnetLinkService));
vnetCommand.AddCommand(CreateStatusSubcommand(logger, vnetLinkService));

var gsaCommand = new Command(
"gsa",
"Turn Global Secure Access on or off for your Agent 365 environment. " +
"Requires the Global Administrator or Power Platform Administrator role.");

gsaCommand.AddCommand(CreateGsaSetSubcommand(logger, gsaService, enabled: true));
gsaCommand.AddCommand(CreateGsaSetSubcommand(logger, gsaService, enabled: false));
gsaCommand.AddCommand(CreateGsaStatusSubcommand(logger, gsaService));

networkCommand.AddCommand(vnetCommand);
networkCommand.AddCommand(gsaCommand);
return networkCommand;
}

Expand Down Expand Up @@ -219,6 +230,126 @@ internal static async Task<int> ReportAsync(
return 0;
}

/// <summary>
/// Creates the gsa enable or disable subcommand. The two differ only in the value they send
/// and the words they use, so they share one builder.
/// </summary>
private static Command CreateGsaSetSubcommand(ILogger logger, IGsaService gsaService, bool enabled)
{
var verb = enabled ? "enable" : "disable";
var command = new Command(
verb,
$"Turn Global Secure Access {(enabled ? "on" : "off")} for your Agent 365 environment.");

var waitOption = new Option<bool>(
"--wait",
"Keep polling until the change appears on the environment, instead of returning while " +
"it is still being applied.");

var verboseOption = new Option<bool>(["--verbose", "-v"], "Enable verbose logging");

command.AddOption(waitOption);
command.AddOption(verboseOption);

command.SetHandler(async (InvocationContext context) =>
{
var wait = context.ParseResult.GetValueForOption(waitOption);
var ct = context.GetCancellationToken();

var result = await gsaService.SetAsync(enabled, ct);
context.ExitCode = await ReportGsaAsync(logger, gsaService, result, wait, enabled, ct);
});

return command;
}

private static Command CreateGsaStatusSubcommand(ILogger logger, IGsaService gsaService)
{
var command = new Command(
"status",
"Show whether Global Secure Access is on for your Agent 365 environment.");

var verboseOption = new Option<bool>(["--verbose", "-v"], "Enable verbose logging");
command.AddOption(verboseOption);

command.SetHandler(async (InvocationContext context) =>
{
var ct = context.GetCancellationToken();

var status = await gsaService.GetStatusAsync(ct);
if (status == null)
{
context.ExitCode = 1;
return;
}

LogGsaStatus(logger, status);
context.ExitCode = 0;
});

return command;
}

/// <summary>
/// Renders the outcome of a Global Secure Access change, optionally waiting for it to appear
/// first, and maps it to a process exit code.
/// </summary>
internal static async Task<int> ReportGsaAsync(
ILogger logger,
IGsaService gsaService,
GsaStatusResponse? result,
bool wait,
bool enabled,
CancellationToken cancellationToken)
{
if (result == null)
{
return 1;
}

var expectedStatus = enabled ? "Enabled" : "Disabled";

if (wait && result.Pending)
{
logger.LogInformation("The change is still being applied. Waiting for it to appear...");
result = await gsaService.WaitForStatusAsync(expectedStatus, DefaultWaitTimeout, cancellationToken);

if (result == null)
{
return 1;
}
}

LogGsaStatus(logger, result);

// Still pending is not a failure. The platform accepted the change and the environment
// will catch up; reporting non-zero here would break scripts that chain on success.
if (result.Pending)
{
logger.LogInformation(
"Still being applied. Check on it with: a365 network gsa status");
}

return 0;
}

private static void LogGsaStatus(ILogger logger, GsaStatusResponse status)
{
logger.LogInformation("Global Secure Access: {Status}", status.Status ?? "Unknown");

if (string.Equals(status.Status, "NotConfigured", StringComparison.OrdinalIgnoreCase))
{
// Worth spelling out: a tenant that has never set this is not the same as one that
// turned it off, and the distinction changes what an admin should do next.
logger.LogInformation("This tenant has never set Global Secure Access, so no value is stored.");
}

if (!string.IsNullOrWhiteSpace(status.Reason))
{
logger.LogWarning("Reason: {Reason}", status.Reason);
}
}

private static void LogStatus(ILogger logger, VNetStatusResponse status)
{
logger.LogInformation("Status: {Status}", status.Status ?? "Unknown");
Expand Down
47 changes: 47 additions & 0 deletions src/Microsoft.Agents.A365.DevTools.Cli/Models/GsaModels.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Text.Json.Serialization;

namespace Microsoft.Agents.A365.DevTools.Cli.Models;

/// <summary>
/// Status of Global Secure Access on the tenant's Agent 365 Power Platform environment, and the
/// shape returned by enable and disable.
/// </summary>
public class GsaStatusResponse
{
/// <summary>
/// Enabled, Disabled, or NotConfigured.
///
/// NotConfigured is not the same as Disabled: it means the tenant has never set the value.
/// The platform keeps the two apart, so the CLI does too.
/// </summary>
[JsonPropertyName("status")]
public string? Status { get; set; }

/// <summary>
/// True when a change was accepted but has not yet appeared on the environment. The
/// accompanying <see cref="Status"/> is then the value it has not yet displaced.
/// </summary>
[JsonPropertyName("pending")]
public bool Pending { get; set; }

/// <summary>
/// Explanation the platform has to offer, when there is one.
/// </summary>
[JsonPropertyName("reason")]
public string? Reason { get; set; }
}

/// <summary>
/// Error body returned by the platform's Global Secure Access endpoints.
/// </summary>
public class GsaErrorResponse
{
/// <summary>
/// Human-readable error message.
/// </summary>
[JsonPropertyName("error")]
public string? Error { get; set; }
}
8 changes: 7 additions & 1 deletion src/Microsoft.Agents.A365.DevTools.Cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,8 @@ await Task.WhenAll(
var networkLogger = serviceProvider.GetRequiredService<ILoggerFactory>().CreateLogger("network");
var vnetLinkService = serviceProvider.GetRequiredService<IVNetLinkService>();
var azureCliService = serviceProvider.GetRequiredService<IAzureCliService>();
rootCommand.AddCommand(NetworkCommand.CreateCommand(networkLogger, vnetLinkService, azureCliService));
var gsaService = serviceProvider.GetRequiredService<IGsaService>();
rootCommand.AddCommand(NetworkCommand.CreateCommand(networkLogger, vnetLinkService, azureCliService, gsaService));

// Build pipeline manually so we can skip UseTypoCorrections() ("Did you mean?" noise)
// and UseParseErrorReporting() (full help dump on any parse error), replacing both
Expand Down Expand Up @@ -388,6 +389,11 @@ private static void ConfigureServices(IServiceCollection services, LogLevel mini
provider.GetRequiredService<IAuthenticationService>(),
provider.GetRequiredService<ArmApiService>(),
provider.GetRequiredService<IAgent365ToolingService>().Environment));
services.AddSingleton<IGsaService>(provider => new GsaService(
provider.GetRequiredService<ILogger<GsaService>>(),
provider.GetRequiredService<IAuthenticationService>(),
provider.GetRequiredService<IAzureCliService>(),
provider.GetRequiredService<IAgent365ToolingService>().Environment));
services.AddSingleton<AgentBlueprintService>();
services.AddSingleton<BlueprintLookupService>();
services.AddSingleton<FederatedCredentialService>();
Expand Down
Loading
Loading