Skip to content

Add a365 network gsa enable|disable|status - #497

Open
Lala Sushant Srivastava (lasrivas) wants to merge 1 commit into
mainfrom
feature/gsa-environment-setting
Open

Lala Sushant Srivastava (lasrivas) wants to merge 1 commit into
mainfrom
feature/gsa-environment-setting

Conversation

@lasrivas

@lasrivas Lala Sushant Srivastava (lasrivas) commented Sep 16, 2026 •

Copy link
Copy Markdown

Adds a365 network gsa enable|disable|status, which turns Global Secure Access on or off for the tenant's Agent 365 environment.

Global Secure Access is a per-environment Power Platform setting. Agent 365 does not publish that environment's id, so the CLI asks the platform to apply the setting to the environment it resolves for the tenant. The platform half shipped in bic/MCP-Platform#3686.

Behavior

  • NotConfigured stays distinct from Disabled.
  • Without --wait, an accepted change returns 0 and labels the requested and current values separately.
  • With --wait, success means the requested value was observed. Timeout or failed verification returns 1, explicitly states that the change was accepted, and points to a365 network gsa status. The ten-minute ceiling also bounds in-flight requests and retry backoff.
  • Status reads retry transient HTTP/network failures up to three attempts; mutations are not retried. Empty or missing status payloads fail rather than reporting Unknown as success.
  • The Azure account is resolved once per invocation and used for confirmation and every service call. Both mutation commands prompt with the tenant/account and support --yes; status is read-only.
  • GSA is public-cloud-only. Unsupported cloud configurations are rejected before authentication, and token acquisition passes the resolved authority host.
  • Scope errors report required_scope separately from administrator-role failures.

Usage and prerequisites: network-gsa.md, including az login --tenant <id> --allow-no-subscriptions for administrators without Azure subscriptions. An optional --tenant-id expansion is intentionally not included.

Integration

Rebuilt the GSA-only net change directly on current main (2a3245d), removing the original VNet commit and its later deletion from this branch's history. This avoids the shared-ancestor modify/delete problem with #494. Whichever PR lands second must still preserve both subcommand trees when resolving additive command-registration, test, and documentation conflicts. The docs index uses network gsa, not a duplicate network row.

Platform deployment prerequisite

The Agent 365 Tools API must expose AgentTools.Gsa.Manage.All and AgentTools.Gsa.Read.All and preauthorize them for the CLI's Microsoft client application. This registration/deployment prerequisite cannot be confirmed by local unit tests and still needs platform-owner confirmation. No platform app registrations were changed by this PR.

Verification

  • Focused GSA/command/HTTP-client tests: 85 passed.
  • Full solution suite: 2475 passed, 12 skipped, 0 failed.
  • Formatting and whitespace checks passed; independent code review found no significant issues.
  • tests.proj required an unavailable traversal SDK, so the equivalent full solution suite was run instead.

Copilot AI lite review requested due to automatic review settings September 16, 2026 19:41
@github-actions

Copy link
Copy Markdown

⚠️ Deprecation Warning: The deny-licenses option is deprecated for possible removal in the next major release. For more information, see issue 997.

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved tenant-targeting, cancellation, validation, and VNet polling issues remain.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds tenant-level GSA enable, disable, and status commands alongside supporting VNet networking functionality.

Changes:

  • Adds GSA services, models, authentication, and convergence polling.
  • Registers network commands, handlers, and dependency injection.
  • Adds tests, documentation, and release notes.
File summaries
File Reviewed change
src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Services/VNetLinkServiceTests.cs VNet service tests
src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Services/GsaServiceTests.cs GSA service tests
src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Services/ArmApiServiceTests.cs ARM policy lookup tests
src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Commands/NetworkCommandTests.cs Network command tests
src/Microsoft.Agents.A365.DevTools.Cli/Services/VNetLinkService.cs VNet operations and polling
src/Microsoft.Agents.A365.DevTools.Cli/Services/IVNetLinkService.cs VNet service contract
src/Microsoft.Agents.A365.DevTools.Cli/Services/IGsaService.cs GSA service contract
src/Microsoft.Agents.A365.DevTools.Cli/Services/GsaService.cs GSA API operations and polling
src/Microsoft.Agents.A365.DevTools.Cli/Services/ArmApiService.cs Enterprise policy resolution
src/Microsoft.Agents.A365.DevTools.Cli/Program.cs Command and service registration
src/Microsoft.Agents.A365.DevTools.Cli/Models/VNetModels.cs VNet models
src/Microsoft.Agents.A365.DevTools.Cli/Models/GsaModels.cs GSA response models
src/Microsoft.Agents.A365.DevTools.Cli/Constants/CommandNames.cs Network command constant
src/Microsoft.Agents.A365.DevTools.Cli/Commands/NetworkCommand.cs Network, VNet, and GSA command tree
docs/commands/README.md Command index entries
docs/commands/network.md VNet command documentation
docs/commands/network-gsa.md GSA command documentation
CHANGELOG.md Release notes
Review details

