Skip to content

PowerShell Setup: Control 2.17 — Multi-Agent Orchestration Limits

Read the FSI PowerShell baseline first

Before running any command in this playbook, read the PowerShell Authoring Baseline for FSI Implementations. It is the canonical source for module version pinning, sovereign-cloud (GCC / GCC High / DoD) endpoints, mutation safety (-WhatIf / SupportsShouldProcess), Dataverse compatibility, and SHA-256 evidence emission. The snippets below are abbreviated; the baseline is authoritative.

Last Updated: April 2026 Modules required: ExchangeOnlineManagement, Microsoft.PowerApps.Administration.PowerShell, Microsoft.Graph.Reports Operation type: Read-only (audit-log query, telemetry inspection, configuration reporting). No mutation cmdlets are required for this control — the orchestration controls themselves are authored in Copilot Studio and Power Automate (see Portal Walkthrough).


1. Prerequisites and Environment Setup

# --- Edition guard (Power Apps admin module is Desktop-only) -----------------
if ($PSVersionTable.PSEdition -ne 'Desktop') {
    throw "Microsoft.PowerApps.Administration.PowerShell requires Windows PowerShell 5.1 (Desktop). Detected: $($PSVersionTable.PSEdition) $($PSVersionTable.PSVersion)."
}

# --- Module install (pin to CAB-approved versions) ---------------------------
# Replace <version> placeholders with the versions approved by your Change Advisory Board.
$modules = @(
    @{ Name = 'ExchangeOnlineManagement';                    Version = '<approved-version>' },
    @{ Name = 'Microsoft.PowerApps.Administration.PowerShell'; Version = '<approved-version>' },
    @{ Name = 'Microsoft.Graph.Reports';                     Version = '<approved-version>' }
)
foreach ($m in $modules) {
    Install-Module -Name $m.Name -RequiredVersion $m.Version `
        -Repository PSGallery -Scope CurrentUser -AllowClobber -AcceptLicense
}
# --- Sovereign-cloud aware authentication ------------------------------------
param(
    [ValidateSet('prod','usgov','usgovhigh','dod')]
    [string]$Endpoint = 'prod'
)

$exoEnv = @{ prod = 'O365Default'; usgov = 'O365USGovGCCHigh'; usgovhigh = 'O365USGovGCCHigh'; dod = 'O365USGovDoD' }[$Endpoint]
Connect-ExchangeOnline -ShowBanner:$false -ExchangeEnvironmentName $exoEnv

Add-PowerAppsAccount -Endpoint $Endpoint
# For unattended execution prefer certificate-based service principal auth:
#   Add-PowerAppsAccount -Endpoint $Endpoint -TenantID <tid> -ApplicationId <aid> -CertificateThumbprint <thumb>
# Avoid -ClientSecret in production: secrets cannot be rotated without code changes and are flagged in audit reviews.

Sovereign-cloud false-clean risk

If you omit -Endpoint (Power Apps) or -ExchangeEnvironmentName (EXO), the cmdlets authenticate against commercial endpoints, return zero results from your gov-cloud tenant, and produce false-clean evidence. Always pass the explicit endpoint and verify the returned tenant ID matches your target before treating output as authoritative.


2. Query Orchestration Events from the Unified Audit Log

<#
.SYNOPSIS
    Queries Microsoft Purview Unified Audit Log for Copilot Studio orchestration events.

.DESCRIPTION
    Read-only. Surfaces agent-to-agent invocations, tool/MCP calls, and any custom
    events emitted by the orchestration topics (Portal Walkthrough Step 6).
    Writes JSON + CSV evidence with SHA-256 manifest per the FSI baseline.

.PARAMETER StartDate
    Start of audit window (UTC). Default: 7 days ago.

.PARAMETER EndDate
    End of audit window (UTC). Default: now.

.PARAMETER EvidencePath
    Output folder for evidence artifacts. Default: .\evidence-2.17

.NOTES
    RecordType "CopilotInteraction" is the current Microsoft-documented record type
    for Copilot Studio agent activity (verified April 2026). Microsoft has historically
    renamed Copilot record types as the product evolved — re-verify against Microsoft
    Learn ("Audit log activities") before each quarterly review and update this script
    if the record type has changed in your tenant.
#>
[CmdletBinding()]
param(
    [datetime]$StartDate = (Get-Date).ToUniversalTime().AddDays(-7),
    [datetime]$EndDate   = (Get-Date).ToUniversalTime(),
    [string]  $EvidencePath = ".\evidence-2.17"
)

$ErrorActionPreference = 'Stop'
New-Item -ItemType Directory -Force -Path $EvidencePath | Out-Null
$ts = Get-Date -Format 'yyyyMMddTHHmmssZ'
Start-Transcript -Path (Join-Path $EvidencePath "transcript-$ts.log") -IncludeInvocationHeader

Write-Host "[INFO] Querying Copilot audit events $StartDate -> $EndDate (UTC)" -ForegroundColor Cyan

# Page through results — Search-UnifiedAuditLog is capped at 5000 per call.
$all = New-Object System.Collections.Generic.List[object]
$session = "fsi-2.17-$ts"
do {
    $page = Search-UnifiedAuditLog `
        -StartDate $StartDate -EndDate $EndDate `
        -RecordType 'CopilotInteraction' `
        -SessionId $session -SessionCommand ReturnLargeSet `
        -ResultSize 5000
    if ($page) { $all.AddRange($page) }
} while ($page -and $page.Count -eq 5000)

Write-Host "[INFO] Retrieved $($all.Count) raw audit records" -ForegroundColor Cyan

$events = foreach ($r in $all) {
    $d = $r.AuditData | ConvertFrom-Json -ErrorAction SilentlyContinue
    [PSCustomObject]@{
        TimestampUtc     = $r.CreationDate.ToUniversalTime().ToString('o')
        UserIds          = $r.UserIds
        Operation        = $d.Operation
        AgentId          = $d.AgentId
        AgentName        = $d.AgentName
        TargetAgent      = $d.TargetAgent
        DelegationDepth  = $d.DelegationDepth
        CorrelationId    = $d.CorrelationId
        Success          = ($d.ResultStatus -eq 'Success')
        ResultStatus     = $d.ResultStatus
    }
}

# Emit evidence (JSON + CSV + SHA-256 manifest per baseline §5)
$jsonPath = Join-Path $EvidencePath "orchestration-events-$ts.json"
$csvPath  = Join-Path $EvidencePath "orchestration-events-$ts.csv"
$events | ConvertTo-Json -Depth 10 | Set-Content -Path $jsonPath -Encoding UTF8
$events | Export-Csv      -Path $csvPath -NoTypeInformation -Encoding UTF8

$manifest = foreach ($f in @($jsonPath, $csvPath)) {
    [PSCustomObject]@{
        file          = Split-Path $f -Leaf
        sha256        = (Get-FileHash -Path $f -Algorithm SHA256).Hash
        bytes         = (Get-Item $f).Length
        generated_utc = $ts
        script        = 'fsi-2.17-orchestration-events'
        window_start  = $StartDate.ToString('o')
        window_end    = $EndDate.ToString('o')
    }
}
$manifest | ConvertTo-Json -Depth 5 | Set-Content -Path (Join-Path $EvidencePath "manifest-$ts.json") -Encoding UTF8

Stop-Transcript
Write-Host "[PASS] Evidence written to $EvidencePath" -ForegroundColor Green

Zero events ≠ control passing

A query that returns zero events is not evidence the control is working. It can equally indicate: (a) no orchestration occurred in the window, (b) audit logging is not enabled (Control 1.7), (c) the record type was renamed, or (d) wrong cloud endpoint. Cross-check against expected orchestration volume from the agent inventory (Control 2.1) and document the interpretation in the evidence manifest.


3. Inspect Application Insights Custom Orchestration Telemetry

The orchestration topics emit five custom events (see Portal Walkthrough Step 6). Query them via Azure CLI or KQL to validate emission volume and surface depth-limit violations.

# Requires Az.ApplicationInsights module (pinned per baseline §1)
$kql = @'
union customEvents
| where timestamp >= ago(7d)
| where name startswith "Orchestration."
| extend correlationId = tostring(customDimensions.correlationId),
         depth         = toint(customDimensions.depth),
         zone          = tostring(customDimensions.zone),
         childAgentId  = tostring(customDimensions.childAgentId)
| summarize Events=count(),
            MaxDepth=max(depth),
            UniqueChains=dcount(correlationId)
        by name, zone
| order by zone, name
'@

$result = Invoke-AzOperationalInsightsQuery `
    -WorkspaceId $WorkspaceId `
    -Query $kql `
    -Timespan (New-TimeSpan -Days 7)

$result.Results | Format-Table -AutoSize

Interpretation guide:

Observation Likely meaning Action
Orchestration.DelegationStart count > 0 but Orchestration.DelegationEnd ≪ start count Delegations time out or fail silently Check circuit-breaker thresholds; review error handling in topics
MaxDepth exceeds zone limit Depth tracking broken or limit not enforced Treat as policy violation; trigger Control 3.4 incident workflow
Orchestration.CircuitBreakerOpen > 0 in 24h Cascading failure or unhealthy downstream Investigate target agent; do not just raise threshold
Orchestration.HitlDecision with very short waitDurationMs Possible rubber-stamping Sample for quality review (FINRA Rule 3110 supervisory review evidence)
Orchestration.MCP.ToolInvocation for unapproved mcpServer Drift from approved registry Block at DLP layer; investigate how the unapproved server was added

4. Validate Per-Agent Tool-Count Headroom

Each Copilot Studio agent is capped at 128 tools. Multi-agent designs that grow over time can hit this ceiling and silently start dropping new tool registrations.

# Pulls tool counts via the Power Apps admin API (read-only).
Get-AdminPowerApp | Where-Object { $_.AppType -eq 'CopilotAgent' } | ForEach-Object {
    [PSCustomObject]@{
        AgentName       = $_.DisplayName
        EnvironmentName = $_.EnvironmentName
        ToolCount       = ($_.Internal.properties.tools | Measure-Object).Count
        Headroom        = 128 - (($_.Internal.properties.tools | Measure-Object).Count)
        Owner           = $_.Owner.email
    }
} | Sort-Object Headroom | Format-Table -AutoSize

API surface volatility

The Copilot Studio agent inventory surface in Power Apps admin cmdlets has changed across module versions. If properties.tools returns $null, your module version may not yet expose this property — fall back to per-agent inspection in the Copilot Studio portal and log a control-debt item.


5. Quarterly Compliance Validation Script

Run this once per quarter as part of your governance cadence (see Control 2.12) and retain evidence per SEC 17a-4 (typically 6 years for FSI).

<#
.SYNOPSIS  Quarterly Control 2.17 attestation pack generator.
.DESCRIPTION  Read-only. Aggregates 90 days of orchestration evidence into a
              single attestation pack with SHA-256 manifest for the AI
              Governance Lead's signature.
#>
[CmdletBinding()]
param(
    [string]$EvidencePath = ".\evidence-2.17-quarterly-$((Get-Date).ToString('yyyyMM'))"
)

$ErrorActionPreference = 'Stop'
New-Item -ItemType Directory -Force -Path $EvidencePath | Out-Null

# 1. 90-day audit-log pull
& "$PSScriptRoot\Get-OrchestrationEvents.ps1" `
    -StartDate (Get-Date).AddDays(-90).ToUniversalTime() `
    -EvidencePath $EvidencePath

# 2. Tool-count snapshot
& "$PSScriptRoot\Get-AgentToolCounts.ps1" `
    -EvidencePath $EvidencePath

# 3. Approved MCP-server registry export (placeholder — adapt to your registry source)
#    Example: pull from SharePoint list, Dataverse, or governance ITSM.

# 4. Generate attestation cover sheet
$cover = [PSCustomObject]@{
    Control                 = '2.17'
    Title                   = 'Multi-Agent Orchestration Limits'
    PeriodStartUtc          = (Get-Date).AddDays(-90).ToUniversalTime().ToString('o')
    PeriodEndUtc            = (Get-Date).ToUniversalTime().ToString('o')
    GeneratedUtc            = (Get-Date).ToUniversalTime().ToString('o')
    GeneratedByUpn          = (whoami /upn 2>$null)
    EvidencePath            = (Resolve-Path $EvidencePath).Path
    AttestationStatement    = 'Reviewed and confirmed compliant — _____________________'
    SignerUpn               = ''
    SignerDateUtc           = ''
}
$cover | ConvertTo-Json -Depth 5 | Set-Content -Path (Join-Path $EvidencePath 'attestation.json') -Encoding UTF8

Write-Host "[PASS] Attestation pack at $EvidencePath" -ForegroundColor Green
Write-Host "[NEXT] AI Governance Lead must review, sign attestation.json, and lodge in WORM storage." -ForegroundColor Yellow

6. What This Script Does NOT Do

To keep the FSI threat model honest, the script set above explicitly does not:

  • Configure orchestration limits — those live in Copilot Studio topics and Power Automate flows (Portal Walkthrough).
  • Enforce depth limits or circuit breakers in the Copilot Studio runtime — there is no admin API surface for this as of April 2026.
  • Modify any tenant or environment state — all cmdlets are read-only.
  • Substitute for SOC monitoring — these scripts produce evidence; real-time detection belongs in Sentinel / your SIEM (Control 1.24).

If a future Microsoft release adds admin API surface for orchestration limits, this playbook will be updated and the change recorded in the control's footer date.


Back to Control 2.17 | Portal Walkthrough | Verification & Testing | Troubleshooting