-
Notifications
You must be signed in to change notification settings - Fork 35
Add a365 network gsa enable|disable|status #497
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
1318bd5
72f3ab4
2649ed8
16ef241
71a6570
ae03dfe
d923c2a
94e71aa
a15fac1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,6 +27,10 @@ There is reference documentation for each command. | |
| | [develop-mcp list-servers](https://learn.microsoft.com/microsoft-agent-365/developer/reference/cli/develop-mcp#develop-mcp-list-servers) | List MCP servers in a specific Dataverse environment. | | ||
| | [develop-mcp publish](https://learn.microsoft.com/microsoft-agent-365/developer/reference/cli/develop-mcp#develop-mcp-publish) | Publish an MCP server to a Dataverse environment. | | ||
| | [develop-mcp unpublish](https://learn.microsoft.com/microsoft-agent-365/developer/reference/cli/develop-mcp#develop-mcp-unpublish) | Unpublish an MCP server from a Dataverse environment. | | ||
| | [network](network-gsa.md) | Configure tenant networking for Agent 365. | | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Low: #494 adds its own |
||
| | [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. | | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| # `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. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Low (docs, optional design):
|
||
| - Public cloud only. Sovereign clouds are not supported. | ||
|
|
||
| The `az login` is what selects the tenant. These commands read `az account show` once per run and | ||
| authenticate against the tenant and account it reports, so `az login --tenant <id>` is how you | ||
| choose which tenant to configure when you have more than one. Nothing else is read from Azure — the | ||
| setting itself lives in Power Platform, not in your subscription. 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] [--yes] | ||
| a365 network gsa disable [--wait] [--yes] | ||
| ``` | ||
|
|
||
| | Option | Description | | ||
| | --- | --- | | ||
| | `--wait` | Keep polling until the change appears on the environment, instead of returning while it is still being applied. | | ||
| | `--yes`, `-y` | Skip the confirmation prompt. | | ||
|
|
||
| Both verbs prompt before changing the tenant-wide setting; pass `--yes` in automation. | ||
| 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. | | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,250 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| using Microsoft.Agents.A365.DevTools.Cli.Constants; | ||
| using Microsoft.Agents.A365.DevTools.Cli.Models; | ||
| using Microsoft.Agents.A365.DevTools.Cli.Services; | ||
| using Microsoft.Extensions.Logging; | ||
| using System.CommandLine; | ||
| using System.CommandLine.Invocation; | ||
|
|
||
| namespace Microsoft.Agents.A365.DevTools.Cli.Commands; | ||
|
|
||
| /// <summary> | ||
| /// Tenant network configuration for Agent 365. | ||
| /// | ||
| /// Global Secure Access is normally set per Power Platform environment, which needs the id of the | ||
| /// environment being configured. Agent 365 does not publish that id, so these subcommands ask the | ||
| /// platform to apply the setting to the environment it resolves for your tenant. | ||
| /// </summary> | ||
| public static class NetworkCommand | ||
| { | ||
| private static readonly TimeSpan DefaultWaitTimeout = TimeSpan.FromMinutes(10); | ||
|
|
||
| /// <summary> | ||
| /// Creates the network command and its gsa subcommand tree. | ||
| /// </summary> | ||
| public static Command CreateCommand( | ||
| ILogger logger, | ||
| IAzureCliService azureCliService, | ||
| IGsaService gsaService, | ||
| IConfirmationProvider confirmationProvider) | ||
| { | ||
| var networkCommand = new Command(CommandNames.Network, "Configure tenant networking for Agent 365"); | ||
|
|
||
| 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, azureCliService, confirmationProvider, enabled: true)); | ||
| gsaCommand.AddCommand(CreateGsaSetSubcommand( | ||
| logger, gsaService, azureCliService, confirmationProvider, enabled: false)); | ||
| gsaCommand.AddCommand(CreateGsaStatusSubcommand(logger, gsaService, azureCliService)); | ||
|
|
||
| networkCommand.AddCommand(gsaCommand); | ||
| return networkCommand; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Resolves the Azure account to act as, or logs why it could not and returns null. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// Resolved once per invocation and passed to every call that follows. <c>az account show</c> | ||
| /// reads mutable local state, so reading it again for authentication after prompting could | ||
| /// confirm one tenant and change another, and a transient CLI failure between two reads could | ||
| /// fail a command that had already succeeded at resolving the tenant. | ||
| /// </remarks> | ||
| internal static async Task<AzureAccountInfo?> ResolveAccountAsync( | ||
| ILogger logger, | ||
| IAzureCliService azureCliService) | ||
| { | ||
| var account = await azureCliService.GetCurrentAccountAsync(); | ||
| if (account is null || string.IsNullOrWhiteSpace(account.TenantId)) | ||
| { | ||
| logger.LogError("Could not determine your Azure tenant. Run 'az login' and try again."); | ||
| return null; | ||
| } | ||
|
|
||
| return account; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Asks the operator to confirm a change to tenant-wide networking, naming the tenant and the | ||
| /// action so the prompt is answerable without scrolling back. | ||
| /// </summary> | ||
| internal static async Task<bool> ConfirmChangeAsync( | ||
| IConfirmationProvider confirmationProvider, | ||
| bool yes, | ||
| string action, | ||
| string tenantId) | ||
| { | ||
| if (yes) | ||
| { | ||
| return true; | ||
| } | ||
|
|
||
| return await confirmationProvider.ConfirmAsync( | ||
| $"{action} for tenant {tenantId}. This changes networking for every Agent 365 agent in the tenant. Continue?"); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Low: two small prompt issues:
|
||
| } | ||
|
|
||
| /// <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, | ||
| IAzureCliService azureCliService, | ||
| IConfirmationProvider confirmationProvider, | ||
| 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 yesOption = new Option<bool>( | ||
| ["--yes", "-y"], | ||
| "Skip the confirmation prompt."); | ||
|
|
||
| var verboseOption = new Option<bool>(["--verbose", "-v"], "Enable verbose logging"); | ||
|
|
||
| command.AddOption(waitOption); | ||
| command.AddOption(yesOption); | ||
| command.AddOption(verboseOption); | ||
|
|
||
| command.SetHandler(async (InvocationContext context) => | ||
| { | ||
| var wait = context.ParseResult.GetValueForOption(waitOption); | ||
| var yes = context.ParseResult.GetValueForOption(yesOption); | ||
| var ct = context.GetCancellationToken(); | ||
|
|
||
| // Resolved once, then used for both the prompt and the call, so the tenant named in | ||
| // the prompt is provably the tenant changed. | ||
| var account = await ResolveAccountAsync(logger, azureCliService); | ||
| if (account == null) | ||
| { | ||
| context.ExitCode = 1; | ||
| return; | ||
| } | ||
|
|
||
| if (!await ConfirmChangeAsync( | ||
| confirmationProvider, yes, $"Turn Global Secure Access {(enabled ? "on" : "off")}", account.TenantId)) | ||
| { | ||
| logger.LogInformation("Cancelled."); | ||
| context.ExitCode = 1; | ||
| return; | ||
| } | ||
|
|
||
| var result = await gsaService.SetAsync(account, enabled, ct); | ||
| context.ExitCode = await ReportGsaAsync(logger, gsaService, account, result, wait, enabled, ct); | ||
| }); | ||
|
|
||
| return command; | ||
| } | ||
|
|
||
| private static Command CreateGsaStatusSubcommand( | ||
| ILogger logger, | ||
| IGsaService gsaService, | ||
| IAzureCliService azureCliService) | ||
| { | ||
| 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 account = await ResolveAccountAsync(logger, azureCliService); | ||
| if (account == null) | ||
| { | ||
| context.ExitCode = 1; | ||
| return; | ||
| } | ||
|
|
||
| var status = await gsaService.GetStatusAsync(account, 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, | ||
| AzureAccountInfo account, | ||
| 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(account, expectedStatus, DefaultWaitTimeout, cancellationToken); | ||
|
|
||
| if (result == null) | ||
| { | ||
| return 1; | ||
| } | ||
| } | ||
|
|
||
| LogGsaStatus(logger, result); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Low (UX): on a 202 the platform returns the value it has not displaced yet, so |
||
|
|
||
| // 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) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Medium-High: after Suggest comparing against the requested value once the wait is over, e.g.: if (wait && !string.Equals(result.Status, expectedStatus, StringComparison.OrdinalIgnoreCase))
{
logger.LogWarning(
"Global Secure Access is not {Expected} yet; the change is still being applied. Check with: a365 network gsa status",
expectedStatus);
return 1;
}The exit code is your call given the "pending is not a failure" principle, but with |
||
| { | ||
| 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); | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: on the question in the earlier thread: the one-sentence rule is written down in
.github/copilot-instructions.md("CHANGELOG": "Each entry is one crisp consumer-facing sentence") and inCLAUDE.md, and the section ships verbatim to nuget.org. This entry is four sentences including rationale, and no other entry uses a relative link (it will not resolve outside GitHub). Suggestion: "a365 network gsa enable|disable|statusturns Global Secure Access on or off for your Agent 365 environment (#497)." Agreed that the older multi-sentence entries deserve a separate cleanup.