Suppressed comments (7)

src/Microsoft.Agents.A365.DevTools.Cli/Commands/NetworkCommand.cs:260

  • The new enable/disable handlers are not exercised through Command.InvokeAsync; the tests only parse gsa enable and call ReportGsaAsync directly. A wiring regression could leave SetAsync uncalled or lose the handler's exit-code propagation while all tests stay green; add invocation tests for both verbs covering service calls and success/failure results.
            var result = await gsaService.SetAsync(enabled, ct);
            context.ExitCode = await ReportGsaAsync(logger, gsaService, result, wait, enabled, ct);

src/Microsoft.Agents.A365.DevTools.Cli/Commands/NetworkCommand.cs:287

  • The new GSA status handler is also only covered by parse/tree assertions; no test invokes it to verify that GetStatusAsync is called and that null responses produce exit code 1. Add an invocation test for the success and request-error branches so handler wiring is covered, not just LogGsaStatus/ReportGsaAsync.
            var status = await gsaService.GetStatusAsync(ct);
            if (status == null)
            {
                context.ExitCode = 1;
                return;
            }

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

src/Microsoft.Agents.A365.DevTools.Cli/Commands/NetworkCommand.cs:111

  • --tenant-id is an Option<string?>; an explicitly supplied empty or whitespace value enters this branch and is silently replaced with the current az tenant. That makes a malformed explicit target indistinguishable from omission and can apply a tenant-wide link to the wrong tenant. Detect whether the option was supplied and reject blank values with a targeted error before falling back.
            if (string.IsNullOrWhiteSpace(tenantId))
            {
                var account = await azureCliService.GetCurrentAccountAsync();
                tenantId = account?.TenantId;
                if (string.IsNullOrWhiteSpace(tenantId))

src/Microsoft.Agents.A365.DevTools.Cli/Services/ArmApiService.cs:328

  • When the command token is cancelled during the policy GET or response read, RetryHelper rethrows the cancellation, but this catch converts it to null. LinkAsync then reports an ordinary policy-resolution failure instead of honoring Ctrl+C; rethrow OperationCanceledException when ct is cancelled before the catch-all.
            catch (Exception ex)
            {
                if (NetworkHelper.IsConnectionResetByProxy(ex))
                    _logger.LogWarning(NetworkHelper.ConnectionResetWarning);
                else

src/Microsoft.Agents.A365.DevTools.Cli/Services/ArmApiService.cs:276

  • policyArmId is CLI-controlled and is concatenated directly into the ARM URL after only a whitespace check. A value containing query, fragment, or path-traversal syntax can alter the resource or api-version portion of the request; validate it as a well-formed /subscriptions/.../providers/Microsoft.PowerPlatform/enterprisePolicies/... resource ID before constructing this URL.
            var url = $"{ArmBaseUrl}{policyArmId}?api-version={apiVersion}";

src/Microsoft.Agents.A365.DevTools.Cli/Services/ArmApiService.cs:269

  • tenantId is a required non-nullable parameter, but it is passed to EnsureArmHeadersAsync without a whitespace guard. A blank value can fall through to common-tenant authentication and defeat the tenant-targeted ARM read; reject it before the first use.
        string tenantId,
        CancellationToken ct = default)
    {
        if (string.IsNullOrWhiteSpace(policyArmId))
            throw new ArgumentException("Policy ARM id is required.", nameof(policyArmId));

        if (!await EnsureArmHeadersAsync(tenantId, ct))

src/Microsoft.Agents.A365.DevTools.Cli/Services/GsaService.cs:120

  • The account lookup validates only TenantId; an az account show result with a missing user name is passed as an empty userId, which disables the user hint and lets the broker select another cached account. For a tenant-wide setting, reject a missing user name before token acquisition so the selected tenant and user are both enforced.
            if (account is null || string.IsNullOrWhiteSpace(account.TenantId))
            {
                _logger.LogError("Could not determine your Azure tenant. Run 'az login' and try again.");
                return null;
  • Files reviewed: 18/18 changed files
  • Comments generated: 5
  • Review effort level: Lite

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread CHANGELOG.md Resolved

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes. The ARM URL construction lets the bearer token be sent to an arbitrary host and needs fixing before merge.

The PR also adds network vnet link/unlink/status and ARM enterprise policy resolution, which aren't in the title. Please retitle, or split the VNet part out, since most of the findings are there.

Comment thread CHANGELOG.md Resolved
Lala Sushant Srivastava (lasrivas) added a commit that referenced this pull request Sep 24, 2026
Carries the vnet fixes from #494 (this branch contains that change set) and
applies the same treatment to the gsa subcommands.

Pin the enterprise-policy ARM id to its expected shape before concatenating it
onto the ARM base URL. The base has no trailing slash and the ARM bearer token
is a default request header, so `--policy-arm-id "@evil.example/x"` produced
`https://management.azure.com@evil.example/x` -- userinfo, not host -- and sent
the token to the attacker.

Also:

- Treat `NotStarted` as in-flight, matching what network.md documents.
- Acquire the Agent 365 token for the resolved tenant rather than the signed-in
  default. `vnet unlink` and `vnet status` gain `--tenant-id` so they can do the
  same; the gsa commands already resolve the az-login tenant themselves.
- Confirm before `vnet link --swap`, `vnet unlink`, `gsa enable` and
  `gsa disable`, with `--yes` for automation. Plain `vnet link` is not gated: a
  different existing link is reported as a conflict rather than replaced.
- Reject an explicitly blank `--tenant-id` instead of silently falling back.
- Use `CommandNames.Network` rather than a literal.
- Drive every handler through `InvokeAsync` in tests. The previous doc comment
  claimed `ReportAsync` covered them, but tenant resolution, confirmation,
  service calls and exit codes were untested.
- Stop the VNet tests shelling out to `az account show` via `AzCliHelper`'s
  static cache, using the repo's existing `loginHintResolver` seam.
- Trim the CHANGELOG entries and reference #494 and #497.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 24, 2026 00:56

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread CHANGELOG.md Resolved

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Reuse one Azure CLI account snapshot for confirmation and authentication to prevent tenant mismatches and transient failures.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 2 High severity

Open (2)
Resolved since last review (3)

Copilot AI review requested due to automatic review settings September 24, 2026 04:49
@lasrivas

Copy link
Copy Markdown
Author

The virtual network change set has been removed from this PR, so it now carries only the GSA work.

This branch was originally cut to include #494, which meant roughly 1400 lines appeared on both PRs and every VNet review finding had to be answered twice. GSA never depended on any of it: GsaService talks to the platform directly, and the only shared code is the tenant-resolution and confirmation helpers in NetworkCommand, which both features need.

Removed in d923c2a: VNetLinkService, IVNetLinkService, VNetModels and their tests; ArmApiService and its tests reverted to main; the vnet subcommand tree along with ReportAsync and LogStatus; the vnet entries in the CHANGELOG and the docs index. NetworkCommand.CreateCommand loses its IVNetLinkService parameter.

Review the VNet code on #494 instead; it is unchanged there.

One consequence worth flagging: both branches now define the network root command and the two shared helpers, so whichever merges second will conflict in NetworkCommand.cs and Program.cs. The conflict is additive and small, and it seemed the better trade against reviewing the same 1400 lines on two PRs.

2063 passed, 0 failed, 12 skipped.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Several moderate service reliability and error-handling issues remain unresolved.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 2 High severity · 2 Medium severity · 1 Low severity

Open (5)

Comment thread docs/commands/network-gsa.md Resolved

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Unresolved critical and moderate service-handling issues must be addressed before approval.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 High severity

Open (1)
Resolved since last review (5)

Copilot AI review requested due to automatic review settings September 24, 2026 05:39
Lala Sushant Srivastava (lasrivas) added a commit that referenced this pull request Sep 24, 2026
CreateAuthenticatedClient built the client with HttpClient's default handler
ownership, so a caller that holds one handler and builds a client per request
lost the handler to the first client's disposal. VNetLinkService is exactly
that shape, so the second poll of WaitForCompletionAsync would have failed
with ObjectDisposedException against any handler that honours Dispose.

A supplied handler now stays owned by whoever supplied it. Found on #497,
which carries the identical factory; not flagged here.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Moderate issues remain in URL error handling and wait-timeout behavior.

Review effort: Lite
Findings: None

Resolved since last review (1)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, the account-once threading is a real fix and the GSA flow reads well. I checked the contract against the platform change this calls (MCP-Platform PR 3686, merged) and built and ran the suite on this branch and merged onto current main (builds; all tests pass). Details are inline; the summary:

Before merge

  • --wait can report the old value as success (inline on NetworkCommand.cs): the platform's status response never sets pending, so after a timed-out wait the command prints the old value and exits 0 with no "still being applied" hint. The tests covering this path feed pending: true into status responses, which the platform does not send.
  • Scopes (please confirm): the platform rejects any token without the exact scope (AgentTools.Gsa.Manage.All for enable/disable, AgentTools.Gsa.Read.All for status) with 403 insufficient_scope. The token is {Agent 365 Tools app}/.default from the Azure PowerShell public client (no client id is passed), so both scopes need to be exposed on the Agent 365 Tools app and pre-authorized for that client, in every cloud the CLI targets.
  • Rebase onto main: needed for #478 (cloud-aware endpoints; inline on GsaService.cs). Also, this branch still carries #494's original commit 1318bd50 plus d923c2a, which deletes those files. A squash merge nets that out, but with a merge commit (enabled in this repo) the two branches share 1318bd50 as a common ancestor, so whichever merges second hits modify/delete conflicts on the VNet files, and resolving them toward this branch deletes the VNet feature. Rebasing onto main (ideally dropping that pair) removes the shared ancestor.
  • Interaction with #494: with squash merges, whichever lands second conflicts in NetworkCommand.cs, Program.cs, NetworkCommandTests.cs, docs/commands/README.md and CHANGELOG.md (reproduced in both orders). The semantics also diverge under one network parent: vnet has --tenant-id and does not confirm a plain link; gsa has no --tenant-id and confirms both verbs.

Verified and looks right

  • Resolving the az account once and passing it down removes the prompt/token tenant split, and the tests pin it.
  • HttpClient's own timeout returns the documented null, while a caller's cancellation and the wait ceiling still propagate.
  • disposeHandler: false in HttpClientFactory is the right ownership model.
  • Routes and shapes match the platform (POST /agents/gsa/enable|disable, GET /agents/gsa/status; status/pending/reason; error), and NotConfigured stays distinct from Disabled.

Nit: Program.cs creates the logger with the literal "network", and CommandNames.Network exists.

Comment thread docs/commands/network-gsa.md Resolved
Comment thread CHANGELOG.md Resolved
Comment thread docs/commands/README.md Resolved

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Empty successful responses currently produce an Unknown status with exit code 0 instead of reporting a failed status read.

Review effort: Balanced
Findings: 1 High severity

Open (1)

Rebuild the GSA-only change set on current main, dropping shared VNet history. Require --wait to observe the requested value, reject unreadable success responses and unsupported clouds, retry transient reads, and distinguish scope errors from role failures. Clarify prompts, pending output, prerequisites and release notes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 0ea31405-e8a4-41a7-b9ec-13e2f66d65a7

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

GSA can ignore config-only sovereign-cloud authority overrides, trigger unrelated authentication before cloud validation, and execute mutations as an administrator other than the confirmed user.

3 open issues
1 resolved since last review

Review effort: Balanced

Comment on lines +193 to +194
rootCommand.AddCommand(NetworkCommand.CreateCommand(
networkLogger, azureCliService, gsaService, confirmationProvider));
Comment on lines +392 to +395
services.AddSingleton<IGsaService>(provider => new GsaService(
provider.GetRequiredService<ILogger<GsaService>>(),
provider.GetRequiredService<IAuthenticationService>(),
provider.GetRequiredService<IAgent365ToolingService>().Environment));
Comment on lines +158 to +160
var authToken = await _authService.GetAccessTokenAsync(
audience, account.TenantId, userId: account.User.Name, ct: cancellationToken,
authorityHost: authorityHost);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants