From 62d4b0210c2e052d7903c8f56e2ef3685e1bdc1c Mon Sep 17 00:00:00 2001 From: Paul Lizer Date: Wed, 14 Oct 2026 13:55:21 -0400 Subject: [PATCH] Harden private networking and add optional AMPLS (#1765) Fix Redis endpoints and cloud DNS, add opt-in private monitoring, document fresh and compact network layouts, and expand the Visio architectures. Update release notes and regenerate their existing stale pages without dropping history. --- application/single_app/config.py | 2 +- .../PRIVATE_ENDPOINTS_NETWORK_REFERENCE.md | 32 + artifacts/architecture.vsdx | Bin 1827023 -> 1874307 bytes artifacts/private_endpoints.vsdx | Bin 649676 -> 711779 bytes deployers/azure.yaml | 20 + deployers/azurecli/README.md | 9 + deployers/azurecli/deploy-simplechat.ps1 | 67 +- deployers/bicep/README.md | 10 +- deployers/bicep/main.bicep | 14 +- deployers/bicep/main.json | 1436 ++++++++++++-- deployers/bicep/main.parameters.json | 3 + .../bicep/modules/applicationInsights.bicep | 4 + .../azureMonitorPrivateNetworking.bicep | 100 + .../azureMonitorPrivateNetworking.json | 522 +++++ .../bicep/modules/logAnalyticsWorkspace.bicep | 3 + .../bicep/modules/privateDNSRecords.bicep | 18 + deployers/bicep/modules/privateDNSZones.json | 22 +- .../bicep/modules/privateNetworking.bicep | 76 +- deployers/bicep/modules/redisCache.bicep | 4 +- deployers/bicep/validate_azd_prerequisites.py | 36 + deployers/terraform/ReadMe.md | 10 + deployers/terraform/main.tf | 4 + deployers/terraform/private_networking.tf | 57 +- deployers/version.txt | 2 +- docs/deploy/special-scenarios.md | 22 +- .../features/AZURE_MANAGED_REDIS_SUPPORT.md | 2 +- .../features/AZURE_MONITOR_PRIVATE_LINK.md | 25 + .../DEPLOYER_PRIVATE_ENDPOINT_DNS_FIX.md | 22 + docs/explanation/release-notes/index.md | 563 ++++-- docs/explanation/release-notes/v0.239.md | 2 + docs/explanation/release-notes/v0.241.md | 10 +- docs/explanation/release-notes/v0.250.md | 13 + docs/explanation/release-notes/v0.260.md | 24 +- .../release-notes/v0.261-part-1.md | 698 +++++++ .../release-notes/v0.261-part-2.md | 673 +++++++ .../release-notes/v0.261-part-3.md | 779 ++++++++ .../release-notes/v0.261-part-4.md | 686 +++++++ .../release-notes/v0.261-part-5.md | 1675 +++++++++++++++++ .../release-notes/v0.261-part-6.md | 945 ++++++++++ .../{v0.261.md => v0.261-part-7.md} | 130 +- docs/explanation/release_notes.md | 25 + docs/guides/enterprise-networking.md | 144 +- ...test_azd_private_dns_zone_config_wiring.py | 253 ++- 43 files changed, 8743 insertions(+), 399 deletions(-) create mode 100644 artifacts/PRIVATE_ENDPOINTS_NETWORK_REFERENCE.md create mode 100644 deployers/bicep/modules/azureMonitorPrivateNetworking.bicep create mode 100644 deployers/bicep/modules/azureMonitorPrivateNetworking.json create mode 100644 deployers/bicep/modules/privateDNSRecords.bicep create mode 100644 docs/explanation/features/AZURE_MONITOR_PRIVATE_LINK.md create mode 100644 docs/explanation/fixes/DEPLOYER_PRIVATE_ENDPOINT_DNS_FIX.md create mode 100644 docs/explanation/release-notes/v0.261-part-1.md create mode 100644 docs/explanation/release-notes/v0.261-part-2.md create mode 100644 docs/explanation/release-notes/v0.261-part-3.md create mode 100644 docs/explanation/release-notes/v0.261-part-4.md create mode 100644 docs/explanation/release-notes/v0.261-part-5.md create mode 100644 docs/explanation/release-notes/v0.261-part-6.md rename docs/explanation/release-notes/{v0.261.md => v0.261-part-7.md} (96%) diff --git a/application/single_app/config.py b/application/single_app/config.py index 32497ce21..e4f365cf8 100644 --- a/application/single_app/config.py +++ b/application/single_app/config.py @@ -101,7 +101,7 @@ EXECUTOR_TYPE = 'thread' EXECUTOR_MAX_WORKERS = 30 SESSION_TYPE = 'filesystem' -VERSION = "0.261.332" +VERSION = "0.261.333" IS_DEVELOPMENT = is_development_env_enabled() # Opt-out for deployments where App Service Easy Auth is active but the platform diff --git a/artifacts/PRIVATE_ENDPOINTS_NETWORK_REFERENCE.md b/artifacts/PRIVATE_ENDPOINTS_NETWORK_REFERENCE.md new file mode 100644 index 000000000..839a698e2 --- /dev/null +++ b/artifacts/PRIVATE_ENDPOINTS_NETWORK_REFERENCE.md @@ -0,0 +1,32 @@ +# Private Endpoint Diagram Reference + +Version: **0.261.333**. Deployer: **1.0.34**. Related issue: [#1765](https://github.com/microsoft/simplechat/issues/1765). + +Use the [Commercial tables](../docs/guides/enterprise-networking.md#networking-in-azure-commercial) and [Government tables](../docs/guides/enterprise-networking.md#networking-in-azure-government) alongside the Visio sources in this directory. Each section separates fresh script-created networks from the compact existing-network design, with address/usable counts and delegation. + +## Fresh Deployment in Either Cloud + +Private networking must be enabled, with no existing VNet/subnet IDs supplied. + +| Network/Subnet | Address space | Addresses | Azure-usable | Delegation / Notes | +|---|---|---:|---:|---| +| **VNet** | **`10.0.0.0/21`** | **2048** | - | Automatically created | +| `AppServiceIntegration` | `10.0.0.0/24` | 256 | 251 | `Microsoft.Web/serverFarms`; outbound | +| `PrivateEndpoints` | `10.0.2.0/24` | 256 | 251 | All created endpoints, including optional AMPLS | +| **Unallocated** | **`10.0.1.0/24`, `10.0.3.0/24`, `10.0.4.0/22`** | **1536** | - | Future subnets | + +## Compact Existing-Network Reference + +| Network/Subnet | Address space | Addresses | Azure-usable | Delegation / Notes | +|---|---|---:|---:|---| +| **VNet** | **`10.42.0.0/24`** | **256** | - | Enterprise reference, not script default | +| `snet-app-outbound` | `10.42.0.0/26` | 64 | 59 | `Microsoft.Web/serverFarms` | +| `snet-platform-pe` | `10.42.0.64/26` | 64 | 59 | ACR, Key Vault, optional AMPLS | +| `snet-data-pe` | `10.42.0.128/27` | 32 | 27 | Cosmos, Blob, optional Redis | +| `snet-ai-pe` | `10.42.0.160/27` | 32 | 27 | Supported AI private endpoints | +| `snet-app-inbound-pe` | `10.42.0.192/28` | 16 | 11 | App inbound endpoint | +| **Unallocated** | **`10.42.0.208-10.42.0.255`** | **48** | - | Future growth | + +The current scripts accept one integration and one shared endpoint subnet. Existing-network reuse does not automatically distribute endpoints into the reference subnets; segmented placement needs IaC customization or enterprise network tooling. Draw PaaS services outside the VNet and endpoint NICs inside; draw DNS zone links to the VNet rather than nesting zones inside subnets. + +Commercial can use Managed Redis and supported Video Indexer private endpoints. Government uses classic Redis and has no Video Indexer PE in the deployed API profile. Both can opt into AMPLS. See the guide's cloud DNS/AMPLS matrix for exact labels. The Visio files remain user-owned and must be updated deliberately to match the selected layout. diff --git a/artifacts/architecture.vsdx b/artifacts/architecture.vsdx index ed256e695a03573aa5de4bf66909f4d8255d0879..69f84a57796ac502ac4d08c2e54a3258dfb74181 100644 GIT binary patch delta 95700 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Μm5W4``Xt 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§lmwU2*pIO1Y-ZuPD$G8_ez&jRzc+{EcXpk4 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 F{{b=l+kyZ9 delta 83034 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 LfaewxB=!FRpk@X= diff --git a/deployers/azure.yaml b/deployers/azure.yaml index e1f47aedd..93ccda002 100644 --- a/deployers/azure.yaml +++ b/deployers/azure.yaml @@ -900,6 +900,16 @@ hooks: fi echo "" + if [ -n "${var_redisCacheResourceId}" ]; then + redis_api_version="2024-11-01" + if [ "${var_redisCacheKind}" = "managed" ]; then + redis_api_version="2025-07-01" + fi + echo "Disabling public network access for Redis..." + az resource update --ids "${var_redisCacheResourceId}" \ + --api-version "${redis_api_version}" \ + --set properties.publicNetworkAccess=Disabled > /dev/null + fi echo "✓ Private networking configured successfully" else echo "" @@ -943,6 +953,8 @@ hooks: 'var_cosmosDb_uri', 'var_enablePrivateNetworking', 'var_keyVaultName', + 'var_redisCacheResourceId', + 'var_redisCacheKind', 'var_rgName', 'var_subscriptionId', 'var_webService' @@ -1086,6 +1098,14 @@ hooks: } Write-Host "✓ Web Application public access disabled" + if (-not [string]::IsNullOrWhiteSpace($env:var_redisCacheResourceId)) { + $redisApiVersion = if ($env:var_redisCacheKind -eq 'managed') { '2025-07-01' } else { '2024-11-01' } + Write-Host 'Disabling public network access for Redis...' + az resource update --ids $env:var_redisCacheResourceId --api-version $redisApiVersion --set properties.publicNetworkAccess=Disabled | Out-Null + if ($LASTEXITCODE -ne 0) { + throw 'Failed to disable Redis public access.' + } + } Write-Host "" Write-Host "✓ Private networking configured successfully" } else { diff --git a/deployers/azurecli/README.md b/deployers/azurecli/README.md index fc6128009..75c3a8202 100644 --- a/deployers/azurecli/README.md +++ b/deployers/azurecli/README.md @@ -201,6 +201,7 @@ The script contains editable configuration variables for: - Azure OpenAI GPT and embedding model names, versions, deployment names, and capacities - optional Entra security group creation - `param_EnablePrivateNetworking` +- `param_EnableAzureMonitorPrivateLink` (default false; requires private networking) - `param_ExistingVirtualNetworkId` - `param_ExistingAppServiceSubnetId` - `param_ExistingPrivateEndpointSubnetId` @@ -211,6 +212,14 @@ The script contains editable configuration variables for: Review those values before running the script. +### AMPLS and Fresh Versus Existing Networks + +Set `$param_EnableAzureMonitorPrivateLink = $true` to opt into private-only Application Insights and Log Analytics in either cloud. The script deploys the generated [shared template](../bicep/modules/azureMonitorPrivateNetworking.json), then disables public ingestion/queries only after that deployment succeeds. Keep the adjacent Bicep directory available. Monitoring clients need private routing/DNS and a `/27` or larger endpoint subnet with sufficient free IPs. Use `/26` or larger when sharing a platform subnet with ACR and Key Vault. Coordinate one AMPLS per shared DNS boundary. + +A fresh private-network deployment without existing network IDs creates `10.0.0.0/21` and two `/24` subnets. The [Commercial tables](../../docs/guides/enterprise-networking.md#networking-in-azure-commercial) and [Government tables](../../docs/guides/enterprise-networking.md#networking-in-azure-government) show script defaults and the compact `10.42.0.0/24` enterprise reference. Existing-network reuse still directs endpoints to one supplied subnet; segmented placement requires customization. + +DNS reuse accepts `monitor`, `monitorOms`, `monitorOds`, `monitorAgent`, and `monitorAdx` (Government), with `blobStorage` and `videoIndexer`, using `zoneResourceId`/`createVNetLink`. Government Key Vault uses `privatelink.vaultcore.usgovcloudapi.net`; Commercial Video Indexer uses `privatelink.api.videoindexer.ai`. The Government Video Indexer profile has no PE, and this script does not provision Redis. + ## Azure OpenAI deployment types and quota handling The Azure CLI deployer can now create the default GPT and embedding model deployments for the Azure OpenAI account it creates or reuses. The Azure OpenAI account still uses the Cognitive Services account SKU, but the model deployments themselves use `Standard`, `DatazoneStandard`, or `GlobalStandard`. diff --git a/deployers/azurecli/deploy-simplechat.ps1 b/deployers/azurecli/deploy-simplechat.ps1 index d3d439a89..91f0358ef 100644 --- a/deployers/azurecli/deploy-simplechat.ps1 +++ b/deployers/azurecli/deploy-simplechat.ps1 @@ -107,7 +107,7 @@ For Azure Government, use the corresponding sovereign-cloud private DNS zones su - `privatelink.search.azure.us` - `privatelink.openai.azure.us` - `privatelink.cognitiveservices.azure.us` -- `privatelink.vaultcore.azure.us` +- `privatelink.vaultcore.usgovcloudapi.net` - `privatelink.azurecr.us` For detailed private networking guidance, see: @@ -217,6 +217,7 @@ $param_Existing_ResourceGroupName = "RG-SimpleChat-Experiemental" # Leave empty # Private networking $param_EnablePrivateNetworking = $false +$param_EnableAzureMonitorPrivateLink = $false $param_ExistingVirtualNetworkId = "" # Optional existing VNet resource ID to reuse. $param_ExistingAppServiceSubnetId = "" # Required when reusing an existing VNet for App Service regional integration. $param_ExistingPrivateEndpointSubnetId = "" # Required when reusing an existing VNet for private endpoints. @@ -293,7 +294,7 @@ if ($globalWhichAzurePlatform -eq "AzureCloud") { cognitiveServices = "privatelink.cognitiveservices.azure.us" containerRegistry = "privatelink.azurecr.us" cosmosDb = "privatelink.documents.azure.us" - keyVault = "privatelink.vaultcore.azure.us" + keyVault = "privatelink.vaultcore.usgovcloudapi.net" openAi = "privatelink.openai.azure.us" webSites = "privatelink.azurewebsites.us" } @@ -329,7 +330,12 @@ if ($globalWhichAzurePlatform -eq "AzureCloud") { $paramDateTime_ScriptStart = Get-Date $paramDateTimeStamp = ($paramDateTime_ScriptStart).ToString("yyyy-MM-dd HH:mm:ss") -$script:SupportedPrivateDnsZoneKeys = @("keyVault", "cosmosDb", "containerRegistry", "aiSearch", "blobStorage", "cognitiveServices", "openAi", "webSites") +$script:SupportedPrivateDnsZoneKeys = @("keyVault", "cosmosDb", "containerRegistry", "aiSearch", "blobStorage", "cognitiveServices", "openAi", "webSites", "videoIndexer", "monitor", "monitorOms", "monitorOds", "monitorAgent", "monitorAdx") +$monitorDnsNames = Get-Content (Join-Path $PSScriptRoot '../bicep/modules/privateDNSZones.json') -Raw | ConvertFrom-Json -AsHashtable +$dnsCloud = if ($globalWhichAzurePlatform -eq 'AzureUSGovernment') { 'azureusgovernment' } else { 'azurecloud' } +foreach ($zoneKey in @('videoIndexer', 'monitor', 'monitorOms', 'monitorOds', 'monitorAgent', 'monitorAdx')) { + $script:PrivateDnsZoneNames[$zoneKey] = $monitorDnsNames[$dnsCloud][$zoneKey] +} $script:PrivateDnsZoneLinkSuffixes = @{ aiSearch = "searchService" blobStorage = "storage" @@ -339,6 +345,7 @@ $script:PrivateDnsZoneLinkSuffixes = @{ keyVault = "kv" openAi = "openAiService" webSites = "webApp" + videoIndexer = "videoIndexer" } $script:UseExistingPrivateNetwork = $false $script:ResolvedVirtualNetworkId = "" @@ -1083,7 +1090,10 @@ function Show-PrivateNetworkingChecklist { Write-Host "`nPrivate DNS zone plan:" -ForegroundColor Yellow foreach ($zoneKey in $script:SupportedPrivateDnsZoneKeys) { + if ($zoneKey -like 'monitor*' -and -not $param_EnableAzureMonitorPrivateLink) { continue } + if ($zoneKey -eq 'videoIndexer' -and (-not $param_DeployVideoIndexerService -or $param_VideoIndexerArmApiVersion -ne '2025-04-01' -or $globalWhichAzurePlatform -eq 'AzureUSGovernment')) { continue } $zoneConfig = Get-PrivateDnsZoneConfig -ZoneKey $zoneKey + if ([string]::IsNullOrWhiteSpace($zoneConfig.ZoneName)) { continue } if ([string]::IsNullOrWhiteSpace($zoneConfig.ZoneResourceId)) { $zoneAction = "create in deployment resource group" } else { @@ -1109,6 +1119,9 @@ function Show-PrivateNetworkingChecklist { } function Test-PrivateNetworkingConfiguration { + if ($param_EnableAzureMonitorPrivateLink -and -not $param_EnablePrivateNetworking) { + throw 'param_EnableAzureMonitorPrivateLink requires param_EnablePrivateNetworking=true.' + } if (-not $param_EnablePrivateNetworking) { return } @@ -1281,12 +1294,9 @@ function Ensure-PrivateEndpoint { if ($LASTEXITCODE -ne 0) { Write-Error "Failed to create private endpoint '$privateEndpointName'."; exit 1 } } - $existingDnsZoneGroup = az network private-endpoint dns-zone-group show --resource-group $resourceGroupName --endpoint-name $privateEndpointName --name $dnsZoneGroupName --query id --output tsv 2>$null - if (-not $existingDnsZoneGroup) { - Write-Host "Associating private endpoint '$privateEndpointName' with private DNS zone '$($zoneConfig.ZoneName)'." - az network private-endpoint dns-zone-group create --resource-group $resourceGroupName --endpoint-name $privateEndpointName --name $dnsZoneGroupName --private-dns-zone $zoneResourceId --zone-name $zoneConfig.ZoneName | Out-Null - if ($LASTEXITCODE -ne 0) { Write-Error "Failed to create the DNS zone group for private endpoint '$privateEndpointName'."; exit 1 } - } + Write-Host "Ensuring private endpoint '$privateEndpointName' uses private DNS zone '$($zoneConfig.ZoneName)'." + az network private-endpoint dns-zone-group create --resource-group $resourceGroupName --endpoint-name $privateEndpointName --name $dnsZoneGroupName --private-dns-zone $zoneResourceId --zone-name $zoneConfig.ZoneName | Out-Null + if ($LASTEXITCODE -ne 0) { Write-Error "Failed to reconcile the DNS zone group for private endpoint '$privateEndpointName'."; exit 1 } } function Update-ResourcePropertiesById { @@ -1318,6 +1328,16 @@ function Configure-PrivateNetworkingForDeployment { Ensure-PrivateNetworkingInfrastructure + if ($param_EnableAzureMonitorPrivateLink) { + $subnetJson = az network vnet subnet show --ids $script:ResolvedPrivateEndpointSubnetId --output json + if ($LASTEXITCODE -ne 0) { throw 'Unable to validate the AMPLS subnet.' } + $subnet = $subnetJson | ConvertFrom-Json + $prefixes = if ($subnet.addressPrefixes) { @($subnet.addressPrefixes) } else { @($subnet.addressPrefix) } + $supportedPrefixes = @($prefixes | Where-Object { $_ -match '^\d+\.\d+\.\d+\.\d+/(\d+)$' -and [int]$Matches[1] -le 27 }) + if ($supportedPrefixes.Count -eq 0) { throw 'AMPLS requires an IPv4 subnet of /27 or larger; use /26 or larger when sharing with platform endpoints.' } + Write-Host 'AMPLS opted in: monitoring ingestion and queries will require private routing and DNS. Coordinate one AMPLS per shared DNS boundary.' + } + Write-Host "`n=====> Configuring App Service VNet integration and web app network settings..." Update-ResourcePropertiesById -ResourceId $(az webapp show --name $appServiceName --resource-group $resourceGroupName --query id --output tsv) -PropertyAssignments @( "properties.publicNetworkAccess=Disabled", @@ -1345,6 +1365,35 @@ function Configure-PrivateNetworkingForDeployment { Ensure-PrivateEndpoint -NameSuffix "docintel" -ServiceResourceId $docIntelResourceId -GroupId "account" -ZoneKey "cognitiveServices" Ensure-PrivateEndpoint -NameSuffix "webapp" -ServiceResourceId $webAppResourceId -GroupId "sites" -ZoneKey "webSites" + if ($param_DeployVideoIndexerService -and $param_VideoIndexerArmApiVersion -eq '2025-04-01' -and $globalWhichAzurePlatform -ne 'AzureUSGovernment') { + $videoIndexerResourceId = az resource show --resource-group $resourceGroupName --name $videoIndexerName --resource-type Microsoft.VideoIndexer/accounts --api-version $param_VideoIndexerArmApiVersion --query id -o tsv + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($videoIndexerResourceId)) { throw 'Unable to resolve Video Indexer for its private endpoint.' } + Ensure-PrivateEndpoint -NameSuffix 'videoIndexer' -ServiceResourceId $videoIndexerResourceId -GroupId 'account' -ZoneKey 'videoIndexer' + } + + if ($param_EnableAzureMonitorPrivateLink) { + $appInsightsResourceId = az monitor app-insights component show --app $appInsightsName --resource-group $resourceGroupName --query id -o tsv + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($appInsightsResourceId) -or [string]::IsNullOrWhiteSpace($logAnalyticsWorkspaceId)) { throw 'Unable to resolve monitoring resources for AMPLS.' } + $monitorParameters = @{ + location = @{ value = $paramLocation } + appName = @{ value = $paramBaseName } + environment = @{ value = $paramEnvironment } + tags = @{ value = $tags } + virtualNetworkId = @{ value = $script:ResolvedVirtualNetworkId } + privateEndpointSubnetId = @{ value = $script:ResolvedPrivateEndpointSubnetId } + privateDnsZoneConfigs = @{ value = $param_PrivateDnsZoneConfigs } + blobStorageDnsZoneId = @{ value = (Ensure-PrivateDnsZone -ZoneKey 'blobStorage') } + monitoringResourceIds = @{ value = @($logAnalyticsWorkspaceId, $appInsightsResourceId) } + } | ConvertTo-Json -Depth 20 -Compress + $monitorTemplate = Join-Path $PSScriptRoot '../bicep/modules/azureMonitorPrivateNetworking.json' + az deployment group create --resource-group $resourceGroupName --name azureMonitorPrivateNetworking --template-file $monitorTemplate --parameters $monitorParameters | Out-Null + if ($LASTEXITCODE -ne 0) { throw 'Failed to deploy AMPLS; monitoring public access has not been disabled.' } + foreach ($monitorResourceId in @($logAnalyticsWorkspaceId, $appInsightsResourceId)) { + az resource update --ids $monitorResourceId --set properties.publicNetworkAccessForIngestion=Disabled properties.publicNetworkAccessForQuery=Disabled | Out-Null + if ($LASTEXITCODE -ne 0) { throw 'Failed to disable monitoring public access.' } + } + } + if (-not [string]::IsNullOrWhiteSpace($OpenAiResourceId)) { Ensure-PrivateEndpoint -NameSuffix "openai" -ServiceResourceId $OpenAiResourceId -GroupId "account" -ZoneKey "openAi" } else { diff --git a/deployers/bicep/README.md b/deployers/bicep/README.md index 808143c36..856409ae9 100644 --- a/deployers/bicep/README.md +++ b/deployers/bicep/README.md @@ -498,7 +498,15 @@ Example `privateDnsZoneConfigs` value: } ``` -Supported `privateDnsZoneConfigs` keys: `keyVault`, `cosmosDb`, `containerRegistry`, `aiSearch`, `blobStorage`, `cognitiveServices`, `openAi`, and `webSites`. +Supported `privateDnsZoneConfigs` keys: `keyVault`, `cosmosDb`, `containerRegistry`, `aiSearch`, `blobStorage`, `cognitiveServices`, `openAi`, `webSites`, `redis`, `videoIndexer`, `monitor`, `monitorOms`, `monitorOds`, `monitorAgent`, and `monitorAdx` (Government). + +#### Optional AMPLS and Subnet References + +Implemented in version **0.261.333**, deployer **1.0.34** ([#1765](https://github.com/microsoft/simplechat/issues/1765)). `enableAzureMonitorPrivateLink` defaults to false. Opt in with `azd env set ENABLE_AZURE_MONITOR_PRIVATE_LINK true` and `ENABLE_PRIVATE_NETWORKING=true`. This creates one AMPLS endpoint, associates Application Insights and Log Analytics, and disables public ingestion and queries. Use a `/27` or larger endpoint subnet; `/26` or larger is recommended when sharing with platform endpoints. All monitoring clients need private routing and DNS; coordinate one AMPLS per shared DNS boundary. + +A **fresh private-network deployment without existing network IDs** creates `10.0.0.0/21`, `AppServiceIntegration` at `10.0.0.0/24`, and `PrivateEndpoints` at `10.0.2.0/24`, in either cloud. See the [Commercial tables](../../docs/guides/enterprise-networking.md#networking-in-azure-commercial) and [Government tables](../../docs/guides/enterprise-networking.md#networking-in-azure-government) for counts and the compact `10.42.0.0/24` existing-network reference. Existing-VNet reuse still accepts one shared endpoint subnet; the segmented reference requires endpoint placement customization. + +Optional Redis now gets an offering-aware endpoint (`redisEnterprise` for Managed Redis, `redisCache` for classic). `azd up` disables public access during `postup`, after bootstrap; `azd provision` or `azd deploy` alone does not run this final lockdown. Later provisioning resets Managed Redis bootstrap access and needs another lockdown. Commercial Video Indexer uses `privatelink.api.videoindexer.ai`; Government's API profile still has no Video Indexer PE. Government Key Vault uses `privatelink.vaultcore.usgovcloudapi.net`. If you use AZD, set `PRIVATE_DNS_ZONE_CONFIGS` to the JSON object above instead of editing the parameter file directly. diff --git a/deployers/bicep/main.bicep b/deployers/bicep/main.bicep index f9e551767..6d8f76c65 100644 --- a/deployers/bicep/main.bicep +++ b/deployers/bicep/main.bicep @@ -185,6 +185,9 @@ param enableDiagLogging bool - Default is false''') param enablePrivateNetworking bool +@description('Opt in to private-only Application Insights and Log Analytics through AMPLS. Requires enablePrivateNetworking and private DNS/routing for all monitoring clients.') +param enableAzureMonitorPrivateLink bool = false + @description('''Optional existing virtual network resource ID to reuse when private networking is enabled. - May reference a virtual network in the same or another resource group or subscription - Leave blank to create a new virtual network''') @@ -205,7 +208,7 @@ param existingPrivateEndpointSubnetId string = '' - For each supported key, provide: - zoneResourceId: Optional existing private DNS zone resource ID to reuse - createVNetLink: Optional bool, defaults to true. Set to false if the customer manages the VNet link separately -- Supported keys: keyVault, cosmosDb, containerRegistry, aiSearch, blobStorage, cognitiveServices, openAi, webSites''') +- Supported keys: keyVault, cosmosDb, containerRegistry, aiSearch, blobStorage, cognitiveServices, openAi, webSites, redis, videoIndexer, monitor, monitorOms, monitorOds, monitorAgent, monitorAdx''') param privateDnsZoneConfigs object = {} @description('''Optional existing Azure OpenAI or Azure AI Foundry OpenAI-compatible endpoint. @@ -471,6 +474,7 @@ module logAnalytics 'modules/logAnalyticsWorkspace.bicep' = { appName: appName environment: environment tags: tags + enableAzureMonitorPrivateLink: enableAzureMonitorPrivateLink } } @@ -486,6 +490,7 @@ module applicationInsights 'modules/applicationInsights.bicep' = { environment: environment tags: tags logAnalyticsId: logAnalytics.outputs.logAnalyticsId + enableAzureMonitorPrivateLink: enableAzureMonitorPrivateLink } } @@ -924,7 +929,7 @@ module setNativeWebAppPermissions 'modules/setNativeWebAppPermissions.bicep' = i //========================================================= // configure private networking //========================================================= -module privateNetworking 'modules/privateNetworking.bicep' = if (enablePrivateNetworking) { +module privateNetworking 'modules/privateNetworking.bicep' = if (enablePrivateNetworking || enableAzureMonitorPrivateLink) { name: 'privateNetworking' scope: rg params: { @@ -932,6 +937,8 @@ module privateNetworking 'modules/privateNetworking.bicep' = if (enablePrivateNe virtualNetworkId: resolvedVirtualNetworkId privateEndpointSubnetId: resolvedPrivateEndpointSubnetId privateDnsZoneConfigs: privateDnsZoneConfigs + enableAzureMonitorPrivateLink: enableAzureMonitorPrivateLink + monitoringResourceIds: [logAnalytics.outputs.logAnalyticsId, applicationInsights.outputs.appInsightsId] location: location appName: appName @@ -948,6 +955,8 @@ module privateNetworking 'modules/privateNetworking.bicep' = if (enablePrivateNe openAIResourceGroupName: openAI.outputs.openAIResourceGroup openAISubscriptionId: openAI.outputs.openAISubscriptionId webAppName: appService.outputs.name + redisCacheResourceId: deployRedisCache ? redisCache!.outputs.redisCacheResourceId : '' + redisCacheKind: redisCacheKind #disable-next-line BCP318 // expect one value to be null contentSafetyName: deployContentSafety ? contentSafety.outputs.contentSafetyName : '' @@ -987,6 +996,7 @@ output var_openAISubscriptionId string = openAI.outputs.openAISubscriptionId #disable-next-line BCP318 // expect one value to be null output var_redisCacheHostName string = deployRedisCache ? redisCache.outputs.redisCacheHostName : '' output var_redisCacheKind string = deployRedisCache ? redisCacheKind : '' +output var_redisCacheResourceId string = deployRedisCache ? redisCache!.outputs.redisCacheResourceId : '' #disable-next-line BCP318 // expect one value to be null output var_redisCachePort string = deployRedisCache ? string(redisCache.outputs.redisCachePort) : '' output var_rgName string = rgName diff --git a/deployers/bicep/main.json b/deployers/bicep/main.json index a825cda9d..bc23ca05d 100644 --- a/deployers/bicep/main.json +++ b/deployers/bicep/main.json @@ -6,7 +6,7 @@ "_generator": { "name": "bicep", "version": "0.44.1.10279", - "templateHash": "3025497095812405668" + "templateHash": "15288510406592690340" } }, "parameters": { @@ -240,6 +240,13 @@ "description": "Enable private endpoints and virtual network integration for deployed resources. \n- Default is false" } }, + "enableAzureMonitorPrivateLink": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Opt in to private-only Application Insights and Log Analytics through AMPLS. Requires enablePrivateNetworking and private DNS/routing for all monitoring clients." + } + }, "existingVirtualNetworkId": { "type": "string", "defaultValue": "", @@ -265,7 +272,7 @@ "type": "object", "defaultValue": {}, "metadata": { - "description": "Optional per-zone private DNS configuration for private networking.\n- Leave empty to create all private DNS zones locally and create VNet links automatically\n- For each supported key, provide:\n - zoneResourceId: Optional existing private DNS zone resource ID to reuse\n - createVNetLink: Optional bool, defaults to true. Set to false if the customer manages the VNet link separately\n- Supported keys: keyVault, cosmosDb, containerRegistry, aiSearch, blobStorage, cognitiveServices, openAi, webSites" + "description": "Optional per-zone private DNS configuration for private networking.\n- Leave empty to create all private DNS zones locally and create VNet links automatically\n- For each supported key, provide:\n - zoneResourceId: Optional existing private DNS zone resource ID to reuse\n - createVNetLink: Optional bool, defaults to true. Set to false if the customer manages the VNet link separately\n- Supported keys: keyVault, cosmosDb, containerRegistry, aiSearch, blobStorage, cognitiveServices, openAi, webSites, redis, videoIndexer, monitor, monitorOms, monitorOds, monitorAgent, monitorAdx" } }, "existingOpenAIEndpoint": { @@ -741,6 +748,9 @@ }, "tags": { "value": "[variables('tags')]" + }, + "enableAzureMonitorPrivateLink": { + "value": "[parameters('enableAzureMonitorPrivateLink')]" } }, "template": { @@ -750,7 +760,7 @@ "_generator": { "name": "bicep", "version": "0.44.1.10279", - "templateHash": "9084211890269320873" + "templateHash": "1217328589617468086" } }, "parameters": { @@ -765,6 +775,10 @@ }, "tags": { "type": "object" + }, + "enableAzureMonitorPrivateLink": { + "type": "bool", + "defaultValue": false } }, "resources": [ @@ -774,6 +788,8 @@ "name": "[toLower(format('{0}-{1}-la', parameters('appName'), parameters('environment')))]", "location": "[parameters('location')]", "properties": { + "publicNetworkAccessForIngestion": "[if(parameters('enableAzureMonitorPrivateLink'), 'Disabled', 'Enabled')]", + "publicNetworkAccessForQuery": "[if(parameters('enableAzureMonitorPrivateLink'), 'Disabled', 'Enabled')]", "sku": { "name": "PerGB2018" } @@ -818,6 +834,9 @@ }, "logAnalyticsId": { "value": "[reference('logAnalytics').outputs.logAnalyticsId.value]" + }, + "enableAzureMonitorPrivateLink": { + "value": "[parameters('enableAzureMonitorPrivateLink')]" } }, "template": { @@ -827,7 +846,7 @@ "_generator": { "name": "bicep", "version": "0.44.1.10279", - "templateHash": "16116738102101306702" + "templateHash": "4300797827117831457" } }, "parameters": { @@ -845,6 +864,10 @@ }, "logAnalyticsId": { "type": "string" + }, + "enableAzureMonitorPrivateLink": { + "type": "bool", + "defaultValue": false } }, "resources": [ @@ -856,7 +879,9 @@ "kind": "web", "properties": { "Application_Type": "web", - "WorkspaceResourceId": "[parameters('logAnalyticsId')]" + "WorkspaceResourceId": "[parameters('logAnalyticsId')]", + "publicNetworkAccessForIngestion": "[if(parameters('enableAzureMonitorPrivateLink'), 'Disabled', 'Enabled')]", + "publicNetworkAccessForQuery": "[if(parameters('enableAzureMonitorPrivateLink'), 'Disabled', 'Enabled')]" }, "tags": "[parameters('tags')]" } @@ -865,6 +890,10 @@ "appInsightsName": { "type": "string", "value": "[toLower(format('{0}-{1}-ai', parameters('appName'), parameters('environment')))]" + }, + "appInsightsId": { + "type": "string", + "value": "[resourceId('Microsoft.Insights/components', toLower(format('{0}-{1}-ai', parameters('appName'), parameters('environment'))))]" } } } @@ -5330,7 +5359,7 @@ "_generator": { "name": "bicep", "version": "0.44.1.10279", - "templateHash": "8590656076085374678" + "templateHash": "16387019594267169649" } }, "parameters": { @@ -5652,6 +5681,10 @@ "redisCacheKind": { "type": "string", "value": "[parameters('redisCacheKind')]" + }, + "redisCacheResourceId": { + "type": "string", + "value": "[if(variables('deployManagedRedis'), resourceId('Microsoft.Cache/redisEnterprise', variables('redisName')), resourceId('Microsoft.Cache/redis', variables('redisName')))]" } } } @@ -7441,7 +7474,7 @@ ] }, "privateNetworking": { - "condition": "[parameters('enablePrivateNetworking')]", + "condition": "[or(parameters('enablePrivateNetworking'), parameters('enableAzureMonitorPrivateLink'))]", "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", "name": "privateNetworking", @@ -7457,6 +7490,15 @@ "privateDnsZoneConfigs": { "value": "[parameters('privateDnsZoneConfigs')]" }, + "enableAzureMonitorPrivateLink": { + "value": "[parameters('enableAzureMonitorPrivateLink')]" + }, + "monitoringResourceIds": { + "value": [ + "[reference('logAnalytics').outputs.logAnalyticsId.value]", + "[reference('applicationInsights').outputs.appInsightsId.value]" + ] + }, "location": { "value": "[parameters('location')]" }, @@ -7499,6 +7541,10 @@ "webAppName": { "value": "[reference('appService').outputs.name.value]" }, + "redisCacheResourceId": "[if(parameters('deployRedisCache'), createObject('value', reference('redisCache').outputs.redisCacheResourceId.value), createObject('value', ''))]", + "redisCacheKind": { + "value": "[parameters('redisCacheKind')]" + }, "contentSafetyName": "[if(parameters('deployContentSafety'), createObject('value', reference('contentSafety').outputs.contentSafetyName.value), createObject('value', ''))]", "speechServiceName": "[if(parameters('deploySpeechService'), createObject('value', reference('speechService').outputs.speechServiceName.value), createObject('value', ''))]", "videoIndexerName": "[if(parameters('deployVideoIndexerService'), createObject('value', reference('videoIndexerService').outputs.videoIndexerServiceName.value), createObject('value', ''))]", @@ -7513,7 +7559,7 @@ "_generator": { "name": "bicep", "version": "0.44.1.10279", - "templateHash": "16990852046160181859" + "templateHash": "16307164890524009485" } }, "parameters": { @@ -7527,6 +7573,14 @@ "type": "object", "defaultValue": {} }, + "enableAzureMonitorPrivateLink": { + "type": "bool", + "defaultValue": false + }, + "monitoringResourceIds": { + "type": "array", + "defaultValue": [] + }, "location": { "type": "string" }, @@ -7569,6 +7623,14 @@ "webAppName": { "type": "string" }, + "redisCacheResourceId": { + "type": "string", + "defaultValue": "" + }, + "redisCacheKind": { + "type": "string", + "defaultValue": "managed" + }, "contentSafetyName": { "type": "string" }, @@ -7593,7 +7655,15 @@ "cosmosDb": "privatelink.documents.azure.com", "keyVault": "privatelink.vaultcore.azure.net", "openAi": "privatelink.openai.azure.com", - "webSites": "privatelink.azurewebsites.net" + "webSites": "privatelink.azurewebsites.net", + "redisClassic": "privatelink.redis.cache.windows.net", + "redisManaged": "privatelink.redis.azure.net", + "videoIndexer": "privatelink.api.videoindexer.ai", + "monitor": "privatelink.monitor.azure.com", + "monitorOms": "privatelink.oms.opinsights.azure.com", + "monitorOds": "privatelink.ods.opinsights.azure.com", + "monitorAgent": "privatelink.agentsvc.azure-automation.net", + "monitorAdx": "" }, "azureusgovernment": { "aisearch": "privatelink.search.azure.us", @@ -7601,9 +7671,17 @@ "cognitiveServices": "privatelink.cognitiveservices.azure.us", "containerRegistry": "privatelink.azurecr.us", "cosmosDb": "privatelink.documents.azure.us", - "keyVault": "privatelink.vaultcore.azure.us", + "keyVault": "privatelink.vaultcore.usgovcloudapi.net", "openAi": "privatelink.openai.azure.us", - "webSites": "privatelink.azurewebsites.us" + "webSites": "privatelink.azurewebsites.us", + "redisClassic": "privatelink.redis.cache.usgovcloudapi.net", + "redisManaged": "", + "videoIndexer": "", + "monitor": "privatelink.monitor.azure.us", + "monitorOms": "privatelink.oms.opinsights.azure.us", + "monitorOds": "privatelink.ods.opinsights.azure.us", + "monitorAgent": "privatelink.agentsvc.azure-automation.us", + "monitorAdx": "privatelink.adx.monitor.azure.us" } }, "useExternalOpenAIResource": "[and(and(not(equals(parameters('openAIName'), '')), not(empty(parameters('openAIResourceGroupName')))), not(empty(parameters('openAISubscriptionId'))))]", @@ -7617,6 +7695,9 @@ "keyVaultDnsZoneName": "[variables('privateDnsZoneData')[variables('cloudName')].keyVault]", "openAiDnsZoneName": "[variables('privateDnsZoneData')[variables('cloudName')].openAi]", "webSitesDnsZoneName": "[variables('privateDnsZoneData')[variables('cloudName')].webSites]", + "redisDnsZoneName": "[if(equals(parameters('redisCacheKind'), 'managed'), variables('privateDnsZoneData')[variables('cloudName')].redisManaged, variables('privateDnsZoneData')[variables('cloudName')].redisClassic)]", + "videoIndexerDnsZoneName": "[variables('privateDnsZoneData')[variables('cloudName')].videoIndexer]", + "deployVideoIndexerPrivateEndpoint": "[and(and(not(equals(parameters('videoIndexerName'), '')), parameters('videoIndexerSupportsPrivateEndpoints')), not(empty(variables('videoIndexerDnsZoneName'))))]", "defaultPrivateDnsZoneConfig": { "zoneResourceId": "", "createVNetLink": true @@ -7628,13 +7709,16 @@ "blobStoragePrivateDnsZoneConfig": "[if(contains(parameters('privateDnsZoneConfigs'), 'blobStorage'), union(variables('defaultPrivateDnsZoneConfig'), parameters('privateDnsZoneConfigs').blobStorage), variables('defaultPrivateDnsZoneConfig'))]", "cognitiveServicesPrivateDnsZoneConfig": "[if(contains(parameters('privateDnsZoneConfigs'), 'cognitiveServices'), union(variables('defaultPrivateDnsZoneConfig'), parameters('privateDnsZoneConfigs').cognitiveServices), variables('defaultPrivateDnsZoneConfig'))]", "openAiPrivateDnsZoneConfig": "[if(contains(parameters('privateDnsZoneConfigs'), 'openAi'), union(variables('defaultPrivateDnsZoneConfig'), parameters('privateDnsZoneConfigs').openAi), variables('defaultPrivateDnsZoneConfig'))]", - "webSitesPrivateDnsZoneConfig": "[if(contains(parameters('privateDnsZoneConfigs'), 'webSites'), union(variables('defaultPrivateDnsZoneConfig'), parameters('privateDnsZoneConfigs').webSites), variables('defaultPrivateDnsZoneConfig'))]" + "webSitesPrivateDnsZoneConfig": "[if(contains(parameters('privateDnsZoneConfigs'), 'webSites'), union(variables('defaultPrivateDnsZoneConfig'), parameters('privateDnsZoneConfigs').webSites), variables('defaultPrivateDnsZoneConfig'))]", + "redisPrivateDnsZoneConfig": "[if(contains(parameters('privateDnsZoneConfigs'), 'redis'), union(variables('defaultPrivateDnsZoneConfig'), parameters('privateDnsZoneConfigs').redis), variables('defaultPrivateDnsZoneConfig'))]", + "videoIndexerPrivateDnsZoneConfig": "[if(contains(parameters('privateDnsZoneConfigs'), 'videoIndexer'), union(variables('defaultPrivateDnsZoneConfig'), parameters('privateDnsZoneConfigs').videoIndexer), variables('defaultPrivateDnsZoneConfig'))]" }, "resources": [ { + "condition": "[not(empty(parameters('redisCacheResourceId')))]", "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", - "name": "keyVaultDNSZone", + "name": "redisDNSZone", "properties": { "expressionEvaluationOptions": { "scope": "inner" @@ -7642,7 +7726,7 @@ "mode": "Incremental", "parameters": { "zoneName": { - "value": "[variables('keyVaultDnsZoneName')]" + "value": "[variables('redisDnsZoneName')]" }, "appName": { "value": "[parameters('appName')]" @@ -7651,7 +7735,7 @@ "value": "[parameters('environment')]" }, "name": { - "value": "kv" + "value": "redis" }, "vNetId": { "value": "[parameters('virtualNetworkId')]" @@ -7660,10 +7744,10 @@ "value": "[parameters('tags')]" }, "existingZoneResourceId": { - "value": "[variables('keyVaultPrivateDnsZoneConfig').zoneResourceId]" + "value": "[variables('redisPrivateDnsZoneConfig').zoneResourceId]" }, "createVNetLink": { - "value": "[variables('keyVaultPrivateDnsZoneConfig').createVNetLink]" + "value": "[variables('redisPrivateDnsZoneConfig').createVNetLink]" } }, "template": { @@ -7812,9 +7896,10 @@ } }, { + "condition": "[not(empty(parameters('redisCacheResourceId')))]", "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", - "name": "keyVaultPE", + "name": "redisPE", "properties": { "expressionEvaluationOptions": { "scope": "inner" @@ -7822,7 +7907,7 @@ "mode": "Incremental", "parameters": { "name": { - "value": "kv" + "value": "redis" }, "location": { "value": "[parameters('location')]" @@ -7834,19 +7919,19 @@ "value": "[parameters('environment')]" }, "serviceResourceID": { - "value": "[resourceId('Microsoft.KeyVault/vaults', parameters('keyVaultName'))]" + "value": "[parameters('redisCacheResourceId')]" }, "subnetId": { "value": "[parameters('privateEndpointSubnetId')]" }, "groupIDs": { "value": [ - "vault" + "[if(equals(parameters('redisCacheKind'), 'managed'), 'redisEnterprise', 'redisCache')]" ] }, "privateDnsZoneIds": { "value": [ - "[reference(resourceId('Microsoft.Resources/deployments', 'keyVaultDNSZone'), '2025-04-01').outputs.privateDnsZoneId.value]" + "[reference(resourceId('Microsoft.Resources/deployments', 'redisDNSZone'), '2025-04-01').outputs.privateDnsZoneId.value]" ] }, "tags": { @@ -7943,13 +8028,13 @@ } }, "dependsOn": [ - "[resourceId('Microsoft.Resources/deployments', 'keyVaultDNSZone')]" + "[resourceId('Microsoft.Resources/deployments', 'redisDNSZone')]" ] }, { "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", - "name": "cosmosDbDNSZone", + "name": "keyVaultDNSZone", "properties": { "expressionEvaluationOptions": { "scope": "inner" @@ -7957,7 +8042,7 @@ "mode": "Incremental", "parameters": { "zoneName": { - "value": "[variables('cosmosDbDnsZoneName')]" + "value": "[variables('keyVaultDnsZoneName')]" }, "appName": { "value": "[parameters('appName')]" @@ -7966,7 +8051,7 @@ "value": "[parameters('environment')]" }, "name": { - "value": "cosmosDb" + "value": "kv" }, "vNetId": { "value": "[parameters('virtualNetworkId')]" @@ -7975,10 +8060,10 @@ "value": "[parameters('tags')]" }, "existingZoneResourceId": { - "value": "[variables('cosmosDbPrivateDnsZoneConfig').zoneResourceId]" + "value": "[variables('keyVaultPrivateDnsZoneConfig').zoneResourceId]" }, "createVNetLink": { - "value": "[variables('cosmosDbPrivateDnsZoneConfig').createVNetLink]" + "value": "[variables('keyVaultPrivateDnsZoneConfig').createVNetLink]" } }, "template": { @@ -8129,7 +8214,7 @@ { "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", - "name": "cosmosDbPE", + "name": "keyVaultPE", "properties": { "expressionEvaluationOptions": { "scope": "inner" @@ -8137,7 +8222,7 @@ "mode": "Incremental", "parameters": { "name": { - "value": "cosmosDb" + "value": "kv" }, "location": { "value": "[parameters('location')]" @@ -8149,19 +8234,19 @@ "value": "[parameters('environment')]" }, "serviceResourceID": { - "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBName'))]" + "value": "[resourceId('Microsoft.KeyVault/vaults', parameters('keyVaultName'))]" }, "subnetId": { "value": "[parameters('privateEndpointSubnetId')]" }, "groupIDs": { "value": [ - "sql" + "vault" ] }, "privateDnsZoneIds": { "value": [ - "[reference(resourceId('Microsoft.Resources/deployments', 'cosmosDbDNSZone'), '2025-04-01').outputs.privateDnsZoneId.value]" + "[reference(resourceId('Microsoft.Resources/deployments', 'keyVaultDNSZone'), '2025-04-01').outputs.privateDnsZoneId.value]" ] }, "tags": { @@ -8258,13 +8343,13 @@ } }, "dependsOn": [ - "[resourceId('Microsoft.Resources/deployments', 'cosmosDbDNSZone')]" + "[resourceId('Microsoft.Resources/deployments', 'keyVaultDNSZone')]" ] }, { "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", - "name": "acrDNSZone", + "name": "cosmosDbDNSZone", "properties": { "expressionEvaluationOptions": { "scope": "inner" @@ -8272,7 +8357,7 @@ "mode": "Incremental", "parameters": { "zoneName": { - "value": "[variables('containerRegistryDnsZoneName')]" + "value": "[variables('cosmosDbDnsZoneName')]" }, "appName": { "value": "[parameters('appName')]" @@ -8281,7 +8366,7 @@ "value": "[parameters('environment')]" }, "name": { - "value": "acr" + "value": "cosmosDb" }, "vNetId": { "value": "[parameters('virtualNetworkId')]" @@ -8290,10 +8375,10 @@ "value": "[parameters('tags')]" }, "existingZoneResourceId": { - "value": "[variables('containerRegistryPrivateDnsZoneConfig').zoneResourceId]" + "value": "[variables('cosmosDbPrivateDnsZoneConfig').zoneResourceId]" }, "createVNetLink": { - "value": "[variables('containerRegistryPrivateDnsZoneConfig').createVNetLink]" + "value": "[variables('cosmosDbPrivateDnsZoneConfig').createVNetLink]" } }, "template": { @@ -8444,7 +8529,7 @@ { "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", - "name": "acrPE", + "name": "cosmosDbPE", "properties": { "expressionEvaluationOptions": { "scope": "inner" @@ -8452,7 +8537,7 @@ "mode": "Incremental", "parameters": { "name": { - "value": "acr" + "value": "cosmosDb" }, "location": { "value": "[parameters('location')]" @@ -8464,19 +8549,19 @@ "value": "[parameters('environment')]" }, "serviceResourceID": { - "value": "[resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName'))]" + "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDBName'))]" }, "subnetId": { "value": "[parameters('privateEndpointSubnetId')]" }, "groupIDs": { "value": [ - "registry" + "sql" ] }, "privateDnsZoneIds": { "value": [ - "[reference(resourceId('Microsoft.Resources/deployments', 'acrDNSZone'), '2025-04-01').outputs.privateDnsZoneId.value]" + "[reference(resourceId('Microsoft.Resources/deployments', 'cosmosDbDNSZone'), '2025-04-01').outputs.privateDnsZoneId.value]" ] }, "tags": { @@ -8573,13 +8658,13 @@ } }, "dependsOn": [ - "[resourceId('Microsoft.Resources/deployments', 'acrDNSZone')]" + "[resourceId('Microsoft.Resources/deployments', 'cosmosDbDNSZone')]" ] }, { "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", - "name": "searchServiceDNSZone", + "name": "acrDNSZone", "properties": { "expressionEvaluationOptions": { "scope": "inner" @@ -8587,7 +8672,7 @@ "mode": "Incremental", "parameters": { "zoneName": { - "value": "[variables('aiSearchDnsZoneName')]" + "value": "[variables('containerRegistryDnsZoneName')]" }, "appName": { "value": "[parameters('appName')]" @@ -8596,7 +8681,7 @@ "value": "[parameters('environment')]" }, "name": { - "value": "searchService" + "value": "acr" }, "vNetId": { "value": "[parameters('virtualNetworkId')]" @@ -8605,10 +8690,10 @@ "value": "[parameters('tags')]" }, "existingZoneResourceId": { - "value": "[variables('aiSearchPrivateDnsZoneConfig').zoneResourceId]" + "value": "[variables('containerRegistryPrivateDnsZoneConfig').zoneResourceId]" }, "createVNetLink": { - "value": "[variables('aiSearchPrivateDnsZoneConfig').createVNetLink]" + "value": "[variables('containerRegistryPrivateDnsZoneConfig').createVNetLink]" } }, "template": { @@ -8759,7 +8844,7 @@ { "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", - "name": "searchServicePE", + "name": "acrPE", "properties": { "expressionEvaluationOptions": { "scope": "inner" @@ -8767,7 +8852,7 @@ "mode": "Incremental", "parameters": { "name": { - "value": "searchService" + "value": "acr" }, "location": { "value": "[parameters('location')]" @@ -8779,19 +8864,19 @@ "value": "[parameters('environment')]" }, "serviceResourceID": { - "value": "[resourceId('Microsoft.Search/searchServices', parameters('searchServiceName'))]" + "value": "[resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName'))]" }, "subnetId": { "value": "[parameters('privateEndpointSubnetId')]" }, "groupIDs": { "value": [ - "searchService" + "registry" ] }, "privateDnsZoneIds": { "value": [ - "[reference(resourceId('Microsoft.Resources/deployments', 'searchServiceDNSZone'), '2025-04-01').outputs.privateDnsZoneId.value]" + "[reference(resourceId('Microsoft.Resources/deployments', 'acrDNSZone'), '2025-04-01').outputs.privateDnsZoneId.value]" ] }, "tags": { @@ -8888,13 +8973,13 @@ } }, "dependsOn": [ - "[resourceId('Microsoft.Resources/deployments', 'searchServiceDNSZone')]" + "[resourceId('Microsoft.Resources/deployments', 'acrDNSZone')]" ] }, { "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", - "name": "docIntelDNSZone", + "name": "searchServiceDNSZone", "properties": { "expressionEvaluationOptions": { "scope": "inner" @@ -8902,7 +8987,7 @@ "mode": "Incremental", "parameters": { "zoneName": { - "value": "[variables('cognitiveServicesDnsZoneName')]" + "value": "[variables('aiSearchDnsZoneName')]" }, "appName": { "value": "[parameters('appName')]" @@ -8911,7 +8996,7 @@ "value": "[parameters('environment')]" }, "name": { - "value": "docIntelService" + "value": "searchService" }, "vNetId": { "value": "[parameters('virtualNetworkId')]" @@ -8920,10 +9005,10 @@ "value": "[parameters('tags')]" }, "existingZoneResourceId": { - "value": "[variables('cognitiveServicesPrivateDnsZoneConfig').zoneResourceId]" + "value": "[variables('aiSearchPrivateDnsZoneConfig').zoneResourceId]" }, "createVNetLink": { - "value": "[variables('cognitiveServicesPrivateDnsZoneConfig').createVNetLink]" + "value": "[variables('aiSearchPrivateDnsZoneConfig').createVNetLink]" } }, "template": { @@ -9074,7 +9159,7 @@ { "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", - "name": "docIntelPE", + "name": "searchServicePE", "properties": { "expressionEvaluationOptions": { "scope": "inner" @@ -9082,7 +9167,7 @@ "mode": "Incremental", "parameters": { "name": { - "value": "docIntelService" + "value": "searchService" }, "location": { "value": "[parameters('location')]" @@ -9094,19 +9179,19 @@ "value": "[parameters('environment')]" }, "serviceResourceID": { - "value": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('docIntelName'))]" + "value": "[resourceId('Microsoft.Search/searchServices', parameters('searchServiceName'))]" }, "subnetId": { "value": "[parameters('privateEndpointSubnetId')]" }, "groupIDs": { "value": [ - "account" + "searchService" ] }, "privateDnsZoneIds": { "value": [ - "[reference(resourceId('Microsoft.Resources/deployments', 'docIntelDNSZone'), '2025-04-01').outputs.privateDnsZoneId.value]" + "[reference(resourceId('Microsoft.Resources/deployments', 'searchServiceDNSZone'), '2025-04-01').outputs.privateDnsZoneId.value]" ] }, "tags": { @@ -9203,13 +9288,13 @@ } }, "dependsOn": [ - "[resourceId('Microsoft.Resources/deployments', 'docIntelDNSZone')]" + "[resourceId('Microsoft.Resources/deployments', 'searchServiceDNSZone')]" ] }, { "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", - "name": "storageAccountDNSZone", + "name": "docIntelDNSZone", "properties": { "expressionEvaluationOptions": { "scope": "inner" @@ -9217,7 +9302,7 @@ "mode": "Incremental", "parameters": { "zoneName": { - "value": "[variables('blobStorageDnsZoneName')]" + "value": "[variables('cognitiveServicesDnsZoneName')]" }, "appName": { "value": "[parameters('appName')]" @@ -9226,7 +9311,7 @@ "value": "[parameters('environment')]" }, "name": { - "value": "storage" + "value": "docIntelService" }, "vNetId": { "value": "[parameters('virtualNetworkId')]" @@ -9235,10 +9320,10 @@ "value": "[parameters('tags')]" }, "existingZoneResourceId": { - "value": "[variables('blobStoragePrivateDnsZoneConfig').zoneResourceId]" + "value": "[variables('cognitiveServicesPrivateDnsZoneConfig').zoneResourceId]" }, "createVNetLink": { - "value": "[variables('blobStoragePrivateDnsZoneConfig').createVNetLink]" + "value": "[variables('cognitiveServicesPrivateDnsZoneConfig').createVNetLink]" } }, "template": { @@ -9389,7 +9474,7 @@ { "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", - "name": "storageAccountPE", + "name": "docIntelPE", "properties": { "expressionEvaluationOptions": { "scope": "inner" @@ -9397,7 +9482,7 @@ "mode": "Incremental", "parameters": { "name": { - "value": "storageAccount" + "value": "docIntelService" }, "location": { "value": "[parameters('location')]" @@ -9409,19 +9494,19 @@ "value": "[parameters('environment')]" }, "serviceResourceID": { - "value": "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]" + "value": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('docIntelName'))]" }, "subnetId": { "value": "[parameters('privateEndpointSubnetId')]" }, "groupIDs": { "value": [ - "blob" + "account" ] }, "privateDnsZoneIds": { "value": [ - "[reference(resourceId('Microsoft.Resources/deployments', 'storageAccountDNSZone'), '2025-04-01').outputs.privateDnsZoneId.value]" + "[reference(resourceId('Microsoft.Resources/deployments', 'docIntelDNSZone'), '2025-04-01').outputs.privateDnsZoneId.value]" ] }, "tags": { @@ -9518,14 +9603,13 @@ } }, "dependsOn": [ - "[resourceId('Microsoft.Resources/deployments', 'storageAccountDNSZone')]" + "[resourceId('Microsoft.Resources/deployments', 'docIntelDNSZone')]" ] }, { - "condition": "[not(equals(parameters('openAIName'), ''))]", "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", - "name": "openAiDNSZone", + "name": "storageAccountDNSZone", "properties": { "expressionEvaluationOptions": { "scope": "inner" @@ -9533,7 +9617,7 @@ "mode": "Incremental", "parameters": { "zoneName": { - "value": "[variables('openAiDnsZoneName')]" + "value": "[variables('blobStorageDnsZoneName')]" }, "appName": { "value": "[parameters('appName')]" @@ -9542,7 +9626,7 @@ "value": "[parameters('environment')]" }, "name": { - "value": "openAiService" + "value": "storage" }, "vNetId": { "value": "[parameters('virtualNetworkId')]" @@ -9551,10 +9635,10 @@ "value": "[parameters('tags')]" }, "existingZoneResourceId": { - "value": "[variables('openAiPrivateDnsZoneConfig').zoneResourceId]" + "value": "[variables('blobStoragePrivateDnsZoneConfig').zoneResourceId]" }, "createVNetLink": { - "value": "[variables('openAiPrivateDnsZoneConfig').createVNetLink]" + "value": "[variables('blobStoragePrivateDnsZoneConfig').createVNetLink]" } }, "template": { @@ -9703,10 +9787,9 @@ } }, { - "condition": "[not(equals(parameters('openAIName'), ''))]", "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", - "name": "openAiPE", + "name": "storageAccountPE", "properties": { "expressionEvaluationOptions": { "scope": "inner" @@ -9714,7 +9797,7 @@ "mode": "Incremental", "parameters": { "name": { - "value": "openAiService" + "value": "storageAccount" }, "location": { "value": "[parameters('location')]" @@ -9725,18 +9808,20 @@ "environment": { "value": "[parameters('environment')]" }, - "serviceResourceID": "[if(variables('useExternalOpenAIResource'), createObject('value', extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('openAISubscriptionId'), parameters('openAIResourceGroupName')), 'Microsoft.CognitiveServices/accounts', parameters('openAIName'))), createObject('value', resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName'))))]", + "serviceResourceID": { + "value": "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]" + }, "subnetId": { "value": "[parameters('privateEndpointSubnetId')]" }, "groupIDs": { "value": [ - "account" + "blob" ] }, "privateDnsZoneIds": { "value": [ - "[reference(resourceId('Microsoft.Resources/deployments', 'openAiDNSZone'), '2025-04-01').outputs.privateDnsZoneId.value]" + "[reference(resourceId('Microsoft.Resources/deployments', 'storageAccountDNSZone'), '2025-04-01').outputs.privateDnsZoneId.value]" ] }, "tags": { @@ -9833,21 +9918,22 @@ } }, "dependsOn": [ - "[resourceId('Microsoft.Resources/deployments', 'openAiDNSZone')]" + "[resourceId('Microsoft.Resources/deployments', 'storageAccountDNSZone')]" ] }, { + "condition": "[parameters('enableAzureMonitorPrivateLink')]", "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", - "name": "webAppDNSZone", + "name": "azureMonitorPrivateNetworking", "properties": { "expressionEvaluationOptions": { "scope": "inner" }, "mode": "Incremental", "parameters": { - "zoneName": { - "value": "[variables('webSitesDnsZoneName')]" + "location": { + "value": "[parameters('location')]" }, "appName": { "value": "[parameters('appName')]" @@ -9855,20 +9941,23 @@ "environment": { "value": "[parameters('environment')]" }, - "name": { - "value": "webApp" + "tags": { + "value": "[parameters('tags')]" }, - "vNetId": { + "virtualNetworkId": { "value": "[parameters('virtualNetworkId')]" }, - "tags": { - "value": "[parameters('tags')]" + "privateEndpointSubnetId": { + "value": "[parameters('privateEndpointSubnetId')]" }, - "existingZoneResourceId": { - "value": "[variables('webSitesPrivateDnsZoneConfig').zoneResourceId]" + "privateDnsZoneConfigs": { + "value": "[parameters('privateDnsZoneConfigs')]" }, - "createVNetLink": { - "value": "[variables('webSitesPrivateDnsZoneConfig').createVNetLink]" + "blobStorageDnsZoneId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'storageAccountDNSZone'), '2025-04-01').outputs.privateDnsZoneId.value]" + }, + "monitoringResourceIds": { + "value": "[parameters('monitoringResourceIds')]" } }, "template": { @@ -9878,11 +9967,11 @@ "_generator": { "name": "bicep", "version": "0.44.1.10279", - "templateHash": "6946008567642736236" + "templateHash": "1191765375410113004" } }, "parameters": { - "zoneName": { + "location": { "type": "string" }, "appName": { @@ -9891,62 +9980,939 @@ "environment": { "type": "string" }, - "name": { - "type": "string" - }, - "vNetId": { - "type": "string" - }, "tags": { "type": "object" }, - "existingZoneResourceId": { + "virtualNetworkId": { + "type": "string" + }, + "privateEndpointSubnetId": { "type": "string", - "defaultValue": "" + "minLength": 1 }, - "createVNetLink": { - "type": "bool", - "defaultValue": true + "privateDnsZoneConfigs": { + "type": "object", + "defaultValue": {} + }, + "blobStorageDnsZoneId": { + "type": "string" + }, + "monitoringResourceIds": { + "type": "array" } }, "variables": { - "useExistingZone": "[not(empty(parameters('existingZoneResourceId')))]", - "linkName": "[toLower(format('{0}-{1}-{2}-pe-dnszonelink', parameters('appName'), parameters('environment'), parameters('name')))]", - "existingZoneSubscriptionId": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[2], '')]", - "existingZoneResourceGroupName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[4], '')]", - "existingZoneName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[8], '')]" + "copy": [ + { + "name": "zoneConfigs", + "count": "[length(variables('zoneKeys'))]", + "input": { + "key": "[variables('zoneKeys')[copyIndex('zoneConfigs')]]", + "zoneName": "[variables('dnsNames')[variables('zoneKeys')[copyIndex('zoneConfigs')]]]", + "zoneResourceId": "[if(contains(parameters('privateDnsZoneConfigs'), variables('zoneKeys')[copyIndex('zoneConfigs')]), coalesce(parameters('privateDnsZoneConfigs')[variables('zoneKeys')[copyIndex('zoneConfigs')]].zoneResourceId, ''), '')]", + "createVNetLink": "[if(contains(parameters('privateDnsZoneConfigs'), variables('zoneKeys')[copyIndex('zoneConfigs')]), coalesce(parameters('privateDnsZoneConfigs')[variables('zoneKeys')[copyIndex('zoneConfigs')]].createVNetLink, true()), true())]" + } + }, + { + "name": "monitorDnsZoneIds", + "count": "[length(take(variables('zoneConfigs'), 4))]", + "input": "[if(empty(take(variables('zoneConfigs'), 4)[copyIndex('monitorDnsZoneIds')].zoneResourceId), resourceId('Microsoft.Network/privateDnsZones', take(variables('zoneConfigs'), 4)[copyIndex('monitorDnsZoneIds')].zoneName), take(variables('zoneConfigs'), 4)[copyIndex('monitorDnsZoneIds')].zoneResourceId)]" + } + ], + "$fxv#0": { + "azurecloud": { + "aisearch": "privatelink.search.windows.net", + "blobStorage": "privatelink.blob.core.windows.net", + "cognitiveServices": "privatelink.cognitiveservices.azure.com", + "containerRegistry": "privatelink.azurecr.io", + "cosmosDb": "privatelink.documents.azure.com", + "keyVault": "privatelink.vaultcore.azure.net", + "openAi": "privatelink.openai.azure.com", + "webSites": "privatelink.azurewebsites.net", + "redisClassic": "privatelink.redis.cache.windows.net", + "redisManaged": "privatelink.redis.azure.net", + "videoIndexer": "privatelink.api.videoindexer.ai", + "monitor": "privatelink.monitor.azure.com", + "monitorOms": "privatelink.oms.opinsights.azure.com", + "monitorOds": "privatelink.ods.opinsights.azure.com", + "monitorAgent": "privatelink.agentsvc.azure-automation.net", + "monitorAdx": "" + }, + "azureusgovernment": { + "aisearch": "privatelink.search.azure.us", + "blobStorage": "privatelink.blob.core.usgovcloudapi.net", + "cognitiveServices": "privatelink.cognitiveservices.azure.us", + "containerRegistry": "privatelink.azurecr.us", + "cosmosDb": "privatelink.documents.azure.us", + "keyVault": "privatelink.vaultcore.usgovcloudapi.net", + "openAi": "privatelink.openai.azure.us", + "webSites": "privatelink.azurewebsites.us", + "redisClassic": "privatelink.redis.cache.usgovcloudapi.net", + "redisManaged": "", + "videoIndexer": "", + "monitor": "privatelink.monitor.azure.us", + "monitorOms": "privatelink.oms.opinsights.azure.us", + "monitorOds": "privatelink.ods.opinsights.azure.us", + "monitorAgent": "privatelink.agentsvc.azure-automation.us", + "monitorAdx": "privatelink.adx.monitor.azure.us" + } + }, + "cloudName": "[toLower(environment().name)]", + "dnsNames": "[variables('$fxv#0')[variables('cloudName')]]", + "zoneKeys": "[concat(createArray('monitor', 'monitorOms', 'monitorOds', 'monitorAgent'), if(empty(variables('dnsNames').monitorAdx), createArray(), createArray('monitorAdx')))]", + "adxZoneConfig": "[coalesce(tryGet(parameters('privateDnsZoneConfigs'), 'monitorAdx'), createObject())]", + "adxZoneResourceId": "[coalesce(variables('adxZoneConfig').zoneResourceId, '')]" }, "resources": [ { - "condition": "[not(variables('useExistingZone'))]", - "type": "Microsoft.Network/privateDnsZones", - "apiVersion": "2020-06-01", - "name": "[parameters('zoneName')]", + "type": "microsoft.insights/privateLinkScopes", + "apiVersion": "2021-07-01-preview", + "name": "[toLower(format('{0}-{1}-ampls', parameters('appName'), parameters('environment')))]", "location": "global", + "properties": { + "accessModeSettings": { + "ingestionAccessMode": "PrivateOnly", + "queryAccessMode": "PrivateOnly" + } + }, "tags": "[parameters('tags')]" }, { - "condition": "[and(not(variables('useExistingZone')), parameters('createVNetLink'))]", - "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", - "apiVersion": "2020-06-01", - "name": "[format('{0}/{1}', parameters('zoneName'), variables('linkName'))]", - "location": "global", + "copy": { + "name": "scopedResources", + "count": "[length(parameters('monitoringResourceIds'))]" + }, + "type": "Microsoft.Insights/privateLinkScopes/scopedResources", + "apiVersion": "2021-07-01-preview", + "name": "[format('{0}/{1}', toLower(format('{0}-{1}-ampls', parameters('appName'), parameters('environment'))), format('monitor-{0}', copyIndex()))]", "properties": { - "registrationEnabled": false, - "virtualNetwork": { - "id": "[parameters('vNetId')]" - } + "linkedResourceId": "[parameters('monitoringResourceIds')[copyIndex()]]" }, "dependsOn": [ - "[resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName'))]" + "[resourceId('microsoft.insights/privateLinkScopes', toLower(format('{0}-{1}-ampls', parameters('appName'), parameters('environment'))))]" ] }, { - "condition": "[and(variables('useExistingZone'), parameters('createVNetLink'))]", + "copy": { + "name": "dnsZones", + "count": "[length(variables('zoneConfigs'))]" + }, "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", - "name": "[format('existing-{0}-privateDnsZoneLink', parameters('name'))]", - "subscriptionId": "[variables('existingZoneSubscriptionId')]", + "name": "[format('{0}DNSZone', variables('zoneConfigs')[copyIndex()].key)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "zoneName": { + "value": "[variables('zoneConfigs')[copyIndex()].zoneName]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "name": { + "value": "[variables('zoneConfigs')[copyIndex()].key]" + }, + "vNetId": { + "value": "[parameters('virtualNetworkId')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "existingZoneResourceId": { + "value": "[variables('zoneConfigs')[copyIndex()].zoneResourceId]" + }, + "createVNetLink": { + "value": "[variables('zoneConfigs')[copyIndex()].createVNetLink]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.44.1.10279", + "templateHash": "6946008567642736236" + } + }, + "parameters": { + "zoneName": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "name": { + "type": "string" + }, + "vNetId": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "existingZoneResourceId": { + "type": "string", + "defaultValue": "" + }, + "createVNetLink": { + "type": "bool", + "defaultValue": true + } + }, + "variables": { + "useExistingZone": "[not(empty(parameters('existingZoneResourceId')))]", + "linkName": "[toLower(format('{0}-{1}-{2}-pe-dnszonelink', parameters('appName'), parameters('environment'), parameters('name')))]", + "existingZoneSubscriptionId": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[2], '')]", + "existingZoneResourceGroupName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[4], '')]", + "existingZoneName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[8], '')]" + }, + "resources": [ + { + "condition": "[not(variables('useExistingZone'))]", + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "[parameters('zoneName')]", + "location": "global", + "tags": "[parameters('tags')]" + }, + { + "condition": "[and(not(variables('useExistingZone')), parameters('createVNetLink'))]", + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), variables('linkName'))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[parameters('vNetId')]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName'))]" + ] + }, + { + "condition": "[and(variables('useExistingZone'), parameters('createVNetLink'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('existing-{0}-privateDnsZoneLink', parameters('name'))]", + "subscriptionId": "[variables('existingZoneSubscriptionId')]", + "resourceGroup": "[variables('existingZoneResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "zoneName": { + "value": "[variables('existingZoneName')]" + }, + "linkName": { + "value": "[variables('linkName')]" + }, + "vNetId": { + "value": "[parameters('vNetId')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.44.1.10279", + "templateHash": "1211059935570939009" + } + }, + "parameters": { + "zoneName": { + "type": "string" + }, + "linkName": { + "type": "string" + }, + "vNetId": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), parameters('linkName'))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[parameters('vNetId')]" + } + } + } + ] + } + } + } + ], + "outputs": { + "privateDnsZoneId": { + "type": "string", + "value": "[if(variables('useExistingZone'), parameters('existingZoneResourceId'), resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName')))]" + }, + "privateDnsZoneName": { + "type": "string", + "value": "[if(variables('useExistingZone'), variables('existingZoneName'), parameters('zoneName'))]" + } + } + } + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "monitorPE", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "ampls" + }, + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "serviceResourceID": { + "value": "[resourceId('microsoft.insights/privateLinkScopes', toLower(format('{0}-{1}-ampls', parameters('appName'), parameters('environment'))))]" + }, + "subnetId": { + "value": "[parameters('privateEndpointSubnetId')]" + }, + "groupIDs": { + "value": [ + "azuremonitor" + ] + }, + "privateDnsZoneIds": { + "value": "[concat(variables('monitorDnsZoneIds'), createArray(parameters('blobStorageDnsZoneId')))]" + }, + "tags": { + "value": "[parameters('tags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.44.1.10279", + "templateHash": "1795431069306274319" + } + }, + "parameters": { + "name": { + "type": "string" + }, + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "serviceResourceID": { + "type": "string" + }, + "subnetId": { + "type": "string" + }, + "groupIDs": { + "type": "array" + }, + "privateDnsZoneIds": { + "type": "array", + "defaultValue": [] + }, + "tags": { + "type": "object" + } + }, + "resources": [ + { + "condition": "[greater(length(parameters('privateDnsZoneIds')), 0)]", + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2021-05-01", + "name": "[format('{0}/{1}', toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name'))), 'default')]", + "properties": { + "copy": [ + { + "name": "privateDnsZoneConfigs", + "count": "[length(parameters('privateDnsZoneIds'))]", + "input": { + "name": "[last(split(parameters('privateDnsZoneIds')[copyIndex('privateDnsZoneConfigs')], '/'))]", + "properties": { + "privateDnsZoneId": "[parameters('privateDnsZoneIds')[copyIndex('privateDnsZoneConfigs')]]" + } + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateEndpoints', toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name'))))]" + ] + }, + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2021-05-01", + "name": "[toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name')))]", + "location": "[parameters('location')]", + "properties": { + "subnet": { + "id": "[parameters('subnetId')]" + }, + "privateLinkServiceConnections": [ + { + "name": "[toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name')))]", + "properties": { + "privateLinkServiceId": "[parameters('serviceResourceID')]", + "groupIds": "[parameters('groupIDs')]" + } + } + ], + "customNetworkInterfaceName": "[toLower(format('{0}-{1}-{2}-nic', parameters('appName'), parameters('environment'), parameters('name')))]" + }, + "tags": "[parameters('tags')]" + } + ] + } + }, + "dependsOn": [ + "dnsZones", + "[resourceId('microsoft.insights/privateLinkScopes', toLower(format('{0}-{1}-ampls', parameters('appName'), parameters('environment'))))]", + "scopedResources" + ] + }, + { + "condition": "[not(empty(variables('dnsNames').monitorAdx))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "monitorAdxDNSRecords", + "subscriptionId": "[if(empty(variables('adxZoneResourceId')), subscription().subscriptionId, split(variables('adxZoneResourceId'), '/')[2])]", + "resourceGroup": "[if(empty(variables('adxZoneResourceId')), resourceGroup().name, split(variables('adxZoneResourceId'), '/')[4])]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "zoneName": { + "value": "[variables('dnsNames').monitorAdx]" + }, + "records": { + "value": "[map(filter(flatten(map(reference(resourceId('Microsoft.Network/networkInterfaces', toLower(format('{0}-{1}-ampls-nic', parameters('appName'), parameters('environment')))), '2023-09-01').ipConfigurations, lambda('ipConfig', map(coalesce(lambdaVariables('ipConfig').properties.privateLinkConnectionProperties.fqdns, createArray()), lambda('fqdn', createObject('fqdn', lambdaVariables('fqdn'), 'ipAddress', lambdaVariables('ipConfig').properties.privateIPAddress)))))), lambda('entry', or(endsWith(lambdaVariables('entry').fqdn, format('.{0}', variables('dnsNames').monitorAdx)), equals(lambdaVariables('entry').fqdn, variables('dnsNames').monitorAdx)))), lambda('entry', createObject('name', if(equals(lambdaVariables('entry').fqdn, variables('dnsNames').monitorAdx), '@', substring(lambdaVariables('entry').fqdn, 0, sub(sub(length(lambdaVariables('entry').fqdn), length(variables('dnsNames').monitorAdx)), 1))), 'ipAddress', lambdaVariables('entry').ipAddress)))]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.44.1.10279", + "templateHash": "17923192730588741193" + } + }, + "parameters": { + "zoneName": { + "type": "string" + }, + "records": { + "type": "array" + } + }, + "variables": { + "recordNames": "[union(map(parameters('records'), lambda('record', lambdaVariables('record').name)), createArray())]" + }, + "resources": [ + { + "copy": { + "name": "dnsRecords", + "count": "[length(variables('recordNames'))]" + }, + "type": "Microsoft.Network/privateDnsZones/A", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), variables('recordNames')[copyIndex()])]", + "properties": { + "ttl": 60, + "aRecords": "[map(filter(parameters('records'), lambda('record', equals(lambdaVariables('record').name, variables('recordNames')[copyIndex()]))), lambda('record', createObject('ipv4Address', lambdaVariables('record').ipAddress)))]" + } + } + ] + } + }, + "dependsOn": [ + "dnsZones", + "[resourceId('Microsoft.Resources/deployments', 'monitorPE')]" + ] + } + ], + "outputs": { + "privateLinkScopeId": { + "type": "string", + "value": "[resourceId('microsoft.insights/privateLinkScopes', toLower(format('{0}-{1}-ampls', parameters('appName'), parameters('environment'))))]" + } + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'storageAccountDNSZone')]" + ] + }, + { + "condition": "[not(equals(parameters('openAIName'), ''))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "openAiDNSZone", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "zoneName": { + "value": "[variables('openAiDnsZoneName')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "name": { + "value": "openAiService" + }, + "vNetId": { + "value": "[parameters('virtualNetworkId')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "existingZoneResourceId": { + "value": "[variables('openAiPrivateDnsZoneConfig').zoneResourceId]" + }, + "createVNetLink": { + "value": "[variables('openAiPrivateDnsZoneConfig').createVNetLink]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.44.1.10279", + "templateHash": "6946008567642736236" + } + }, + "parameters": { + "zoneName": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "name": { + "type": "string" + }, + "vNetId": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "existingZoneResourceId": { + "type": "string", + "defaultValue": "" + }, + "createVNetLink": { + "type": "bool", + "defaultValue": true + } + }, + "variables": { + "useExistingZone": "[not(empty(parameters('existingZoneResourceId')))]", + "linkName": "[toLower(format('{0}-{1}-{2}-pe-dnszonelink', parameters('appName'), parameters('environment'), parameters('name')))]", + "existingZoneSubscriptionId": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[2], '')]", + "existingZoneResourceGroupName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[4], '')]", + "existingZoneName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[8], '')]" + }, + "resources": [ + { + "condition": "[not(variables('useExistingZone'))]", + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "[parameters('zoneName')]", + "location": "global", + "tags": "[parameters('tags')]" + }, + { + "condition": "[and(not(variables('useExistingZone')), parameters('createVNetLink'))]", + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), variables('linkName'))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[parameters('vNetId')]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName'))]" + ] + }, + { + "condition": "[and(variables('useExistingZone'), parameters('createVNetLink'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('existing-{0}-privateDnsZoneLink', parameters('name'))]", + "subscriptionId": "[variables('existingZoneSubscriptionId')]", + "resourceGroup": "[variables('existingZoneResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "zoneName": { + "value": "[variables('existingZoneName')]" + }, + "linkName": { + "value": "[variables('linkName')]" + }, + "vNetId": { + "value": "[parameters('vNetId')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.44.1.10279", + "templateHash": "1211059935570939009" + } + }, + "parameters": { + "zoneName": { + "type": "string" + }, + "linkName": { + "type": "string" + }, + "vNetId": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), parameters('linkName'))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[parameters('vNetId')]" + } + } + } + ] + } + } + } + ], + "outputs": { + "privateDnsZoneId": { + "type": "string", + "value": "[if(variables('useExistingZone'), parameters('existingZoneResourceId'), resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName')))]" + }, + "privateDnsZoneName": { + "type": "string", + "value": "[if(variables('useExistingZone'), variables('existingZoneName'), parameters('zoneName'))]" + } + } + } + } + }, + { + "condition": "[not(equals(parameters('openAIName'), ''))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "openAiPE", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "openAiService" + }, + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "serviceResourceID": "[if(variables('useExternalOpenAIResource'), createObject('value', extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('openAISubscriptionId'), parameters('openAIResourceGroupName')), 'Microsoft.CognitiveServices/accounts', parameters('openAIName'))), createObject('value', resourceId('Microsoft.CognitiveServices/accounts', parameters('openAIName'))))]", + "subnetId": { + "value": "[parameters('privateEndpointSubnetId')]" + }, + "groupIDs": { + "value": [ + "account" + ] + }, + "privateDnsZoneIds": { + "value": [ + "[reference(resourceId('Microsoft.Resources/deployments', 'openAiDNSZone'), '2025-04-01').outputs.privateDnsZoneId.value]" + ] + }, + "tags": { + "value": "[parameters('tags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.44.1.10279", + "templateHash": "1795431069306274319" + } + }, + "parameters": { + "name": { + "type": "string" + }, + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "serviceResourceID": { + "type": "string" + }, + "subnetId": { + "type": "string" + }, + "groupIDs": { + "type": "array" + }, + "privateDnsZoneIds": { + "type": "array", + "defaultValue": [] + }, + "tags": { + "type": "object" + } + }, + "resources": [ + { + "condition": "[greater(length(parameters('privateDnsZoneIds')), 0)]", + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2021-05-01", + "name": "[format('{0}/{1}', toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name'))), 'default')]", + "properties": { + "copy": [ + { + "name": "privateDnsZoneConfigs", + "count": "[length(parameters('privateDnsZoneIds'))]", + "input": { + "name": "[last(split(parameters('privateDnsZoneIds')[copyIndex('privateDnsZoneConfigs')], '/'))]", + "properties": { + "privateDnsZoneId": "[parameters('privateDnsZoneIds')[copyIndex('privateDnsZoneConfigs')]]" + } + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateEndpoints', toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name'))))]" + ] + }, + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2021-05-01", + "name": "[toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name')))]", + "location": "[parameters('location')]", + "properties": { + "subnet": { + "id": "[parameters('subnetId')]" + }, + "privateLinkServiceConnections": [ + { + "name": "[toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name')))]", + "properties": { + "privateLinkServiceId": "[parameters('serviceResourceID')]", + "groupIds": "[parameters('groupIDs')]" + } + } + ], + "customNetworkInterfaceName": "[toLower(format('{0}-{1}-{2}-nic', parameters('appName'), parameters('environment'), parameters('name')))]" + }, + "tags": "[parameters('tags')]" + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'openAiDNSZone')]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "webAppDNSZone", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "zoneName": { + "value": "[variables('webSitesDnsZoneName')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "name": { + "value": "webApp" + }, + "vNetId": { + "value": "[parameters('virtualNetworkId')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "existingZoneResourceId": { + "value": "[variables('webSitesPrivateDnsZoneConfig').zoneResourceId]" + }, + "createVNetLink": { + "value": "[variables('webSitesPrivateDnsZoneConfig').createVNetLink]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.44.1.10279", + "templateHash": "6946008567642736236" + } + }, + "parameters": { + "zoneName": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "name": { + "type": "string" + }, + "vNetId": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "existingZoneResourceId": { + "type": "string", + "defaultValue": "" + }, + "createVNetLink": { + "type": "bool", + "defaultValue": true + } + }, + "variables": { + "useExistingZone": "[not(empty(parameters('existingZoneResourceId')))]", + "linkName": "[toLower(format('{0}-{1}-{2}-pe-dnszonelink', parameters('appName'), parameters('environment'), parameters('name')))]", + "existingZoneSubscriptionId": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[2], '')]", + "existingZoneResourceGroupName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[4], '')]", + "existingZoneName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[8], '')]" + }, + "resources": [ + { + "condition": "[not(variables('useExistingZone'))]", + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "[parameters('zoneName')]", + "location": "global", + "tags": "[parameters('tags')]" + }, + { + "condition": "[and(not(variables('useExistingZone')), parameters('createVNetLink'))]", + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), variables('linkName'))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[parameters('vNetId')]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName'))]" + ] + }, + { + "condition": "[and(variables('useExistingZone'), parameters('createVNetLink'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('existing-{0}-privateDnsZoneLink', parameters('name'))]", + "subscriptionId": "[variables('existingZoneSubscriptionId')]", "resourceGroup": "[variables('existingZoneResourceGroupName')]", "properties": { "expressionEvaluationOptions": { @@ -10424,7 +11390,188 @@ ] }, { - "condition": "[and(not(equals(parameters('videoIndexerName'), '')), parameters('videoIndexerSupportsPrivateEndpoints'))]", + "condition": "[variables('deployVideoIndexerPrivateEndpoint')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "videoIndexerDNSZone", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "zoneName": { + "value": "[variables('videoIndexerDnsZoneName')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "name": { + "value": "videoIndexer" + }, + "vNetId": { + "value": "[parameters('virtualNetworkId')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "existingZoneResourceId": { + "value": "[variables('videoIndexerPrivateDnsZoneConfig').zoneResourceId]" + }, + "createVNetLink": { + "value": "[variables('videoIndexerPrivateDnsZoneConfig').createVNetLink]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.44.1.10279", + "templateHash": "6946008567642736236" + } + }, + "parameters": { + "zoneName": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "name": { + "type": "string" + }, + "vNetId": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "existingZoneResourceId": { + "type": "string", + "defaultValue": "" + }, + "createVNetLink": { + "type": "bool", + "defaultValue": true + } + }, + "variables": { + "useExistingZone": "[not(empty(parameters('existingZoneResourceId')))]", + "linkName": "[toLower(format('{0}-{1}-{2}-pe-dnszonelink', parameters('appName'), parameters('environment'), parameters('name')))]", + "existingZoneSubscriptionId": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[2], '')]", + "existingZoneResourceGroupName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[4], '')]", + "existingZoneName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[8], '')]" + }, + "resources": [ + { + "condition": "[not(variables('useExistingZone'))]", + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "[parameters('zoneName')]", + "location": "global", + "tags": "[parameters('tags')]" + }, + { + "condition": "[and(not(variables('useExistingZone')), parameters('createVNetLink'))]", + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), variables('linkName'))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[parameters('vNetId')]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName'))]" + ] + }, + { + "condition": "[and(variables('useExistingZone'), parameters('createVNetLink'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('existing-{0}-privateDnsZoneLink', parameters('name'))]", + "subscriptionId": "[variables('existingZoneSubscriptionId')]", + "resourceGroup": "[variables('existingZoneResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "zoneName": { + "value": "[variables('existingZoneName')]" + }, + "linkName": { + "value": "[variables('linkName')]" + }, + "vNetId": { + "value": "[parameters('vNetId')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.44.1.10279", + "templateHash": "1211059935570939009" + } + }, + "parameters": { + "zoneName": { + "type": "string" + }, + "linkName": { + "type": "string" + }, + "vNetId": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), parameters('linkName'))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[parameters('vNetId')]" + } + } + } + ] + } + } + } + ], + "outputs": { + "privateDnsZoneId": { + "type": "string", + "value": "[if(variables('useExistingZone'), parameters('existingZoneResourceId'), resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName')))]" + }, + "privateDnsZoneName": { + "type": "string", + "value": "[if(variables('useExistingZone'), variables('existingZoneName'), parameters('zoneName'))]" + } + } + } + } + }, + { + "condition": "[variables('deployVideoIndexerPrivateEndpoint')]", "type": "Microsoft.Resources/deployments", "apiVersion": "2025-04-01", "name": "videoIndexerPE", @@ -10459,7 +11606,7 @@ }, "privateDnsZoneIds": { "value": [ - "[reference(resourceId('Microsoft.Resources/deployments', 'docIntelDNSZone'), '2025-04-01').outputs.privateDnsZoneId.value]" + "[reference(resourceId('Microsoft.Resources/deployments', 'videoIndexerDNSZone'), '2025-04-01').outputs.privateDnsZoneId.value]" ] }, "tags": { @@ -10556,7 +11703,7 @@ } }, "dependsOn": [ - "[resourceId('Microsoft.Resources/deployments', 'docIntelDNSZone')]" + "[resourceId('Microsoft.Resources/deployments', 'videoIndexerDNSZone')]" ] } ] @@ -10564,12 +11711,15 @@ }, "dependsOn": [ "acr", + "applicationInsights", "appService", "contentSafety", "cosmosDB", "docIntel", "keyVault", + "logAnalytics", "openAI", + "redisCache", "rg", "searchService", "speechService", @@ -10656,6 +11806,10 @@ "type": "string", "value": "[if(parameters('deployRedisCache'), parameters('redisCacheKind'), '')]" }, + "var_redisCacheResourceId": { + "type": "string", + "value": "[if(parameters('deployRedisCache'), reference('redisCache').outputs.redisCacheResourceId.value, '')]" + }, "var_redisCachePort": { "type": "string", "value": "[if(parameters('deployRedisCache'), string(reference('redisCache').outputs.redisCachePort.value), '')]" diff --git a/deployers/bicep/main.parameters.json b/deployers/bicep/main.parameters.json index e43157627..84c2ac8df 100644 --- a/deployers/bicep/main.parameters.json +++ b/deployers/bicep/main.parameters.json @@ -65,6 +65,9 @@ "enablePrivateNetworking": { "value": "${ENABLE_PRIVATE_NETWORKING}" }, + "enableAzureMonitorPrivateLink": { + "value": "${ENABLE_AZURE_MONITOR_PRIVATE_LINK=false}" + }, "existingVirtualNetworkId": { "value": "${EXISTING_VNET_RESOURCE_ID}" }, diff --git a/deployers/bicep/modules/applicationInsights.bicep b/deployers/bicep/modules/applicationInsights.bicep index d3ede5765..9bf8dec1f 100644 --- a/deployers/bicep/modules/applicationInsights.bicep +++ b/deployers/bicep/modules/applicationInsights.bicep @@ -6,6 +6,7 @@ param environment string param tags object param logAnalyticsId string +param enableAzureMonitorPrivateLink bool = false // application insights resource resource appInsights 'Microsoft.Insights/components@2020-02-02' = { @@ -15,8 +16,11 @@ resource appInsights 'Microsoft.Insights/components@2020-02-02' = { properties: { Application_Type: 'web' WorkspaceResourceId: logAnalyticsId + publicNetworkAccessForIngestion: enableAzureMonitorPrivateLink ? 'Disabled' : 'Enabled' + publicNetworkAccessForQuery: enableAzureMonitorPrivateLink ? 'Disabled' : 'Enabled' } tags: tags } output appInsightsName string = appInsights.name +output appInsightsId string = appInsights.id diff --git a/deployers/bicep/modules/azureMonitorPrivateNetworking.bicep b/deployers/bicep/modules/azureMonitorPrivateNetworking.bicep new file mode 100644 index 000000000..b05349d87 --- /dev/null +++ b/deployers/bicep/modules/azureMonitorPrivateNetworking.bicep @@ -0,0 +1,100 @@ +targetScope = 'resourceGroup' + +param location string +param appName string +param environment string +param tags object +param virtualNetworkId string +@minLength(1) +param privateEndpointSubnetId string +param privateDnsZoneConfigs object = {} +param blobStorageDnsZoneId string +param monitoringResourceIds array + +var cloudName = toLower(az.environment().name) +var dnsNames = loadJsonContent('privateDNSZones.json')[cloudName] +var zoneKeys = concat(['monitor', 'monitorOms', 'monitorOds', 'monitorAgent'], empty(dnsNames.monitorAdx) ? [] : ['monitorAdx']) +var zoneConfigs = [for zoneKey in zoneKeys: { + key: zoneKey + zoneName: dnsNames[zoneKey] + zoneResourceId: contains(privateDnsZoneConfigs, zoneKey) ? (privateDnsZoneConfigs[zoneKey].zoneResourceId ?? '') : '' + createVNetLink: contains(privateDnsZoneConfigs, zoneKey) ? (privateDnsZoneConfigs[zoneKey].createVNetLink ?? true) : true +}] + +resource scope 'Microsoft.Insights/privateLinkScopes@2021-07-01-preview' = { + name: toLower('${appName}-${environment}-ampls') + location: 'global' + properties: { + accessModeSettings: { + ingestionAccessMode: 'PrivateOnly' + queryAccessMode: 'PrivateOnly' + } + } + tags: tags +} + +resource scopedResources 'Microsoft.Insights/privateLinkScopes/scopedResources@2021-07-01-preview' = [for (monitoringResourceId, resourceIndex) in monitoringResourceIds: { + parent: scope + name: 'monitor-${resourceIndex}' + properties: { + linkedResourceId: monitoringResourceId + } +}] + +module dnsZones 'privateDNS.bicep' = [for zoneConfig in zoneConfigs: { + name: '${zoneConfig.key}DNSZone' + params: { + zoneName: zoneConfig.zoneName + appName: appName + environment: environment + name: zoneConfig.key + vNetId: virtualNetworkId + tags: tags + existingZoneResourceId: zoneConfig.zoneResourceId + createVNetLink: zoneConfig.createVNetLink + } +}] + +var monitorDnsZoneIds = [for zoneConfig in take(zoneConfigs, 4): empty(zoneConfig.zoneResourceId) ? resourceId('Microsoft.Network/privateDnsZones', zoneConfig.zoneName) : zoneConfig.zoneResourceId] + +module monitorPE 'privateEndpoint.bicep' = { + name: 'monitorPE' + params: { + name: 'ampls' + location: location + appName: appName + environment: environment + serviceResourceID: scope.id + subnetId: privateEndpointSubnetId + groupIDs: ['azuremonitor'] + privateDnsZoneIds: concat(monitorDnsZoneIds, [blobStorageDnsZoneId]) + tags: tags + } + dependsOn: [scopedResources, dnsZones] +} + +resource monitorNic 'Microsoft.Network/networkInterfaces@2023-09-01' existing = { + name: toLower('${appName}-${environment}-ampls-nic') +} + +var adxZoneConfig = privateDnsZoneConfigs.?monitorAdx ?? {} +var adxZoneResourceId = adxZoneConfig.zoneResourceId ?? '' +var adxDnsEntries = flatten(map(monitorNic.properties.ipConfigurations, ipConfig => map(ipConfig.properties.privateLinkConnectionProperties.fqdns ?? [], fqdn => { + fqdn: fqdn + ipAddress: ipConfig.properties.privateIPAddress +}))) + +module adxDnsRecords 'privateDNSRecords.bicep' = if (!empty(dnsNames.monitorAdx)) { + name: 'monitorAdxDNSRecords' + scope: resourceGroup(empty(adxZoneResourceId) ? subscription().subscriptionId : split(adxZoneResourceId, '/')[2], empty(adxZoneResourceId) ? resourceGroup().name : split(adxZoneResourceId, '/')[4]) + params: { + zoneName: dnsNames.monitorAdx + records: map(filter(adxDnsEntries, entry => endsWith(entry.fqdn, '.${dnsNames.monitorAdx}') || entry.fqdn == dnsNames.monitorAdx), entry => { + name: entry.fqdn == dnsNames.monitorAdx ? '@' : substring(entry.fqdn, 0, length(entry.fqdn) - length(dnsNames.monitorAdx) - 1) + ipAddress: entry.ipAddress + }) + } + dependsOn: [monitorPE, dnsZones] +} + +output privateLinkScopeId string = scope.id diff --git a/deployers/bicep/modules/azureMonitorPrivateNetworking.json b/deployers/bicep/modules/azureMonitorPrivateNetworking.json new file mode 100644 index 000000000..21380d0aa --- /dev/null +++ b/deployers/bicep/modules/azureMonitorPrivateNetworking.json @@ -0,0 +1,522 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.44.1.10279", + "templateHash": "1191765375410113004" + } + }, + "parameters": { + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "virtualNetworkId": { + "type": "string" + }, + "privateEndpointSubnetId": { + "type": "string", + "minLength": 1 + }, + "privateDnsZoneConfigs": { + "type": "object", + "defaultValue": {} + }, + "blobStorageDnsZoneId": { + "type": "string" + }, + "monitoringResourceIds": { + "type": "array" + } + }, + "variables": { + "copy": [ + { + "name": "zoneConfigs", + "count": "[length(variables('zoneKeys'))]", + "input": { + "key": "[variables('zoneKeys')[copyIndex('zoneConfigs')]]", + "zoneName": "[variables('dnsNames')[variables('zoneKeys')[copyIndex('zoneConfigs')]]]", + "zoneResourceId": "[if(contains(parameters('privateDnsZoneConfigs'), variables('zoneKeys')[copyIndex('zoneConfigs')]), coalesce(parameters('privateDnsZoneConfigs')[variables('zoneKeys')[copyIndex('zoneConfigs')]].zoneResourceId, ''), '')]", + "createVNetLink": "[if(contains(parameters('privateDnsZoneConfigs'), variables('zoneKeys')[copyIndex('zoneConfigs')]), coalesce(parameters('privateDnsZoneConfigs')[variables('zoneKeys')[copyIndex('zoneConfigs')]].createVNetLink, true()), true())]" + } + }, + { + "name": "monitorDnsZoneIds", + "count": "[length(take(variables('zoneConfigs'), 4))]", + "input": "[if(empty(take(variables('zoneConfigs'), 4)[copyIndex('monitorDnsZoneIds')].zoneResourceId), resourceId('Microsoft.Network/privateDnsZones', take(variables('zoneConfigs'), 4)[copyIndex('monitorDnsZoneIds')].zoneName), take(variables('zoneConfigs'), 4)[copyIndex('monitorDnsZoneIds')].zoneResourceId)]" + } + ], + "$fxv#0": { + "azurecloud": { + "aisearch": "privatelink.search.windows.net", + "blobStorage": "privatelink.blob.core.windows.net", + "cognitiveServices": "privatelink.cognitiveservices.azure.com", + "containerRegistry": "privatelink.azurecr.io", + "cosmosDb": "privatelink.documents.azure.com", + "keyVault": "privatelink.vaultcore.azure.net", + "openAi": "privatelink.openai.azure.com", + "webSites": "privatelink.azurewebsites.net", + "redisClassic": "privatelink.redis.cache.windows.net", + "redisManaged": "privatelink.redis.azure.net", + "videoIndexer": "privatelink.api.videoindexer.ai", + "monitor": "privatelink.monitor.azure.com", + "monitorOms": "privatelink.oms.opinsights.azure.com", + "monitorOds": "privatelink.ods.opinsights.azure.com", + "monitorAgent": "privatelink.agentsvc.azure-automation.net", + "monitorAdx": "" + }, + "azureusgovernment": { + "aisearch": "privatelink.search.azure.us", + "blobStorage": "privatelink.blob.core.usgovcloudapi.net", + "cognitiveServices": "privatelink.cognitiveservices.azure.us", + "containerRegistry": "privatelink.azurecr.us", + "cosmosDb": "privatelink.documents.azure.us", + "keyVault": "privatelink.vaultcore.usgovcloudapi.net", + "openAi": "privatelink.openai.azure.us", + "webSites": "privatelink.azurewebsites.us", + "redisClassic": "privatelink.redis.cache.usgovcloudapi.net", + "redisManaged": "", + "videoIndexer": "", + "monitor": "privatelink.monitor.azure.us", + "monitorOms": "privatelink.oms.opinsights.azure.us", + "monitorOds": "privatelink.ods.opinsights.azure.us", + "monitorAgent": "privatelink.agentsvc.azure-automation.us", + "monitorAdx": "privatelink.adx.monitor.azure.us" + } + }, + "cloudName": "[toLower(environment().name)]", + "dnsNames": "[variables('$fxv#0')[variables('cloudName')]]", + "zoneKeys": "[concat(createArray('monitor', 'monitorOms', 'monitorOds', 'monitorAgent'), if(empty(variables('dnsNames').monitorAdx), createArray(), createArray('monitorAdx')))]", + "adxZoneConfig": "[coalesce(tryGet(parameters('privateDnsZoneConfigs'), 'monitorAdx'), createObject())]", + "adxZoneResourceId": "[coalesce(variables('adxZoneConfig').zoneResourceId, '')]" + }, + "resources": [ + { + "type": "microsoft.insights/privateLinkScopes", + "apiVersion": "2021-07-01-preview", + "name": "[toLower(format('{0}-{1}-ampls', parameters('appName'), parameters('environment')))]", + "location": "global", + "properties": { + "accessModeSettings": { + "ingestionAccessMode": "PrivateOnly", + "queryAccessMode": "PrivateOnly" + } + }, + "tags": "[parameters('tags')]" + }, + { + "copy": { + "name": "scopedResources", + "count": "[length(parameters('monitoringResourceIds'))]" + }, + "type": "Microsoft.Insights/privateLinkScopes/scopedResources", + "apiVersion": "2021-07-01-preview", + "name": "[format('{0}/{1}', toLower(format('{0}-{1}-ampls', parameters('appName'), parameters('environment'))), format('monitor-{0}', copyIndex()))]", + "properties": { + "linkedResourceId": "[parameters('monitoringResourceIds')[copyIndex()]]" + }, + "dependsOn": [ + "[resourceId('microsoft.insights/privateLinkScopes', toLower(format('{0}-{1}-ampls', parameters('appName'), parameters('environment'))))]" + ] + }, + { + "copy": { + "name": "dnsZones", + "count": "[length(variables('zoneConfigs'))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}DNSZone', variables('zoneConfigs')[copyIndex()].key)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "zoneName": { + "value": "[variables('zoneConfigs')[copyIndex()].zoneName]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "name": { + "value": "[variables('zoneConfigs')[copyIndex()].key]" + }, + "vNetId": { + "value": "[parameters('virtualNetworkId')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "existingZoneResourceId": { + "value": "[variables('zoneConfigs')[copyIndex()].zoneResourceId]" + }, + "createVNetLink": { + "value": "[variables('zoneConfigs')[copyIndex()].createVNetLink]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.44.1.10279", + "templateHash": "6946008567642736236" + } + }, + "parameters": { + "zoneName": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "name": { + "type": "string" + }, + "vNetId": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "existingZoneResourceId": { + "type": "string", + "defaultValue": "" + }, + "createVNetLink": { + "type": "bool", + "defaultValue": true + } + }, + "variables": { + "useExistingZone": "[not(empty(parameters('existingZoneResourceId')))]", + "linkName": "[toLower(format('{0}-{1}-{2}-pe-dnszonelink', parameters('appName'), parameters('environment'), parameters('name')))]", + "existingZoneSubscriptionId": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[2], '')]", + "existingZoneResourceGroupName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[4], '')]", + "existingZoneName": "[if(variables('useExistingZone'), split(parameters('existingZoneResourceId'), '/')[8], '')]" + }, + "resources": [ + { + "condition": "[not(variables('useExistingZone'))]", + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "[parameters('zoneName')]", + "location": "global", + "tags": "[parameters('tags')]" + }, + { + "condition": "[and(not(variables('useExistingZone')), parameters('createVNetLink'))]", + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), variables('linkName'))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[parameters('vNetId')]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName'))]" + ] + }, + { + "condition": "[and(variables('useExistingZone'), parameters('createVNetLink'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('existing-{0}-privateDnsZoneLink', parameters('name'))]", + "subscriptionId": "[variables('existingZoneSubscriptionId')]", + "resourceGroup": "[variables('existingZoneResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "zoneName": { + "value": "[variables('existingZoneName')]" + }, + "linkName": { + "value": "[variables('linkName')]" + }, + "vNetId": { + "value": "[parameters('vNetId')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.44.1.10279", + "templateHash": "1211059935570939009" + } + }, + "parameters": { + "zoneName": { + "type": "string" + }, + "linkName": { + "type": "string" + }, + "vNetId": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), parameters('linkName'))]", + "location": "global", + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[parameters('vNetId')]" + } + } + } + ] + } + } + } + ], + "outputs": { + "privateDnsZoneId": { + "type": "string", + "value": "[if(variables('useExistingZone'), parameters('existingZoneResourceId'), resourceId('Microsoft.Network/privateDnsZones', parameters('zoneName')))]" + }, + "privateDnsZoneName": { + "type": "string", + "value": "[if(variables('useExistingZone'), variables('existingZoneName'), parameters('zoneName'))]" + } + } + } + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "monitorPE", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "ampls" + }, + "location": { + "value": "[parameters('location')]" + }, + "appName": { + "value": "[parameters('appName')]" + }, + "environment": { + "value": "[parameters('environment')]" + }, + "serviceResourceID": { + "value": "[resourceId('microsoft.insights/privateLinkScopes', toLower(format('{0}-{1}-ampls', parameters('appName'), parameters('environment'))))]" + }, + "subnetId": { + "value": "[parameters('privateEndpointSubnetId')]" + }, + "groupIDs": { + "value": [ + "azuremonitor" + ] + }, + "privateDnsZoneIds": { + "value": "[concat(variables('monitorDnsZoneIds'), createArray(parameters('blobStorageDnsZoneId')))]" + }, + "tags": { + "value": "[parameters('tags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.44.1.10279", + "templateHash": "1795431069306274319" + } + }, + "parameters": { + "name": { + "type": "string" + }, + "location": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "serviceResourceID": { + "type": "string" + }, + "subnetId": { + "type": "string" + }, + "groupIDs": { + "type": "array" + }, + "privateDnsZoneIds": { + "type": "array", + "defaultValue": [] + }, + "tags": { + "type": "object" + } + }, + "resources": [ + { + "condition": "[greater(length(parameters('privateDnsZoneIds')), 0)]", + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2021-05-01", + "name": "[format('{0}/{1}', toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name'))), 'default')]", + "properties": { + "copy": [ + { + "name": "privateDnsZoneConfigs", + "count": "[length(parameters('privateDnsZoneIds'))]", + "input": { + "name": "[last(split(parameters('privateDnsZoneIds')[copyIndex('privateDnsZoneConfigs')], '/'))]", + "properties": { + "privateDnsZoneId": "[parameters('privateDnsZoneIds')[copyIndex('privateDnsZoneConfigs')]]" + } + } + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateEndpoints', toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name'))))]" + ] + }, + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2021-05-01", + "name": "[toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name')))]", + "location": "[parameters('location')]", + "properties": { + "subnet": { + "id": "[parameters('subnetId')]" + }, + "privateLinkServiceConnections": [ + { + "name": "[toLower(format('{0}-{1}-{2}-pe', parameters('appName'), parameters('environment'), parameters('name')))]", + "properties": { + "privateLinkServiceId": "[parameters('serviceResourceID')]", + "groupIds": "[parameters('groupIDs')]" + } + } + ], + "customNetworkInterfaceName": "[toLower(format('{0}-{1}-{2}-nic', parameters('appName'), parameters('environment'), parameters('name')))]" + }, + "tags": "[parameters('tags')]" + } + ] + } + }, + "dependsOn": [ + "dnsZones", + "[resourceId('microsoft.insights/privateLinkScopes', toLower(format('{0}-{1}-ampls', parameters('appName'), parameters('environment'))))]", + "scopedResources" + ] + }, + { + "condition": "[not(empty(variables('dnsNames').monitorAdx))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "monitorAdxDNSRecords", + "subscriptionId": "[if(empty(variables('adxZoneResourceId')), subscription().subscriptionId, split(variables('adxZoneResourceId'), '/')[2])]", + "resourceGroup": "[if(empty(variables('adxZoneResourceId')), resourceGroup().name, split(variables('adxZoneResourceId'), '/')[4])]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "zoneName": { + "value": "[variables('dnsNames').monitorAdx]" + }, + "records": { + "value": "[map(filter(flatten(map(reference(resourceId('Microsoft.Network/networkInterfaces', toLower(format('{0}-{1}-ampls-nic', parameters('appName'), parameters('environment')))), '2023-09-01').ipConfigurations, lambda('ipConfig', map(coalesce(lambdaVariables('ipConfig').properties.privateLinkConnectionProperties.fqdns, createArray()), lambda('fqdn', createObject('fqdn', lambdaVariables('fqdn'), 'ipAddress', lambdaVariables('ipConfig').properties.privateIPAddress)))))), lambda('entry', or(endsWith(lambdaVariables('entry').fqdn, format('.{0}', variables('dnsNames').monitorAdx)), equals(lambdaVariables('entry').fqdn, variables('dnsNames').monitorAdx)))), lambda('entry', createObject('name', if(equals(lambdaVariables('entry').fqdn, variables('dnsNames').monitorAdx), '@', substring(lambdaVariables('entry').fqdn, 0, sub(sub(length(lambdaVariables('entry').fqdn), length(variables('dnsNames').monitorAdx)), 1))), 'ipAddress', lambdaVariables('entry').ipAddress)))]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.44.1.10279", + "templateHash": "17923192730588741193" + } + }, + "parameters": { + "zoneName": { + "type": "string" + }, + "records": { + "type": "array" + } + }, + "variables": { + "recordNames": "[union(map(parameters('records'), lambda('record', lambdaVariables('record').name)), createArray())]" + }, + "resources": [ + { + "copy": { + "name": "dnsRecords", + "count": "[length(variables('recordNames'))]" + }, + "type": "Microsoft.Network/privateDnsZones/A", + "apiVersion": "2020-06-01", + "name": "[format('{0}/{1}', parameters('zoneName'), variables('recordNames')[copyIndex()])]", + "properties": { + "ttl": 60, + "aRecords": "[map(filter(parameters('records'), lambda('record', equals(lambdaVariables('record').name, variables('recordNames')[copyIndex()]))), lambda('record', createObject('ipv4Address', lambdaVariables('record').ipAddress)))]" + } + } + ] + } + }, + "dependsOn": [ + "dnsZones", + "[resourceId('Microsoft.Resources/deployments', 'monitorPE')]" + ] + } + ], + "outputs": { + "privateLinkScopeId": { + "type": "string", + "value": "[resourceId('microsoft.insights/privateLinkScopes', toLower(format('{0}-{1}-ampls', parameters('appName'), parameters('environment'))))]" + } + } +} diff --git a/deployers/bicep/modules/logAnalyticsWorkspace.bicep b/deployers/bicep/modules/logAnalyticsWorkspace.bicep index 4b51ddf37..43b587894 100644 --- a/deployers/bicep/modules/logAnalyticsWorkspace.bicep +++ b/deployers/bicep/modules/logAnalyticsWorkspace.bicep @@ -4,12 +4,15 @@ param location string param appName string param environment string param tags object +param enableAzureMonitorPrivateLink bool = false // log analytics workspace resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2022-10-01' = { name: toLower('${appName}-${environment}-la') location: location properties: { + publicNetworkAccessForIngestion: enableAzureMonitorPrivateLink ? 'Disabled' : 'Enabled' + publicNetworkAccessForQuery: enableAzureMonitorPrivateLink ? 'Disabled' : 'Enabled' sku: { name: 'PerGB2018' } diff --git a/deployers/bicep/modules/privateDNSRecords.bicep b/deployers/bicep/modules/privateDNSRecords.bicep new file mode 100644 index 000000000..642097a08 --- /dev/null +++ b/deployers/bicep/modules/privateDNSRecords.bicep @@ -0,0 +1,18 @@ +targetScope = 'resourceGroup' + +param zoneName string +param records array +var recordNames = union(map(records, record => record.name), []) + +resource zone 'Microsoft.Network/privateDnsZones@2020-06-01' existing = { + name: zoneName +} + +resource dnsRecords 'Microsoft.Network/privateDnsZones/A@2020-06-01' = [for recordName in recordNames: { + parent: zone + name: recordName + properties: { + ttl: 60 + aRecords: map(filter(records, record => record.name == recordName), record => { ipv4Address: record.ipAddress }) + } +}] diff --git a/deployers/bicep/modules/privateDNSZones.json b/deployers/bicep/modules/privateDNSZones.json index 66d22738b..365576d4d 100644 --- a/deployers/bicep/modules/privateDNSZones.json +++ b/deployers/bicep/modules/privateDNSZones.json @@ -7,7 +7,15 @@ "cosmosDb": "privatelink.documents.azure.com", "keyVault": "privatelink.vaultcore.azure.net", "openAi": "privatelink.openai.azure.com", - "webSites": "privatelink.azurewebsites.net" + "webSites": "privatelink.azurewebsites.net", + "redisClassic": "privatelink.redis.cache.windows.net", + "redisManaged": "privatelink.redis.azure.net", + "videoIndexer": "privatelink.api.videoindexer.ai", + "monitor": "privatelink.monitor.azure.com", + "monitorOms": "privatelink.oms.opinsights.azure.com", + "monitorOds": "privatelink.ods.opinsights.azure.com", + "monitorAgent": "privatelink.agentsvc.azure-automation.net", + "monitorAdx": "" }, "azureusgovernment": { "aisearch": "privatelink.search.azure.us", @@ -15,8 +23,16 @@ "cognitiveServices": "privatelink.cognitiveservices.azure.us", "containerRegistry": "privatelink.azurecr.us", "cosmosDb": "privatelink.documents.azure.us", - "keyVault": "privatelink.vaultcore.azure.us", + "keyVault": "privatelink.vaultcore.usgovcloudapi.net", "openAi": "privatelink.openai.azure.us", - "webSites": "privatelink.azurewebsites.us" + "webSites": "privatelink.azurewebsites.us", + "redisClassic": "privatelink.redis.cache.usgovcloudapi.net", + "redisManaged": "", + "videoIndexer": "", + "monitor": "privatelink.monitor.azure.us", + "monitorOms": "privatelink.oms.opinsights.azure.us", + "monitorOds": "privatelink.ods.opinsights.azure.us", + "monitorAgent": "privatelink.agentsvc.azure-automation.us", + "monitorAdx": "privatelink.adx.monitor.azure.us" } } \ No newline at end of file diff --git a/deployers/bicep/modules/privateNetworking.bicep b/deployers/bicep/modules/privateNetworking.bicep index d4850959a..052e962bf 100644 --- a/deployers/bicep/modules/privateNetworking.bicep +++ b/deployers/bicep/modules/privateNetworking.bicep @@ -3,6 +3,8 @@ targetScope = 'resourceGroup' param virtualNetworkId string param privateEndpointSubnetId string param privateDnsZoneConfigs object = {} +param enableAzureMonitorPrivateLink bool = false +param monitoringResourceIds array = [] param location string param appName string @@ -19,9 +21,8 @@ param openAIName string param openAIResourceGroupName string param openAISubscriptionId string param webAppName string - - -// // redis cache +param redisCacheResourceId string = '' +param redisCacheKind string = 'managed' param contentSafetyName string param speechServiceName string param videoIndexerName string @@ -42,6 +43,9 @@ var cosmosDbDnsZoneName = privateDnsZoneData[cloudName].cosmosDb var keyVaultDnsZoneName = privateDnsZoneData[cloudName].keyVault var openAiDnsZoneName = privateDnsZoneData[cloudName].openAi var webSitesDnsZoneName = privateDnsZoneData[cloudName].webSites +var redisDnsZoneName = redisCacheKind == 'managed' ? privateDnsZoneData[cloudName].redisManaged : privateDnsZoneData[cloudName].redisClassic +var videoIndexerDnsZoneName = privateDnsZoneData[cloudName].videoIndexer +var deployVideoIndexerPrivateEndpoint = videoIndexerName != '' && videoIndexerSupportsPrivateEndpoints && !empty(videoIndexerDnsZoneName) var defaultPrivateDnsZoneConfig = { zoneResourceId: '' createVNetLink: true @@ -54,6 +58,37 @@ var blobStoragePrivateDnsZoneConfig = contains(privateDnsZoneConfigs, 'blobStora var cognitiveServicesPrivateDnsZoneConfig = contains(privateDnsZoneConfigs, 'cognitiveServices') ? union(defaultPrivateDnsZoneConfig, privateDnsZoneConfigs.cognitiveServices) : defaultPrivateDnsZoneConfig var openAiPrivateDnsZoneConfig = contains(privateDnsZoneConfigs, 'openAi') ? union(defaultPrivateDnsZoneConfig, privateDnsZoneConfigs.openAi) : defaultPrivateDnsZoneConfig var webSitesPrivateDnsZoneConfig = contains(privateDnsZoneConfigs, 'webSites') ? union(defaultPrivateDnsZoneConfig, privateDnsZoneConfigs.webSites) : defaultPrivateDnsZoneConfig +var redisPrivateDnsZoneConfig = contains(privateDnsZoneConfigs, 'redis') ? union(defaultPrivateDnsZoneConfig, privateDnsZoneConfigs.redis) : defaultPrivateDnsZoneConfig +var videoIndexerPrivateDnsZoneConfig = contains(privateDnsZoneConfigs, 'videoIndexer') ? union(defaultPrivateDnsZoneConfig, privateDnsZoneConfigs.videoIndexer) : defaultPrivateDnsZoneConfig + +module redisDNSZone 'privateDNS.bicep' = if (!empty(redisCacheResourceId)) { + name: 'redisDNSZone' + params: { + zoneName: redisDnsZoneName + appName: appName + environment: environment + name: 'redis' + vNetId: virtualNetworkId + tags: tags + existingZoneResourceId: redisPrivateDnsZoneConfig.zoneResourceId + createVNetLink: redisPrivateDnsZoneConfig.createVNetLink + } +} + +module redisPE 'privateEndpoint.bicep' = if (!empty(redisCacheResourceId)) { + name: 'redisPE' + params: { + name: 'redis' + location: location + appName: appName + environment: environment + serviceResourceID: redisCacheResourceId + subnetId: privateEndpointSubnetId + groupIDs: [redisCacheKind == 'managed' ? 'redisEnterprise' : 'redisCache'] + privateDnsZoneIds: [redisDNSZone!.outputs.privateDnsZoneId] + tags: tags + } +} //========================================================= // key vault @@ -312,6 +347,21 @@ resource openAiService 'Microsoft.CognitiveServices/accounts@2024-10-01' existin name: openAIName } +module azureMonitorPrivateNetworking 'azureMonitorPrivateNetworking.bicep' = if (enableAzureMonitorPrivateLink) { + name: 'azureMonitorPrivateNetworking' + params: { + location: location + appName: appName + environment: environment + tags: tags + virtualNetworkId: virtualNetworkId + privateEndpointSubnetId: privateEndpointSubnetId + privateDnsZoneConfigs: privateDnsZoneConfigs + blobStorageDnsZoneId: storageAccountDNSZone.outputs.privateDnsZoneId + monitoringResourceIds: monitoringResourceIds + } +} + resource openAiServiceExternal 'Microsoft.CognitiveServices/accounts@2024-10-01' existing = if (useExternalOpenAIResource) { name: openAIName scope: resourceGroup(openAISubscriptionId, openAIResourceGroupName) @@ -453,11 +503,25 @@ module speechServicePE 'privateEndpoint.bicep' = if (speechServiceName != '') { //========================================================= // video indexer service - Optional //========================================================= -resource videoIndexerService 'Microsoft.VideoIndexer/accounts@2025-04-01' existing = if (videoIndexerName != '' && videoIndexerSupportsPrivateEndpoints) { +resource videoIndexerService 'Microsoft.VideoIndexer/accounts@2025-04-01' existing = if (deployVideoIndexerPrivateEndpoint) { name: videoIndexerName } -module videoIndexerPE 'privateEndpoint.bicep' = if (videoIndexerName != '' && videoIndexerSupportsPrivateEndpoints) { +module videoIndexerDNSZone 'privateDNS.bicep' = if (deployVideoIndexerPrivateEndpoint) { + name: 'videoIndexerDNSZone' + params: { + zoneName: videoIndexerDnsZoneName + appName: appName + environment: environment + name: 'videoIndexer' + vNetId: virtualNetworkId + tags: tags + existingZoneResourceId: videoIndexerPrivateDnsZoneConfig.zoneResourceId + createVNetLink: videoIndexerPrivateDnsZoneConfig.createVNetLink + } +} + +module videoIndexerPE 'privateEndpoint.bicep' = if (deployVideoIndexerPrivateEndpoint) { name: 'videoIndexerPE' dependsOn: [ videoIndexerService @@ -473,7 +537,7 @@ module videoIndexerPE 'privateEndpoint.bicep' = if (videoIndexerName != '' && vi 'account' ] privateDnsZoneIds: [ - docIntelDNSZone.outputs.privateDnsZoneId + videoIndexerDNSZone!.outputs.privateDnsZoneId ] tags: tags } diff --git a/deployers/bicep/modules/redisCache.bicep b/deployers/bicep/modules/redisCache.bicep index 8268b18ee..066607d71 100644 --- a/deployers/bicep/modules/redisCache.bicep +++ b/deployers/bicep/modules/redisCache.bicep @@ -77,8 +77,6 @@ resource managedRedis 'Microsoft.Cache/redisEnterprise@2025-07-01' = if (deployM properties: { highAvailability: redisHighAvailability minimumTlsVersion: '1.2' - // Azure Managed Redis does not support virtual network injection; the deployer does not - // provision a Redis private endpoint today, matching the classic cache behavior. publicNetworkAccess: 'Enabled' } tags: tags @@ -165,3 +163,5 @@ output redisCacheName string = deployManagedRedis ? managedRedis.name : classicR output redisCacheHostName string = deployManagedRedis ? managedRedis.properties.hostName : classicRedisCache.properties.hostName output redisCachePort int = deployManagedRedis ? 10000 : 6380 output redisCacheKind string = redisCacheKind +#disable-next-line BCP318 // exactly one of the two cache resources is deployed +output redisCacheResourceId string = deployManagedRedis ? managedRedis.id : classicRedisCache.id diff --git a/deployers/bicep/validate_azd_prerequisites.py b/deployers/bicep/validate_azd_prerequisites.py index 29a1a3640..d388271d9 100644 --- a/deployers/bicep/validate_azd_prerequisites.py +++ b/deployers/bicep/validate_azd_prerequisites.py @@ -17,6 +17,7 @@ from __future__ import annotations import fnmatch +import ipaddress import json import os import shutil @@ -466,13 +467,48 @@ def _ensure_runner_ip_is_allowed(enable_private_networking: bool) -> None: print(' If this runner needs public access to protected resources, set ALLOWED_IP_RANGES manually before rerunning azd.') +def _validate_monitor_private_link(enable_private_networking: bool, enabled: bool, subnet_id: str) -> bool: + if not enabled: + return True + if not enable_private_networking: + print('ERROR: ENABLE_AZURE_MONITOR_PRIVATE_LINK requires ENABLE_PRIVATE_NETWORKING=true.', file=sys.stderr) + return False + + if subnet_id: + exit_code, stdout, stderr = _run_command(['az', 'network', 'vnet', 'subnet', 'show', '--ids', subnet_id, '-o', 'json']) + if exit_code != 0: + print(f'ERROR: Unable to validate the AMPLS private endpoint subnet: {stderr}', file=sys.stderr) + return False + try: + subnet = json.loads(stdout) + prefixes = subnet.get('addressPrefixes') or [subnet.get('addressPrefix')] + networks = [ipaddress.ip_network(prefix) for prefix in prefixes if prefix] + except (ValueError, TypeError, AttributeError): + print('ERROR: Unable to parse the AMPLS subnet address prefixes.', file=sys.stderr) + return False + if not any(network.version == 4 and network.prefixlen <= 27 for network in networks): + print('ERROR: AMPLS requires an IPv4 subnet of /27 or larger; use /26 or larger for shared platform endpoints.', file=sys.stderr) + return False + + _print_header('AZURE MONITOR PRIVATE LINK: OPTED IN') + print('Application Insights and Log Analytics public ingestion and queries will be disabled.') + print('AMPLS uses at least 11 subnet IPs. The new shared endpoint subnet is /24; allow sufficient free IPs when reusing a subnet.') + print('All monitoring clients and administrators need private routing and DNS resolution.') + print('Use one AMPLS per shared DNS boundary; coordinate with your central monitoring/network team before enabling this option.') + return True + + def main() -> int: enable_private_networking = _to_bool(_get_env('AZURE_ENV_ENABLE_PRIVATE_NETWORKING', 'ENABLE_PRIVATE_NETWORKING')) + enable_monitor_private_link = _to_bool(_get_env('AZURE_ENV_ENABLE_AZURE_MONITOR_PRIVATE_LINK', 'ENABLE_AZURE_MONITOR_PRIVATE_LINK')) existing_vnet_id = _get_env('AZURE_ENV_EXISTING_VNET_RESOURCE_ID', 'EXISTING_VNET_RESOURCE_ID').strip() app_subnet_id = _get_env('AZURE_ENV_EXISTING_APP_SERVICE_SUBNET_RESOURCE_ID', 'EXISTING_APP_SERVICE_SUBNET_RESOURCE_ID').strip() pe_subnet_id = _get_env('AZURE_ENV_EXISTING_PRIVATE_ENDPOINT_SUBNET_RESOURCE_ID', 'EXISTING_PRIVATE_ENDPOINT_SUBNET_RESOURCE_ID').strip() private_dns_raw = _get_env('AZURE_ENV_PRIVATE_DNS_ZONE_CONFIGS', 'PRIVATE_DNS_ZONE_CONFIGS') + if not _validate_monitor_private_link(enable_private_networking, enable_monitor_private_link, pe_subnet_id): + return 1 + if not _validate_managed_identity_preflight(): return 1 diff --git a/deployers/terraform/ReadMe.md b/deployers/terraform/ReadMe.md index 33cc2fb3c..783da5c25 100644 --- a/deployers/terraform/ReadMe.md +++ b/deployers/terraform/ReadMe.md @@ -207,6 +207,8 @@ param_existing_azure_openai_subscription_id = "00000000-0000-0000-0000-000000000 #### Private Networking Variables param_enable_private_networking = true +# Default off; private-only monitoring is optional in either cloud +# param_enable_azure_monitor_private_link = true # Option 1: let Terraform create the VNet and required subnets # param_existing_virtual_network_id = "" @@ -249,6 +251,14 @@ terraform destroy -var-file="./params/rudy1.tfvars" -auto-approve ## Post-Deployment Manual Steps +### AMPLS and Network Sizing + +`param_enable_azure_monitor_private_link` defaults false and requires `param_enable_private_networking=true`. It associates monitoring resources with AMPLS and disables public ingestion and queries. Telemetry senders and monitoring users need private DNS/routing. Coordinate with the owner of any existing AMPLS in the shared DNS boundary before opting into a new scope. + +This uses the generated [shared ARM template](../bicep/modules/azureMonitorPrivateNetworking.json) through Terraform's template-deployment resource. Retain the adjacent Bicep directory when distributing the deployer. With no existing network IDs, the default VNet is `10.0.0.0/21` with two `/24` subnets. See the [Commercial fresh/compact tables](../../docs/guides/enterprise-networking.md#networking-in-azure-commercial) and [Government fresh/compact tables](../../docs/guides/enterprise-networking.md#networking-in-azure-government). AMPLS needs `/27` or larger and at least 11 monitoring IPs; plan `/26` or larger for shared platform endpoints. Existing-VNet reuse does not implement the five-subnet compact reference automatically. + +DNS overrides accept `monitor`, `monitorOms`, `monitorOds`, `monitorAgent`, `monitorAdx` (Government), and `videoIndexer`, in addition to core service keys, using `zone_resource_id`/`create_vnet_link`. AMPLS reuses Blob DNS. Video Indexer uses dedicated Commercial DNS only when supported. Government Key Vault uses `privatelink.vaultcore.usgovcloudapi.net`. Terraform does not provision Redis. + ### Private Networking Notes - If `param_enable_private_networking = true` and you supply any existing VNet or subnet IDs, supply **both** `param_existing_app_service_subnet_id` and `param_existing_private_endpoint_subnet_id`. diff --git a/deployers/terraform/main.tf b/deployers/terraform/main.tf index 7b7ea94b3..c1a79f9ef 100644 --- a/deployers/terraform/main.tf +++ b/deployers/terraform/main.tf @@ -517,6 +517,8 @@ resource "azurerm_log_analytics_workspace" "la" { location = azurerm_resource_group.rg.location resource_group_name = azurerm_resource_group.rg.name sku = "PerGB2018" # Pay-as-you-go SKU + internet_ingestion_enabled = !var.param_enable_azure_monitor_private_link + internet_query_enabled = !var.param_enable_azure_monitor_private_link tags = local.common_tags } @@ -559,6 +561,8 @@ resource "azurerm_application_insights" "ai" { resource_group_name = azurerm_resource_group.rg.name application_type = "web" workspace_id = azurerm_log_analytics_workspace.la.id + internet_ingestion_enabled = !var.param_enable_azure_monitor_private_link + internet_query_enabled = !var.param_enable_azure_monitor_private_link tags = local.common_tags } diff --git a/deployers/terraform/private_networking.tf b/deployers/terraform/private_networking.tf index 72350a305..07fa1613e 100644 --- a/deployers/terraform/private_networking.tf +++ b/deployers/terraform/private_networking.tf @@ -19,6 +19,12 @@ variable "param_existing_virtual_network_id" { default = "" } +variable "param_enable_azure_monitor_private_link" { + description = "Opt in to private-only Application Insights and Log Analytics through AMPLS. Requires private networking and private DNS/routing for all monitoring clients." + type = bool + default = false +} + variable "param_existing_app_service_subnet_id" { description = "Optional existing subnet resource ID to use for App Service VNet integration." type = string @@ -50,7 +56,7 @@ variable "param_private_endpoint_subnet_prefixes" { } variable "param_private_dns_zone_configs" { - description = "Optional per-zone configuration for private DNS create or reuse behavior. Supported keys: keyVault, cosmosDb, containerRegistry, aiSearch, blobStorage, cognitiveServices, openAi, webSites." + description = "Optional per-zone configuration for private DNS create or reuse behavior, including videoIndexer and monitor/monitorOms/monitorOds/monitorAgent/monitorAdx." type = map(object({ zone_resource_id = optional(string, "") create_vnet_link = optional(bool, true) @@ -60,9 +66,9 @@ variable "param_private_dns_zone_configs" { validation { condition = length(setsubtract( toset(keys(var.param_private_dns_zone_configs)), - toset(["keyVault", "cosmosDb", "containerRegistry", "aiSearch", "blobStorage", "cognitiveServices", "openAi", "webSites"]) + toset(["keyVault", "cosmosDb", "containerRegistry", "aiSearch", "blobStorage", "cognitiveServices", "openAi", "webSites", "videoIndexer", "monitor", "monitorOms", "monitorOds", "monitorAgent", "monitorAdx"]) )) == 0 - error_message = "param_private_dns_zone_configs only supports these keys: keyVault, cosmosDb, containerRegistry, aiSearch, blobStorage, cognitiveServices, openAi, webSites." + error_message = "Unsupported private DNS key. Use a core service key, videoIndexer, monitor, monitorOms, monitorOds, monitorAgent, or monitorAdx." } } @@ -71,13 +77,13 @@ locals { resolved_existing_virtual_network_id = var.param_existing_virtual_network_id != "" ? var.param_existing_virtual_network_id : local.inferred_virtual_network_id use_existing_private_network = var.param_enable_private_networking && (local.resolved_existing_virtual_network_id != "" || var.param_existing_app_service_subnet_id != "" || var.param_existing_private_endpoint_subnet_id != "") - private_dns_zone_names = var.global_which_azure_platform == "AzureUSGovernment" ? { + private_dns_zone_names = merge(var.global_which_azure_platform == "AzureUSGovernment" ? { aiSearch = "privatelink.search.azure.us" blobStorage = "privatelink.blob.core.usgovcloudapi.net" cognitiveServices = "privatelink.cognitiveservices.azure.us" containerRegistry = "privatelink.azurecr.us" cosmosDb = "privatelink.documents.azure.us" - keyVault = "privatelink.vaultcore.azure.us" + keyVault = "privatelink.vaultcore.usgovcloudapi.net" openAi = "privatelink.openai.azure.us" webSites = "privatelink.azurewebsites.us" } : { @@ -89,7 +95,9 @@ locals { keyVault = "privatelink.vaultcore.azure.net" openAi = "privatelink.openai.azure.com" webSites = "privatelink.azurewebsites.net" - } + }, var.param_deploy_video_indexer_service && local.video_indexer_supports_private_endpoints && var.global_which_azure_platform != "AzureUSGovernment" ? { + videoIndexer = "privatelink.api.videoindexer.ai" + } : {}) private_dns_zone_link_suffixes = { aiSearch = "searchService" @@ -107,7 +115,7 @@ locals { name = zone_name zone_resource_id = try(var.param_private_dns_zone_configs[zone_key].zone_resource_id, "") create_vnet_link = try(var.param_private_dns_zone_configs[zone_key].create_vnet_link, true) - link_name = lower("${var.param_base_name}-${var.param_environment}-${local.private_dns_zone_link_suffixes[zone_key]}-pe-dnszonelink") + link_name = lower("${var.param_base_name}-${var.param_environment}-${lookup(local.private_dns_zone_link_suffixes, zone_key, zone_key)}-pe-dnszonelink") } } } @@ -118,6 +126,11 @@ resource "terraform_data" "private_networking_validation" { } lifecycle { + precondition { + condition = !var.param_enable_azure_monitor_private_link || var.param_enable_private_networking + error_message = "param_enable_azure_monitor_private_link requires param_enable_private_networking=true." + } + precondition { condition = !var.param_enable_private_networking || !local.use_existing_private_network || (var.param_existing_app_service_subnet_id != "" && var.param_existing_private_endpoint_subnet_id != "") error_message = "When reusing an existing virtual network for private networking, both param_existing_app_service_subnet_id and param_existing_private_endpoint_subnet_id must be provided." @@ -283,12 +296,12 @@ locals { dns_zone_key = "openAi" } } : {}, - var.param_deploy_video_indexer_service && local.video_indexer_supports_private_endpoints ? { + var.param_deploy_video_indexer_service && local.video_indexer_supports_private_endpoints && var.global_which_azure_platform != "AzureUSGovernment" ? { videoIndexer = { name = "videoindexer" resource_id = azapi_resource.video_indexer[0].id subresource = "account" - dns_zone_key = "cognitiveServices" + dns_zone_key = "videoIndexer" } } : {} ) : {} @@ -326,6 +339,32 @@ output "virtual_network_id" { value = local.resolved_virtual_network_id } +resource "azurerm_resource_group_template_deployment" "azure_monitor_private_networking" { + count = var.param_enable_azure_monitor_private_link ? 1 : 0 + name = "azureMonitorPrivateNetworking" + resource_group_name = azurerm_resource_group.rg.name + deployment_mode = "Incremental" + template_content = file("${path.module}/../bicep/modules/azureMonitorPrivateNetworking.json") + parameters_content = jsonencode({ + location = { value = azurerm_resource_group.rg.location } + appName = { value = var.param_base_name } + environment = { value = var.param_environment } + tags = { value = local.common_tags } + virtualNetworkId = { value = local.resolved_virtual_network_id } + privateEndpointSubnetId = { value = local.resolved_private_endpoint_subnet_id != null ? local.resolved_private_endpoint_subnet_id : "" } + blobStorageDnsZoneId = { value = try(local.private_dns_zone_ids["blobStorage"], "") } + monitoringResourceIds = { value = [azurerm_log_analytics_workspace.la.id, azurerm_application_insights.ai.id] } + privateDnsZoneConfigs = { + value = { for key, config in var.param_private_dns_zone_configs : key => { + zoneResourceId = config.zone_resource_id + createVNetLink = config.create_vnet_link + } } + } + }) + + depends_on = [terraform_data.private_networking_validation, azapi_resource.private_dns_zone_link] +} + output "app_service_subnet_id" { description = "The App Service VNet integration subnet ID used for private networking, if enabled." value = local.resolved_app_service_subnet_id diff --git a/deployers/version.txt b/deployers/version.txt index e8eb7f11f..ffcbe7136 100644 --- a/deployers/version.txt +++ b/deployers/version.txt @@ -1 +1 @@ -1.0.33 +1.0.34 diff --git a/docs/deploy/special-scenarios.md b/docs/deploy/special-scenarios.md index 64f8444bc..6a4f12e0f 100644 --- a/docs/deploy/special-scenarios.md +++ b/docs/deploy/special-scenarios.md @@ -161,13 +161,18 @@ The architecture consists of a single VNet with multiple subnets and a set of Pr - **App Inbound:** Hosts the Private Endpoint for inbound traffic to the App Service (front end). This allows secure, private access from internal users or jump hosts. - **App Outbound:** Used for VNet Integration, enabling the App Service to make outbound calls to private endpoints of backend services (Cosmos DB, Redis, Storage, etc.). - **Data:** Hosts Private Endpoints for data services (Cosmos DB, Redis, Storage). -- **AI Services:** Hosts Private Endpoints for AI-related services (OpenAI, AI Search, Document Intelligence, Content Safety, Speech, Video Indexer). +- **AI Services:** Hosts Private Endpoints where supported (OpenAI, AI Search, Document Intelligence, Content Safety, Speech, and Commercial Video Indexer). +- **Platform:** Hosts ACR, Key Vault, and optional Azure Monitor Private Link Scope (AMPLS). + +These are reference architecture segments, not the unmodified deployer output. A **fresh private-network deployment without existing VNet/subnet IDs** creates a `10.0.0.0/21` VNet, delegated `10.0.0.0/24` App Service integration subnet, and shared `10.0.2.0/24` private endpoint subnet. This is identical in Commercial and Government; the scripts do not automatically split endpoint subnets by service. + +The [Commercial subnet tables](../guides/enterprise-networking.md#networking-in-azure-commercial) and [Government subnet tables](../guides/enterprise-networking.md#networking-in-azure-government) show **fresh deployment defaults** and a **compact existing-network reference** using `10.42.0.0/24`, with address counts, Azure-usable counts, delegation, and reserved growth space. The compact reference needs enterprise-managed endpoint placement or IaC customization; existing-VNet reuse still sends script-created endpoints to one supplied endpoint subnet. **Key Points:** - The App Service has **two network connections**: - **Private Endpoint (Inbound):** Exposes the app privately for internal access (e.g., from a corporate network or jump box). - **VNet Integration (Outbound):** Allows the app to reach private endpoints of backend services via the VNet. -- All backend services are deployed with Private Endpoints in their respective subnets. +- Script-created endpoints share one subnet; segmented placement requires customization. Government Video Indexer has no private endpoint in these scripts. - Each service type requires a corresponding Private DNS Zone for name resolution. #### **Required Private DNS Zones** @@ -178,16 +183,19 @@ For each Azure service with a Private Endpoint, you must create and link the fol |------------------------|----------------------------------------------| | App Service | privatelink.azurewebsites.net | | Cosmos DB | privatelink.documents.azure.com | -| Redis Cache | privatelink.redis.cache.windows.net | +| Managed Redis | privatelink.redis.azure.net | +| Classic Redis | privatelink.redis.cache.windows.net | +| Container Registry | privatelink.azurecr.io | +| Key Vault | privatelink.vaultcore.azure.net | | Storage Account | privatelink.blob.core.windows.net | | Azure AI Search | privatelink.search.windows.net | | Azure OpenAI | privatelink.openai.azure.com | | Document Intelligence | privatelink.cognitiveservices.azure.com | | Content Safety | privatelink.cognitiveservices.azure.com | | Speech Service | privatelink.cognitiveservices.azure.com | -| Video Indexer | privatelink.cognitiveservices.azure.com | +| Video Indexer | privatelink.api.videoindexer.ai | -> **Note:** Some services (like Document Intelligence, Content Safety, Speech, Video Indexer) share the same DNS zone: `privatelink.cognitiveservices.azure.com`. +This table is **Azure Commercial**. Document Intelligence, Content Safety, and Speech share Cognitive Services DNS, but Video Indexer does not. See the [cloud-specific DNS matrix and AMPLS setup](../guides/enterprise-networking.md#step-5-configure-private-dns) for Government and monitoring. Government Key Vault uses `privatelink.vaultcore.usgovcloudapi.net`; Government Redis uses `privatelink.redis.cache.usgovcloudapi.net`. **You must link each DNS zone to the VNet and ensure the correct records are created for each private endpoint.** @@ -199,7 +207,7 @@ For each Azure service with a Private Endpoint, you must create and link the fol #### **Summary of Steps for Enterprise Network Isolation** -1. **Create a VNet** with at least four subnets: App Inbound, App Outbound, Data, and AI Services. +1. **Choose fresh-deployment defaults or the compact enterprise layout**, avoiding ranges overlapping connected networks. 2. **Deploy Private Endpoints** for each backend service (Cosmos DB, Redis, Storage, AI Search, OpenAI, etc.) into the appropriate subnet. 3. **Create and link Private DNS Zones** for each service type to the VNet. 4. **Configure App Service:** @@ -208,6 +216,6 @@ For each Azure service with a Private Endpoint, you must create and link the fol 5. **Restrict public network access** on all backend services (set "Deny public network access" to "Yes"). 6. **Test connectivity** from the App Service to each backend service to ensure all traffic is private. -> This architecture is recommended for all production, government, and regulated environments. The pattern is identical for Azure Commercial, Azure Government, and custom/sovereign clouds—only the region and service endpoints differ. +AMPLS is a default-off deployment option in both clouds. Its endpoint needs at least 11 IPs and a `/27` or larger subnet; the compact reference uses a `/26` platform subnet. Review shared-DNS impacts before enabling private-only monitoring. Service and private-endpoint availability differ between clouds, not just hostname suffixes. For more details, see the [Azure documentation on Private Endpoints](https://learn.microsoft.com/azure/private-link/private-endpoint-overview) and [App Service VNet Integration](https://learn.microsoft.com/azure/app-service/overview-vnet-integration). diff --git a/docs/explanation/features/AZURE_MANAGED_REDIS_SUPPORT.md b/docs/explanation/features/AZURE_MANAGED_REDIS_SUPPORT.md index c26198d54..c4401e6c4 100644 --- a/docs/explanation/features/AZURE_MANAGED_REDIS_SUPPORT.md +++ b/docs/explanation/features/AZURE_MANAGED_REDIS_SUPPORT.md @@ -179,7 +179,7 @@ the change outside business hours. - Azure Managed Redis is unavailable in Azure Government and Azure operated by 21Vianet. Deployments in those clouds must set `redisCacheKind` to `classic`. - Azure Managed Redis does not support virtual network injection. Network isolation requires - Azure Private Link, which this deployer does not provision for Redis. + Azure Private Link. From **0.261.333** (deployer **1.0.34**, [#1765](https://github.com/microsoft/simplechat/issues/1765)), Bicep/AZD creates Redis private endpoints when private networking is enabled. Redis public access stays open during bootstrap and is disabled by the final `azd up` hook; separate provision/deploy workflows must perform equivalent lockdown after configuration succeeds. See the [networking guide](../../guides/enterprise-networking.md) for fresh and compact layouts. - Some Redis `INFO` counters shown in Redis Metrics are unavailable on the Redis Enterprise engine and render as "Not available". - Keyspace notifications are not available on Azure Managed Redis. SimpleChat does not use diff --git a/docs/explanation/features/AZURE_MONITOR_PRIVATE_LINK.md b/docs/explanation/features/AZURE_MONITOR_PRIVATE_LINK.md new file mode 100644 index 000000000..2a8b594cc --- /dev/null +++ b/docs/explanation/features/AZURE_MONITOR_PRIVATE_LINK.md @@ -0,0 +1,25 @@ +# Optional Azure Monitor Private Link (v0.261.333) + +Implemented in version: **0.261.333**. Deployer version: **1.0.34**. +Related issue: [#1765](https://github.com/microsoft/simplechat/issues/1765). +The application version is recorded in `application/single_app/config.py`. + +## Purpose and Dependencies + +Private monitoring keeps telemetry ingestion and monitoring queries on private network paths. Administrators opt in at deployment time; public monitoring remains the default. Requires private networking, linked private DNS, a `/27` or larger endpoint subnet with at least 11 monitoring IPs, and private routes for senders and query clients. + +## Architecture and Configuration + +`deployers/bicep/modules/azureMonitorPrivateNetworking.bicep` owns the AMPLS, two scoped resource associations, private endpoint, and cloud-aware DNS. Its generated ARM template is reused by Terraform and CLI. Blob DNS comes from the existing Storage zone. The endpoint DNS group contains five zones; Government ADX DNS is linked separately, with matching endpoint NIC metadata grouped into A-record sets by `privateDNSRecords.bicep`. + +Bicep/AZD uses `enableAzureMonitorPrivateLink` / `ENABLE_AZURE_MONITOR_PRIVATE_LINK`; Terraform uses `param_enable_azure_monitor_private_link`; PowerShell uses `param_EnableAzureMonitorPrivateLink`. All default false and require private networking. Monitoring resources deny public ingestion and queries when enabled, with AMPLS access modes set to `PrivateOnly`. Per-zone reuse uses the existing DNS override object. + +## Administration + +See the [Enterprise Networking guide](../../guides/enterprise-networking.md) for Commercial and Government **fresh deployment defaults**, **compact existing-network reference**, subnet/IP tables, DNS, opt-in commands, and shared-DNS considerations. Fresh deployments create one shared endpoint subnet, not the segmented reference. + +Coordinate one AMPLS per shared DNS boundary. This option creates a new scope; where enterprise AMPLS exists, have its owner associate these monitoring resources instead. Reprovision Government overflow records after endpoint DNS/IP changes. Incremental Bicep/CLI deployment does not remove an old scope or DNS configuration when the option is disabled. No application UI route or API is added. + +## Testing and Limitations + +`functional_tests/test_azd_private_dns_zone_config_wiring.py` covers preflight failures/successes, cloud DNS, template wiring, and Redis hooks. Bicep compilation validates the resource graph and artifacts; Terraform validation and PowerShell parsing check adapters. Live telemetry and queries still require a cloud environment. AMPLS does not privatize all control-plane requests or external services. Coordinate explicit cleanup and validate all affected clients before changing shared DNS. diff --git a/docs/explanation/fixes/DEPLOYER_PRIVATE_ENDPOINT_DNS_FIX.md b/docs/explanation/fixes/DEPLOYER_PRIVATE_ENDPOINT_DNS_FIX.md new file mode 100644 index 000000000..eea24d5c3 --- /dev/null +++ b/docs/explanation/fixes/DEPLOYER_PRIVATE_ENDPOINT_DNS_FIX.md @@ -0,0 +1,22 @@ +# Deployer Private Endpoint and DNS Fix (v0.261.333) + +Fixed in version: **0.261.333**. Deployer version: **1.0.34**. +Related issue: [#1765](https://github.com/microsoft/simplechat/issues/1765). +The application version is recorded in `application/single_app/config.py`. + +## Issue and Root Cause + +Bicep deployed Redis without a private endpoint and left Managed Redis public. Video Indexer shared Cognitive Services DNS instead of its Commercial zone. Government Key Vault had an incorrect private zone. These gaps made private-network deployments look more complete than their actual data paths. + +## Technical Changes + +- Bicep passes the optional Redis resource ID, selects `redisEnterprise`/`redisCache`, and creates offering/cloud DNS and an endpoint. Redis remains public during bootstrap; AZD's final `postup` disables it with the appropriate API version after configuration/deployment succeeds. +- Video Indexer uses `privatelink.api.videoindexer.ai` in Commercial. Government remains excluded from PE creation. Terraform's association is corrected and CLI reconciles the supported endpoint's zone on redeployment. +- Government Key Vault uses `privatelink.vaultcore.usgovcloudapi.net` in all three deployers. +- Optional [AMPLS](../features/AZURE_MONITOR_PRIVATE_LINK.md) defaults off in both clouds. + +## Validation and Impact + +The existing private DNS functional suite covers cloud maps, generated wiring, AMPLS preflight, and Redis hooks. Bicep, Terraform, and PowerShell validation do not provision cloud resources. Live DNS resolution, Redis connectivity, ingestion, and queries need deployment validation. `azd provision`/`azd deploy` alone do not execute `postup`; separate workflows must perform equivalent lockdown. Later Managed Redis provisioning resets bootstrap access and requires final lockdown again. + +The [networking guide](../../guides/enterprise-networking.md) distinguishes script-created greenfield defaults from compact enterprise reference tables in both clouds. User-owned Visio source files are not rewritten by this fix. diff --git a/docs/explanation/release-notes/index.md b/docs/explanation/release-notes/index.md index 8f0a083c5..ec17b49b3 100644 --- a/docs/explanation/release-notes/index.md +++ b/docs/explanation/release-notes/index.md @@ -20,27 +20,265 @@ This page includes the latest release notes inline. Older release sections are s | Version | Page | | --- | --- | -| v0.261.023 | [Release notes index]({{ '/explanation/release_notes/' | relative_url }}) | -| v0.261.021 | [Release notes index]({{ '/explanation/release_notes/' | relative_url }}) | -| v0.261.020 | [Release notes index]({{ '/explanation/release_notes/' | relative_url }}) | -| v0.261.019 | [Release notes index]({{ '/explanation/release_notes/' | relative_url }}) | -| v0.261.018 | [Release notes index]({{ '/explanation/release_notes/' | relative_url }}) | -| v0.261.017 | [Release notes index]({{ '/explanation/release_notes/' | relative_url }}) | -| v0.261.016 | [Release notes index]({{ '/explanation/release_notes/' | relative_url }}) | -| v0.261.015 | [Release notes index]({{ '/explanation/release_notes/' | relative_url }}) | -| v0.261.014 | [Release notes index]({{ '/explanation/release_notes/' | relative_url }}) | -| v0.261.013 | [Release notes index]({{ '/explanation/release_notes/' | relative_url }}) | -| v0.261.012 | [Release notes index]({{ '/explanation/release_notes/' | relative_url }}) | -| v0.261.011 | [Release notes index]({{ '/explanation/release_notes/' | relative_url }}) | -| v0.261.010 | [Release notes 0.261 series]({{ '/explanation/release-notes/v0.261/' | relative_url }}) | -| v0.261.009 | [Release notes 0.261 series]({{ '/explanation/release-notes/v0.261/' | relative_url }}) | -| v0.261.007 | [Release notes 0.261 series]({{ '/explanation/release-notes/v0.261/' | relative_url }}) | -| v0.261.006 | [Release notes 0.261 series]({{ '/explanation/release-notes/v0.261/' | relative_url }}) | -| v0.261.005 | [Release notes 0.261 series]({{ '/explanation/release-notes/v0.261/' | relative_url }}) | -| v0.261.004 | [Release notes 0.261 series]({{ '/explanation/release-notes/v0.261/' | relative_url }}) | -| v0.261.003 | [Release notes 0.261 series]({{ '/explanation/release-notes/v0.261/' | relative_url }}) | -| v0.261.002 | [Release notes 0.261 series]({{ '/explanation/release-notes/v0.261/' | relative_url }}) | -| v0.261.001 | [Release notes 0.261 series]({{ '/explanation/release-notes/v0.261/' | relative_url }}) | +| v0.261.333 | [Release notes index]({{ '/explanation/release_notes/' | relative_url }}) | +| v0.261.315 | [Release notes index]({{ '/explanation/release_notes/' | relative_url }}) | +| v0.261.309 | [Release notes index]({{ '/explanation/release_notes/' | relative_url }}) | +| v0.261.308 | [Release notes index]({{ '/explanation/release_notes/' | relative_url }}) | +| v0.261.306 | [Release notes index]({{ '/explanation/release_notes/' | relative_url }}) | +| v0.261.305 | [Release notes index]({{ '/explanation/release_notes/' | relative_url }}) | +| v0.261.304 | [Release notes index]({{ '/explanation/release_notes/' | relative_url }}) | +| v0.261.303 | [Release notes index]({{ '/explanation/release_notes/' | relative_url }}) | +| v0.261.302 | [Release notes index]({{ '/explanation/release_notes/' | relative_url }}) | +| v0.261.301 | [Release notes index]({{ '/explanation/release_notes/' | relative_url }}) | +| v0.261.300 | [Release notes index]({{ '/explanation/release_notes/' | relative_url }}) | +| v0.261.299 | [Release notes index]({{ '/explanation/release_notes/' | relative_url }}) | +| v0.261.298 | [Release notes 0.261.298 – 0.261.252]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.297 | [Release notes 0.261.298 – 0.261.252]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.296 | [Release notes 0.261.298 – 0.261.252]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.295 | [Release notes 0.261.298 – 0.261.252]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.293 | [Release notes 0.261.298 – 0.261.252]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.292 | [Release notes 0.261.298 – 0.261.252]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.291 | [Release notes 0.261.298 – 0.261.252]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.290 | [Release notes 0.261.298 – 0.261.252]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.289 | [Release notes 0.261.298 – 0.261.252]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.288 | [Release notes 0.261.298 – 0.261.252]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.287 | [Release notes 0.261.298 – 0.261.252]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.286 | [Release notes 0.261.298 – 0.261.252]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.285 | [Release notes 0.261.298 – 0.261.252]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.284 | [Release notes 0.261.298 – 0.261.252]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.283 | [Release notes 0.261.298 – 0.261.252]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.282 | [Release notes 0.261.298 – 0.261.252]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.280 | [Release notes 0.261.298 – 0.261.252]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.279 | [Release notes 0.261.298 – 0.261.252]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.278 | [Release notes 0.261.298 – 0.261.252]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.277 | [Release notes 0.261.298 – 0.261.252]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.276 | [Release notes 0.261.298 – 0.261.252]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.275 | [Release notes 0.261.298 – 0.261.252]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.274 | [Release notes 0.261.298 – 0.261.252]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.271 | [Release notes 0.261.298 – 0.261.252]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.270 | [Release notes 0.261.298 – 0.261.252]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.269 | [Release notes 0.261.298 – 0.261.252]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.268 | [Release notes 0.261.298 – 0.261.252]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.267 | [Release notes 0.261.298 – 0.261.252]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.265 | [Release notes 0.261.298 – 0.261.252]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.264 | [Release notes 0.261.298 – 0.261.252]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.263 | [Release notes 0.261.298 – 0.261.252]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.262 | [Release notes 0.261.298 – 0.261.252]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.261 | [Release notes 0.261.298 – 0.261.252]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.260 | [Release notes 0.261.298 – 0.261.252]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.259 | [Release notes 0.261.298 – 0.261.252]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.258 | [Release notes 0.261.298 – 0.261.252]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.257 | [Release notes 0.261.298 – 0.261.252]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.256 | [Release notes 0.261.298 – 0.261.252]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.253 | [Release notes 0.261.298 – 0.261.252]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.252 | [Release notes 0.261.298 – 0.261.252]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.251 | [Release notes 0.261.251 – 0.261.210]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.250 | [Release notes 0.261.251 – 0.261.210]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.248 | [Release notes 0.261.251 – 0.261.210]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.247 | [Release notes 0.261.251 – 0.261.210]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.246 | [Release notes 0.261.251 – 0.261.210]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.245 | [Release notes 0.261.251 – 0.261.210]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.244 | [Release notes 0.261.251 – 0.261.210]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.243 | [Release notes 0.261.251 – 0.261.210]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.242 | [Release notes 0.261.251 – 0.261.210]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.241 | [Release notes 0.261.251 – 0.261.210]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.240 | [Release notes 0.261.251 – 0.261.210]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.239 | [Release notes 0.261.251 – 0.261.210]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.238 | [Release notes 0.261.251 – 0.261.210]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.237 | [Release notes 0.261.251 – 0.261.210]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.236 | [Release notes 0.261.251 – 0.261.210]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.235 | [Release notes 0.261.251 – 0.261.210]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.234 | [Release notes 0.261.251 – 0.261.210]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.233 | [Release notes 0.261.251 – 0.261.210]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.232 | [Release notes 0.261.251 – 0.261.210]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.231 | [Release notes 0.261.251 – 0.261.210]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.230 | [Release notes 0.261.251 – 0.261.210]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.229 | [Release notes 0.261.251 – 0.261.210]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.228 | [Release notes 0.261.251 – 0.261.210]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.227 | [Release notes 0.261.251 – 0.261.210]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.226 | [Release notes 0.261.251 – 0.261.210]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.225 | [Release notes 0.261.251 – 0.261.210]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.224 | [Release notes 0.261.251 – 0.261.210]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.223 | [Release notes 0.261.251 – 0.261.210]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.222 | [Release notes 0.261.251 – 0.261.210]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.221 | [Release notes 0.261.251 – 0.261.210]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.220 | [Release notes 0.261.251 – 0.261.210]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.219 | [Release notes 0.261.251 – 0.261.210]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.218 | [Release notes 0.261.251 – 0.261.210]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.217 | [Release notes 0.261.251 – 0.261.210]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.216 | [Release notes 0.261.251 – 0.261.210]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.215 | [Release notes 0.261.251 – 0.261.210]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.214 | [Release notes 0.261.251 – 0.261.210]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.213 | [Release notes 0.261.251 – 0.261.210]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.212 | [Release notes 0.261.251 – 0.261.210]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.210 | [Release notes 0.261.251 – 0.261.210]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.209 | [Release notes 0.261.209 – 0.261.166]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.208 | [Release notes 0.261.209 – 0.261.166]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.207 | [Release notes 0.261.209 – 0.261.166]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.205 | [Release notes 0.261.209 – 0.261.166]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.204 | [Release notes 0.261.209 – 0.261.166]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.203 | [Release notes 0.261.209 – 0.261.166]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.202 | [Release notes 0.261.209 – 0.261.166]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.201 | [Release notes 0.261.209 – 0.261.166]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.200 | [Release notes 0.261.209 – 0.261.166]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.199 | [Release notes 0.261.209 – 0.261.166]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.195 | [Release notes 0.261.209 – 0.261.166]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.194 | [Release notes 0.261.209 – 0.261.166]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.193 | [Release notes 0.261.209 – 0.261.166]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.192 | [Release notes 0.261.209 – 0.261.166]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.191 | [Release notes 0.261.209 – 0.261.166]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.190 | [Release notes 0.261.209 – 0.261.166]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.189 | [Release notes 0.261.209 – 0.261.166]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.188 | [Release notes 0.261.209 – 0.261.166]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.187 | [Release notes 0.261.209 – 0.261.166]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.186 | [Release notes 0.261.209 – 0.261.166]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.185 | [Release notes 0.261.209 – 0.261.166]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.184 | [Release notes 0.261.209 – 0.261.166]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.183 | [Release notes 0.261.209 – 0.261.166]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.182 | [Release notes 0.261.209 – 0.261.166]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.181 | [Release notes 0.261.209 – 0.261.166]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.180 | [Release notes 0.261.209 – 0.261.166]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.179 | [Release notes 0.261.209 – 0.261.166]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.178 | [Release notes 0.261.209 – 0.261.166]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.177 | [Release notes 0.261.209 – 0.261.166]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.176 | [Release notes 0.261.209 – 0.261.166]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.175 | [Release notes 0.261.209 – 0.261.166]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.174 | [Release notes 0.261.209 – 0.261.166]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.173 | [Release notes 0.261.209 – 0.261.166]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.172 | [Release notes 0.261.209 – 0.261.166]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.171 | [Release notes 0.261.209 – 0.261.166]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.170 | [Release notes 0.261.209 – 0.261.166]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.169 | [Release notes 0.261.209 – 0.261.166]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.168 | [Release notes 0.261.209 – 0.261.166]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.167 | [Release notes 0.261.209 – 0.261.166]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.166 | [Release notes 0.261.209 – 0.261.166]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.165 | [Release notes 0.261.165 – 0.261.104]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.164 | [Release notes 0.261.165 – 0.261.104]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.163 | [Release notes 0.261.165 – 0.261.104]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.162 | [Release notes 0.261.165 – 0.261.104]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.161 | [Release notes 0.261.165 – 0.261.104]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.160 | [Release notes 0.261.165 – 0.261.104]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.159 | [Release notes 0.261.165 – 0.261.104]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.158 | [Release notes 0.261.165 – 0.261.104]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.157 | [Release notes 0.261.165 – 0.261.104]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.156 | [Release notes 0.261.165 – 0.261.104]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.155 | [Release notes 0.261.165 – 0.261.104]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.154 | [Release notes 0.261.165 – 0.261.104]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.153 | [Release notes 0.261.165 – 0.261.104]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.152 | [Release notes 0.261.165 – 0.261.104]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.151 | [Release notes 0.261.165 – 0.261.104]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.150 | [Release notes 0.261.165 – 0.261.104]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.149 | [Release notes 0.261.165 – 0.261.104]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.148 | [Release notes 0.261.165 – 0.261.104]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.147 | [Release notes 0.261.165 – 0.261.104]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.146 | [Release notes 0.261.165 – 0.261.104]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.145 | [Release notes 0.261.165 – 0.261.104]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.144 | [Release notes 0.261.165 – 0.261.104]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.143 | [Release notes 0.261.165 – 0.261.104]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.142 | [Release notes 0.261.165 – 0.261.104]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.141 | [Release notes 0.261.165 – 0.261.104]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.140 | [Release notes 0.261.165 – 0.261.104]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.139 | [Release notes 0.261.165 – 0.261.104]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.138 | [Release notes 0.261.165 – 0.261.104]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.137 | [Release notes 0.261.165 – 0.261.104]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.136 | [Release notes 0.261.165 – 0.261.104]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.135 | [Release notes 0.261.165 – 0.261.104]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.134 | [Release notes 0.261.165 – 0.261.104]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.133 | [Release notes 0.261.165 – 0.261.104]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.132 | [Release notes 0.261.165 – 0.261.104]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.131 | [Release notes 0.261.165 – 0.261.104]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.129 | [Release notes 0.261.165 – 0.261.104]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.128 | [Release notes 0.261.165 – 0.261.104]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.127 | [Release notes 0.261.165 – 0.261.104]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.123 | [Release notes 0.261.165 – 0.261.104]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.104 | [Release notes 0.261.165 – 0.261.104]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.102 | [Release notes 0.261.102 – 0.261.048]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.100 | [Release notes 0.261.102 – 0.261.048]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.099 | [Release notes 0.261.102 – 0.261.048]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.098 | [Release notes 0.261.102 – 0.261.048]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.096 | [Release notes 0.261.102 – 0.261.048]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.095 | [Release notes 0.261.102 – 0.261.048]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.093 | [Release notes 0.261.102 – 0.261.048]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.092 | [Release notes 0.261.102 – 0.261.048]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.091 | [Release notes 0.261.102 – 0.261.048]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.090 | [Release notes 0.261.102 – 0.261.048]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.089 | [Release notes 0.261.102 – 0.261.048]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.088 | [Release notes 0.261.102 – 0.261.048]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.087 | [Release notes 0.261.102 – 0.261.048]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.086 | [Release notes 0.261.102 – 0.261.048]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.085 | [Release notes 0.261.102 – 0.261.048]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.084 | [Release notes 0.261.102 – 0.261.048]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.083 | [Release notes 0.261.102 – 0.261.048]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.082 | [Release notes 0.261.102 – 0.261.048]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.081 | [Release notes 0.261.102 – 0.261.048]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.074 | [Release notes 0.261.102 – 0.261.048]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.071 | [Release notes 0.261.102 – 0.261.048]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.070 | [Release notes 0.261.102 – 0.261.048]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.065 | [Release notes 0.261.102 – 0.261.048]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.064 | [Release notes 0.261.102 – 0.261.048]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.063 | [Release notes 0.261.102 – 0.261.048]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.062 | [Release notes 0.261.102 – 0.261.048]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.061 | [Release notes 0.261.102 – 0.261.048]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.060 | [Release notes 0.261.102 – 0.261.048]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.059 | [Release notes 0.261.102 – 0.261.048]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.058 | [Release notes 0.261.102 – 0.261.048]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.057 | [Release notes 0.261.102 – 0.261.048]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.056 | [Release notes 0.261.102 – 0.261.048]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.055 | [Release notes 0.261.102 – 0.261.048]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.054 | [Release notes 0.261.102 – 0.261.048]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.053 | [Release notes 0.261.102 – 0.261.048]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.052 | [Release notes 0.261.102 – 0.261.048]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.051 | [Release notes 0.261.102 – 0.261.048]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.050 | [Release notes 0.261.102 – 0.261.048]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.049 | [Release notes 0.261.102 – 0.261.048]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.048 | [Release notes 0.261.102 – 0.261.048]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.047 | [Release notes 0.261.047 – 0.261.008]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.046 | [Release notes 0.261.047 – 0.261.008]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.045 | [Release notes 0.261.047 – 0.261.008]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.044 | [Release notes 0.261.047 – 0.261.008]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.043 | [Release notes 0.261.047 – 0.261.008]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.042 | [Release notes 0.261.047 – 0.261.008]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.041 | [Release notes 0.261.047 – 0.261.008]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.040 | [Release notes 0.261.047 – 0.261.008]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.039 | [Release notes 0.261.047 – 0.261.008]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.038 | [Release notes 0.261.047 – 0.261.008]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.037 | [Release notes 0.261.047 – 0.261.008]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.036 | [Release notes 0.261.047 – 0.261.008]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.035 | [Release notes 0.261.047 – 0.261.008]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.034 | [Release notes 0.261.047 – 0.261.008]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.033 | [Release notes 0.261.047 – 0.261.008]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.032 | [Release notes 0.261.047 – 0.261.008]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.031 | [Release notes 0.261.047 – 0.261.008]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.030 | [Release notes 0.261.047 – 0.261.008]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.029 | [Release notes 0.261.047 – 0.261.008]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.028 | [Release notes 0.261.047 – 0.261.008]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.027 | [Release notes 0.261.047 – 0.261.008]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.026 | [Release notes 0.261.047 – 0.261.008]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.025 | [Release notes 0.261.047 – 0.261.008]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.024 | [Release notes 0.261.047 – 0.261.008]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.023 | [Release notes 0.261.047 – 0.261.008]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.022 | [Release notes 0.261.047 – 0.261.008]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.021 | [Release notes 0.261.047 – 0.261.008]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.020 | [Release notes 0.261.047 – 0.261.008]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.019 | [Release notes 0.261.047 – 0.261.008]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.018 | [Release notes 0.261.047 – 0.261.008]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.017 | [Release notes 0.261.047 – 0.261.008]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.016 | [Release notes 0.261.047 – 0.261.008]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.015 | [Release notes 0.261.047 – 0.261.008]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.014 | [Release notes 0.261.047 – 0.261.008]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.013 | [Release notes 0.261.047 – 0.261.008]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.012 | [Release notes 0.261.047 – 0.261.008]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.011 | [Release notes 0.261.047 – 0.261.008]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.010 | [Release notes 0.261.047 – 0.261.008]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.009 | [Release notes 0.261.047 – 0.261.008]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.008 | [Release notes 0.261.047 – 0.261.008]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.007 | [Release notes 0.261.007 – 0.261.001]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | +| v0.261.006 | [Release notes 0.261.007 – 0.261.001]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | +| v0.261.005 | [Release notes 0.261.007 – 0.261.001]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | +| v0.261.004 | [Release notes 0.261.007 – 0.261.001]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | +| v0.261.003 | [Release notes 0.261.007 – 0.261.001]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | +| v0.261.002 | [Release notes 0.261.007 – 0.261.001]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | +| v0.261.001 | [Release notes 0.261.007 – 0.261.001]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | | v0.260.025 | [Release notes 0.260 series]({{ '/explanation/release-notes/v0.260/' | relative_url }}) | | v0.260.024 | [Release notes 0.260 series]({{ '/explanation/release-notes/v0.260/' | relative_url }}) | | v0.260.023 | [Release notes 0.260 series]({{ '/explanation/release-notes/v0.260/' | relative_url }}) | @@ -89,154 +327,233 @@ This page includes the latest release notes inline. Older release sections are s ## Latest release notes -### **(v0.261.023)** +### **(v0.261.333)** #### New Features -* **XSD Ingestion and Schema-Validated XML Generation** - * Added `.xsd` ingestion for personal, group, public, chat-upload, generated-artifact promotion, and File Sync paths when Enhanced Citations storage is available. - * Preserves exact schema bytes in immutable Blob locations and indexes one bounded metadata summary instead of fragmenting the schema as narrative content. - * Resolves same-workspace includes and imports, tracks schema readiness, and revalidates dependent schemas when current revisions change. - * Uses an explicitly selected ready XSD as the authoritative contract for XML generated by Chat, Analyze, agents, and Analyze workflows, with whole-document validation before publication. - * Implements the fail-closed `simplechat-xsd10-subset-profile/1`; unsupported XSD 1.1 and other excluded constructs are retained with diagnostics but cannot govern generation. - * (Ref: #1212, XSD ingestion, mixed-source orchestration, schema-bound XML artifacts, `functions_xsd_schema.py`, `functions_documents.py`) +* **Optional Private Monitoring In Commercial And Government** + * Deployment administrators can opt into Azure Monitor Private Link Scope (AMPLS) for Application Insights and Log Analytics in either cloud. The option defaults off and requires private networking; when enabled, monitoring ingestion and queries use private-only access. + * Bicep/AZD, Terraform, and Azure CLI support cloud-specific monitoring DNS and existing-zone reuse. AMPLS reuses Blob DNS, and Government's additional ADX zone is handled separately from the five-zone private endpoint DNS group. + * (Ref: `enableAzureMonitorPrivateLink`, `ENABLE_AZURE_MONITOR_PRIVATE_LINK`, `azureMonitorPrivateNetworking.bicep`, [Azure Monitor Private Link](features/AZURE_MONITOR_PRIVATE_LINK.md), [#1765](https://github.com/microsoft/simplechat/issues/1765)) #### Bug Fixes -* **XSD Upload Errors Use Allowlisted Browser Responses** - * Replaced exception-derived chat-upload responses with an explicit allowlist of stable XSD error codes, messages, and HTTP statuses. - * Unknown XSD ingestion failures now return a generic error without exposing exception details. - * (Ref: `route_frontend_chats.py`, XSD upload error handling) +* **Redis Private Endpoints And Cloud DNS Corrections** + * Private Bicep/AZD deployments now create the appropriate Managed or classic Redis private endpoint. Redis remains reachable during bootstrap, and the final `azd up` hook disables public access after configuration succeeds. Separate provision/deploy workflows must perform equivalent final lockdown. + * Commercial Video Indexer uses `privatelink.api.videoindexer.ai`, while unsupported Government private endpoint creation remains disabled. Government Key Vault now uses `privatelink.vaultcore.usgovcloudapi.net` across all three deployers. + * (Ref: `privateNetworking.bicep`, `redisCache.bicep`, `privateDNSZones.json`, `deployers/azure.yaml`, [Private Endpoint And DNS Fix](fixes/DEPLOYER_PRIVATE_ENDPOINT_DNS_FIX.md), [#1765](https://github.com/microsoft/simplechat/issues/1765)) -### **(v0.261.021)** +#### Documentation And Architecture -#### Bug Fixes +* **Expanded Visio Architectures And Deployment Network References** + * Updated `artifacts/architecture.vsdx` and `artifacts/private_endpoints.vsdx` with Azure Container Registry, Key Vault, platform networking, compact subnet sizing, monitoring DNS, and Commercial/Government service distinctions, including Managed versus classic Redis. + * Cloud-specific tables distinguish fresh script-created networks from a compact existing-network reference, showing CIDRs, total and Azure-usable addresses, delegation, service placement, and growth space. The compact segmented reference is not automatically created by existing-VNet reuse. + * Added a Visio companion and deployment guidance covering AMPLS capacity, shared-DNS impacts, bootstrap lockdown, and cloud limitations. Diagram labels do not override the documented service/private-endpoint availability requirements. + * Deployer version: **1.0.34**. Live-cloud connectivity and monitoring validation remain deployment checks. + * (Ref: `artifacts/architecture.vsdx`, `artifacts/private_endpoints.vsdx`, [Diagram Network Reference](../../artifacts/PRIVATE_ENDPOINTS_NETWORK_REFERENCE.md), [Enterprise Networking](../guides/enterprise-networking.md), [#1765](https://github.com/microsoft/simplechat/issues/1765)) + +### **(v0.261.315)** -* **OAuth2 Token Endpoints Are Now Validated When The Token Is Fetched** - * A Custom endpoint's OAuth2 token URL was checked when the endpoint was saved, but not when the token was actually requested. Validating only at save time leaves the request itself unguarded, since settings can be written by another path, restored from backup, or changed after validation. Code scanning correctly identified this as a server-side request forgery. - * The token URL is now revalidated at request time against the same outbound policy as the inference endpoint, and the request runs on the same pinned transport, so its addresses are validated at connection time and redirects are refused. - * Refusing redirects is safe for this grant: redirects belong to the browser-based authorization-code flow, whereas a client-credentials token endpoint answers a server-to-server POST with a JSON body. The previous code allowed them based on an incorrect assumption. - * (Ref: `functions_model_endpoint_auth.py`, [#1437](https://github.com/microsoft/simplechat/pull/1437)) +#### New Features -* **Endpoint URL Version Matching No Longer Backtracks** - * The pattern recognising a version path segment allowed its optional suffix to begin with a digit, making it ambiguous with the preceding digits and quadratic on a long run of them. A 8,000-character segment took roughly 0.19 seconds to reject; it now takes 0.0003 seconds. - * The suffix must now begin with a letter, which removes the ambiguity while matching exactly the same version segments. - * (Ref: `model_endpoint_clients.py`, [#1437](https://github.com/microsoft/simplechat/pull/1437)) +* **Create And Edit Workspace Identities In V2** + * Personal users can create and edit reusable identities directly in My Workspace. Group and public workspace identity authoring continues to use the native shared editor, so authorized managers do not need to visit classic pages. + * Personal edits and deletes require the version opened by the editor. A stale save keeps the draft available to rebase, while stored credentials remain server-side and unchanged secrets are preserved. + * (Ref: `functions_personal_identity_access.py`, `route_backend_personal_identities_scoped.py`, `IdentityWorkbenchSection.tsx`, [V2 Workspace Identities](features/V2_WORKSPACE_IDENTITIES.md), [#1722](https://github.com/microsoft/simplechat/issues/1722)) -### **(v0.261.020)** +### **(v0.261.309)** #### New Features -* **Custom Model Endpoints Support Bearer Tokens, OAuth2, And Client Certificates** - * Custom endpoints accepted one authentication scheme: an API key sent in whichever header the built-in providers happened to use. That covers OpenAI and Anthropic and nothing else, so a gateway expecting `x-goog-api-key`, a corporate gateway issuing short-lived tokens, and an appliance requiring a client certificate were all unreachable. - * **The API key header name and value prefix are now configurable**, so a single scheme covers `Authorization: Bearer`, Anthropic's `x-api-key`, Google's `x-goog-api-key`, and any bespoke gateway header. - * **Added static bearer token authentication.** - * **Added OAuth2 client credentials**, with token caching and refresh ahead of expiry so a token cannot lapse mid-request. The token endpoint is validated against the same outbound policy as the inference endpoint, so it cannot become an unchecked request target, and a failing token response is sanitized before it reaches the browser. - * **Added mTLS client certificates.** Certificates are referenced by file path so a private key is mounted into the deployment and never written to the configuration database. - * (Ref: `functions_model_endpoint_auth.py`, `functions_model_endpoint_providers.py`, `functions_model_endpoint_validation.py`, [#1228](https://github.com/microsoft/simplechat/pull/1228)) +* **Plans Can Wait For A Quick Saved Workflow And Use Its Result** + * With **Wait For Quick Workflows In Chat** on, a chat plan that starts one of the user's saved personal workflows can wait for the run and use its result in the same answer, for example "run my sales digest, then compare its totals with the Q3 report in my workspace". Before, the planner dropped any run step whose result another step needed, and the result arrived later as a separate chat post. + * The server decides whether a plan waits, from the saved workflow and the settings, never from the model. It waits only for a quick workflow: personal, durable execution on, one to five tasks, no For each or Repeat until, no approval, Microsoft 365 run-as user, File Sync or review publication, and not a hand-off or structured workflow. Only answer-writing steps may read the result, and every step must be able to run without the user's sign-in. The server checks again when the run starts and on every check while the plan waits. + * A plan waits at most **Wait for a workflow from chat** (default 300 seconds, range 60-1800), and always keeps enough of its run time to write the answer. A run that finishes in time reaches the answer through the existing workflow results reader, which checks access again and marks the content as untrusted. A failed or cancelled run says so, and the steps that needed its result don't run. A run still going when the wait ends is posted to the chat by the existing post-back when it finishes. + * One compare-and-set write on the run's delivery record decides whether the answer or the post-back delivers the result, so it reaches the chat once. Each check while waiting confirms that the conversation is still private and the user's, the setting is still on, and the workflow is still the user's and still quick. A failed check ends the wait and leaves delivery to the post-back's own rules. + * Off by default, and with it off planning and execution are unchanged. A plan that starts a workflow still always waits for the user's approval, and hand-off workflows are never waited for. + * (Ref: `functions_orchestration_workflow_run_wait.py` `SAVED_WORKFLOW_RUN_WAIT_KIND` and `quick_run_eligibility`, `functions_orchestration_workflow_runs.py` `resume_workflow_run_wait`, `functions_workflow_chat_delivery.py` `apply_plan_wait_consume`, `functions_orchestration_continuation.py`, [Chat Orchestration Workflow Run Wait](features/CHAT_ORCHESTRATION_WORKFLOW_RUN_WAIT.md)) + +#### User Interface Enhancements -### **(v0.261.019)** +* **The Plan Card Names The Workflow It Waits For, And The Run Card Says When A Result Was Used** + * While a plan waits for a saved workflow, its progress line names the workflow and how long the plan waits, such as `Waiting for "Sales digest" to finish (up to 5 min).`, instead of "Waiting for results". + * A run whose result a plan used says **Its results were used in a chat answer.** on its card under the answer, because there is no posted result to jump to. No bell notification is sent for it. + * Workflow names are shown as text. + * (Ref: `orchestrationPlan.ts` `describeRunProgress`, `workflowRunStatus.ts` `workflowFinishedResultsText`, `WorkflowRunCard.tsx`) + +### **(v0.261.308)** #### New Features -* **On-Premises Custom Model Endpoints Now Work** - * The administrator gate named "allow private Custom endpoint hosts" did not actually permit the two most common on-premises address forms. An IP address such as `https://10.20.30.40/v1` and a short host name such as `https://llm-gateway/v1` were both rejected even with the gate enabled, and both were refused with a message claiming the URL was an IP address, which was wrong for the short host name. - * With the gate enabled, IP addresses, short host names, and hosts resolving to private ranges are now accepted. Loopback, link-local, and cloud metadata addresses remain rejected regardless of any setting, and every address is still revalidated at connection time. - * **Added a CA bundle setting.** Custom endpoints trust only public certificate authorities and deliberately ignore ambient environment variables, so an on-premises gateway using an internally issued certificate previously could not be trusted at all. An administrator can now name a PEM bundle. A bundle that cannot be loaded fails loudly rather than silently falling back to weaker trust. - * **Added a separate plaintext HTTP gate** for isolated networks where TLS cannot be terminated. It requires the private-hosts gate as well, and is labelled with its consequence: prompts and API keys travel unencrypted. - * Saving an endpoint no longer requires the host name to resolve from the application tier, so configuration can be seeded or restored from backup ahead of connectivity. Policy violations are still refused at save time, and the connection-time check is unchanged. - * (Ref: `functions_model_endpoint_validation.py`, `model_endpoint_clients.py`, `allow_insecure_custom_model_endpoints`, `custom_model_endpoint_ca_bundle_path`, [#1228](https://github.com/microsoft/simplechat/pull/1228)) +* **Repeat a Chat Plan on a Schedule** + * With **Repeat Chat Plans On A Schedule** on (`enable_workflow_plan_replay`, off by default; it needs Chat Orchestration and personal workflows), a finished orchestrated answer offers **Repeat on a schedule**. The card freezes the plan that just ran and shows the request, how dates are handled, the shortest allowed interval, the step cap and every step with its capability before anything is saved. Saving creates a paused personal workflow unless the user turns the schedule on. + * Each run replays the frozen plan as the person who saved it, in the workflow's own conversation, never the chat it came from. The plan is SHA-256 hashed when it's saved and checked again before every run, and the model never adds, removes or rewrites a step. Each run also checks again that the setting, Chat Orchestration, every step's capability, and every document, group and public workspace the plan reads are still available to that person. A refused run fails with a fixed reason before any step runs. + * Only steps that are safe to repeat are allowed: Search documents, Analyse documents, Compare documents, Merge documents, Inspect spreadsheets, Prepare content and Generate image. Web search, page fetches, deep research, actions, agents, workflow steps, steps that wait for a file or result, and plans that asked the user a question are refused, with a reason for each step. A run is limited to 8 steps and 15 minutes, and the schedule follows the chat-created workflow minimum interval and quota. + * The run inspector shows a typed result for each run: the orchestration run, each step's status, the final response and its image or file artifacts. The V2 workflow editor shows the frozen steps read-only; the name, schedule and alerts stay editable. The Classic workflow page sends these workflows to V2. + * A saved workflow starts with one **Run failed** alert rule, so a refused or failed scheduled run isn't missed: it sends a high-severity notification to the bell, never a pop-up, with the fixed reason. The card says so before saving, and the rule stays editable under **Alerts**. + * Part of #1550, refs #1543. + * (Ref: `functions_workflow_plan_replay.py`, `functions_workflow_runner.py`, `route_backend_orchestration.py` `/api/v2/orchestration/runs//plan-replay`, `PlanReplaySaveCard.tsx`, `WorkflowEditorDialog.tsx`, `WorkflowRunHistory.tsx`, [Workflow plan replay](features/WORKFLOW_PLAN_REPLAY.md)) -### **(v0.261.018)** +### **(v0.261.306)** #### Bug Fixes -* **Tool-Calling Responses Now Actually Stream** - * SimpleChat only supports streaming responses, but tool calling cannot stream, because a tool call has to arrive whole. The completed answer was delivered through the streaming interface as a single chunk, so the user saw nothing at all and then the entire response at once, which reads as a hang. Because agents and plugins rely on tool calling, this was a common path rather than an edge case. - * A completed answer is now split into chunks at word boundaries and delivered progressively, so it reads like a real stream. Chunking is lossless — the reassembled text is byte-for-byte identical. - * Tool calls still arrive whole, on the final chunk, alongside the finish reason and usage metadata. Emitting metadata once means token usage is no longer at risk of being counted per chunk. - * **Streaming responses can report token usage again.** `stream_options` was stripped from every OpenAI-compatible request, which suppressed usage reporting for all of them. It is now dropped only for surfaces that reject it. - * (Ref: `model_endpoint_clients.py`, `functions_model_endpoint_providers.py`, [#1228](https://github.com/microsoft/simplechat/pull/1228)) +* **Consistent V2 Scope Response Contract** + * Scope confirmation now uses the same response tuple format for successful reads and error paths, resolving the CodeQL mixed-tuple-returns finding without changing status codes, response bodies, or no-store behavior. + * (Ref: `route_backend_v2.py` `v2_scope`, `test_v2_scope_and_bootstrap_timings.py`) -### **(v0.261.017)** +### **(v0.261.305)** #### Bug Fixes -* **Custom Endpoint URLs Are No Longer Rewritten Into 404s** - * SimpleChat appended `/v1` to every Custom OpenAI-compatible endpoint, even when the configured URL already said where the API lived. A gateway at `https://apim.example.com/inference/chat/completions` was called at `https://apim.example.com/inference/v1/`, and any base carrying its own version segment, such as `/v1beta` or `/v2`, was broken the same way. - * `/v1` is now appended only when the URL does not already name the API surface. A path whose last segment is a version is left alone, and a full operation URL is treated as stating the base exactly. - * Added a **Use this URL exactly as entered** option for gateways that serve the API at a path SimpleChat cannot infer. - * **Test Connection now reports the URL that was actually called.** URL normalization rewrites the configured endpoint, and that rewrite was previously invisible, so a misdirected request looked identical to a correct one. - * (Ref: `model_endpoint_clients.py`, `route_backend_models.py`, `_multiendpoint_modal.html`, `admin_model_endpoints.js`, `workspace_model_endpoints.js`, [#1228](https://github.com/microsoft/simplechat/pull/1228)) +* **V2 Group Switching No Longer Rebuilds All Startup Data** + * A group switch now confirms the saved selection through a lightweight scope read instead of waiting for the complete startup payload. Current-access checks, sign-in checks, and read-only recovery after an uncertain save remain in place. + * An overlapping startup refresh cannot restore the previous selection. Startup and scope reads bypass the browser's HTTP cache. + * Startup requests now report per-phase timing so the remaining first-load bottleneck can be measured; first-load optimization is a separate follow-up. + * (Ref: `route_backend_v2.py`, `bootstrapStore.ts`, `groupWorkspaceStore.ts`, [V2 Group Switch Latency Fix](fixes/V2_GROUP_SWITCH_LATENCY_FIX.md), [#1725](https://github.com/microsoft/simplechat/issues/1725)) -### **(v0.261.016)** +### **(v0.261.304)** #### Bug Fixes -* **Custom Model Endpoint Failures Are Now Diagnosable** - * Every Custom endpoint failure produced the same sentence — "Custom model request failed." — with the underlying cause discarded before it reached the log. A wrong path, a wrong API key, a wrong model name, a TLS failure, and a blocked address were indistinguishable, and nothing anywhere explained which had happened. - * The browser message stays sanitized, because an upstream error body can echo back a URL, a header, or an API key. The real cause is now recorded server-side with the API type, the resolved request URL, the upstream status code, and the upstream error body. - * Both the message and the log entry now carry a short reference id, so an administrator can join the message a user reports to the log entry that explains it. - * Credentials are redacted before anything is written to the log, covering API keys, bearer tokens, `x-api-key`, `x-goog-api-key`, and key query parameters. A logging failure never replaces the original error. - * The resolved request URL is included deliberately: URL normalization can rewrite what the administrator typed, and that rewrite was previously invisible. - * (Ref: `functions_model_endpoint_diagnostics.py`, `model_endpoint_clients.py`, `functions_model_endpoint_runtime.py`, [#1228](https://github.com/microsoft/simplechat/pull/1228)) +* **Microsoft 365 Approvals In A Plan Are Decided In The Chat** + * With Orchestrate on, a SharePoint or OneDrive step that had to read more of a file than a quick read covers stopped for the user's extended-analysis approval and only linked to **Approvals**. The user had to decide there, come back and retry. The stop wasn't about access: a quick read covers 3 downloads, a 25 MB file and 12,000 tokens of file content, and the per-source preference defaults to **Ask**. + * The stopped message now shows the approval itself. It says why the step stopped, from the approval's recorded counts, and offers **Allow this time**, **Always allow for SharePoint** (or OneDrive) and **Quick read only**. A choice is saved through the approvals API, and the plan continues straight away. An approval already decided in Approvals offers **Continue**. Other approvals still link to Approvals. + * The step's failure now carries its pending approval's id. Only an id in the shape the application mints is kept, and the approvals API still checks the signed-in user. + * (Ref: `M365ApprovalInlineCard.tsx`, `OrchestrationRecoveryNotice.tsx`, `functions_orchestration_schema.py` `build_failure`, `lib/orchestration.ts`, [Orchestration M365 Approval and Retry Flow Fix](fixes/ORCHESTRATION_M365_APPROVAL_RETRY_FLOW_FIX.md)) + +* **Retrying A Microsoft 365 Stop Needs No Confirmation, And A Confirmation Closes When The Retry Starts** + * A **Use an action** step that stopped for Microsoft 365 sign-in, approval or policy is retried without "Retry this failed step?". It ran only its one Microsoft 365 action, and a plan step can only read Microsoft 365 data. The server decides this when it prepares the retry. Agent steps, and action steps that failed for other reasons, still ask. + * A confirmation that is still needed now closes as soon as the retry is admitted. Before, it stayed open and busy until the whole retry finished. + * (Ref: `functions_orchestration_recovery.py` `_stopped_without_effects`, `orchestrationController.ts` `launchSavedPlan`) + +* **A Retry's Answer Replaces The Stopped Attempt** + * After a successful retry, the stopped attempt's "The request could not be completed." message stayed in the thread, the answer carried a "Saved execution attempt - Attempt 2" box, and the stopped attempt was sent to the model on every later turn. + * The retry's answer now replaces the attempt it retried in the thread, including after a reload. The stopped attempt is left out of the model's history, conversation exports and the **Documents** drawer, but stays stored. A completed retry shows no attempt notice, and **Message details** offers **View previous attempt** and **Review saved attempt**. + * (Ref: `functions_orchestration_attempts.py`, `functions_orchestration_context.py`, `route_backend_chats.py` `build_conversation_history_segments`, `route_backend_conversation_export.py`, `MessageList.tsx`, `MessageInspector.tsx`, `conversationGeneratedFiles.ts`) -### **(v0.261.015)** +### **(v0.261.303)** #### New Features -* **Custom Model Endpoints Now Support Google Gemini, Through A Provider Registry** - * Custom endpoints supported exactly three API types, and each one was hard-coded in five separate places: the allowlist, the request-model resolver, the protocol inference chain, the admin template's option list, and the admin JavaScript. Adding a provider meant editing all five and hoping none were missed. - * An API type is now a single declarative registry entry that carries its wire protocol, which field names the model, how its URL is built, which authentication types it accepts, and which version field applies. The admin API Type list, the model identifier label, and the version fields all render from that registry. - * **Google Gemini is now selectable as a Custom endpoint API type**, reached through its OpenAI-compatible surface so it still runs on SimpleChat's validated-DNS pinned transport. - * Fixed URL handling for endpoints that already carry a version segment. SimpleChat appended `/v1` unconditionally, which turned Gemini's `…/v1beta/openai` base into `…/v1beta/openai/v1` and produced a 404. URL construction is now per-provider, so `/v1` is appended only where it belongs. - * The three existing API types are unchanged, and an unregistered API type is still refused. - * (Ref: `functions_model_endpoint_providers.py`, `functions_model_endpoint_types.py`, `model_endpoint_clients.py`, `admin_model_endpoints.js`, `workspace_model_endpoints.js`, `_multiendpoint_modal.html`, [#1228](https://github.com/microsoft/simplechat/pull/1228)) +* **Microsoft 365 Files, Emails and Events as Citations** + * Answers that use SharePoint or OneDrive files, emails or calendar events through a Microsoft 365 action now cite them like workspace documents, in chat with an agent and in Orchestrate mode. An orchestrated SharePoint answer that used to end with plain "Source: … (SharePoint file 20170010188.pdf)" text now cites the file after each claim. + * In V2 a chip shows the file name or the email's or event's subject. Clicking it opens a source card with the item's details and **Open in SharePoint**, **Open in OneDrive** or **Open in Outlook**, which opens the item with the reader's own sign-in. The classic chat links the cited title to the item. + * The Documents pane gains **SharePoint & OneDrive**, **Email** and **Calendar** sections listing every cited item, and every file whose content was read, with **Open online** instead of a download. Only the conversation owner sees them. + * Email and event lists always use the same layout: a header such as "10 most recent emails, all unread, newest first:" and one numbered line per item with the subject in bold, the sender or time span and the citation. Times are written in the reader's browser time zone. + * Each item gets a deterministic `m365-` id and a bounded record; only an `https` link from Microsoft Graph becomes a link, and no message body or file content is stored. Records come only from the Microsoft 365 actions themselves and are captured before the tool result is truncated, so output from any other tool that merely looks like a Graph result never becomes a Microsoft 365 citation or link. They are carried by shared conversations and exports and count as Microsoft 365 data when a conversation is shared. + * Workflow answers are cited the same way. When a turn returns more than a message keeps, the cited items are kept first, and an item Microsoft 365 returns with an unusual value is simply left uncited instead of failing the tool. + * (Ref: `functions_m365_citations.py`, `msgraph_plugin.py`, `functions_m365_retrieval.py`, `plugin_invocation_logger.py`, `route_backend_chats.py`, `functions_workflow_runner.py`, `functions_orchestration_adapters.py`, `functions_orchestration_execution.py`, `M365CitationChip.tsx`, `ConversationDrawer.tsx`, `chat-citations.js`, [Microsoft 365 Source Citations](features/M365_SOURCE_CITATIONS.md)) +#### Bug Fixes -### **(v0.261.014)** +* **Orchestrated Runs Reported Failure While Their Work Was Still Running** + * A plan waiting on a long tabular analysis could reply "The request could not be completed. Saved step inputs changed." while the analysis was still running. The plan panel kept showing the step as **Waiting for results** and the answer as **In progress**, and offered no retry. A saved run's checkpoints were bound to a fingerprint of the whole settings document, so any settings save, including the Cosmos DB throughput autoscale recording a scale action, made the waiting run, and later retries of failed runs, look like their inputs had changed. + * The fingerprint now leaves out storage metadata and the runtime state background tasks save. A configuration change by an administrator still stops saved work from being reused. + * A run that fails or is stopped while a step is waiting shows that step as **Not finished** and its deliverable as **Not delivered**, and offers **Retry from failed step**, which runs the waiting step again and reuses completed ones. + * Runs saved before this version can't be retried after the upgrade; ask again. + * (Ref: `functions_settings_runtime_state.py`, `functions_orchestration_checkpoints.py`, `functions_orchestration_recovery.py`, `OrchestrationRunView.tsx`, `orchestrationPlan.ts`, [Orchestration Runs Failing While Their Work Was Still Running Fix](fixes/ORCHESTRATION_SETTINGS_WRITE_RECOVERY_FIX.md)) -#### New Features +* **Files Built From Web, Agent or Action Results Failed Without a Reason** + * An orchestrated file whose content came from web search, linked pages, deep research, an agent or an action rendered, then failed with only "This file could not be created." and no retry. Publishing it rechecked its sources through a new service that didn't have the user's sign-in. Publication now uses the rendering service that ran the attempt, which has it. + * A file that still can't be checked without a sign-in, such as one prepared in the background, says so. A failed file now explains why it could not be created. When a failed file can't be retried on its own, the chat offers **Retry from failed step**. + * (Ref: `functions_orchestration_artifacts.py` `render_attempt_scope`, `functions_orchestration_rendering.py`, `functions_orchestration_output_store.py`, `OrchestrationRecoveryNotice.tsx`, [Orchestration Files From External Results Failing Without a Reason Fix](fixes/ORCHESTRATION_FILE_SIGN_IN_PUBLICATION_FIX.md)) + +### **(v0.261.302)** -* **Model Capabilities Now Come From The Model Catalog Instead Of The Model's Name** - * SimpleChat previously worked out what a model could do by pattern-matching its name, so a model the catalog did not know about — an on-premises or customer-supplied model reached through a Custom endpoint — silently received wrong answers for vision, tool calling, streaming, and reasoning. A model named `corp-llm-v2` was treated as having no capabilities at all. - * Capability answers now resolve through a precedence chain: a per-model override, then an endpoint-level override, then the shipped model catalog, then the original name heuristics. Administrators can describe a model the catalog has never heard of without waiting for a catalog update, and models absent from the catalog behave exactly as before. - * The catalog gained `supportsStreaming` and `reasoning` flags for every model, and now covers Google Gemini, which had no entries at all. Claude, Llama 4, and Phi-4 multimodal models are correctly recognised as vision-capable for the first time; the `-chat` variants of the GPT-5.x families are correctly recognised as not vision-capable. - * The catalog is now validated against a published JSON schema, so a malformed or incomplete model record fails a test rather than silently degrading capability answers at runtime. - * (Ref: `functions_model_capabilities.py`, `model_capabilities.json`, `model_capabilities.schema.json`, `MODEL_CAPABILITY_CATALOG.md`, [#1228](https://github.com/microsoft/simplechat/pull/1228)) +#### Bug Fixes -### **(v0.261.013)** +* **Microsoft 365 Sign-In No Longer Fails With `m365_auth_state_invalid`** + * Reconnecting Microsoft 365 for chat could fail seconds after the user signed in, leaving raw JSON in the popup. The session store wrote the whole session back at the end of every request, so the request that finished last won. In V2, the startup-data refresh that runs when the window regains focus took about five seconds, loaded the session before the connect call saved the pending sign-in, and wrote its stale copy back afterwards. + * A request now saves only the session keys it changed, merged into the latest stored copy, and a request that changed nothing no longer writes its copy back. A session deleted during a request stays deleted. This also stops overlapping requests from dropping token-cache refreshes, the Microsoft 365 CSRF token and other session writes. + * V2 skips the focus refresh while one is running or one started in the last 15 seconds. + * Sign-in callbacks show a result page instead of JSON. In a popup it reports back to the page that opened it and closes; opened as a full page, it continues on success or explains the error with a way to try again. + * (Ref: `functions_session_store.py`, `app.py`, `m365_connection_result.html`, `route_backend_m365.py`, `bootstrapStore.ts`, [#1714](https://github.com/microsoft/simplechat/issues/1714), [Microsoft 365 Connection Session Race Fix](fixes/M365_CONNECTION_SESSION_RACE_FIX.md)) -#### New Features +* **Workflow Connections Create Their Own Encryption Key** + * **Connect for workflows** failed with "Configure Key Vault and a dedicated workflow encryption-key secret" unless an operator had created a key and set `M365_WORKFLOW_TOKEN_KEY_SECRET_NAME`. SimpleChat now creates one deployment-wide key in the configured Key Vault the first time someone connects, and checks for it whenever an administrator saves settings. The app setting still overrides the secret name, so existing keys keep working. + * This needs Key Vault secret storage on and the application identity allowed to create secrets. **Connect for workflows** is disabled with an explanation until Key Vault secret storage and a Key Vault name are configured. + * Workflow sign-in now returns through `/getAToken`, which the app registration already lists, so no extra redirect URI is needed. + * (Ref: `functions_m365_connections.py` `ensure_m365_workflow_encryption_key`, `workflow_connection_readiness`, `route_frontend_authentication.py`, `route_frontend_admin_settings.py`, `route_backend_v2.py`, [#1604](https://github.com/microsoft/simplechat/issues/1604)) + +* **Chat Connection Status Matches What Chat Can Use** + * The chat connection card said "Sources saved for this session: none" while chat was reading mail, OneDrive and SharePoint, because it listed only sources from an explicit reconnect. It now lists the sources the current sign-in can use, in both V2 Settings and classic Profile. + * (Ref: `functions_m365_connections.py` `read_chat_connection`, `profile-m365.js`, `M365Cards.tsx`) -* **Custom Model Endpoint Provider** - * Added manually configured Custom endpoints for OpenAI API, Azure OpenAI API, and Anthropic chat models across global, personal, and group scopes. - * Added type-specific model identifiers, API-key authentication, connection testing, response-length controls, and Anthropic Version support without model discovery. - * Enforced HTTPS, DNS/address safety with connection-time address pinning, runtime URL revalidation, redirect refusal, Key Vault secret handling, and an administrator-controlled private-host policy. - * (Ref: #1222, Custom model endpoints, `functions_model_endpoint_runtime.py`, `_multiendpoint_modal.html`, `CUSTOM_MODEL_ENDPOINT_PROVIDER.md`) +* **Microsoft 365 Links Stay in V2** + * Connect, reconnect and approval links in V2 chat, workflow cards and Approvals opened the classic Profile and Approvals pages. They now open V2 Settings at the Microsoft 365 section and V2 Approvals, and connecting for workflows in V2 uses a popup, like chat, instead of leaving the page. + * (Ref: `m365Links.ts`, `m365Connect.ts`, `M365Cards.tsx`, `SettingsPage.tsx`, `OrchestrationM365Notice.tsx`, `WorkflowProposalCard.tsx`, `WorkflowRunCard.tsx`, `PendingActionsPanel.tsx`, `PausedRequestsPanel.tsx`) -### **(v0.261.012)** +### **(v0.261.301)** #### New Features -* **Yamcs Actions Can Reach Servers Behind An Authenticating Proxy** - * Ground segments commonly publish Yamcs through a reverse proxy, such as Apache, that challenges every request with HTTP Basic authentication against a directory before the request reaches Yamcs. Yamcs behind that proxy often has no authentication of its own. Until now a Yamcs action could not answer that challenge, so such a server was unreachable even when the Yamcs settings were correct. - * A new **Reverse Proxy Authentication** option on the Yamcs action sends an HTTP Basic `Authorization` header on every request. It is off by default, so a Yamcs server reached directly, such as a local simulator, is unaffected. - * The proxy credential can be typed on the action, with the password stored in Key Vault, or supplied by a reusable **username and password identity**. Where a directory issues temporary passwords, the identity is rotated once under **Workspace → Identities** and every action that references it picks up the new password without being edited. - * The proxy credential has its own identity reference, separate from the Yamcs credential, so one action can use both. - * Proxy authentication combines with the **No Authentication** and **API Key** Yamcs methods. It cannot combine with **Username and Password** or **Access Token**, because only one `Authorization` header can be sent and the Yamcs token request would itself be refused by the proxy. The conflict is reported when saving the action and when running **Test Yamcs Connection** rather than failing later at run time. - * **Test Yamcs Connection** exercises the proxy credential and distinguishes a proxy rejection from a Yamcs rejection. - * (Ref: `functions_yamcs_operations.py`, `yamcs_plugin.py`, `functions_workspace_identities.py`, `plugin_health_checker.py`, `route_backend_plugins.py`, `_plugin_modal.html`, `plugin_modal_stepper.js`, `test_yamcs_basic_auth.py`, [Yamcs Action](features/YAMCS_ACTION.md), [#1435](https://github.com/microsoft/simplechat/issues/1435)) +* **Chat With the Control Center Dashboard** + * The V2 Control Center Dashboard's **Chat with this dashboard** opens a new chat with Orchestrate on and a prompt that describes the dashboard's dates and token filters, ready to read, change and send. The prompt travels in the page's navigation state, never the URL, so a link can't put text in anyone's composer, and a reload doesn't apply it again. + * When something is missing, a checklist marks each requirement **Ready** or **Needs set-up** and says what to do: agents on, Chat Orchestration on, action access for orchestration, a Control Center action, and the User or Admin app role. Administrators get a link straight to each setting; dashboard readers are told what to ask for. **Check again** rechecks after a change. + * (Ref: `DashboardChat.tsx`, `composerDraftHandoff.ts`, `Composer.tsx`, `functions_control_center_dashboard_chat.py`, `GET /api/v2/control-center/dashboard/chat-readiness`, [Control Center Action](features/CONTROL_CENTER_ACTION.md)) -### **(v0.261.011)** +* **Control Center Action** + * A new read-only, global-only action type answers usage questions in chat from the dashboard's own data: sign-ins and daily, weekly and monthly active users, conversations, uploads, processing failures, and token use by day, model, usage type, user, group or public workspace, plus the most active users, groups and workspaces. Answers come back as rows orchestration can chart and match the dashboard for the same dates. + * Only people who can view the Control Center dashboard see it in chat, and it checks their roles again on every call. Scheduled workflow runs, which have no signed-in session, are refused. It needs no connection settings; create it under **Admin Settings › Agents & Actions › Global Actions**. + * (Ref: `control_center_plugin.py`, `functions_control_center_dashboard.py`, `functions_action_catalog.py`, `functions_governance.py` `GLOBAL_ONLY_ACTION_TYPES`, [Control Center action reference](../reference/actions/control-center.md)) + +#### User Interface Enhancements + +* **Control Center Dashboard Reorganized and Explained** + * The dashboard reads top to bottom as **Directory**, **Sign-ins**, **Conversations and documents**, **Token usage** and **Most active**. Every figure says what it counts: daily, weekly and monthly active users name the end date and window and count each person once, replacing the bare "unique users" label. + * The token filters now sit inside **Token usage**, say they change only that section, and have **Clear filters**. Before, they sat above the whole page, so choosing a user seemed not to change the sign-in, conversation and upload charts. + * Top token consumers and the most active rankings show names, with a user's email, instead of IDs. The ID is shown on hover, and a ranked group or workspace that was deleted says so. + * Sign-ins and conversations have their own charts, and empty ranges say so instead of drawing a flat line. + * (Ref: `DashboardSection.tsx`, `DashboardParts.tsx`, `functions_control_center_dashboard.py` `resolve_entity_names`, [V2 Control Center Dashboard Usability Fix](fixes/V2_CONTROL_CENTER_DASHBOARD_USABILITY_FIX.md)) + +* **Admin Settings Deep Links and Action Type Links** + * `/admin/settings/
` opens V2 Admin Settings on one section, and `/admin/actions/new?type=` starts a new global action of that type, so other pages can link to the exact setting they need. + * (Ref: `App.tsx` `AdminSettingsSectionRoute`, `ActionEditorPage.tsx`) #### Bug Fixes -* **Redis Connection Test No Longer Returns Credential Errors To The Browser** - * Restored the hardening that a refactor had dropped: when the admin Redis connection test fails while resolving credentials, the details are logged under `[REDIS_TEST]` and the browser receives a generic message instead of the raw exception, which could carry Key Vault secret names, vault URIs, or token details. - * Validation problems such as a missing host name or access key are still returned directly, because those messages are generated by SimpleChat and are what the admin needs to fix the form. - * (Ref: `route_backend_settings.py`, `test_redis_client_factory.py`) +* **Chat With These Trends Failed With "User Not Authenticated"** + * The classic trends chat endpoint read a session key the application never sets, so it answered HTTP 401 for every administrator, and past that it wrote a malformed conversation that V2 then linked to a page that doesn't exist. The endpoint and the classic page's unreachable chat modal were removed; **Chat with this dashboard** replaces them. + * (Ref: `route_backend_control_center.py`, `control_center.html`, `control-center.js`, [V2 Control Center Dashboard Usability Fix](fixes/V2_CONTROL_CENTER_DASHBOARD_USABILITY_FIX.md)) + +* **Dashboard Drill-Through Opened the Wrong Records** + * Clicking a sign-in point on the combined chart opened conversation records, and clicking any segment of a stacked bar opened the first series at that date, such as personal uploads or chat tokens. A click now opens the series under the pointer, or the whole day when the click is beside a stack. + * Token drill-throughs keep the token filters, and dashboard readers no longer see links into Users, Groups or Activity Logs, which they can't open. + * (Ref: `DashboardSection.tsx`, `ui_tests/test_v2_control_center_dashboard.py`) + +### **(v0.261.300)** + +#### Bug Fixes + +* **Orchestrated SharePoint and OneDrive Actions Find Documents Again** + * With Orchestrate on, a **Use an action** step with a Microsoft 365 SharePoint or OneDrive action never searched or read any file. Every file function refused the call with `model_context_unavailable` before reaching Microsoft Graph, because the step had no model token budget. Chat binds that budget through the selected agent, and a plan step has no agent. The step still reported completed, so the answer said the documents couldn't be searched, while the same agent in chat found them. Production telemetry for the reported run showed one `search_files` refusal for `spo` with that code. + * A file step now measures what it reads against the model it runs on, the same way chat does with the agent's model. The model's token limits come from the model catalog or Model Endpoints. + * (Ref: `functions_orchestration_actions.py` `_build_file_action_model`, `_action_model_budget`, `functions_orchestration_m365.py` `file_step_filter`, [Orchestration M365 File Action Evidence Fix](fixes/ORCHESTRATION_M365_FILE_ACTION_EVIDENCE_FIX.md)) + +* **Deeper File Analysis Finishes in a Plan After Approval** + * In a plan, `analyze_file` stopped for the user's extended-analysis approval. After the user approved and retried, it failed with "Microsoft 365 access for this step could not be confirmed", because analysis took its model only from a chat agent. Every retry failed the same way. + * Analysis now runs on the step's own model, for that step's request only. A retry after the approval reuses the captured file and completes. + * (Ref: `functions_m365_agent_continuation.py` `get_m365_analysis_model`, `m365_step_model_binder`, `functions_m365_analysis_runtime.py`) + +* **File Steps Fail Clearly Instead of Reporting Missing Documents** + * A SharePoint or OneDrive step whose model has no verified token limits now stops before reading anything, with the new `m365_model_limits_required` failure. Its message says to have an admin set the model's limits in Model Endpoints, then ask again. + * File evidence that can't be saved or read for the conversation now stops the step with the new `m365_evidence_unavailable` failure. Previously both reached the model as findings, so the step completed with a misleading answer. + * Bounded-coverage outcomes, such as a full context window or a request's file limits, are still reported as findings. + * (Ref: `functions_orchestration_m365.py` `result_refusal`, `failure_code`, `functions_orchestration_schema.py` `FAILURE_MESSAGES`, `M365_STEP_FAILURE_CODES`) + +### **(v0.261.299)** + +#### New Features -* **Deployer Redis Kind Detection Matches The Full Host Name Suffix** - * The postprovision fallback that infers the Redis offering from a host name used a substring check, so a host name that merely contained `.redis.azure.net` anywhere could be misread as Azure Managed Redis and configured with the wrong port. - * It now matches the full suffix, consistent with the application's own detection. - * (Ref: `deployers/bicep/postconfig.py`) +* **AI Assist In The Admin Review Center** + * An optional assistant suggests reviews for user feedback and safety violations in the V2 Review center. **Ask AI** in a record's editor analyzes it and fills the unsaved draft, marking each field it changed and offering **Undo**. **Triage with AI** on the workbenches' bulk bar sends the checked records ten at a time, each user's records together, with progress and **Cancel**, and stores a suggested review on each; records that get none stay checked. A new **AI suggestions** page in each section lists them with what each would change and why, and shows in full, under **Visible to the user**, the text each saves that its user can read. Any eligible reviewer can approve them one at a time or together, edit the user's notification first, or dismiss them. + * The model never writes or acts. Its answers are checked on the server against a strict schema, with one correction round, and approving a suggestion runs the same save as a hand-written review: a warning is sent when it is approved, and a suspension or block creates an approval request for a second reviewer. Suspensions and blocks are never part of **Approve all**, the confirmation counts the warnings and requests an approval sets off and the reviews that save text a user can read, and the queue never requests again a suspension or block the violation already records. + * Records are read on the server by id and shown to the model under request-local handles, without ids, names or email addresses; email addresses and GUIDs in the text are replaced and long text is shortened. A model call only ever covers one user's records, so text one user wrote can't steer what the model writes for another user; records it doesn't reach in time are answered `deferred` and sent again by the browser. Text a user can read is refused when it is too long or repeats a long passage of another record in the request. Policy is enforced by the server: never Escalate, no warning or restriction over AI-generated content, and never a weaker action than one already applied. When the model service's content filter declines a group, each record is retried alone, so the others still get suggestions. + * A stored suggestion carries a fingerprint of the fields it was based on, and for a violation its request and warning state, so one whose record changes afterwards reads as out of date and can only be dismissed. Storing, applying or dismissing a suggestion doesn't refuse a reviewer's open editor: the lists and record reads return the fingerprint, editors send it with their version, and a save whose fingerprint still matches goes ahead on the current version. Approvals and dismissals are recorded in the admin activity log and credited to the suggestion, and one whose record can't be read fails on its own without stopping the rest of the request. + * New settings on **Security > Access & Roles**: **Enable AI Assist in the Review Center** (`enable_admin_review_ai_assistant`, off by default) and **Review Guidance for the AI Assistant** (`admin_review_ai_guidance`, up to 2,000 characters, sent only to the model). Each reviewer can send 60 assist requests per 10 minutes. While it is off, no suggestion can be made, applied or dismissed. + * New APIs: `POST /api/admin/review/feedback/assist` and `POST /api/admin/review/safety/assist`. Bulk `update` operations accept `suggestion_id`, `dismiss_suggestion` is a new bulk operation, the review lists and their `/ids` routes accept `ai=pending`, the `/ids` routes return each record's user as `owners`, and record saves accept `fingerprint` alongside `etag`. + * (Ref: `functions_review_assist.py`, `functions_review_assist_runtime.py`, `functions_review_center.py`, `route_backend_feedback.py`, `route_backend_safety.py`, `ReviewAskAiPanel.tsx`, `useReviewTriage.ts`, `SuggestionsQueue.tsx`, `lib/reviewSuggestions.ts`, `lib/reviewAssistApi.ts`, [AI Assist in the Admin Review Center](features/ADMIN_REVIEW_AI_ASSISTANT.md)) + +* **Feedback Themes** + * Feedback reviews have a **Theme**: Accuracy, Citations, Retrieval, Formatting, Tone, Speed, Safety, Praise or Other. Reviewers set it in the editor, or approving an AI suggestion sets it. The feedback dashboard counts the period's feedback by theme and how much is not classified yet, each theme opens the filtered list, and the queue filters by theme. Users never see the theme. + * (Ref: `route_backend_feedback.py` `theme`, `theme_mix`, `unthemed_count_in_window`, `FeedbackDashboard.tsx`, `FeedbackEditorPage.tsx`, `FeedbackWorkbench.tsx`) diff --git a/docs/explanation/release-notes/v0.239.md b/docs/explanation/release-notes/v0.239.md index 930cb3c21..325f18b93 100644 --- a/docs/explanation/release-notes/v0.239.md +++ b/docs/explanation/release-notes/v0.239.md @@ -25,6 +25,7 @@ layout: page * **Retention Policy UI for Groups and Public Workspaces** * Can now configure conversation and document retention periods directly from the workspace and group management page. * Choose from preset retention periods ranging from 7 days to 10 years, use the organization default, or disable automatic deletion entirely. + * **Owner-Only Group Agent and Action Management** * New admin setting to restrict group agent and group action management (create, edit, delete) to only the group Owner role. * **Admin Toggle**: "Require Owner to Manage Group Agents and Actions" located in Admin Settings > My Groups section, under the existing group creation membership setting. @@ -115,6 +116,7 @@ layout: page * Removed the membership verification from the `setActive` endpoint; the route still requires authentication (`@login_required`, `@user_required`) and the public workspaces feature flag (`@enabled_required`). * Other admin-level endpoints (listing members, viewing stats, ownership transfer) retain their membership checks. * (Ref: `route_backend_public_workspaces.py`, `api_set_active_public_workspace`) + * **Chats Page User Settings Hardening** * Fixed a user-specific chats page failure where only one affected user could not load `/chats` due to malformed per-user settings data. * **Root Cause**: The chats route assumed `user_settings["settings"]` was always a dictionary. If that field existed but had an invalid type (for example string, null, or list), the page could fail before rendering. diff --git a/docs/explanation/release-notes/v0.241.md b/docs/explanation/release-notes/v0.241.md index 50a33aa8e..51e7de75f 100644 --- a/docs/explanation/release-notes/v0.241.md +++ b/docs/explanation/release-notes/v0.241.md @@ -110,7 +110,7 @@ layout: page * Personal Workspace and Group Workspace now surface dedicated model endpoint management cards, and agent/model selection can use combined global plus workspace endpoint lists instead of relying on a single shared deployment. * The endpoint workflow supports Azure OpenAI and Azure AI Foundry discovery flows, including model fetch/test operations and endpoint-based Foundry agent import. * (Ref: `route_backend_models.py`, `route_frontend_admin_settings.py`, `workspace_model_endpoints.js`, `admin_model_endpoints.js`, `workspace.html`, `group_workspaces.html`, `test_workspace_multi_endpoints.py`) - + * **Guided Chat Tutorial** * Expanded the in-app chat tutorial into a fuller guided walkthrough of the current chat experience so new users can learn the live interface in context. * The tutorial now walks through the main chat toolbar, workspace and scope controls, conversation search, advanced search, selection mode, bulk actions, export-related flows, and message-level actions such as retry, edit, feedback, thoughts, and citations. @@ -323,7 +323,7 @@ layout: page * For SQL action types, the redundant additional fields UI in Step 4 is hidden entirely since all SQL configuration is already handled in Step 3. * Step 5 (Summary) no longer shows the raw additional fields JSON dump for SQL types, since that data is already shown in the SQL Database Configuration summary card. * (Ref: `_plugin_modal.html`, `plugin_modal_stepper.js`) - + #### Bug Fixes * **Chat History Citation Replay Improvements** @@ -338,7 +338,7 @@ layout: page * Document deletion now offers a choice between deleting only the current revision or deleting all stored revisions for that document family. * Blob storage now preserves older source files by keeping the active document at the existing alias path and archiving prior current revisions into a revision-family hierarchy before the alias path is overwritten. * (Ref: document revision families, current-only workspace visibility, hybrid blob alias plus archived revision storage, `functions_documents.py`, `functions_search.py`, `route_enhanced_citations.py`, workspace/group/public document flows) - + * **Python Runtime Dependency Refresh and Supply-Chain Hardening** * Continued the requirements hardening work from `v0.240.014` by tightening the main application runtime to exact package pins, reducing dependency drift across local development, CI, and Azure deployments to help mitigate supply-chain exposure. * Upgraded the Flask runtime stack to `Flask==3.1.3` and `Werkzeug==3.1.6`, and updated the shared `Markup` import path to `markupsafe` so the app starts correctly with Flask 3's package boundary changes. @@ -362,7 +362,7 @@ layout: page * Legacy `.ppt` uploads now extract slide text and available summary metadata from the OLE presentation streams while keeping the same enhanced-citation and final-metadata workflow used by `.pptx` uploads. * `.pptx` uploads now also populate presentation metadata such as title, author, subject, and keywords during the initial metadata update when metadata extraction is enabled. * (Ref: `functions_content.py`, `functions_documents.py`, `test_legacy_doc_ole_extraction.py`, `test_legacy_ppt_ole_extraction.py`, legacy Office OLE support and metadata parity) - + * **Pillow PSD Upload Hardening** * Updated the application to use `pillow==12.1.1`, moving the app off the vulnerable Pillow range for specially crafted PSD image parsing. * Hardened admin logo and favicon uploads so Pillow now only opens the PNG and JPEG formats already allowed by the route, preventing disguised PSD content from being decoded during upload processing. @@ -430,7 +430,7 @@ layout: page * Streaming chat now polls pending thoughts while the response is still in flight, allowing the active status badge to switch from model-sending text to the currently executing plugin call during long-running tools such as `WaitPlugin.wait`. * Completed plugin thoughts still include the richer human-readable summaries for wait, math, and generic plugin executions, and broader plugin coverage remains enabled through auto-wrapping for manifest-loaded plugins. * (Ref: `plugin_invocation_logger.py`, `plugin_invocation_thoughts.py`, `chat-thoughts.js`, `chat-streaming.js`, `logged_plugin_loader.py`, `test_logged_core_plugins.py`) - + * **Multi-Sheet Workbook Tabular Analysis** * Fixed multi-sheet Excel workbooks being analyzed from the wrong worksheet during tabular chat responses. Questions that clearly target a specific tab, such as asset values in a workbook with `Assets`, `Balance`, and `Income` sheets, no longer silently default to the first sheet. * Tabular runtime analysis now requires explicit `sheet_name` or `sheet_index` selection for analytical calls on multi-sheet workbooks, and the SK mini-agent preload now includes workbook sheet inventory and per-sheet schemas so the model can choose the correct worksheet before computing results. diff --git a/docs/explanation/release-notes/v0.250.md b/docs/explanation/release-notes/v0.250.md index a68bd0502..b599a6166 100644 --- a/docs/explanation/release-notes/v0.250.md +++ b/docs/explanation/release-notes/v0.250.md @@ -97,46 +97,59 @@ layout: page * The admin route was already computing and passing `support_latest_feature_release_groups_preview` for a panel that could never display, so admins had no way to review previous and archive release cards alongside their sharing status. * Closed the disabled block after the legacy cards so the preview panel renders again, and namespaced its element ids to avoid colliding with the admin release-group cards. * (Ref: `admin_settings.html` Latest Features tab, `support_latest_feature_release_groups_preview`, `route_frontend_admin_settings.py`) + * **Latest Features Sidebar Card Id Special Case Removed** * The admin sidebar built the previous-release section link through a redundant conditional that produced the same id as the general dynamic expression. * Simplified to the dynamic form so every non-current tier, including the new archive tier, is handled the same way. * (Ref: `_sidebar_nav.html`, admin Latest Features navigation) + * **Orphaned Latest Features Metadata Tables Removed** * Removed `_SUPPORT_CURRENT_FEATURE_IMAGE_METADATA` and `_SUPPORT_CURRENT_FEATURE_USER_METADATA` along with the two helpers that consumed them. Their keys matched no feature id in any release tier, so both helpers were no-ops, and the `_CURRENT_` naming became misleading after the release tiers shifted. * Verified behavior-neutral: the full serialized catalog output across every accessor and several settings permutations is byte-identical before and after removal. * Added a regression test that generically detects orphaned per-feature metadata tables, so this class of drift is caught in future rather than only these two names. * (Ref: `support_menu_config.py`, `test_support_menu_config_dead_metadata_removal.py`) + * **Citations and Source Rendering** * Citation parsing preserves line breaks after inline citations, agent document search results render as document sources, retrieved and cited sources remain separated, prior grounded references resolve in follow-up turns, and source-reading intent is no longer misclassified as artifact generation. * (Ref: citation parser, agent document search, grounded source references, generated artifacts) + * **Shared Conversations and Conversation Forks** * Shared conversations load, render messages, generate AI responses, refresh uploads/task documents, and survive Blueprint security hardening. * Forking from group/public workspace knowledge no longer returns HTTP 500, and structured logging normalization prevents logger errors from replacing HTTP responses. * (Ref: shared conversations, streaming bridge, conversation fork, structured logging) + * **Workflow Execution Reliability** * File Sync summaries reach task-based workflow models, task instructions scope document search queries, run history preserves per-task document status, zero-retry settings persist, and invalid task document actions are contained. * (Ref: workflow execution, task document status, retries, File Sync summaries) + * **Backup, Restore, and Data Management Reliability** * Backup ETag normalization, Cosmos pagination, checkpoint batching, and provider status diagnostics improve backup completeness and troubleshooting. * (Ref: Data Management backup, Backup Inventory, Cosmos pagination, checkpoint batching) + * **Tabular Analyze/Search Durability** * Exhaustive Markdown output includes all rows, line-phrased requests route to the durable pipeline, artifact lifecycle finishes before run completion, stale settings migrations correct disabled preflight flags, and `filter_rows contains` semantics match across foreground and durable paths. * (Ref: tabular analyze, durable preflight, generated artifacts, filter_rows) + * **Document Extraction and Grounded Search** * Legacy DOC/PPT embedded images are analyzed, image chunks merge into the correct surrounding-text chunk, document picker search matches file names, and multi-word grounded queries support punctuation word breaks. * (Ref: embedded images, chunk placement, document picker search, grounded search) + * **Cosmos, Redis, and Cache Performance** * Settings container idle RU usage is reduced, no-op read invalidation is skipped, Docker multi-worker startup conflicts recover, DAI Redis TTLs are bounded/refreshed, cache invalidation fails closed on unknown safety state, and cache parity improves for pending shared documents, legacy revisions, and generated-artifact identity. * (Ref: Cosmos RU usage, conversation cache invalidation, DAI Redis cache, public workspace artifacts) + * **Authentication and Security** * Credential-like field names are no longer logged in clear text, action secret references are scoped to their owning action, Terms of Use redirects happen server-side with HTTPS enforcement, and `/getAToken` without an authorization code redirects to sign-in. * (Ref: credential redaction, action secrets, Terms of Use, authentication redirects) + * **Model Endpoints and MCP Outbound** * Managed identity cloud values are normalized, vision test connection uses the correct multi-endpoint target, GPT 5.6+ models appear in the multi-modal vision selector, and MCP tool arguments no longer get wrapped in incompatible `kwargs` payloads. * (Ref: model endpoints, vision selector, managed identity, MCP outbound) + * **Governance, Navigation, and Admin Settings** * Retargeting policies no longer creates duplicates, block-list modals stack correctly, Group Workflows appears in the sidebar, hidden tabs hide sidebar links, logo/favicon save paths no longer 500, and update banner version comparison no longer shows stale releases as newer. * (Ref: governance policies, workspace sidebar, admin settings, update banner) + * **Retention, Notifications, Logging, and Public Workspace Edge Cases** * Group and collaboration conversations now use correct retention policy/activity timestamps, unauthenticated pages avoid notification polling 401 noise, Application Insights events carry sanitized diagnostic values with standardized tags, custom Databricks-prefixed plugin discovery avoids built-in defaults, and hidden public workspace documents can ground chat searches. * (Ref: retention policy, notification polling, Application Insights logging, plugin discovery, public workspace search) diff --git a/docs/explanation/release-notes/v0.260.md b/docs/explanation/release-notes/v0.260.md index 1e4050cb6..a575265a7 100644 --- a/docs/explanation/release-notes/v0.260.md +++ b/docs/explanation/release-notes/v0.260.md @@ -314,8 +314,6 @@ layout: page * It now sits outside the tabs and is visible wherever you are in Governance. * (Ref: `governance-status`, `admin_governance.js`) -#### Bug Fixes - * **Reliable File Generation From Agent Action Results** * Asking an agent for a downloadable file built from action results now produces the complete dataset in the requested format. Previously these requests could fail outright, publish a three-row sample of a large result, overwrite the assistant's written answer, or return nothing at all. Delivered across v0.260.004 through v0.260.011. * **Files no longer fail to generate.** A CSV built from several actions in one turn could stop with `Generated output schema mismatch at row 2`, because each action returned a different set of columns. The export now pins a union of every column before the run starts and pads the missing cells, so mixed-shape results serialize instead of failing. @@ -472,6 +470,7 @@ layout: page * Previously the modal only collected a connection string even though the backend accepted other methods, so managed identity was not reachable through the UI. * The endpoint field is validated against the Azure Blob hostname allowlist before the action is saved. * (Ref: `_plugin_modal.html`, `plugin_modal_stepper.js`, `blob_storage.definition.json`) + * **Shared Conversations No Longer Fail With "Stream interrupted: Forbidden"** * Fixed invited participants being unable to invoke the AI at all in a shared conversation. Any explicit AI request returned `Forbidden` with no content. * Root cause was the hidden source conversation behind every shared conversation being owned by its creator, so participants failed a plain ownership comparison in the chat streaming route even though they are legitimate members. @@ -480,8 +479,6 @@ layout: page * Also fixed background CSV exports queued by a participant becoming unreadable for the owner, because publication checks looked up the export run under the wrong user partition. * (Ref: `build_conversation_participation_context`, `route_backend_chats.py`, `route_enhanced_citations.py`, `functions_simplechat_operations.py`) -#### User Interface Enhancements - * **Clearer Group Workspace Save Errors** * Attempting to save a generated document into a group workspace without document rights now names the roles that can complete it and suggests requesting the content as a downloadable file instead of failing with a bare permission error. * (Ref: `_resolve_group_upload_target_for_current_user`) @@ -560,14 +557,17 @@ v0.260.001 consolidates all work released after v0.250.001 into one major releas * The legacy single `alert_priority` workflow field is superseded by `alert_mode`, `alert_rules`, and `alert_evaluation` for rules-based workflow notifications. * Existing workflows are auto-migrated on read into equivalent failed-run and completed-run notification rules. * **Migration**: Review upgraded workflow alert rules and prune any always-notify completed-run rule that is no longer desired. + * **New Yamcs Client Dependency** * The Yamcs Mission Control action adds `yamcs-client==2.1.0`, the repository's first LGPL-3.0 dynamically linked pip dependency. * SimpleChat can still start without it, but Yamcs actions return an actionable dependency error until installed. * **Migration**: Run `pip install -r requirements.txt` or rebuild deployment images so the Yamcs client dependency is present where Yamcs actions are used. + * **Internal Route Name Hardening** * Blueprint security hardening changed internal route names and required broad route policy/test updates. * Shared-conversation streaming regressions from the rename sequence were fixed in the consolidated patch history. * **Migration**: Update any custom integrations that call SimpleChat by internal endpoint name rather than public route URL. + * **Conversation Cache Fallback Behavior** * Volatile chat bootstrap and conversation cache payloads no longer fall back to the Cosmos `settings` container when Redis is unavailable. * Deployments without Redis keep full functionality, but bypass these cache benefits. @@ -578,16 +578,20 @@ v0.260.001 consolidates all work released after v0.250.001 into one major releas * **Enhanced Extraction Settings Migration** * Deployments already configured for Enhanced or Auto extraction are automatically migrated on first settings read to preserve their existing extraction mode. * No manual change is required unless admins want to revise extraction defaults after upgrade. + * **Embedded Image Chunk Placement** * Word and PowerPoint figures are now merged into chunks with surrounding text instead of appended as extra chunks. * Existing documents keep their current chunk layout until re-extracted. * Use Change Extraction or re-upload documents to benefit from the new placement behavior. + * **Workflow Alert Review** * Rules-based workflow notifications can express run status, text/regex matches, File Sync results, AI-judged outcomes, and agent signals. * Review workflow owner expectations after upgrade because migrated rules intentionally preserve prior notification behavior. + * **Redis-Backed Cache Operations** * DAI document/tag caches and conversation list/feed caches now include Redis-backed invalidation and metrics behavior. * Monitor Redis availability to keep cache acceleration active; safety-sensitive cache invalidation fails closed when state is unknown. + * **Detailed Patch Traceability** * The original v0.250.003 through v0.250.229 entries are preserved verbatim below with demoted headings for audit and support lookup. @@ -598,31 +602,37 @@ v0.260.001 consolidates all work released after v0.250.001 into one major releas * Embedded Office images, including EMF/WMF diagrams and legacy DOC/PPT media, are rasterized, analyzed, and indexed as citable chunks. * Optional Document Intelligence formula extraction adds LaTeX equation capture for PDFs when enabled. * (Ref: Azure AI Content Understanding, Document Intelligence, embedded image extraction, formula extraction) + * **Workflow Multi-Task Automation and Alerts** * Workflows now support ordered instruction tasks with prior-task context chaining, per-task document actions, retry/failure handling, and configurable task limits. * Conditional alert rules cover run status, text/regex matches, File Sync summaries, AI-judged results, and agent-raised signals across five severity levels. * Active workflow runs can be cancelled from workspace rows, run history, or activity surfaces. * (Ref: workflow task sequencing, workflow alert rules, `raise_workflow_alert`, run cancellation) + * **Expanded Agent and Action Integrations** * Yamcs and RocksDB action types add mission-control and HTTP/JSON data-service integrations. * Inbound MCP exposes governed SimpleChat capabilities for conversations, documents, prompts, tags, and workflows. * Action connection testing now covers OpenAPI, Maps, Blob, Databricks, Log Analytics, MCP, Snowflake, Tableau, RocksDB, Yamcs, SQL, and Cosmos DB. * (Ref: Yamcs action, RocksDB action, MCP inbound server, action test connection) + * **Governance, Security, and Model Administration** * Governance policies support explicit block lists for feature and delegated item policies alongside allow rules. * Key Vault secret expiration reminders track per-action secrets with background sweeps, notifications, and telemetry. * Model requests can include HMAC-hashed user identity headers, and admins can configure per-model output token ceilings. * (Ref: governance policies, Key Vault secret inventory, model endpoint identity header, output token limits) + * **Chat Productivity, Grounding, and Notifications** * Users can opt into response completion sounds, desktop notifications, configurable AI notices, and per-message MP3 export. * Conversation grounding now exposes model/workspace/document/agent context, used-document panes, assistant-response forks, and a contents drawer. * User font size preferences, generated JSON/XML export artifacts, and smarter scroll behavior improve long-session usability. * (Ref: chat notifications, grounding citations, used documents pane, conversation fork, contents drawer, export artifacts) + * **Workspace, Sync, and Data Management Operations** * Azure Blob Storage File Sync adds SAS, managed identity, service principal auth, virtual-folder browsing, and ETag change detection. * Admin operations add automatic Control Center statistics refresh, backup cleanup/retention, restore workflows, Cosmos JSON editing, Redis Explorer, feedback/safety lifecycle controls, and file-processing log cleanup. * Multi-select metadata extraction, configurable Public Workspace naming, and index auto-login improve workspace administration. * (Ref: File Sync, Control Center, Backup Inventory, Data Management, Redis Explorer, metadata extraction) + * **Caching, Runtime, and Durable Processing Capabilities** * DAI Redis read-through caches document lists, tag lists, and legacy counts with scope-version invalidation. * Conversation list/feed caching adds Redis hit/miss metrics for Admin Settings visibility. @@ -633,14 +643,17 @@ v0.260.001 consolidates all work released after v0.250.001 into one major releas * Shifted the end-user Latest Features page and Admin Settings tab into current, previous, and archive release tiers for the v0.260.001 rollout. * Preserved per-tenant visibility choices across the shift. The new v0.260.001 user-facing cards ship hidden until their placeholder screenshots are replaced, so admins publish each card once its real capture is in place. * (Ref: Latest Features release groups, support catalog, admin catalog, visibility normalization) + * **Deeper End-User Feature Cards** * Added 20 v0.260.001 end-user cards with seven concrete How To Try It steps and a three-image gallery each. * Expanded the Latest Features card helper with the `images=[...]` gallery form for multi-image cards. * (Ref: `_latest_feature_card`, `_SUPPORT_RELEASE_260_FEATURE_CATALOG`, Latest Features image galleries) + * **Admin Latest Features Archive Tier** * Brought the Admin Settings Latest Features tab to the same three-tier current, previous, and archive model used by the end-user page. * Keeps v0.250.001 admin cards and older v0.241.x admin highlights available without crowding the current release tier. * (Ref: `_ADMIN_LATEST_FEATURE_RELEASE_GROUPS`, `_ADMIN_RELEASE_260_FEATURE_CATALOG`, Admin Settings Latest Features tab) + * **Latest Features PR Workflow Hooks** * Added a Latest Features authoring prompt, PR template checklist, and CI warning path so feature PRs consider release notes, cards, and screenshots together. * Helps future releases keep in-app Latest Features content aligned with shipped user and admin changes. @@ -652,16 +665,19 @@ v0.260.001 consolidates all work released after v0.250.001 into one major releas * Agent configuration now follows Actions → Knowledge → Instructions, with selected actions visible in the Instructions step. * Workflows use a stepped General/Trigger/Tasks/Reliability/Review builder with per-task runner controls and alert-rule editing. * (Ref: agent modal, workflow builder, workflow runner controls, alert rules editor) + * **Administration and Configuration UX Improvements** * Workspace sections now use a consistent Documents → Prompts → Identities → Sync → Endpoints → Actions → Agents → Workflows order. * Governance policy copy/inverse/show-users actions, dedicated Log Analytics configuration, refreshed backup/migrate/restore flows, and reviewed data migration steps reduce admin friction. * External links can be reordered, custom pages can be opened directly, and non-blocking Bootstrap toasts replace browser alerts across admin, workspace, and profile pages. * (Ref: workspace section order, governance UI, Log Analytics settings, data migration UI, toast notifications) + * **Chat, Navigation, and Accessibility Enhancements** * Chat, navigation, and sidebar layouts remain usable at 200% zoom and with large text. * The Conversation Contents drawer adds safe labels, keyboard focus handling, active-location tracking, and responsive desktop/mobile navigation. * Long source lists collapse behind a disclosure, document picker rows show file-name context, and Refresh Documents preserves selection with clearer status. * (Ref: 508 usability, conversation contents drawer, source disclosure, document picker, refresh documents) + * **Data Explorer and Extraction Status UX** * Redis Explorer uses a fixed-height modal with independent key-list and preview scrolling. * Cosmos query results open in a scrollable modal so the main editor stays focused on query setup. diff --git a/docs/explanation/release-notes/v0.261-part-1.md b/docs/explanation/release-notes/v0.261-part-1.md new file mode 100644 index 000000000..a96f869eb --- /dev/null +++ b/docs/explanation/release-notes/v0.261-part-1.md @@ -0,0 +1,698 @@ +--- +title: "Release notes 0.261.298 – 0.261.252" +description: "SimpleChat release notes for 0.261.298 – 0.261.252." +section: "Reference" +layout: page +--- + + + +# Release notes 0.261.298 – 0.261.252 + +[Back to release notes index]({{ '/explanation/release_notes/' | relative_url }}) + +### **(v0.261.298)** + +#### New Features + +* **Admin Review Center** + * The V2 Feedback Review and Safety Violations pages are replaced by one **Review center**, opened from a single account menu entry when either section is open to you. A rail lists each section's pages: a **Dashboard**, a workbench, and for safety the **Unchecked chat content** queue. Section access follows the same roles and feature switches as the server: FeedbackAdmin or Admin with user feedback on, SafetyViolationAdmin or Admin with content safety or screening on. A user with neither section sees a clear not-available state. + * Dashboards cover the last 7, 30 or 90 days. Feedback shows feedback awaiting review, negative feedback, the acknowledgement rate, archived feedback, feedback per day by rating and the oldest feedback awaiting review. Safety shows open violations, remediation awaiting approval, users restricted now, warnings sent and acknowledged, unchecked chat content, violations per day by category, severity and action breakdowns, and repeat users. Every figure opens the workbench filtered to what it counts, and every chart offers a data table. + * Workbenches list records beside the selected record's detail, with search (including the user's name), filters, page and selection in the address. Check rows, Shift+click a range or select every matching record (up to 500), then act on all of them: Acknowledge, Archive, Restore or Delete feedback; Set status, Archive, Restore or Delete violations; recheck unchecked messages one after another. Each record is changed as its own save would change it, and the report names every record that could not be changed and why. + * Records open in full-page editors with **Back**, a prompt before discarding unsaved changes, and **Reload** when the record changed underneath. The feedback editor records who reviewed it and can notify the user with the response. The violation editor prefills the notification, offers 24-hour, 7-day, 30-day or custom suspensions, and shows the approval request and warning acknowledgment. + * New and extended APIs: list `search` and filters with display names, `GET /feedback/review/ids` and `GET /api/safety/logs/ids`, `days` on both stats endpoints (existing fields unchanged), `GET /api/safety/logs/`, and `POST /feedback/review/bulk` and `POST /api/safety/logs/bulk` (up to 100 operations, per-item results). Exports now take the list filters. The old V2 addresses redirect; the classic pages are unchanged. + * (Ref: `pages/review/`, `components/review/`, `CategoryRail.tsx`, `DashboardParts.tsx`, `lib/reviewAccess.ts`, `lib/reviewCenter.ts`, `functions_review_center.py`, `route_backend_feedback.py`, `route_backend_safety.py`, [V2 Admin Review Center](features/V2_ADMIN_REVIEW_CENTER.md)) + +* **Approvals Dashboard And Safety Remediation Category** + * The V2 Approvals rail adds a **Dashboard**: requests waiting on you and those expiring within 24 hours, your own pending requests, decisions in the last 7, 30 or 90 days by outcome, pending requests by type, and the oldest waiting on you. Each figure opens the list filtered through its address. **All requests** is still where the page opens. + * A **Safety remediation** category lists warn, suspend and block requests for the Admin, ControlCenterAdmin and SafetyViolationAdmin roles, and **Group requests** no longer includes them. + * `GET /api/approvals/stats` counts only the requests `GET /api/approvals` shows the caller. + * (Ref: `ApprovalsPage.tsx`, `ApprovalsDashboard.tsx`, `GenericApprovalsPanel.tsx`, `functions_approvals.py` `summarize_visible_approvals`, `route_backend_control_center.py`) + +#### Bug Fixes + +* **Denied And Expired Remediation Requests Unlock Their Violation** + * Denying a suspend or block request, or letting it expire, left its violation pending for good: it could not be edited or deleted. The violation is now released as denied or expired when the request is decided, and any violation still waiting on a request that was decided or no longer exists is settled the next time it is listed or opened. A request that is still pending keeps its violation locked. + * (Ref: `functions_approvals.py` `deny_request`, `functions_safety_remediation.py` `release_safety_log_after_approval_decision`, `reconcile_pending_safety_logs`, [Safety Remediation Approval State Fix](fixes/SAFETY_REMEDIATION_APPROVAL_STATE_FIX.md)) + +* **An Applied Suspension Or Block Is Not Requested Twice** + * Saving a violation whose suspension or block was already applied or requested, for example to change only its status or notes, created another approval request. A new request is now created only when the action changes or the reviewer asks to request it again (`reissue`); otherwise the save reports `remediation_already_applied` or `remediation_unchanged`. + * Both the classic **Safety Violations** review and the V2 violation editor offer **Request this suspension again** (or block) whenever the violation already records that action and no request is waiting, including after a request was denied, expired or failed. Until it is ticked they say where the last request stands and send no notification or restore time; ticking it sends the new ones with the request. The classic dialog no longer offers a restore time that has passed. + * (Ref: `route_backend_safety.py` `update_safety_log`, `admin-safety-violations.js`, `admin_safety_violations.html`, `SafetyEditorPage.tsx`, [Safety Remediation Approval State Fix](fixes/SAFETY_REMEDIATION_APPROVAL_STATE_FIX.md)) + +* **Overlapping Safety Saves Leave One Consistent Request** + * A suspension or block saved while another reviewer's warning was being sent, or while another suspension or block was saved, could be recorded over the other save: the violation could read as applied while its request still waited for approval, a warning could go untracked, or a request could stay approvable with nothing linked to it. The request is now recorded only on the violation as the save read it. Otherwise the save is refused with `409 record_changed`, and its request is withdrawn with its notices removed. + * An approved warn, suspend or block request is carried out only while its violation waits on it, so a request that couldn't be withdrawn still changes nothing. + * (Ref: `route_backend_safety.py`, `functions_approvals.py` `withdraw_approval_request`, `functions_safety_remediation.py` `safety_remediation_state`, `safety_log_awaits_request`, `route_backend_control_center.py` `_execute_safety_violation_request`, [Safety Remediation Approval State Fix](fixes/SAFETY_REMEDIATION_APPROVAL_STATE_FIX.md)) + +* **Review Saves No Longer Overwrite Concurrent Changes** + * Safety and feedback review saves replaced the whole record, so a warning acknowledged while a reviewer was saving could be lost. Saves, archives and deletes are now conditional on the stored version and write only their own fields. + * A save that names the version it read (`etag`), as the V2 editors do and bulk operations can, is written on that version or not at all. A conflict is refused with `409 record_changed` rather than merged onto the newer version, which the V2 editors offer to reload. This covers PATCH, archive and the bulk `update` and `archive` operations. + * (Ref: `functions_safety_remediation.py` `write_safety_log_updates`, `functions_review_center.py` `replace_review_record`, `route_backend_feedback.py`, [Safety Remediation Approval State Fix](fixes/SAFETY_REMEDIATION_APPROVAL_STATE_FIX.md)) + +### **(v0.261.297)** + +#### New Features + +* **Access Restricted Screen For Suspended And Blocked Users** + * A user whose access an administrator suspended or blocked can still sign in, but every page now opens an **Access restricted** screen instead of a bare "Access Denied" error. It shows the notice the user was sent, whether access returns on its own and when (in the user's own time zone), the safety violation reference, and **Sign out**. Once the suspension ends or access is restored, the screen offers **Continue**. + * V2 pages go to `/v2/access-restricted` and classic pages to `/access-restricted`. API calls get `403 {"error": "access_restricted", "message", "restriction", "restricted_url"}` with the caller's own restriction, and the V2 interface follows it to the screen from any page, including when a restriction is applied while a tab is open. + * An approved safety suspension or block now stores the notice with the restriction, using the same title and message as the notification. Control Center restrictions carry no notice and show generic text; restoring access from Control Center clears it. + * The screen's routes require sign-in but not an unrestricted account, describe only the signed-in user's own restriction, and are exempt from the Terms of Use gate so a restricted user is never bounced between the two. Idle-session timeout still applies. Accounts with the Admin role are still never restricted. + * (Ref: `functions_access_restriction.py`, `functions_authentication.py` `user_required`, `get_user_access_restriction`, `route_access_restriction.py`, `access_restricted.html`, `AccessRestrictedPage.tsx`, `apiClient.ts`, [Access Restricted Sign-In Screen](features/ACCESS_RESTRICTED_SIGN_IN_SCREEN.md)) + +* **Safety Warnings Must Be Acknowledged** + * A warning from a safety reviewer now opens a dialog, **A warning from your administrators**, the next time the user opens the V2 interface, and appears again in every tab and on every device until they select **I understand**. Escape and clicks outside the dialog don't dismiss it. + * Reviewers see **Warning acknowledged** with the date, or **Not yet acknowledged**, on the violation, and users see the same state in **Settings > Violations**. Warnings sent before this version are never shown again. + * A reviewer can warn about the same violation again. The user then has to acknowledge the newer warning, even in a tab where they acknowledged the earlier one, and an acknowledgment is only recorded against the warning the user read: one replaced while on screen is answered with the newer warning instead. + * New routes `GET /api/safety/warnings/pending` and `POST /api/safety/warnings//acknowledge` return and change only the caller's own warnings. Bootstrap carries the pending count, so a user with no warnings makes no extra request. + * (Ref: `functions_safety_remediation.py`, `route_backend_safety.py`, `route_backend_v2.py` bootstrap `safety_warnings`, `SafetyWarningDialog.tsx`, `useSafetyWarningRuntime.ts`, `ViolationsTab.tsx`, [Safety Remediation Actions Fix](fixes/SAFETY_REMEDIATION_ACTIONS_FIX.md)) + +#### Bug Fixes + +* **Safety Warnings Are Sent Without A Second Reviewer** + * In a deployment with one administrator, **Warn user** created an approval request that nobody could approve, because a requester can never approve their own request, so the warning was never sent. A warning restricts nothing, so it is now sent as soon as the reviewer saves the review, and recorded in the activity log. Saving the record again, for example to resolve it, doesn't send it twice. + * Two saves that overlap, such as a double-click or two reviewers at once, send it once. A save claims the violation with a write conditional on the version it read before anything is sent, and the other save is refused with `409 safety_warning_in_progress`. While a warning is being sent the violation reads **Sending**, can't be changed or deleted, and never counts as a warning to acknowledge. A claim left by a save that stopped is released after five minutes. The classic page also disables **Save Review** while its request is in flight. + * **Suspend user** and **Block user** still create an approval request that another eligible reviewer must approve. A **Warn User** request created before this version still completes when approved. + * The review's guidance text in both interfaces now describes which actions wait for a second reviewer. + * (Ref: `route_backend_safety.py` `update_safety_log`, `functions_safety_remediation.py` `claim_safety_warning_send`, `record_safety_warning_send`, `route_backend_control_center.py` `_execute_safety_violation_request`, `AdminSafetyViolationsPage.tsx`, `admin-safety-violations.js`, [Safety Remediation Actions Fix](fixes/SAFETY_REMEDIATION_ACTIONS_FIX.md)) + +#### Breaking Changes + +* **Escalate Removed As A Safety Action** + * **Escalate** was a label with no workflow behind it. It can no longer be chosen in either interface, and the API rejects a change to `Escalate` with 400. Records that already carry it keep it, are labelled **Escalated (legacy)**, and can still be saved. `escalate_count` remains in the statistics; the V2 "Escalated or blocked" tile is now **Blocked**. + * **Migration**: None required. Resolve or re-action legacy escalated records as needed. + * (Ref: `route_backend_safety.py`, `AdminSafetyViolationsPage.tsx`, `ViolationsTab.tsx`, `admin_safety_violations.html`, `my_safety_violations.html`, `profile.html`) + +* **Access-Restriction Responses Changed** + * API calls from a restricted user now return `403` with `"error": "access_restricted"` and a structured `restriction`, instead of `"error": "Access Denied"` with the reason as `message`. Browser page requests are redirected to the Access restricted screen instead of returning a plain-text 403. + * **Migration**: Integrations that matched the old `Access Denied` error string should check for `access_restricted` instead. + * (Ref: `functions_authentication.py` `access_restricted_response`) + +### **(v0.261.296)** + +#### User Interface Enhancements + +* **Open Groups and Public Workspaces From User Settings** + * In V2 **User Settings**, every row on the **Groups** and **Public** tabs now has **Open** next to **Set active**, so you can go straight to a workspace you own or use instead of only making it active. + * **Open** goes to the workspace by its ID, like **Open** in the group and public directories. Opening a group also makes it your active group, because a group's pages work in the active group. Opening a public workspace leaves your active one unchanged. + * On narrow screens the row's buttons wrap below the workspace name, so the name and role badge are no longer squeezed. + * (Ref: `WorkspaceListTab.tsx`, `lib/workspaces.ts` `WorkspaceKind.openPath`, [V2 User Settings Redesign](features/V2_USER_SETTINGS_REDESIGN.md), `ui_tests/test_v2_settings_workspace_open.py`) + +### **(v0.261.295)** + +#### New Features + +* **Workflow Hand-Off Card In V2 Chat** + * An answer whose plan handed large work off to a one-time workflow now shows a **Workflow hand-off** card, so users can accept a hand-off in the browser. The card says what the workflow covers, such as "200 documents" or "up to 500 best-matching documents", the workspaces a search reads, each task and the model or agent it runs on, and when the offer expires. Workflow, task, agent and workspace names render as plain text, and instructions and document ids are never shown. + * **Accept** asks you to confirm, then creates the workflow and starts its one run. **Edit** opens the prepared workflow in the workflow editor, and **Save** asks you to confirm, then creates the edited workflow and starts its run. If the server refuses the edit, the editor stays open with your draft and lists what to change, and a draft with URL Access on is refused before anything is sent. Saving an untouched draft creates the same workflow and isn't recorded as an edit. **Decline** asks you to confirm and creates nothing. A hand-off whose workflow exists but whose run didn't start offers **Start its run**. + * Once its run is queued, the card follows it through the tab's workflow run tracker, with **Cancel run**, **Retry**, **Review and approve**, **Reconnect Microsoft 365** and **Open run** as they apply, and the outcome is posted back into the chat as for any run started from chat. A run that paused because more documents matched than the hand-off may review says so and offers **Cancel run**. + * Each refusal reads as a sentence, never a bare code or "Forbidden". A refused accept offers **Try again** only when the same accept can still succeed: while another accept is in progress, or when the workflow was created but its run couldn't be queued or started, which retries that same run. Hand-offs that couldn't be loaded offer **Try again** to read them again. + * The card appears in private personal chats, under an answer whose plan completed a hand-off step, in place of the proposal and started-workflows cards. Hand-off itself stays off by default, behind **Hand Off Large Work From Chat**. + * (Ref: #1549, #1543, `WorkflowHandoffCard.tsx`, `workflowHandoffs.ts`, `MessageList.tsx`, `WorkflowRunCard.tsx`, [Chat Orchestration Workflow Hand-off](features/CHAT_ORCHESTRATION_WORKFLOW_HANDOFF.md#the-v2-hand-off-card), [Chat controls](../reference/chat-controls.md#workflow-hand-offs-v2-interface)) + +#### User Interface Enhancements + +* **Hand-Off Plans Always Wait For Approval In V2** + * The plan card says when a plan hands work off to a one-time workflow, so it always waits for your approval, and that approving it only prepares the workflow. V2 makes any plan with an enabled hand-off step wait for approval, so a countdown or Auto never runs it by itself, even if the server's approval-floor marker is missing. + * The Run view labels the step **Hand off large work** and puts what it prepares in words: the workflow's name, what it reviews, such as "3 named documents" or "a search of 2 workspaces, the 50 best matches", and its task titles. The tasks' instructions, the document and workspace references, the content filter and the tags aren't shown. + * (Ref: `OrchestrationWorkflowHandoffNotice.tsx`, `OrchestrationPlanCard.tsx`, `OrchestrationRunView.tsx`, `orchestrationPlan.ts`) + +### **(v0.261.293)** + +#### Bug Fixes + +* **Orchestrated Charts From an Action Are Shown Again** + * With Orchestrate on, a request such as "Plot BatteryVoltage1 over the last 15 minutes" could be planned as a single Gather step that ran the action, with no Reason step to write the answer. The action drew the chart, but only an answer step places charts, so the chart was dropped. The reply said only "The requested content is prepared. No downloadable files were created.", the plan panel listed the chart as delivered, and Retry wasn't offered. + * A chart an action or agent step draws is now always shown, where the answer places it or after the answer, the same way generated images are. + * The answer step now receives such a chart as its placement token instead of a second copy of the chart's data. The retrieved rows are still sent in full. + * (Ref: `functions_orchestration_deliverables.py` `gathered_charts`, `show_gathered_charts`, `functions_orchestration_execution.py` `_finalize`, `functions_orchestration_composition.py` `_model_inputs`, [Orchestration Action Chart Not Delivered Fix](fixes/ORCHESTRATION_ACTION_CHART_NOT_DELIVERED_FIX.md)) + +* **Gathered Results Are Always Read by a Reason Step** + * Before 0.261.139 every plan ended with an Answer step, so everything a plan gathered was written up. Since then the planner adds an answer step only when it chooses to, and it was told an action could deliver a chart on its own. + * While planning, every Gather step except a `workflow_run` step (**Run workflows**) must now feed the answer, a file or a generated image, and every planned chart or diagram must reach the answer or a file. A plan that breaks either rule gets the planner's one correction round with the server's explanation. Plans saved before this version still open and run. + * A saved plan that gathered without answering now says so and asks the user to ask again, instead of "The requested content is prepared." + * (Ref: `functions_orchestration_deliverables.py` `_require_shown_work`, `functions_orchestration_planner.py`, [Orchestration Deliverables](features/ORCHESTRATION_DELIVERABLES.md)) + +* **Undelivered Charts Are Reported and Can Be Retried** + * A chart or diagram the user asked for that the answer doesn't show is now listed as not delivered, with the reason, and the run is partial with the new `visual_not_delivered` failure. + * **Retry from failed step** runs the step that draws the chart again, along with every step computed from it. Rerunning an action or agent step repeats its calls, so that retry asks for confirmation first. + * The V2 plan panel uses the server's check of each chart and diagram after a run, instead of the status of the step that makes it. + * (Ref: `functions_orchestration_deliverables.py` `visual_delivery`, `functions_orchestration_recovery.py` `redraw_step_ids`, `deliverable_states`, `OrchestrationDeliverables.tsx`, `orchestrationPlan.ts`) + +* **Faster Saving of Large Gathered Results** + * A structured step result was saved as one record per JSON token. The reported 202 KB action result took 258 storage pages and about 200 of the step's 231 seconds. + * Results are now saved in 16,384-character records, so the same result takes a few pages. Results saved before this version still read. + * (Ref: `functions_orchestration_results.py` `_value_records`) + +### **(v0.261.292)** + +#### Bug Fixes + +* **V2 Control Center Dashboard, Users and Groups Load Again** + * The Dashboard showed "Failed to retrieve dashboard insights.", and Users and Groups showed "Unable to retrieve users." and "Unable to retrieve groups." Cosmos DB rejected their queries with HTTP 400. The azure-cosmos Python SDK cannot run cross-partition `GROUP BY` or `COUNT` over `DISTINCT` values, and the Users sort used a two-property `ORDER BY` that needs a composite index the `user_settings` container does not have. + * The affected queries now use shapes the SDK supports. Distinct values and counts are aggregated in the application, the Groups inventory streams narrow projections, and Users orders by one property at a time, listing accounts without a recorded sort value last. Existing deployments need no index change or App Maintenance step. + * The login heatmap now totals each UTC weekday and hour across the whole period instead of showing a single date's count. The Users CSV export now reports a storage failure as an error instead of returning a file with only the header row. + * A compatibility test scans every V2 Control Center query so these shapes cannot return. + * (Ref: `route_backend_control_center.py`, `functions_control_center_groups.py`, `test_v2_control_center_cosmos_query_compatibility.py`, [V2 Control Center Cosmos Query Compatibility Fix](fixes/V2_CONTROL_CENTER_COSMOS_QUERY_COMPATIBILITY_FIX.md)) + +#### Breaking Changes + +* **V2 Control Center Data Health Removed** + * The Data health section has been removed, together with the activity-log backfill APIs that only it used: `GET /api/admin/control-center/migrate/status` and `POST /api/admin/control-center/migrate/all`. The classic Control Center had already stopped calling them, and normal application workflows record activity themselves. + * **Migration**: None required. A bookmark to the old Data health page now opens the Dashboard. + * (Ref: `ControlCenterPage.tsx`, `route_backend_control_center.py`, `functions_activity_logging.py`, [Activity Log Migration Prompt Fix](fixes/ACTIVITY_LOG_MIGRATION_PROMPT_FIX.md)) + +### **(v0.261.291)** + +#### Bug Fixes + +* **Orchestrated Ask an Agent Steps Run Again** + * With Orchestrate on, every Ask an agent step failed with "This step's agent or action isn't available to you right now", in personal and shared conversations, while the same agent answered with Orchestrate off. + * Cosmos DB returns stored agents as a subclass of a dictionary, and the step's access check accepted only a plain dictionary. Stored agents are now read back as plain dictionaries, and the check accepts either while still requiring the exact agent the user can reach. No setting or data change is needed. + * When a step's web, linked-page, deep research, agent, action or memory source is refused, `[ORCHESTRATION_EXTERNAL_SOURCES] A step's source was refused.` now names the check that failed in `sc_reason`. + * (Ref: `functions_orchestration_external_sources.py`, `functions_agent_delegation.py` `_canonical_agent`, [#1699](https://github.com/microsoft/simplechat/issues/1699), [Orchestration Agent Document Type Fix](fixes/ORCHESTRATION_AGENT_DOCUMENT_TYPE_FIX.md)) + +### **(v0.261.290)** + +#### Bug Fixes + +* **V2 Sidebar Links Opened the Classic Interface** + * Every primary link in the V2 left rail opened the classic page at the same path instead of the V2 page. This covered Chats, Agents, My Workspace, Group Workspaces, Public Workspaces, Approval requests and Content review. The rail also never highlighted the open page. + * Rail links now stay relative to `/v2`, so each one opens its V2 page without a reload and is highlighted. + * A functional test now flags any router link built from the page origin in the V2 source, and a UI test clicks each rail link. + * (Ref: `Sidebar.tsx` `safeNavHref`, [#1698](https://github.com/microsoft/simplechat/issues/1698), [V2 Sidebar Navigation Links Fix](fixes/V2_SIDEBAR_NAV_LINKS_FIX.md)) + +### **(v0.261.289)** + +#### Bug Fixes + +* **Picked Agents Pass the Agents Setting in Orchestrate** + * An Ask an agent step for an agent the user picked, or tagged with @, was refused when the user's own agents setting was off, the agents button on the classic chat page. V2 has no control for it, so many V2 users have it off. + * Picking an agent now counts as permission to use that agent, as it does in chat. Its scope, membership, governance and enabled state are still checked, and the setting still applies to agents the user didn't pick. + * These steps still failed at a later check until 0.261.291. + * (Ref: `functions_orchestration_external_sources.py` `_seeded_agent`, `test_orchestration_seeded_agent_preference_fix.py`, [Orchestration Selected Agent With Agents Turned Off Fix](fixes/ORCHESTRATION_SELECTED_AGENT_DISABLED_PREFERENCE_FIX.md)) + +### **(v0.261.288)** + +#### New Features + +* **Ask AI in the Agent and Action Editors** + * The V2 agent and action editors for personal, group and global agents and actions now have an **Ask AI** panel, matching the workflow editor. Describe what you want and the assistant fills in the unsaved draft, highlighting each changed field, with **Jump to** and **Undo this change** for every turn. It never saves and never sees or enters keys or other credentials. + * In the agent editor it can set the name, description, instructions, model, assigned actions, token limit, reasoning effort and knowledge. In the action editor it can choose the action type and fill in its configuration, capabilities and sign-in method. + * The agent assistant can draft up to three new actions for the agent. They are listed under **New actions from Ask AI** and created only when the agent is saved. One that still needs something, such as an endpoint, blocks the save and can be completed with **Finish in action editor**, which returns to the agent with the action assigned. + * Administrators can turn each panel off with **Enable Ask AI in the Agent Editor** and **Enable Ask AI in the Action Editor** in Agents and Actions settings. Both are on by default. + * (Ref: `functions_editor_assist.py`, `POST /api/agents/assist`, `POST /api/actions/assist`, `EditorAskAiPanel.tsx`, `agentEditorAssist.ts`, `actionEditorAssist.ts`, `AgentActionPicker.tsx`, `workspaceEditorDrafts.ts`, [Ask AI in the Agent and Action Editors](features/AGENT_ACTION_AI_ASSISTANT.md)) + +### **(v0.261.287)** + +#### New Features + +* **V2 Terms of Use Page** + * V2 users who must accept the Terms of Use now see a V2 page at `/v2/terms-of-use` instead of being sent to the classic interface. Accepting returns them to the V2 page they were opening; declining signs them out to the configured destination. + * Acceptance, versioning, recurrence, redirect safety, and audit logging are shared with the classic page. A V2 tab whose acceptance lapses while open is sent to the terms page on its next request. + * (Ref: `app.py` `build_terms_of_use_url`, `/api/v2/terms-of-use` routes in `route_backend_v2.py`, `TermsOfUsePage.tsx`, [V2 Terms of Use and Approval Requests](features/V2_TERMS_AND_APPROVALS.md)) + +* **V2 Approval Requests Page** + * **Approval requests** in the V2 sidebar opens a full-page view laid out like Admin Settings: a collapsible category rail, the request list, and a detail pane with the decision controls. + * Covers everything the classic page does: all requests, group requests, Microsoft 365 approvals, content screening (when enabled), outgoing Microsoft 365 actions, waiting requests with resume or connect-and-resume, and admin agent template approvals. + * Each request has its own address. Notification links and classic bookmarks open the matching request, and the Admin Settings agent template link opens the V2 queue. + * (Ref: `ApprovalsPage.tsx`, `components/approvals/`, `approvalsApi.ts`, `notificationLinks.ts`, `Sidebar.tsx`, `v2ApprovalsRailCollapsed` user setting) + +### **(v0.261.286)** + +#### Bug Fixes + +* **Public Workspace Validation Error Safety** + * Public Workspace list, detail, status, bulk-status and export APIs return stable validation messages rather than exception text. + * Preserves authorization and HTTP status behavior; adds sensitive-error regression coverage. + * (Ref: `route_backend_control_center.py`, `test_control_center_safe_exception_responses.py`, [Control Center Validation Error Safety](fixes/V2_CONTROL_CENTER_VALIDATION_ERRORS_FIX.md)) + +### **(v0.261.285)** + +#### Bug Fixes + +* **Public Workspace Validation Error Safety** + * List, detail, bulk status, individual status, and export validation failures return stable messages instead of exception text, preserving HTTP 400 and input bounds. + * (Ref: `route_backend_control_center.py`, `test_control_center_safe_exception_responses.py`, [Control Center Validation Error Safety](fixes/V2_CONTROL_CENTER_VALIDATION_ERRORS_FIX.md)) + +#### New Features + +* **V2 Control Center Activity Logs** + * Adds URL-filtered activity investigations, deterministic keyset paging, a bounded histogram and activity-type facets, saved browser views, density controls, and escaped JSON detail drawers with related entity/approval links. + * CSV export streams the same filters, escapes spreadsheet formulas and caps exports at 10,000 activity records. Summary charts disclose sampling above 5,000 matching records. + * Existing deployments must apply the expected activity-log composite index through App Maintenance and wait for index transformation; legacy activity browsing remains unchanged. + * (Ref: `ActivityLogsSection.tsx`, `functions_control_center_activity.py`, `functions_cosmos_indexing.py`, `route_backend_control_center.py`, [V2 Control Center](features/V2_CONTROL_CENTER.md)) + +### **(v0.261.284)** + +#### New Features + +* **V2 Control Center Public Workspace Management** + * Adds query-level search/status/owner filters, server sorting and pagination, bounded cross-page bulk status changes, and injection-safe filtered CSV export. + * Reuses detail tabs for ownership, supported manager roles/CSV, status history, retention, recent activity and document totals. Missing recorded metrics remain unavailable, not zero. + * Preserves existing approval semantics and native membership permissions; public retention cannot reset custom values to defaults through its existing save API. + * (Ref: `PublicWorkspacesSection.tsx`, `GroupDetailDrawer.tsx`, `functions_control_center_public_workspaces.py`, [V2 Control Center](features/V2_CONTROL_CENTER.md)) + +### **(v0.261.283)** + +#### Bug Fixes + +* **V2 Control Center Validation Error Safety** + * Users and Groups APIs return stable validation messages instead of exception text, preventing internal failure details from reaching the browser. + * (Ref: `route_backend_control_center.py`, [Control Center Validation Error Safety](fixes/V2_CONTROL_CENTER_VALIDATION_ERRORS_FIX.md)) + +### **(v0.261.282)** + +#### New Features + +* **V2 Control Center Group Management** + * Adds server-filtered group inventory, sortable usage and activity, cross-page bulk status updates with required reasons for locking/inactivation, and filtered CSV export. + * Group drawers include membership search/CSV import, supported member controls, ownership requests, status history, retention, recent activity with JSON/export, and document summaries. + * Deletion and ownership changes remain approval-gated. Existing group membership requirements for role changes, removal and retention are preserved. + * (Ref: `GroupsSection.tsx`, `GroupDetailDrawer.tsx`, `functions_control_center_groups.py`, `route_backend_control_center.py`, [V2 Control Center](features/V2_CONTROL_CENTER.md)) + +### **(v0.261.280)** + +#### New Features + +* **V2 Control Center User Management** + * Adds filtered, sortable user administration with cached usage metrics, account detail and activity, cross-page bulk access/upload updates, and server-side CSV export. + * Document deletion requests require a reason and are submitted for approval; dashboard drill-through filters and selected-user links are preserved. + * (Ref: `UsersSection.tsx`, `route_backend_control_center.py`, [V2 Control Center](features/V2_CONTROL_CENTER.md)) + +* **Guided Tours and Latest Features in V2** + * Chat and Personal Workspace have step-by-step guided tours, started from a help button in the page header or with **Start now** in User Settings. Tours skip controls that are not on screen, support the keyboard, and respect reduced motion. + * A new **Help and guidance** group in Preferences turns tours off one by one, or all at once with **Show tour buttons**. That switch is shared with the classic tutorial buttons. + * A **Latest Features** shortcut appears in the V2 navigation rail when the Support menu offers it. Hiding it lasts until the next release, and the setting is shared with the classic interface. + * (Ref: `lib/tours.ts`, `GuidedTour.tsx`, `TourLauncher.tsx`, `LatestFeaturesLink.tsx`, `GuidanceCards.tsx`, `_build_latest_features_nav`, `tutorialVisibility`, [V2 User Settings Redesign](features/V2_USER_SETTINGS_REDESIGN.md)) + +#### Bug Fixes + +* **Dashboard Date-Range Error Safety** + * Dashboard summary and insights APIs now return a generic validation message for invalid date ranges instead of returning exception text. + * (Ref: `route_backend_control_center.py`, `DASHBOARD_INVALID_RANGE_ERROR`) + +### **(v0.261.279)** + +#### Bug Fixes + +* **MCP Fingerprint Import Cycle** + * Shared MCP fingerprint metadata normalization now lives in a dependency-neutral module, avoiding cyclic imports during action loading and notification delivery. + * (Ref: `functions_mcp_fingerprint_metadata.py`, `functions_mcp_operations.py`, `functions_mcp_tool_pinning.py`) + +#### New Features + +* **Fact Memory Workbench and Microsoft 365 Cards in V2 User Settings** + * Fact memory is now one workbench: add, search, filter, edit, and delete (with confirmation) instructions and facts. + * Microsoft 365 sharing, chat connection, workflow connection, and workflow authorizations match the classic page, including CSRF-protected writes and confirmed revocations. + * (Ref: `FactMemoryBench.tsx`, `M365Cards.tsx`, `PreferencesTab.tsx`) + +* **V2 Control Center Dashboard** + * Adds period-based summary metrics, correct group and public-workspace status counts, login/activity/token charts, token filters, CSV export, and chat-with-trends. + * Aggregates use activity-log and workspace data with a 90-second cache. Metrics without retained historical snapshots are identified as current state instead of presenting misleading period deltas. + * Dashboard readers can access the dashboard APIs without receiving management capabilities. + * (Ref: `route_backend_control_center.py`, `DashboardSection.tsx`, [V2 Control Center](features/V2_CONTROL_CENTER.md)) + +* **Feedback and Safety Review in V2** + * Administrators and designated reviewers can review user feedback and safety violations in V2, with filtering, statistics, exports, detail editing, and archive management. Feedback reviewers can retest prompts; safety reviewers can manage remediation requests and recheck unchecked chat messages with confirmation before potentially removing saved or shared replies. + * Reviewer links appear in the account menu when the corresponding feature is enabled and the signed-in user has the configured administrator or reviewer role. Existing backend authorization remains authoritative. + * (Ref: `AdminFeedbackReviewPage.tsx`, `AdminSafetyViolationsPage.tsx`, `Sidebar.tsx`, [Feedback Review](../guides/admin-review-feedback.md), [Safety Violation Review](../guides/admin-review-safety-violations.md)) + +### **(v0.261.278)** + +#### New Features + +* **Voice, Audio and Retention in V2 User Settings** + * Completion sounds with preview and mute, spoken reply voice, speed, and auto-play, microphone permission status, and personal retention for conversations and documents. + * (Ref: `PreferencesTab.tsx`, `lib/speechPlayback.ts`, `/api/retention-policy/user`) + +* **V2 Control Center Foundation** + * Adds the permission-aware V2 Control Center pane, reusable management primitives, and a manually invoked activity-log data-health check and backfill. + * The classic page no longer scans for migration status on every visit; the retained backfill skips existing activity events and uses stable IDs for reruns. + * (Ref: `ControlCenterPage.tsx`, `functions_authentication.py`, `route_backend_v2.py`, [V2 Control Center](features/V2_CONTROL_CENTER.md), [Activity Log Migration Prompt Fix](fixes/ACTIVITY_LOG_MIGRATION_PROMPT_FIX.md)) + +#### Bug Fixes + +* **Personal Retention Default Value** + * Choosing the organization default for personal retention is now accepted. + * (Ref: [Personal Retention Default Value Fix](fixes/PERSONAL_RETENTION_DEFAULT_VALUE_FIX.md)) + +### **(v0.261.277)** + +#### User Interface Enhancements + +* **V2 User Settings Follows the Admin Settings Design** + * A collapsible section rail remembered per user, full-width settings cards, and an **On this page** index on every tab. Preferences are grouped instead of listed, and the Violations tab is always listed. + * (Ref: `SettingsPage.tsx`, `SettingsCard.tsx`, `v2UserSettingsRailCollapsed`) + +* **Agent Editor No Longer Shows GUIDs** + * The agent editor for personal, group and global agents no longer shows internal identifiers. The header shows the agent's own icon, such as the Microsoft 365 logo, and its description instead of a stable ID. + * Model & connection › Current selection lists the deployment, model, connection name and provider (for example "Azure OpenAI") instead of endpoint and model IDs. + * Action choices, knowledge workspaces and documents no longer show IDs. Workspaces show their type, and documents show their workspace, file name and tags. + * (Ref: `AgentEditorPage.tsx`, `AgentModelFields.tsx`, `AgentActionPicker.tsx`, `AgentKnowledgeFields.tsx`, [Agent Editor Internal ID Exposure Fix](fixes/AGENT_EDITOR_INTERNAL_ID_EXPOSURE_FIX.md)) + +#### Bug Fixes + +* **Feedback Tab Loads in V2** + * The Feedback tab failed with "too many values to unpack". The user's own feedback now loads. + * (Ref: [Feedback My Routes Filter Unpack Fix](fixes/FEEDBACK_MY_FILTER_UNPACK_FIX.md)) + +### **(v0.261.276)** + +#### New Features + +* **Action Configuration Pane Cleanup** + * The V2 action editor now hides empty configuration sections, removes ad hoc custom fields, keeps unknown legacy values in Advanced JSON, and moves connection checks into Authentication where credentials are visible. + * Action-specific configuration now reflects the actual supported settings: Document Search scopes and summary targets, Microsoft 365 capability switches and read-only Graph endpoint, OpenAPI spec-derived base URL and operation switches, MCP server templates and tool fingerprint approval, and Yamcs reverse proxy authentication. + * (Ref: `ActionEditorPage.tsx`, `ActionConfigurationFields.tsx`, `ActionAuthentication.tsx`, `DocumentSearchActionConfiguration.tsx`, `McpActionConfiguration.tsx`, `OpenApiActionConfiguration.tsx`, [Action Configuration Pane Cleanup](features/ACTION_CONFIGURATION_PANE_CLEANUP.md)) + +#### Bug Fixes + +* **Action Catalogue Deduplication and Legacy Type Hiding** + * Databricks no longer appears multiple times in the new-action picker, SQL actions display as SQL Database with schema discovery included, and legacy/internal-only types are hidden from new-action creation while existing actions remain editable. + * (Ref: `route_backend_plugins.py`, `functions_workspace_authoring.py`, action reference docs) + +#### User Interface Enhancements + +* **Safer Action Validation and Review Badges** + * MCP actions now require discovery approval before saving new or changed server manifests, runtime calls block changed tools, and Actions/Agents lists flag drift with a "Tools changed — review" badge. + * OpenAPI actions show the chosen spec file name, offer explicit operation enablement, and reject disabled operations before an HTTP request is sent. + * (Ref: `functions_mcp_tool_pinning.py`, `mcp_plugin_factory.py`, `openapi_plugin.py`, `ActionsSection.tsx`, `AgentsSection.tsx`) + +* **V2 Admin Help Group Matches the Classic Page** + * **Support** now offers the Menu Name, the Send Feedback destination and its Support Recipient Email, nested under the Support menu switch. The card reads **Needs configuration** while Send Feedback is on with no recipient, and the empty recipient field is marked **Required**. A malformed address is refused beside the field instead of being cleared, and turning the menu on no longer switches Send Feedback off. + * **Send Feedback** has an Overview and the **Report a Bug** and **Request a Feature** forms. They prefill your name and email, check each field, record the submission, and open a text-only email draft to the SimpleChat team, with a link to open it again. The overview explains how this differs from your users' own Send Feedback and links to the Support settings. + * **User-Facing Latest Features** lists every announcement by release, with a share checkbox, **Share all** and **Hide all**, and a preview of exactly what users see, including screenshots and the shortcuts your unsaved choices would show. It stays editable while Support is off, so you can prepare announcements before publishing. A **Publication** notice says whether users can reach the page and links to the Support settings. + * **Admin Latest Features** is now a V2 card marked **New**, and the Help category carries the same marker. Each release opens to details, why it matters, rollout notes, screenshots, and shortcuts. Shortcuts jump to the matching V2 card, or open the classic tab when V2 does not show it yet. Page search finds announcements by their content. + * The **Registered** / **Unregistered** release notifications badge now sits beside the version in V2 and opens the registration dialog. + * (Ref: `admin_settings_fields.py`, `functions_support_latest_features.py`, `GET /api/v2/admin/latest-features`, `AdminSettingsPage.tsx`, `SendFeedback.tsx`, `LatestFeaturesVisibility.tsx`, `AdminLatestFeatures.tsx`, `ReleaseNotificationsBadge.tsx`, [V2 Admin Help Settings](features/V2_ADMIN_HELP_SETTINGS.md)) + +### **(v0.261.275)** + +#### New Features + +* **Backup & Recovery in V2 Admin Settings** + * The Backup & Recovery group no longer shows "No settings match". Each of its nine sections now has a V2 card with the same capabilities as the classic page: a Start Here readiness checklist with guides, on-demand full and partial backups, the schedule and retention window, backup storage and encryption, a six-step migration wizard, a backup inventory with restore, the Cosmos DB editor, and job history. + * Backup settings join the page's Save bar and save after the other Admin Settings. Queueing a backup, running retention cleanup, reviewing or queueing a restore, and starting a migration save pending Backup & Recovery changes first; retrying or resuming a job never does. When unsaved Enhanced Citations or Key Vault settings would change how an action is checked, the action offers **Save all and continue**. + * Restore and migration queue only from a current, passing review, with typed confirmations for overwrite restores and mirror migrations. Cosmos DB editor saves are ETag-guarded, and leaving the page with unsaved work asks first. + * (Ref: `components/admin/dataManagement/`, `dataManagementStore.ts`, `dataManagement.ts`, `dataManagementLogic.ts`, `AdminSettingsPage.tsx`, `admin_settings_fields.py`, [V2 Admin Backup & Recovery](features/V2_ADMIN_BACKUP_RECOVERY.md)) + +#### Bug Fixes + +* **Backup Inventory & Restore Label** + * The navigation label showed a literal `&` on both the classic and V2 admin pages. It now reads "Backup Inventory & Restore". + * (Ref: `admin_settings_nav.py`, `docs/_data/app_surface.yml`) + +### **(v0.261.274)** + +#### New Features + +* **Scale Settings in V2 Admin Settings** + * The V2 Scale group now covers everything the classic page offers. Redis Cache has its connection settings, an access key field that becomes the Key Vault secret name with Key Vault authentication, and **Test Redis connection**. Redis Metrics shows health and capacity with an inline, read-only Redis Explorer, and Conversation Cache has its TTL and the last 15 minutes of cache activity. + * DAI Metrics shows the always-on index components, backfill progress, and read and cache metrics. Its diagnostics, batch sizes, and backfill actions appear only while **Document Access Index diagnostics** (`enable_dai_debug`) is on under **Operations › Logging & Health › Debug Logging**, matching the classic page, which shows them only while that flag is set. + * Cosmos Maintenance shows indexing and stale cache status and runs its tasks on demand. Cosmos DB Throughput adds the resource, guardrails, access validation, and manual scaling, and Cosmos Metrics is a container workbench where each container's policy is edited in place and saved with the page. + * Applying indexes, deleting stale cache documents, resetting the backfill, and scaling or converting throughput each ask for confirmation first. A scale states the current RU/s and the target the server will most likely choose. + * Saves enforce the classic throughput rules, with each error beside the value that caused it, and report any RU/s value the server rounds. Container policy saves keep the automation's own timestamps, so a page loaded before a scale cannot reset a cooldown. + * (Ref: `admin_settings_fields.py`, `functions_cosmos_throughput.py`, `route_backend_v2.py`, `AdminSettingsPage.tsx`, `CosmosThroughputConsole.tsx`, `CosmosContainerMetrics.tsx`, `RedisMonitoringPanel.tsx`, `DocumentAccessIndexPanel.tsx`, [V2 Admin Scale Settings](features/V2_ADMIN_SCALE_SETTINGS.md)) + +#### User Interface Enhancements + +* **Settings Cards Show What Depends on Them** + * A section other sections rely on now lists them under **Used by**. Redis Cache, for example, lists Conversation Cache, Redis Metrics, and File Sync. The list follows the same rules as the prerequisite notices on those cards, and each entry moves the page to that section. + * A field's label can follow another setting, and a prerequisite can apply to one configuration only. The schema descriptors are `label_variants` and `requires.when`. + * (Ref: `SettingsSection.tsx`, `adminSections.ts`, `adminFields.ts`, `admin_settings_fields.py`) + +### **(v0.261.271)** + +#### New Features + +* **Global Agents and Global Actions in V2 Admin Settings** + * Admin Settings › Agents & Actions now lists the organisation's global agents and actions. From the lists you can create, edit, enable, disable and delete them, and choose the default agent. Each change saves on its own rather than through the settings save bar, and Admin Settings asks before opening an editor would drop settings you have not saved. + * Agents and actions open in the same full-page editors My Workspace uses. A global agent's assigned knowledge is limited to public workspaces, its actions are the global actions, and **New action** creates one and attaches it to the agent you came from. Action connection tests and MCP tool discovery run in the global scope. + * **Start from a template** opens a new agent with the approved gallery in view, and **Publish as template** adds a global agent's recipe to the gallery straight away. Agent Template Approvals now links to the approvals queue. + * The separate **Global agent delegation** card is gone. A Call agent action is now an ordinary global action: create it under Global Actions with the **Call agent** type, which lists only global agents as targets, then attach it in a global agent's editor. + * Thirteen admin-only routes under `/api/v2/admin/agents` and `/api/v2/admin/actions` serve the editors through the shared authoring engine, with masked credentials, global Key Vault names, case-insensitive name checks, and revision checks that refuse to overwrite another administrator's save. The classic page can still edit and delete agents and actions saved in V2. + * (Ref: `route_backend_v2_admin_agents_actions.py`, `functions_global_editor_access.py`, `functions_workspace_authoring.py`, `functions_global_agents.py`, `GlobalAgentsManager.tsx`, `GlobalActionsManager.tsx`, `AdminGlobalEditorPages.tsx`, `agentWorkbench.ts`, `actionWorkbench.ts`, `test_v2_admin_global_agents_actions.py`, [V2 Admin Global Agents and Actions](features/V2_ADMIN_GLOBAL_AGENTS_ACTIONS.md)) + +#### Bug Fixes + +* **Choosing a Settings Category Starts at Its Top** + * Choosing a category in the V2 Admin Settings rail, or in the category list on a phone, kept the scroll position of the category you left, so leaving Knowledge near its end opened Security near its end. A category now always opens at its first section, including when you choose the one already shown, and the **On this page** list marks that section. + * (Ref: `AdminSettingsPage.tsx` `selectCategory`, `test_v2_admin_global_agents_actions.py`, [V2 Admin Category Scroll Reset Fix](fixes/V2_ADMIN_CATEGORY_SCROLL_RESET_FIX.md)) + +* **Call Agent Targets Come From the Action's Workspace** + * The V2 Call agent target picker always listed the signed-in person's personal targets. It now asks for the targets of the workspace the action belongs to: global agents for a global action, and for a group action the group's agents plus any permitted global ones, which matches what the server accepts when the action is saved. + * (Ref: `CallAgentActionConfiguration.tsx`, `/api/plugins/agent-targets`) + +#### User Interface Enhancements + +* **Agent and Action Editors Share the Admin Settings Design** + * The full-page agent and action editors, the same ones in My Workspace, group workspaces and Admin Settings, are now laid out as Admin Settings is. Each section is a card headed by an icon, its title and what it holds. Settings are rows with the label and help beside the control on a wide screen, stacked on a narrow one. A setting others depend on, such as **Restrict to assigned knowledge** or **Track secret expiration**, is highlighted with the settings it controls indented beneath it, and collapsible groups and panels match Admin Settings. + * The section list on the left stays, now with an icon per section, and marks the section in view. Choosing a section, or arriving through **Start from a template**, keeps that section in view while lists above it finish loading, until you scroll or start editing. + * The Global Agents and Global Actions lists now match AI Connections: an Enabled or Disabled badge, a Default agent badge, and icon buttons to make an agent the default, enable or disable, edit, and delete. + * (Ref: `EditorLayout.tsx`, `WorkspaceEditorFrame.tsx`, `theme.css` `.editor-field`, `AgentFields.tsx`, `ActionFields.tsx`, `GlobalAgentsManager.tsx`, `GlobalActionsManager.tsx`, `AdminListPill.tsx`, [V2 Admin Global Agents and Actions](features/V2_ADMIN_GLOBAL_AGENTS_ACTIONS.md)) + +### **(v0.261.270)** + +#### Bug Fixes + +* **Orchestrate Action and Agent Steps Work Again, Including Microsoft 365** + * With **Orchestrate** on, "what are my emails" failed with "A required retained result is unavailable or changed" in personal and shared chats, while the same request worked with Orchestrate off. Agent and action steps now trust the signed-in session the way manual chat does: access to the conversation, run and exact agent or action is checked, and the step runs without comparing configuration. Web search, linked-page reading and deep research keep their configuration checks. + * Local agents now run as plan steps. An agent that uses Microsoft 365 actions gets a Microsoft 365 request for its own actions, with the same read-only and signed-in-user rules as an action step. + * An @agent or @model tag in an Orchestrate message now chooses that agent or model, like the pickers do. + * Editing an agent or action no longer invalidates results it already produced. A result stays readable until the user loses access to that agent or action. + * (Ref: #1660, #1661, `functions_orchestration_external_sources.py`, `functions_orchestration_bootstrap.py`, `functions_orchestration_adapters.py`, `functions_orchestration_m365.py`, `agent_delegation_runtime.py`, `Composer.tsx`, [Session-Trusted Action and Agent Steps Fix](fixes/ORCHESTRATION_SESSION_TRUSTED_ACTION_AGENT_STEPS_FIX.md)) + +* **Orchestrate Respects Shared Conversations** + * In a shared conversation with Orchestrate on, a message that addresses only people, such as "@person hey", is now posted to them instead of being sent to the model, as it is with Orchestrate off. + * Only the person who started the shared conversation plans in it. Their question and the run's answer are posted to the shared thread, where every participant sees them arrive. Plans run in a hidden conversation with the shared conversation's ID and workspace lock instead of a private copy, and earlier private copies no longer appear in conversation lists. Other participants' requests are answered the classic way. + * Reopening the shared conversation, and its images, summaries, exports, uploads and Microsoft 365 action cards, keep using the shared conversation for every participant. An answer that finishes later, for example after a waiting step, updates its shared copy. Nothing is posted once the person who started the conversation has left it, and deleting the shared conversation deletes its plans. + * Asking for your own Microsoft 365 data in a shared conversation is now your consent to share what that request reads, in chat and in plans, recorded in your audit history without a prompt. Sharing earlier answers' history and workflow Run as approvals still ask. + * (Ref: #1659, `functions_orchestration_collaboration.py`, `route_backend_orchestration.py`, `collaboration_models.py`, `route_backend_conversations.py`, `functions_m365_approvals.py`, `functions_m365_runtime.py`, `mentions.ts`, `chatStore.ts`, [Shared Conversations Fix](fixes/ORCHESTRATION_SHARED_CONVERSATIONS_FIX.md)) + +### **(v0.261.269)** + +#### New Features + +* **V2 Operations Settings Match the Classic Page** + * All seven Operations sections are now described in V2 Admin Settings, so none fall back to guessed switches named after their settings keys. Automatic Data Refresh, Health Check, and API Documentation appear in V2 for the first time. + * Automatic Data Refresh shows the next refresh in the schedule's timezone and in yours, when it last ran, and what a change would schedule before you save. **Use my timezone** sets the schedule to your browser's zone, and V2 refuses a time or timezone it cannot read instead of quietly falling back to the default. + * Debug and file processing logs nest their automatic turnoff under the switch it belongs to, state each unit's limit, and show the turnoff time in your own timezone. Turnoff times are now stored in UTC; timers set before the upgrade still end when they were meant to. + * Control Center Access shows who can open the dashboard and the management features under the switches as they stand, unsaved changes included, with role values to copy. + * Health Check and API Documentation list each endpoint as a full, copyable address for the deployment, with its sign-in requirement and whether it answers now. Application Insights global logging and Swagger say when a restart is still needed, and Application Insights reports whether its connection string is set. + * File Process Logging can delete stored logs by age or all at once, after a confirmation that states exactly what will be removed. + * **Document Access Index diagnostics** (`enable_dai_debug`) can now be switched on from Debug Logging, with a description and a link to the DAI Metrics card it affects. The classic page has no control for it, and V2 previously showed it only as an unexplained "Dai debug" switch. + * (Ref: `admin_settings_fields.py`, `functions_logging_timers.py`, `functions_control_center_schedule.py`, `route_backend_v2.py`, `AdminSettingsPage.tsx`, `adminOperations.ts`, `test_v2_admin_operations_settings.py`, [V2 Admin Operations Settings](features/V2_ADMIN_OPERATIONS_SETTINGS.md)) + +* **In-App Setup Guides for Operations** + * The classic page's role setup, health check configuration, and "Why enable Swagger?" dialogs are available from the matching V2 section headers, with values to copy and a link to the documentation. + * The V2 guides correct three statements the classic dialogs make: health checks return the server time as text or a two-field JSON status, not per-dependency results or HTTP 503; Swagger is open to any signed-in user, not only admins; and ControlCenterAdmin does not also require the Admin role. + * (Ref: `components/admin/guides/`, `docs/admin/operations.md`) + +#### Bug Fixes + +* **ControlCenterDashboardReader Described Correctly in App Role Requirements** + * The app role registry said the dashboard reader role only worked alongside the ControlCenterAdmin requirement. It works on its own, and the description now says so. + * (Ref: `admin_app_roles.py`, `control_center_required`) + +### **(v0.261.268)** + +#### Bug Fixes + +* **Metadata and Tag Saves on Large Documents Finish Immediately** + * Saving tags, titles, authors, file names, or classification no longer waits for every search chunk to update. The save returns immediately, and a background sync merges the change into the search index. Saves on large documents no longer time out with "invalid JSON" errors in the personal, group, or public workspaces. + * The search index update sends only the changed fields instead of re-reading every chunk with its embedding and writing it back one chunk at a time. A 10,000-chunk document now takes about 40 Azure AI Search calls instead of about 20,000, with a similar drop in Cosmos write-fence operations, which sharply reduces Search and Cosmos load for everyone on the server. + * Bulk tagging, tag rename, and tag delete no longer update every chunk twice inside one request, so they finish quickly in large workspaces. + * Group and public metadata edits through the V2 explorer no longer fail with "propagation incomplete" on documents with more than one search chunk. + * What users will notice: saves and tag changes finish right away, and a brief notice says that search and chat results will reflect the change once the search index finishes updating. That usually takes seconds, and up to a minute or two on very large documents. Failed or interrupted updates retry automatically until every chunk matches the document, so chat tag filters and citation titles converge instead of staying partly stale. + * When the background sync finishes, it clears cached search results for the document's workspace and for every user or group with an approved share, so a search that ran during the sync does not keep returning the old values for the search cache lifetime (5 minutes by default). + * (Ref: `update_document()`, `run_document_search_metadata_sync()`, `background_tasks.py`, document metadata routes and native document APIs, `documentOperations.ts`, [Document Search Metadata Sync Fix](fixes/DOCUMENT_SEARCH_METADATA_SYNC_FIX.md), #1657, #1658, #1673) + +* **Group Document Sharing Changes Reach Search** + * Approving, removing, or revoking a group share through the classic workspace now updates the search index. Previously these changes never reached the search chunks, so approved shares might not be searchable, and a revoked group could keep search access to chunks rebuilt after an earlier approval. + * Revocations are enforced in the search index before they are saved. When the index cannot apply a sharing change, users get a clear, retryable error instead of a silent success. + * Personal share approvals and unshares also update the search index before they are saved. + * Approving a share that is already approved now repairs its search access, so a share whose search update failed can be completed by approving it again. + * (Ref: `update_document()`, `project_document_acl_to_chunks()`, `reproject_document_search_acl()`, group and personal sharing routes, `functions_group_document_collaboration.py`, #1657) + +### **(v0.261.267)** + +#### User Interface Enhancements + +* **Collapsible Category List in V2 Admin Settings** + * The category list on the left of V2 Admin Settings can now collapse to a strip of icons, as the workspace section rail does, giving its width to the settings cards. **Collapse** sits at the top of the list. Collapsed, each icon still switches category, shows its name as a tooltip, and is announced by name to screen readers. + * The choice is remembered per administrator in a new `v2AdminRailCollapsed` user preference, kept separate from the workspace section rail and the main navigation so collapsing one leaves the others alone. Narrower windows keep the category drop-down. + * (Ref: `AdminSettingsPage.tsx`, `userSettings.ts`, `route_backend_users.py`, `test_v2_admin_settings_rail_collapse.py`, `ui_tests/test_v2_admin_settings_rail_collapse.py`, [V2 Admin Settings Rail Collapse](features/V2_ADMIN_SETTINGS_RAIL_COLLAPSE.md)) + +### **(v0.261.265)** + +#### User Interface Enhancements + +* **Enhanced Extraction Leads Its Own Admin Section** + * In V2 Admin Settings, **Enable Enhanced extraction** now leads a new **Enhanced Extraction** section under Knowledge › Document Extraction instead of sitting in a collapsed group of the Document Intelligence card. The extraction mode, Auto sample pages, formula extraction and the Azure AI Content Understanding connection sit beneath it and are hidden while it is off, so Content Understanding can no longer be filled in while it could never run. Document Intelligence now holds only its connection. + * The section names the engine Enhanced extraction will use with the settings on screen: Azure AI Content Understanding, or Document Intelligence Layout with the reason. The Content Understanding connection opens on its own while Enhanced is on and no endpoint is set. + * (Ref: `admin_settings_fields.py`, `admin_settings_nav.py`, `SettingsSection.tsx`, `EnhancedExtractionEngine.tsx`, `enhancedExtraction.ts`, [V2 Enhanced Extraction Admin Section](features/V2_ENHANCED_EXTRACTION_ADMIN_SECTION.md)) + +#### Bug Fixes + +* **Turning On Enhanced Extraction in V2 Now Takes Effect** + * Turning Enhanced extraction on in V2 left the extraction mode on Standard, so new uploads never used Enhanced and Content Understanding never ran. Turning it on now moves a Standard mode to Auto, as the classic page always has. The change shows on screen before saving, and the server applies it to a save that omits the mode. An explicit Standard choice is kept. + * Content Understanding settings are hidden in Azure clouds that do not offer it, matching the classic page, through a new `content_understanding_supported` runtime flag. V2 cards now receive runtime flags, so settings gated on one, including the Inbound MCP settings, are no longer dropped by the card's own visibility check. + * The Document Intelligence connection test sends the extraction mode only while Enhanced is on, so with Enhanced off it tests Read, which is what documents then use. + * (Ref: `on_enable`, `_apply_enable_defaults`, `applyEnableEffect`, `route_backend_v2.py`, `test_v2_admin_enhanced_extraction_section.py`, `ui_tests/test_v2_admin_enhanced_extraction_section.py`) + +### **(v0.261.264)** + +#### Bug Fixes + +* **Video Indexer Deploys Outside the App Region and Works Without Manual Role Grants** + * Azure Video Indexer is not offered in every region. The azd, Terraform, and Azure CLI deployers now accept a separate Video Indexer region (`VIDEO_INDEXER_LOCATION`, `param_video_indexer_location`, `$param_VideoIndexerLocation`) and pair it with a storage account in that region. Previously, enabling Video Indexer in a region such as North Central US failed the whole deployment. The azd preprovision hook now stops before provisioning and lists the supported regions. + * Deployer-created Video Indexer accounts now grant the App Service identity **Video Indexer Account Contributor** (**Contributor** in Azure Government and custom clouds), so video processing works without adding a role by hand. + * Post-provision configuration writes the Video Indexer settings only when the deployment created the account, and records the account's own region. Previously, any provision without Video Indexer blanked the account name and ID and reset the location, which left **AI Video Intelligence** showing *Needs configuration*. + * Terraform now reads the Video Indexer identity from the azapi 2.x `output` object directly instead of passing it through `jsondecode()`. + * (Ref: #1666, `videoIndexer.bicep`, `setVideoIndexerPermissions.bicep`, `postconfig.py`, `validate_azd_prerequisites.py`, `main.tf`, `deploy-simplechat.ps1`, deployer version 1.0.33, [Video Indexer Deployment Region and Permissions Fix](fixes/VIDEO_INDEXER_DEPLOYMENT_REGION_AND_PERMISSIONS_FIX.md)) + +### **(v0.261.263)** + +#### User Interface Enhancements + +* **Search Result Cache Explained Under Azure AI Search** + * V2 Admin Settings now shows the workspace search result cache in a **Search result cache** group under **Knowledge › Search Index › Azure AI Search**. The group explains what is cached, what forces a fresh search, and why the cache should stay on. Previously the fallback scan matched the word "search" and placed it under Web Search as a bare switch labelled with its key, `enable_search_result_caching`. + * **Cache lifetime (seconds)** can now be changed, from 60 to 3,600 seconds with a default of 300. It appears only while caching is on. Caching stays on by default, and a value an administrator saved as off is kept. + * (Ref: `admin_settings_fields.py`, `test_search_result_cache_admin_setting.py`, `test_v2_admin_search_result_cache.py`, [Search Result Cache Admin Setting Fix](fixes/SEARCH_RESULT_CACHE_ADMIN_SETTING_FIX.md)) + +### **(v0.261.262)** + +#### User Interface Enhancements + +* **Media Galleries in V2 Replies** + * Two or more images or clips in a row now show as a gallery of tiles, three to a row, each with its caption underneath, instead of one full-width card each. A short caption line above an image or clip becomes its caption. In a list, the clip and still at the end of each item sit side by side under the item's text. A single image keeps its full-width card, and recordings keep their player bar. + * A clip tile shows its first frame and length, and plays where it is. **View larger** continues it in the viewer. + * Grouping happens on the parsed markdown: raw HTML stays disabled and react-markdown's URL checks still apply to every image and link. + * (Ref: `mediaGallery.ts`, `MediaGallery.tsx`, `MediaTiles.tsx`, `AssistantMarkdown.tsx`, [V2 Media Galleries and Viewer](features/V2_MEDIA_GALLERIES_AND_VIEWER.md)) + +* **Media Viewer** + * Opening an image or clip shows it large, with **Previous** and **Next** buttons and the arrow keys stepping through the gallery, its position ("3 of 15"), View at actual size, Download and Open in a new tab. Focus returns to the tile when it closes. + * (Ref: `MediaViewer.tsx`, `mediaDownload.ts`, `useMediaDownload.ts`) + +* **Drawer Media Section Groups Images, Videos and Audio** + * The Documents drawer now lists media under **Images**, **Videos** and **Audio** with counts. Clips show as tiles with their first frame instead of text rows, and images and clips open one viewer across all of them, with **Show in conversation** to scroll to the message. + * Recordings play in the drawer, with play, stop, seek, volume, speed, Download and Show in conversation. + * (Ref: `DrawerAssets.tsx`, `ConversationDrawer.tsx`, `InlineAudioPlayer.tsx`) + +* **Download for Recordings, Clips and Images** + * Recordings, clips and images can be saved from the player and the viewer, under the file name in their link. When a host does not let the page read its files, the file opens in a new tab instead, where the browser can save it. An expired link reports that it has expired. + * (Ref: `mediaDownload.ts`, `InlineAudioPlayer.tsx`, `MediaViewer.tsx`) + +#### Bug Fixes + +* **Escape in a Drawer Dialog No Longer Closes the Drawer** + * Pressing Escape in a dialog opened from the conversation drawer, such as a document preview or the media viewer, also closed the drawer. Escape now closes only the dialog, and a second Escape closes the drawer. + * (Ref: `ConversationDrawer.tsx`, `test_v2_media_galleries.py`) + +### **(v0.261.261)** + +#### Bug Fixes + +* **Always-On Tabular Switches Removed From V2 Admin Settings** + * V2 Admin Settings no longer shows an unlabelled **Tabular search shared preflight** switch under Workspaces › Files & Sharing › Shared Conversation File Approvals. It landed there only because its key contains the word "shared". It also no longer shows `enable_tabular_analyze_durable_preflight` or `enable_tabular_hierarchical_analysis` under Other capabilities. + * All three flags send exhaustive row-by-row requests against CSV and XLSX files to a durable background job that covers every row, instead of a foreground answer that truncates. They are always on: settings loading resets them to on every time, so the switches reverted after every save. Behavior is unchanged, and the `SIMPLECHAT_DISABLE_TABULAR_PARITY_DURABLE_PREFLIGHT` environment variable is still the emergency off switch. + * The capability placement test now fails if any flag that settings loading resets is drawn as a switch, or if a guessed switch lands in the Workspaces group. + * (Ref: `admin_settings_fields.SUPPRESSED_CAPABILITY_KEYS`, `TABULAR_PARITY_DURABLE_PREFLIGHT_ACTIVE_DEFAULTS`, `test_v2_admin_capability_placement.py`, [V2 Admin Tabular Preflight Switch Fix](fixes/V2_ADMIN_TABULAR_PREFLIGHT_SWITCH_FIX.md)) + +### **(v0.261.260)** + +#### Bug Fixes + +* **V2 Web Search Test Uses the Saved Foundry Connection** + * **Test web search** in V2 Admin Settings reported "Foundry Project Endpoint is required", "Foundry API Version is required" and "Foundry Agent ID is required" for a saved connection whose values were on screen. The Foundry connection is saved inside `web_search_agent`, but the test read each field as a top-level setting, so it only worked before the values were saved. It now reads the saved values, including a service principal's authentication type and masked client secret, which the server swaps for the stored secret. + * The same fix shows **Managed Identity Type**, or the service principal fields, for the saved authentication type, and marks a saved connection as configured. Clearing a saved client secret now shows the removal warning and **Undo**. + * The **Document action capabilities** card now shows the chat and workflow limits for Analyze, Comparison and Merge while that action is enabled. The section card used to hide them once the setting was saved. + * (Ref: `adminFields.ts` `readSettingValue` and `buildConnectionTestPayload`, `adminSections.ts`, `ConnectionTest.tsx`, `SettingsSection.tsx`, `AdminSettingsPage.tsx`, `test_v2_admin_nested_setting_reads.py`, [V2 Admin Web Search Nested Settings Fix](fixes/V2_ADMIN_WEB_SEARCH_NESTED_SETTINGS_FIX.md)) + +### **(v0.261.259)** + +#### Bug Fixes + +* **Rejected Group Conversation Invites Stop Before History Reads** + * Converting an existing group conversation now verifies every invited participant against the group's current membership before preparing Microsoft 365 publication evidence or reading the conversation transcript. An identity outside the group is still rejected without changing any records, but the rejection now happens before message history is queried. + * The document collaboration regression fixture now exposes the current workflow-alert safety contract, so its sharing and repair checks reach their assertions instead of stopping during module import. + * (Ref: #1651, `ensure_group_collaboration_for_legacy_conversation`, `test_group_collaboration_source_storage_fix.py`, `test_group_document_collaboration.py`, [Group Collaboration Invitee Validation Order Fix](fixes/GROUP_COLLABORATION_INVITEE_VALIDATION_ORDER_FIX.md)) + +### **(v0.261.258)** + +#### User Interface Enhancements + +* **V2 Admin Settings Use the Full Width** + * Admin Settings now fills the screen instead of a 768px column. On wide screens an **On this page** list sits beside the cards with each section's status (Configured, Needs configuration, Off, or Prerequisite missing), a count of sections that need attention, and a marker for the section you are reading. Select an entry to jump to it. + * Every section uses the card the Agents settings introduced: an icon from the navigation, a larger title, and stronger group boundaries. The switch a section depends on is highlighted, and settings that only apply while a switch is on are indented beneath it. That nesting is read from the field schema, so it appears in every section that declares it. + * On a wide card each setting's label and description sit beside its control, and independent switches pair up in two columns. Narrow screens and larger text sizes keep the stacked layout. + * (Ref: `AdminSettingsPage.tsx`, `SettingsSection.tsx`, `SettingsIndex.tsx`, `adminSections.ts`, `fields.tsx`, `theme.css`, [V2 Admin Settings Layout and Hierarchy](features/V2_ADMIN_SETTINGS_LAYOUT_AND_HIERARCHY.md)) + +* **Model Catalog Workbench in V2** + * The V2 Model Catalog is now drawn natively as a workbench. Each profile is one line, with its name, publisher, and the number of global models connected to it, and the list and the selected profile scroll separately. + * A profile's detail is split into **Overview**, **Capabilities**, **Connections**, and **Evidence** tabs, with **Favorite** and **Priority** in its header. Selecting the connected-model count opens the Connections tab, and **Open in AI Connections** opens that model's connection editor with the model outlined. + * The admin catalog response now includes each linked model's connection and model ids. The classic catalog is unchanged. + * (Ref: `ModelCatalogManager.tsx`, `ModelCatalogDetail.tsx`, `ModelCatalogEditor.tsx`, `modelCatalog.ts`, `ModelConnectionsManager.tsx`, `route_backend_models.py`, [V2 Model Catalog Workbench](features/V2_MODEL_CATALOG_WORKBENCH.md)) + +#### Bug Fixes + +* **V2 Admin Settings Links Scroll to Their Target** + * **Go to setting** in App Role Requirements and other links between settings cleared the search and category but never scrolled, because they looked up an id the cards no longer rendered. They now scroll to the section and focus its heading, keep the current category when it already shows the target, and respect reduced motion. + * (Ref: `AdminSettingsPage.tsx`, `test_v2_admin_settings_layout.py`, [V2 Admin Section Jump Fix](fixes/V2_ADMIN_SECTION_JUMP_FIX.md)) + +### **(v0.261.257)** + +#### Bug Fixes + +* **Deleted Messages No Longer Come Back As Masked** + * With **Enable Conversation Archiving** on, a message deleted in the V2 chat no longer reappears as "This message is masked", either right after the delete or when the conversation is opened again. Deleted messages are left out wherever messages are shown, copied, searched or sent to the model: the message list, attempt switching, retry and edit, forks, sharing a conversation, conversation search, the inbound MCP message tool, conversation summaries and chat history. Their archived copies are kept as before. + * Delete, retry, edit, attempt switching and both mask actions treat a deleted message as gone, so its fail-safe mask can't be cleared. Shared conversations that already hold copies of deleted messages hide them, with no data migration. + * (Ref: #1649, `functions_message_deletion.py`, `route_backend_conversations.py`, `route_backend_chats.py`, `functions_collaboration.py`, `chatStore.ts`, `deletedMessages.ts`, [Deleted Messages Shown as Masked Fix](fixes/SOFT_DELETED_MESSAGES_SHOWN_AS_MASKED_FIX.md)) + +* **Deleting An Answer No Longer Brings Back An Earlier Attempt** + * Deleting only the latest answer of a retried turn re-activated the lowest-numbered earlier attempt, so an old question and answer showed beside the latest question. Another attempt now takes over only when the active attempt's question is deleted, a deleted attempt is never chosen, and only one attempt stays active. This applies whether or not archiving is on. + * The V2 attempt control counts the attempts that remain, so it reads "2/2" rather than "3/2" after one is removed. + * (Ref: #1649, `_promote_remaining_thread_attempt`, `threads.ts`, [Deleted Messages Shown as Masked Fix](fixes/SOFT_DELETED_MESSAGES_SHOWN_AS_MASKED_FIX.md)) + +### **(v0.261.256)** + +#### Bug Fixes + +* **One Progress Indicator While Orchestrate Runs a Plan** + * Once a plan started running, the V2 chat showed **Thinking** in a reply bubble above the plan card, which was already showing the run's progress. The plan card is now the only indicator while a plan runs, and the answer appears when it arrives. Run notices, such as a saved-memory warning, are still under the finished answer's reasoning steps. + * A chat reply sent while a plan waits in the same conversation still shows **Thinking**, and **Stop** works as before. + * (Ref: `MessageList.tsx`, `chatStore.ts` `orchestrationSurface`, `executeSavedPlan`, `selectActiveTurnRunInFlight`, `test_v2_orchestration_streaming_bubble.py`, `test_v2_orchestration_streaming_surface.mjs`, [Orchestration Duplicate Thinking Indicator Fix](fixes/ORCHESTRATION_RUN_DUPLICATE_THINKING_INDICATOR_FIX.md)) + +#### User Interface Enhancements + +* **Orchestrate Says What It's Doing** + * While Orchestrate plans a question, the reply bubble now says **Planning** instead of **Thinking**. + * The running plan card's status line names the work instead of a bare role. It shows "Starting", then each step's kind of work and title, such as "Gathering: Read quarterly reports", then "Preparing the answer" once every step has settled. "Waiting for results" is unchanged. + * (Ref: `describeRunProgress` in `orchestrationPlan.ts`, `OrchestrationPlanCard.tsx`, `test_v2_orchestration_run_progress_status.mjs`, [Chat Orchestration](features/CHAT_ORCHESTRATION.md#stream-events)) + +### **(v0.261.253)** + +#### Bug Fixes + +* **Workflow Reply Shown Once in the Conversation It Created** + * An agent workflow that creates a conversation and posts its opening message there through the Simple Chat action no longer shows the same findings twice. Once the run's full reply, with its maps and sources, is mirrored into that conversation, the run's own post (labelled "posted through an agent") is left out of the thread in the classic and V2 chat views. The post stays stored and stays in the conversation's AI history. + * The workflow's own conversation, **Workflow: *name***, is hidden from the chat list the first time a run delivers its result into a conversation it created. It still records every run, and opens from the workflow alert's **Show more** links or the "show hidden conversations" toggle. If you show it again, later runs leave it visible. + * Messages people write are never hidden, and agent posts made outside a workflow run are unaffected. Conversations created before this version keep both messages until a new run delivers into them. + * (Ref: `functions_workflow_runner.py`, `sharedMessage.ts`, `MessageList.tsx`, `chat-messages.js`, `chat-collaboration.js`, `test_workflow_reply_replaces_agent_posted_message.py`, [Workflow Reply Replaces the Agent-Posted Message Fix](fixes/WORKFLOW_REPLY_REPLACES_AGENT_POSTED_MESSAGE_FIX.md)) + +### **(v0.261.252)** + +#### Bug Fixes + +* **Workflow Hand-Off Reports Re-Prove Their Lineage** + * The workflow result reader now re-proves a chat hand-off's report with the shared node lineage authorizer before it describes or excerpts it, as the reader's general path does for each task row. Each consumed-input receipt must chain to a real parent result of the run, with matching hashes and references, so a report whose inputs don't check out is no longer posted to chat. + * A malformed receipt or a damaged parent result is refused with `workflow_result_invalid`, and a missing parent with `workflow_result_not_found`, the same codes the general path uses. The check runs for descriptor-only reads, such as re-checking a stored chat context, as well as excerpt reads. It walks the saved workflow, so an edited or re-enabled workflow still fails closed before anything is read. + * Hand-off stays off by default, behind **Hand Off Large Work From Chat**. + * (Ref: #1549, #1543, follow-up to #1640, `functions_workflow_result_reader.py`, `authorize_workflow_node_result_read`, `test_workflow_handoff_result_reader.py`, [Workflow Hand-off Result Lineage Fix](fixes/WORKFLOW_HANDOFF_RESULT_LINEAGE_FIX.md)) + +* **Hand-Off Off-Golden Fixture Recaptured** + * The fixture behind `test_orchestration_workflow_handoff_off_golden.py`, which checks that planning is unchanged when hand-off is unavailable, was captured on 0.261.233, before file merging (#1641) changed the planner's content, so 4 of its 10 cases failed on V2. + * It's now recaptured on unmodified V2 at 0.261.248, taken unchanged from the Phase 7a branch's last commit, and the test passes 10 of 10. Planning itself didn't change. + * (Ref: #1640, #1641, `test_orchestration_workflow_handoff_off_golden.py`, `test_support/orchestration_workflow_handoff_off_golden.json`, [Workflow Hand-off Result Lineage Fix](fixes/WORKFLOW_HANDOFF_RESULT_LINEAGE_FIX.md#recaptured-hand-off-off-golden)) diff --git a/docs/explanation/release-notes/v0.261-part-2.md b/docs/explanation/release-notes/v0.261-part-2.md new file mode 100644 index 000000000..4a80992f1 --- /dev/null +++ b/docs/explanation/release-notes/v0.261-part-2.md @@ -0,0 +1,673 @@ +--- +title: "Release notes 0.261.251 – 0.261.210" +description: "SimpleChat release notes for 0.261.251 – 0.261.210." +section: "Reference" +layout: page +--- + + + +# Release notes 0.261.251 – 0.261.210 + +[Back to release notes index]({{ '/explanation/release_notes/' | relative_url }}) + +### **(v0.261.251)** + +#### New Features + +* **Live Status For Workflows Started From V2 Chat** + * Under a plan's answer, each workflow the plan started now shows its run's current status (Queued, Running, Needs you, Completed, Partly completed, Failed, Timed out or Cancelled), with the step it's on, the time elapsed, why it's waiting and where its results went. A status V2 can't read with certainty shows **Status unavailable** with only **Open run**, never a guess. + * **Check now** reads the chat's runs straight away and shows when they were last checked. **Cancel run** asks you to confirm, then asks the run to stop. **Retry** resumes the same failed durable run after a fresh read of its runtime and never starts a new run; when the server refuses, for example because the workflow changed or another run is in progress, the card says why. **Review and approve** opens the run in Workflows, where the step's prompt and choices are shown; nothing is approved from the chat. **Reconnect Microsoft 365** opens the connection in your profile. + * Shown in private personal chats while `allow_user_workflows` and `enable_chat_orchestration_workflow_runs` are both on. With either one off, the started-workflow links show as before and no status is requested. + * (Ref: #1546, #1543, #1610, `WorkflowRunCard.tsx`, `WorkflowRunLinks.tsx`, `workflowRunStatus.ts`, `workflowRunActions.ts`, `useWorkflowRunAction.ts`, `workflowEditor.ts`, [Workflow result delivery to chat](features/CHAT_WORKFLOW_RESULT_DELIVERY.md#v2-experience-6b-2)) + +* **One Workflow Run Tracker Per Browser Tab** + * V2 checks chat-started runs from the app shell, on any page, with one request per check to the batched status route, never one per run, so the chat list, the bell and the run card stay current. It checks first after 15 seconds, then less often up to every 5 minutes, pauses while the tab is hidden unless desktop notifications are on, and stops once nothing is in flight. A new plan answer, the run card or **Check now** starts it again, and it stops when the session ends. + * When a run's results are posted while the page is open, the open chat reloads its messages once any reply in progress finishes, without replacing a source you already chose. Another chat gets its unread dot, and the bell count refreshes. The server's one bell notification is the only one, and a desktop notification, if you've turned them on, shows only while you aren't looking at the page. + * The first check after the page loads records what was already posted, so reloading never announces a result, marks a chat unread or notifies again. + * (Ref: `workflowRunTracker.ts`, `useWorkflowRunTracker.ts`, `workflowRunTrackerStore.ts`, `chatStore.ts`, `App.tsx`, [Workflow result delivery to chat](features/CHAT_WORKFLOW_RESULT_DELIVERY.md#v2-experience-6b-2)) + +* **Follow Up, Retry And Open Run On Posted Workflow Results** + * A message a workflow run posted to its chat now ends with its own actions. **Follow up** makes a posted result or saved analysis the source of your next question in the same chat, while **Use Workflow Results In Chat** is on and the result is still available. **Open run** opens the run in Workflows. A note that the run failed offers **Retry workflow run** only while a fresh check says the same run can still be resumed. + * The chat's own **Retry** and **Edit** are hidden on these messages, since the server refuses both. If a refusal still comes back, its message is shown. + * (Ref: `WorkflowDeliveryFooter.tsx`, `workflowDelivery.ts`, `MessageActions.tsx`, `MessageList.tsx`, [Chat controls](../reference/chat-controls.md#results-posted-to-the-chat)) + +* **Next Run And Last Run On The Recurring-Workflow Card** + * The card for a workflow created from a chat proposal now shows when it runs next, or **Due now**, and its newest run's status and time, or **No runs yet**. **Open latest results** opens the newest completed or partly completed run, and **Follow up** opens a new chat about the newest run chat can answer from. + * The card reads these once when it's shown and doesn't keep checking. If the read fails, it says "Run details aren't available right now." + * (Ref: `WorkflowProposalRunSummary.tsx`, `WorkflowProposalCard.tsx`, `workflowEditor.ts`) + +* **Workflow Notifications Open The Run In V2** + * In V2, a notification that links to a run on the classic workflow activity page now opens that run in the Workflows run history when the link and the notice agree on the workflow, the run and its workspace: personal, or a group with its id. Microsoft 365 notices, and notices whose link and details disagree or don't say which workspace, keep their classic link. + * A workflow alert, or a notice that a run's results couldn't be posted to its chat, that has no link of its own but names its run and workspace now opens that run. Previously it had no link. + * (Ref: `notificationLinks.ts`, `workflowRunLink.ts`, `workflowAlertNotices.ts`, [V2 Notifications Bell](features/V2_NOTIFICATIONS_BELL.md)) + +#### User Interface Enhancements + +* **Running Tag In The V2 Chat List** + * A small spinner beside a chat shows that a workflow it started is still running or its results are being posted, labelled for example "Running Weekly digest" or "Running 2 workflows". It uses what the tracker already knows, so the list sends no requests of its own, and it gives way to the unread dot once the results arrive. + * (Ref: `WorkflowRunningTag.tsx`, `ConversationRail.tsx`) + +* **Workflow Results Notices And Alert Cards** + * The bell labels notices about a run whose results couldn't be posted to its chat as **Workflow results**. + * A workflow alert card that names its run now offers **Open run**, which opens the same place **Open workflow** did. Alerts without a run keep **Open workflow**. + * (Ref: `notifications.ts`, `WorkflowAlertCard.tsx`, `workflowAlertNotices.ts`, [V2 Workflow Alert Notices](features/V2_WORKFLOW_ALERT_NOTICES.md)) + +### **(v0.261.250)** + +#### New Features + +* **Hand Off Large Chat Requests To A One-Time Workflow** + * When a request is too big for one chat plan, such as "review every contract in the Legal workspace and list the ones that renew this year", the planner can hand it to a one-time durable workflow. The workflow reviews each document with Analyze inside a For each, collects every finding and writes one report. + * The user approves the plan, then accepts, edits or declines a hand-off card. Accepting creates the workflow turned off, so it never runs on a schedule, and queues exactly one durable run. The workflow id and the run's request id come from the hand-off, so a retry, a second tab or a crash finds the same workflow and run instead of making new ones. When the run finishes, workflow result delivery posts the report back into the conversation. + * New admin setting **Hand Off Large Work From Chat** (`enable_chat_orchestration_workflow_handoff`), off by default because a hand-off starts background work that keeps running with the user's access after the chat turn ends. It requires Chat Orchestration, personal workflows, Propose Workflows From Chat, Run Workflows From Chat and Use Workflow Results In Chat, and hand-offs are offered only in the user's own private conversations. New limit **Hand-Offs From Chat Per User Per Day** (`chat_orchestration_max_workflow_handoffs_per_day`), 1 to 100, default 5. Hand-off workflows don't count toward Workflows Created From Chat Per User. + * A hand-off covers at most 25 named documents, or 2,000 documents from a workspace query, lower if the administrator's loop item limit is lower. A query that matches more pauses before reviewing anything. + * When hand-off is offered, the planner prompt lists the plan's limits, and a plan over its step or document budget fails with `plan_budget_exceeded`, whose repair message suggests handing the work off. With the setting off, planning is unchanged. + * New requester-only routes under `/api/v2/orchestration/runs//workflow-handoffs` read a plan's hand-offs, accept one as proposed or edited, decline one, and read its editor draft. The V2 hand-off card is a separate change; until it ships, users can't accept a hand-off in the browser, so leave the setting off. + * (Ref: #1549, #1543, `functions_orchestration_workflow_handoffs.py`, `functions_orchestration_workflow_handoff_decisions.py`, `functions_workflow_handoff_builder.py`, `functions_workflow_drafts.py`, `functions_workflow_runtime.py`, `functions_workflow_result_reader.py`, `functions_orchestration_registry.py`, `functions_orchestration_planner.py`, `route_backend_orchestration.py`, [Chat Orchestration Workflow Hand-off](features/CHAT_ORCHESTRATION_WORKFLOW_HANDOFF.md)) + +### **(v0.261.248)** + +#### Bug Fixes + +* **Merge Code Passes CodeQL Analysis** + * CodeQL flagged import cycles and small code-quality issues in the new merge code. The code that the PDF, Word, PowerPoint and workbook assemblers share moved to `functions_document_merge_core.py`, so no assembler imports `functions_document_merge.py`, the module that loads them. Each assembler still loads only when its kind of file is merged. + * The workflow runner imports the upload function for merged files along with its other upload functions. Cleanup errors that are deliberately ignored now say why, the workbook check for NaN uses `math.isnan`, and the merge tests no longer call helpers with side effects inside `assert`, use both `import` and `from ... import` for one module, or close results by hand. + * Merges behave exactly as before. + * (Ref: #1619, `functions_document_merge.py`, `functions_document_merge_core.py`, `functions_document_merge_workbook.py`, `functions_tabular_merge.py`, `functions_workflow_runner.py`, [Phase 4](features/V2_FILE_MERGE_PHASE_4_PDF_WORKBOOKS.md#document-merge-engine)) + +### **(v0.261.247)** + +#### Bug Fixes + +* **Large Merges Spill To The Container's Scratch Directory** + * A merge keeps its data in memory up to 16 MiB for an assembled PDF, Word document, deck or workbook, and 8 MiB of rows for a spreadsheet merge. Larger merges now spill into `/sc-temp-files`, the directory the container image creates for scratch files, like file renders and downloads since 0.261.233. Before, they used the platform temp directory (`/tmp` in the image), which also works. + * Where `/sc-temp-files` doesn't exist or can't be written, such as on a development machine, merges still use the platform temp directory, and never the working directory. Spilling doesn't change the merged file. + * (Ref: #1619, `functions_document_merge.py`, `functions_tabular_merge.py`, `functions_temp_files.py`, `test_file_merge_scratch_dir.py`, [Phase 7](features/V2_FILE_MERGE_PHASE_7_CHAT_DOCUMENTS.md#where-merges-spill-to-disk)) + +### **(v0.261.246)** + +#### User Interface Enhancements + +* **Chat Asks Whether Merged Spreadsheets Share One Sheet** + * Merging several CSV or Excel files into one Excel file can mean every row on one sheet or each file on its own sheet. When a request such as "merge these files into one Excel file" doesn't say which, V2 chat now asks first, with two choices: **All rows on one sheet** or **Each file on its own sheet**. Before, it assumed one sheet. + * It doesn't ask when the request already decides the layout. CSV output holds one table. Words like "stack the rows", "one table" or "a sheet per file" also decide it, as do a saved preference or an earlier answer. Choosing **Decline** puts every row on one sheet and the plan says so. + * The question is offered only while **Merge spreadsheets**, **Merge documents** and **Create a file** are all available, so a deployment that narrows out **Merge documents** keeps stacking rows. Workflows that chat proposes for repeated merges ask the same question. + * The planner's general rule to prefer a stated assumption over a question now names its exceptions: capability guidance, deliverables facts and workflow instructions that say to ask. + * (Ref: #1619, `functions_orchestration_deliverables.py`, `functions_orchestration_planner.py`, [Phase 7](features/V2_FILE_MERGE_PHASE_7_CHAT_DOCUMENTS.md#one-sheet-or-a-sheet-per-file), [Merge files](../guides/merge-files.md#one-sheet-or-a-sheet-per-file)) + +### **(v0.261.245)** + +#### New Features + +* **Merge PDFs, Word Documents, Decks And Workbooks In Chat** + * V2 chat orchestration can join whole documents in a chat turn, without a workflow: "combine these contracts into one PDF" plans a **Merge documents** step (`document_merge`) and a file to create. It joins PDFs into one PDF, Word documents into one Word document or PowerPoint decks into one deck, or puts CSV and Excel files on separate sheets of one workbook, with the same deterministic engine and options as workflow merges. No model reads or rewrites the files. + * The merge step checks the merged file and retains only a description of it: the kind, options, files in order, and the file's size and SHA-256. Rendering with the new `assembled_document_v1` profile reads only that merge's own files, with current access checked again, assembles them again, and delivers the file only if it is byte-for-byte identical. A file that changed in between fails as `output_source_changed` and nothing is uploaded. + * The plan card says what a document merge creates and lists only the settings that differ from the defaults. Plans must name the kind, use only that kind's settings, and render the merge in its own format. Failures use application-owned messages, including the new `document_merge_active_content`. + * Merges stay governed by **Enable Merge** and the Merge chat file limit; deployments with a narrowed orchestration capability list must add **Merge documents**. The planner now proposes merge workflows only for scheduled, synced or very large merges. + * (Ref: #1619, `functions_document_merge_assembly.py`, `functions_orchestration_document_merge.py`, `functions_orchestration_registry.py`, `functions_orchestration_adapters.py`, `functions_orchestration_schema.py`, `functions_orchestration_rendering.py`, `functions_generated_office_adapters.py`, `functions_generated_export_registry.py`, `orchestrationMerge.ts`, [Phase 7](features/V2_FILE_MERGE_PHASE_7_CHAT_DOCUMENTS.md), [Merge files](../guides/merge-files.md#merge-pdfs-word-documents-decks-or-workbooks)) + +#### Bug Fixes + +* **Hostile Files Can't Make A Merged File Run Programs Or Read Server Files** + * Word documents with fields that start other programs (DDE or DDEAUTO) are refused, however the field code is split, nested, cased or encoded, in the body, headers, footers or notes. The first document's link to its template is removed so Word never fetches it, and other linked content is reported. + * PowerPoint click and hover actions that start a program or run a macro are removed from every merged deck, including the first, in every part declared as Office XML whatever its name or encoding; web links are kept. + * Word, PowerPoint and Excel packages with an XML part that declares a document type, which Office Open XML forbids and which is how a part asks a parser to read other files, or that is written or declared in an encoding other than UTF-8 or UTF-16, are refused before any library parses them, in document merges and in spreadsheet merges of `.xlsx` and `.xlsm` files. Parts are chosen by name and by declared content type, and only each part's prolog is read, within a fixed budget per package. + * A storage fault while a merged file's originals are read again for delivery now stays retryable instead of reading as a screening hold. + * Verified that 100 files of 1,000 rows merge in one workflow run, that merged CSV and Excel files never carry a live formula, and that the merge modules import without loading any Office or PDF library. + * (Ref: #1619, `functions_document_merge_docx.py`, `functions_document_merge_pptx.py`, `functions_ooxml_package_guard.py`, `functions_document_merge.py`, `functions_tabular_merge.py`, `functions_orchestration_rendering.py`, [Phase 7 hardening](features/V2_FILE_MERGE_PHASE_7_CHAT_DOCUMENTS.md#hardening)) + +* **Word Merges With Lists Give The Same File Every Time** + * docxcompose gave every list it copied from a later document a random ID, so a Word merge whose later documents had ordinary bulleted or numbered lists produced different bytes on each run. A retried workflow Word merge could attach a second copy, and a chat merge of such documents could never be delivered. The ID is now derived and unique within the document, so each document's list still restarts and the same documents always give the same file. + * (Ref: #1619, `functions_document_merge_docx.py`, [Phase 5](features/V2_FILE_MERGE_PHASE_5_WORD.md)) + +### **(v0.261.244)** + +#### New Features + +* **Merge PowerPoint Decks In Workflows** + * A workflow **Merge files** task can use **Combine PowerPoint decks** as its **Merge type**: the slides of PowerPoint decks are appended, in order, into one deck with code and no model, choosing files the same four ways as every merge. Every merge type is now available in workflows. + * **PowerPoint formatting** keeps each deck's layouts, masters and themes (reusing identical ones) or places every slide on the first deck's matching layout and theme. **Create one section per deck** groups each deck's slides under its file name. Pictures, media, charts with their workbooks, diagrams, tables and speaker notes come along. + * The first deck decides the slide size, and a deck with a different size is reported; embedded fonts of later decks and slide comments aren't carried over. Encrypted, damaged, oversized and macro-enabled decks are refused, and a merge assembles up to 2,000 slides from at most 300 MB of decks. + * The same decks always produce the same bytes, so a retried task reuses the file it attached, and errors name only the file. Run activity reports slides per deck. + * Chat can propose PowerPoint merges (blueprint `merge.kind` `pptx`, with `formatting` and `sections`). + * (Ref: #1619, `functions_document_merge_pptx.py`, `functions_document_merge.py`, `functions_document_actions.py`, `functions_workflow_drafts.py`, `functions_orchestration_planner.py`, [Phase 6](features/V2_FILE_MERGE_PHASE_6_POWERPOINT.md), [Create a workflow](../guides/create-a-workflow.md#merge-files-in-a-workflow)) + +### **(v0.261.243)** + +#### New Features + +* **Merge Word Documents In Workflows** + * A workflow **Merge files** task can use **Combine Word documents** as its **Merge type**: Word documents are appended, in order, into one Word document with code and no model, choosing files the same four ways as every merge. + * **Word formatting** keeps each document's look, copying a style that shares a name but looks different under a new name, or uses the first document's styles for all of them. **Page break between documents** and **Add source headings** are optional. Styles, numbering, images, tables, footnotes and shapes come along; comments don't, and the merged document uses the first document's headers, footers and page setup. + * The same documents always produce the same bytes, so a retried task reuses the file it attached. A Word merge reads at most 300 MB of documents; encrypted, damaged and macro-enabled files are refused, and errors name only the file. + * Chat can propose Word merges (blueprint `merge.kind` `docx`, with `formatting`, `page_breaks` and `source_headings`), and the `merge_options_invalid` repair hint now lists the options each kind takes. + * Adds the `docxcompose` 2.2.0 dependency (MIT). + * (Ref: #1619, `functions_document_merge_docx.py`, `functions_document_merge.py`, `functions_document_actions.py`, `functions_workflow_drafts.py`, `functions_orchestration_planner.py`, `requirements.txt`, [Phase 5](features/V2_FILE_MERGE_PHASE_5_WORD.md), [Create a workflow](../guides/create-a-workflow.md#merge-files-in-a-workflow)) + +### **(v0.261.242)** + +#### New Features + +* **Merge PDFs And Build Workbooks In Workflows** + * A workflow **Merge files** task now has a **Merge type**. **Combine PDFs** joins PDFs, in order, into one PDF with a bookmark for each file (keeping each file's own bookmarks beneath it); **One workbook, a sheet per file** puts each CSV or Excel file on its own sheet of one Excel workbook, named after the file. **Combine rows (CSV/Excel)** is the existing row merge. + * Both use the same four ways to choose files as row merges (selected files in order, every matching file, recent files, or the files File Sync changed), authorize every file again, run with code and no model, and attach one file to the run. A workflow can assemble up to 10,000 pages or 500 sheets; a PDF merge reads at most 300 MB of PDFs, because every source stays in memory until the merged PDF is written. + * PDF pages are copied unchanged; encrypted PDFs are refused rather than decrypted, and scripts and actions that could run code, open files or programs, or submit forms are removed wherever they appear (open actions, page, link and form-field scripts, XFA forms), while links to pages and to web and email addresses are kept. Workbooks keep Excel cell types and number formats, copy CSV values as text so codes keep their leading zeros, never evaluate text that looks like a formula, and remove control characters Excel can't store. The same files always produce the same bytes, so a retried task reuses the file it attached. Library errors are reported by file name only, never with text from the file. The task's answer lists the files merged and anything left out or removed. + * Chat can propose PDF and workbook merges: the workflow blueprint's `merge` field gains `kind` (`tabular`, `workbook` or `pdf`), with the new draft code `merge_format_invalid`, and the proposal card describes each kind, for example "Merges every PDF in your personal workspace into one PDF with code. No model runs." + * Merging PDFs or workbooks directly in a chat turn, without a workflow, is planned with Word and PowerPoint for a later release. + * (Ref: #1619, `functions_document_merge.py`, `functions_document_merge_pdf.py`, `functions_document_merge_workbook.py`, `functions_workflow_merge.py`, `functions_document_actions.py`, `functions_workflow_drafts.py`, [Phase 4](features/V2_FILE_MERGE_PHASE_4_PDF_WORKBOOKS.md), [Create a workflow](../guides/create-a-workflow.md#merge-files-in-a-workflow)) + +### **(v0.261.241)** + +#### New Features + +* **Merge Files As A Workflow Task** + * A V2 workflow task can now merge many CSV and Excel files into one CSV or Excel file with code: choose **Merge files** as the task's **Document action**. It suits merges too big for one chat turn, such as a year of weekly exports, and merges that should happen on their own, every week or whenever a synced folder gets a new export. A workflow merges up to 100 files and 1,000,000 rows by default, using the existing Merge workflow limits. + * **Files to merge** can be files chosen in the editor, in an order the owner sets; every CSV and Excel file in the workflow's workspace; files added or changed in a recent window (60 minutes by default); or the files File Sync added or changed in that run. Files found when the run starts are merged in file-name order, and more matching files than the limit fail the task rather than merging only some. + * The task takes the same column, sheet, duplicate and sort settings as a chat merge, and an output format and file name. Every file is authorized again as the workflow's owner and read through the access boundary, no model runs, and one file is attached to the run's conversation, even when a durable run resumes. + * Files found at run time that are unavailable or aren't spreadsheets are skipped and named in the task's answer; a run with nothing to merge says so without creating a file. Merge failures, such as a selected file that's gone or columns that don't fit, are shown on the task in terms of the owner's files and aren't retried. A durable run repeats an interrupted merge instead of pausing for review, and reuses the file it already attached when the bytes are the same. + * A save that breaks a merge rule now says why, naming the task by position, for example "Workflow task 2: Select at least two files to merge." The editor learns from the server whether Merge is on and how many files a run may merge, so it doesn't offer **Merge files** while an administrator has Merge off and checks the file limit before saving. + * Chat can propose a workflow with a merge task when a user asks for a recurring merge. The blueprint gains a `merge` task field, with new repairable draft codes `merge_unavailable`, `merge_inputs_required`, `merge_trigger_required`, `merge_options_invalid` and `merge_runner_invalid`, and a merge task never needs the default model. + * (Ref: #1619, `functions_workflow_merge.py`, `functions_workflow_runner.py`, `functions_document_actions.py`, `functions_personal_workflows.py`, `functions_workflow_editor.py`, `functions_workflow_drafts.py`, `functions_orchestration_workflows.py`, `functions_orchestration_planner.py`, [Phase 3](features/V2_FILE_MERGE_PHASE_3_WORKFLOWS.md), [Create a workflow](../guides/create-a-workflow.md#merge-files-in-a-workflow), [Merge files](../guides/merge-files.md)) + +#### User Interface Enhancements + +* **Merge Files In The V2 Workflow Editor And Proposal Card** + * The V2 workflow editor's **Merge files** action offers **Files to merge**, **Merge order** with **Up** and **Down**, **Output format**, **Output file name**, the recent window, and **More merge options** for column matching, aliases, sheets, header row, incompatible files, the source column, duplicates and sorting. The **Analysis mode** choice no longer appears for actions that don't analyze. + * A proposed workflow's card describes a merge task as code that runs no model, for example "Merges the input files below, in order, into one Excel file with code. No model runs.", and lists its files under "Files to merge, in order". + * The classic workflow editor doesn't open a workflow with a Merge files task, which it can't represent, and points to V2 instead; Run and Cancel still work there. + * (Ref: `application/v2_ui/src/lib/workflowEditor.ts`, `WorkflowTaskFields.tsx`, `WorkflowFileSyncFields.tsx`, `workflowProposals.ts`, `WorkflowProposalCard.tsx`, `static/js/workspace/workspace_workflows.js`) + +### **(v0.261.240)** + +#### New Features + +* **Merge Spreadsheets Whose Columns Differ** + * V2 chat merges can now reconcile files whose columns don't match. A merge can keep every column from every file (a file without a column leaves it blank), keep only a listed set of columns, treat differently named headers as one column (for example `cust_id` and `CustomerID` as `Customer ID`), or leave out and report the files or sheets that don't fit instead of failing. + * Merges can also read headers below a title row, merge every visible sheet of each workbook with a **Source Sheet** column, remove duplicate rows (identical rows, or rows with the same key columns, keeping the first or last copy), and sort by up to three columns as text, numbers or dates (up to 250,000 rows). + * When the user asks chat to line the columns up, the plan inspects the files, a **Prepare content** step prepares a column mapping with the new `tabular_column_mapping_v1` profile, and the merge validates that mapping again before code applies it. No model copies or rewrites rows, and the merge report lists mapped, left-out and low-confidence columns. + * Merged columns that some files lack are retained as nullable and render as blank cells. A merge that leaves out files says so in its limitations and step summary. + * New fixed failure messages cover settings that can't be combined, an invalid prepared mapping, missing duplicate or sort columns, and merges where nothing fits; the "different columns" message now suggests the new options. + * (Ref: #1619, `functions_tabular_merge.py`, `functions_orchestration_merge.py`, `functions_orchestration_adapters.py`, `functions_orchestration_registry.py`, `functions_orchestration_schema.py`, `functions_orchestration_services.py`, [Phase 2](features/V2_FILE_MERGE_PHASE_2_RECONCILIATION.md), [Merge files in chat](../guides/merge-files.md)) + +* **Inspect Spreadsheets In V2 Chat** + * A new Gather capability, **Inspect spreadsheets** (`tabular_inspect`), reads the sheet names, column headers, row counts and a few sample rows of CSV or Excel files, and reports which files share the same columns, which columns look like the same thing under different names, likely title rows, and a suggested merge policy. A file that can't be read is reported instead of stopping the step, and no model is used. + * It answers questions such as "what columns do these files have?" and prepares merges whose columns differ. The **Enable Merge** switch and its chat file limit govern it, and it appears in the orchestration Capabilities list. + * (Ref: #1619, `run_tabular_inspect`, `inspect_tabular_sources`, `admin_settings_fields.py`, [Phase 2](features/V2_FILE_MERGE_PHASE_2_RECONCILIATION.md)) + +#### User Interface Enhancements + +* **Merge Settings In Words On The Plan Review** + * The V2 plan review now states merge and inspection settings in plain words, such as "Keep every column from every file" or "Remove rows with the same Customer ID, keeping the last", leaves out values that only repeat a default, and shows column and sheet names as plain text. + * (Ref: `application/v2_ui/src/lib/orchestrationMerge.ts`, `OrchestrationRunView.tsx`) + +### **(v0.261.239)** + +#### New Features + +* **Merge Spreadsheets In V2 Chat** + * V2 chat orchestration can now combine two or more CSV or Excel files that share the same columns into one table and deliver it as a CSV or Excel file, for example "merge these three regional sales files into one Excel file". It is a new Reason capability, **Merge spreadsheets** (`tabular_merge`), alongside Analyze and Compare; a following **Create a file** step renders the result. + * Code appends the rows, not a model: every row is copied exactly in the order the files are listed, values stay text so codes such as `007` keep their leading zeros, and a **Source File** column records where each row came from. Columns may appear in any order in each file; files whose columns differ are not merged, and nothing is created. + * Files can be selected, or found by a search step whose source set the merge binds. Every file is authorized again and read through the screening-aware byte reader, at the revision the plan approved. CSV encodings and delimiters are detected, Excel values become canonical text (ISO dates, `TRUE`/`FALSE`), and encrypted or damaged workbooks are refused. + * A new **Merge** entry under **Document Action Capabilities** turns the feature on or off (on by default) and sets how many files and merged rows one chat request may merge (10 files, 250,000 rows by default) and, for upcoming workflow merges, the workflow limits (100 files, 1,000,000 rows). + * Merge failures reach users only as fixed, application-owned messages, such as "The selected files don't all have the same columns, so nothing was merged." + * (Ref: #1619, `functions_tabular_merge.py`, `functions_orchestration_merge.py`, `functions_orchestration_adapters.py`, `functions_orchestration_registry.py`, `functions_orchestration_schema.py`, `functions_orchestration_executor.py`, `functions_document_actions.py`, [V2 File Merge](features/V2_FILE_MERGE.md), [Phase 1](features/V2_FILE_MERGE_PHASE_1_SPREADSHEETS.md), [Merge files in chat](../guides/merge-files.md)) + +* **Exact-Schema CSV And Excel Exports** + * Two new generated-file profiles, `exact_tabular_records_v1` (CSV) and `exact_tabular_workbook_v1` (XLSX, optional sheet name), export every retained column in its retained order. They let a plan deliver a table whose columns are only known when it runs, such as a merge of files a search found, without the planner having to name the columns. + * (Ref: `functions_generated_export_registry.py`, `functions_structured_file_renderers.py`, `functions_generated_office_adapters.py`, [Generated File Export Framework](features/GENERATED_FILE_EXPORT_FRAMEWORK.md)) + +### **(v0.261.238)** + +#### Bug Fixes + +* **Microsoft 365 Actions Now Work In Chat Orchestration Plans** + * A plan step that used a Microsoft 365 action, such as "what are my latest emails", never read Microsoft 365 data. The step reported completed, and the answer said the data couldn't be retrieved. No Microsoft Graph request was made. + * Each plan step runs in its own request context, and Microsoft 365 functions require a Microsoft 365 execution context on that request. Classic chat and workflows create one, but orchestration never did, so every call was refused with `m365_context_required`. The plugin returned the refusal as a function result, and the step reported it as findings. + * An action step now gets its own Microsoft 365 request as the signed-in user. It selects only the step's saved action, checks delegated sign-in before the model or Microsoft Graph is called, and records the request so sharing the conversation waits for it. A retried step reuses the request, so an approval given after the first attempt applies. + * Plans only read Microsoft 365 data: send mail, calendar invitation and mark-as-read functions aren't loaded. Shared conversations are refused before any Microsoft 365 work. + * A sign-in, approval or authorization refusal now stops the step with `m365_sign_in_required`, `m365_approval_required`, `m365_unavailable`, `m365_shared_conversation` or `m365_read_only`. Each message says what to do next. Ordinary Microsoft Graph outcomes, such as nothing found, are still reported as findings. + * `[MS_GRAPH_PLUGIN] Microsoft 365 operation could not complete.` now logs the refusal code as `sc_failure_code` and the source as `sc_resource`. A stopped step logs `[ORCHESTRATION_M365] A Microsoft 365 step stopped.` + * (Ref: `functions_orchestration_m365.py`, `functions_m365_runtime.py`, `functions_orchestration_actions.py`, `functions_orchestration_adapters.py`, `functions_orchestration_schema.py`, `functions_m365_transport.py`, `test_orchestration_m365_actions.py`, [Orchestration Microsoft 365 Action Context Fix](fixes/ORCHESTRATION_M365_ACTION_CONTEXT_FIX.md)) + +#### User Interface Enhancements + +* **Connect Microsoft 365 From A Failed Plan Step** + * When a V2 plan step stops for Microsoft 365 sign-in, the run details show **Connect Microsoft 365** for the sources the step needs. It opens the Profile reconnect sign-in in a pop-up. When it finishes, select **Retry from failed step**. + * A step that stopped for approval links to Approvals instead. + * (Ref: `v2_ui/src/lib/m365Connect.ts`, `OrchestrationM365Notice.tsx`, `OrchestrationRecoveryNotice.tsx`, `test_v2_orchestration_m365_recovery.py`, [Microsoft 365 data and approvals](../guides/microsoft-365-conversation-data.md)) + +### **(v0.261.237)** + +#### Bug Fixes + +* **Orchestration Runs No Longer Fail When Settings Are Read During A Save** + * A chat orchestration run could finish every step and still end as **Partially completed** with "A required retained result is unavailable or changed." It affected runs that kept web search, linked-page, deep research, agent, action or Fact Memory results, and retrying could fail the same way. + * Orchestration rechecks a kept result's access with the current admin settings, and that check requires exactly a `dict`. Settings read from the shared Redis copy were a `dict`, but settings read from Cosmos DB came back as the SDK's `CosmosDict` type, so the check refused them. Cosmos serves the read while any settings save is in progress, such as the Cosmos throughput autoscale saving its status, when the shared copy is repaired or unreachable, and on every read when Redis is off. + * The settings store now returns a plain `dict` from every read and write, whichever backend served it. The orchestration check is unchanged. No setting or deployment change is needed. + * (Ref: `app_settings_store.py`, `functions_orchestration_external_sources.py`, `test_app_settings_store_plain_dict_reads.py`, [Orchestration Settings Document Type Fix](fixes/ORCHESTRATION_SETTINGS_DOCUMENT_TYPE_FIX.md)) + +### **(v0.261.236)** + +#### User Interface Enhancements + +* **V2 Chat Rail Scrolls As One Panel** + * On the V2 chat page, everything between **New chat** and the theme and account controls now scrolls as one panel, as the classic sidebar does. As you read down the conversation list, the navigation and any **External Links** or **Custom Pages** groups move out of view, and **Search chats** stays right under **New chat**. The list gets nearly the whole rail: a 720-pixel-tall window with two external links went from two visible conversations to twelve. + * The held search box gets a solid backing and an edge only while it is held, so the rail looks as it did at rest. Searching while it is held no longer makes the box jump. Keyboard focus on a conversation is never hidden under it. The next page of conversations now loads just before you reach the end of the list. + * The collapsed icon strip and other pages are unchanged. + * (Ref: #1642, `Sidebar.tsx`, `ConversationRail.tsx`, `test_v2_sidebar_conversation_scroll.py`, [V2 Sidebar Conversation Scroll Fix](fixes/V2_SIDEBAR_CONVERSATION_SCROLL_FIX.md)) + +* **V2 Workflow Alerts Pop Up From The Bell** + * Workflow alert notices now hang from the notification bell instead of **My Workspace**. With the rail expanded, the notice drops down from the bell the way the bell's panel does, hanging past the rail's edge so most of **New chat** stays clickable. When the rail is collapsed or on a phone, it flies out beside the bell. + * An alert that needs acknowledgment still never covers anything in the expanded rail. It now takes a row of its own right under the rail's header, pointing up at the bell and pushing **New chat** and the navigation down, instead of a space under **My Workspace**. As a flyout it still steps aside from focus it would cover. + * The notice comes right after the bell in the tab order. Because the bell never scrolls, a notice can no longer be carried off screen with the navigation. + * Timers, claims, the tuck into the bell, the alert card and the announcements are unchanged. + * (Ref: #1642, `NotificationBell.tsx`, `WorkflowAlertNotice.tsx`, `test_v2_workflow_alert_notices.py`, [V2 Workflow Alert Notices](features/V2_WORKFLOW_ALERT_NOTICES.md)) + +### **(v0.261.235)** + +#### New Features + +* **Must-Acknowledge Workflow Alerts, Alert Sounds, Sizes And Team Delivery** + * Each workflow alert rule can now require acknowledgment. Such an alert keeps coming back after a refresh, in every tab and on every device, whatever its age, until someone selects **Acknowledge** or opens it. It is shown ahead of other alerts. Closing it only shrinks it: V2 keeps its notice under **My Workspace**, in room of its own so it never covers the items below, and classic keeps a banner with **Review** at the bottom of the page. Mark read, Dismiss and **Mark all read** never acknowledge an alert. + * A rule can play a sound once, or repeat it every five seconds until the alert is acknowledged. Three tones, chosen by severity, were synthesized for SimpleChat and are served locally. One tab per browser plays, shared between classic and V2: several repeating alerts sound as one tone, the most urgent one's, and **Play once** chimes once per browser, not once per tab or refresh, with alerts that arrive together chiming once in the most urgent tone. When the browser blocks sound, the alert offers **Enable sound** until a sound plays. + * A rule can choose a size: small (the usual pop-up), medium (the full alert opens straight away) or large (full screen, for wall displays). + * In a group workflow, a rule can alert everyone in the group. The run raises one shared alert, and any current member can acknowledge it for everyone; the bell then shows "Acknowledged by *name* at *time*". Members other than the owner see a simpler alert with the severity, workflow, matched rules and **Open** to what the run created in the group. + * When several rules match, each option takes the strongest value any matched rule asked for. A model-evaluated rule that asks for a stronger option is still evaluated when a deterministic rule matched at a higher severity. + * New admin setting **Enable Workflow Alert Sounds** (`enable_workflow_alert_sounds`, on by default). Each device has its own **Play alert sounds** switch, and V2 **Preferences** adds an **Alert monitor** switch that checks for workflow alerts every 30 seconds, even in a background tab, for operations screens. + * New route `POST /api/notifications//acknowledge`, open only to the alert's recipients. `GET /api/notifications/workflow-alerts` now also returns pending alerts that need acknowledgment and team alerts from the user's groups, with `complete` and `sounds_enabled`. Read, dismiss and acknowledge writes are now conditional on the stored version, so concurrent writes to a shared alert can't drop each other. + * Both the V2 and classic workflow editors have the new options, and both mirror the server's reviewed validation messages. + * (Ref: #1634, `functions_workflow_alerts.py`, `functions_notifications.py`, `route_backend_notifications.py`, `functions_workflow_runner.py`, `WorkflowAlertEditor.tsx`, `workflowAlertStore.ts`, `workflowAlertSound.ts`, `PreferencesTab.tsx`, `notifications.js`, `workflow-alert-sound.js`, `workspace_workflows.js`, [Workflow Alert Acknowledgment](features/WORKFLOW_ALERT_ACKNOWLEDGMENT.md)) + +### **(v0.261.234)** + +#### Bug Fixes + +* **Clearer Messages When A Saved Result Can't Be Used** + * Errors about saved and generated results no longer say that source access could not be confirmed. Since saved results stopped re-checking their source documents, most of these errors mean something else, and the message now says which kind of problem it is. + * A source document that can't be read now, or that changed while it was read: "A source document for this analysis is no longer available or has changed." Only reading an uploaded document as an input can raise this. + * A conversation, workflow or run that no longer exists or isn't yours: "The conversation, workflow or run that holds this result no longer exists or isn't available to you." + * A saved result whose earlier result, record or loop and Repeat state is missing or changed: "This saved result can't be used because something it depends on is missing or has changed." + * Workflow task errors, a task result that couldn't be saved, a paused loop item, saved analysis explanations, Analyze saves, the Analyze publication check and the workflow progress view now show the matching message. Placeholders in chat history for an unavailable saved analysis or saved workflow output no longer mention access confirmation. + * Error codes and HTTP statuses are unchanged, apart from two internal codes for a message or workflow binding mismatch (`analysis_message_unavailable` and `analysis_workflow_unavailable`) that map to the same response codes as before. + * (Ref: #1621, `functions_analysis_access.py`, `functions_workflow_runner.py`, `functions_workflow_flow_runner.py`, `route_backend_chats.py`, `route_backend_workflows.py`, `test_analysis_unavailable_message_families.py`, [Saved-Result Error Messages Fix](fixes/ANALYSIS_RESULT_UNAVAILABLE_MESSAGES_FIX.md)) + +### **(v0.261.233)** + +#### Bug Fixes + +* **Orchestration File Generation Fix** + * Chat orchestration could no longer create downloadable files. A request such as "create a csv showing the states and their capitals" planned correctly and prepared its rows, then the file step failed with "This file could not be created." Every CSV, JSON, XML, YAML, TXT and Markdown file failed this way. Word and PDF files rendered, but downloading any orchestration-generated file used the same failing step, so those downloads would have failed too. + * Two changes interacted to cause this. Since the OneNote extractor was added to the container image, `/app` has been owned by root, while the app runs as a non-root user with `/app` as its working directory. The file renderer and the verified download stream created their scratch files in the working directory, so the operating system refused them. The refusal was reported as `output_access_denied`, a code meant for access refusals, and the real cause wasn't logged. + * Scratch files now go to `/sc-temp-files` when it's writable, and otherwise to the platform temp directory, never to the working directory. The image creates `/app` for the runtime user again. The OneNote extractor binary is added after that and stays root-owned. + * For a failed attempt, the `[ORCHESTRATION_EXECUTOR] A file render attempt finished.` event now also records `sc_error_type`, `sc_error_cause_type` and `sc_error_errno`, so a file-system error can be told apart from an access refusal. Messages and paths aren't logged. + * (Ref: #1623, `functions_temp_files.py`, `functions_structured_file_renderers.py`, `functions_simplechat_operations.py`, `functions_orchestration_rendering.py`, `Dockerfile`, [Orchestration File Render Permission Fix](fixes/ORCHESTRATION_FILE_RENDER_PERMISSION_FIX.md)) + +### **(v0.261.232)** + +#### Breaking Changes + +* **Saved Chat, Analyze And Orchestration Results Take Their Container's Access** + * Results built from documents are no longer checked against those documents again. This covers chat AI replies and their stored citations, tool results in history, saved Analyze results, orchestration files and results, generated chat files, and conversation and tabular exports. They stay available to everyone who can open their conversation or orchestration run, even after a source document is deleted, re-uploaded or held for content screening. + * Chat history no longer replaces a whole AI reply with "Source content is unavailable pending document screening and review." when a document it cited changes. A workspace attachment shown in history is still replaced while its document is held or unavailable. + * One input check stays. Search retrieval, document selection (including chat uploads), file bytes and previews, opening a cited document, and orchestration or Analyze steps that read documents still need current access to an uploaded document, and the document must not be held. After such a read is refused, the model fence still blocks later model calls in that request or step. + * A follow-up orchestration step that reads documents named by an earlier run reads their current version instead of failing. Version checks still protect a read in progress, such as a resumed Analyze checkpoint. + * Container, lineage and integrity checks are unchanged. Nothing is withdrawn after the fact, and no data migration runs: results that earlier versions withheld when they were read become visible again. Error messages now name the conversation or saved result instead of source access. + * **Migration**: none. Deployments that relied on source re-checks to hide existing results from people who can open their conversation should review shared conversations; opening the full cited document still requires access. + * (Ref: #1621, `content_screening/access.py`, `functions_saved_analysis.py`, `functions_orchestration_results.py`, `functions_orchestration_source_access.py`, `functions_orchestration_executor.py`, `route_backend_chats.py`, `route_enhanced_citations.py`, `functions_tabular_generated_exports.py`, `OrchestrationOutputs.tsx`, `test_saved_results_container_access_chat_orchestration.py`, [Upload-Only Content Screening](features/UPLOAD_ONLY_CONTENT_SCREENING.md)) + +### **(v0.261.231)** + +#### Breaking Changes + +* **Saved Workflow Results No Longer Re-check Their Source Documents** + * Saved and generated workflow results now take their access from the workflow, run and group they belong to. Deleting, re-uploading or holding a document a run used no longer hides or fails its task results, node results, structured execution history, For each and Repeat history, run history previews, or chat answers about a workflow result. + * A later task can read an earlier task's output after that output's source changed. Runs no longer pause with "A collected source changed." or "The Repeat state's original sources are no longer available." for generated output. + * Checks that stay: a workflow still refuses a held or inaccessible uploaded document when it reads it as an input (For each document items, document selections, reference inputs, File Sync and other configured sources), and that held input still blocks its own task's model call. Each task now gets its own model fence, so a held input can't block a later task. Container checks are unchanged: you need access to the workflow, group results stay limited to group members, and someone else's run still answers like a missing one. Forged or mismatched results are still refused. + * Opening a saved workflow's Flow no longer fails when one referenced document is missing, held or outside your access. That source is marked "Not available to you right now" on its own row, and the rest of the definition shows. + * API changes: a result that fails its lineage or hash check now answers 409 instead of 403 on task result pages ("The saved result or requested page is unavailable.") and execution history ("This saved execution record could not be verified."). Flow and execution history 403 messages no longer mention sources. Activity streams end with `activity_unavailable` instead of `source_access_denied`. Chat follow-ups report a lineage failure as `workflow_result_invalid`. `source_snapshot_changed` is always `false` and kept for older clients. + * **Migration**: none. Nothing is migrated or withdrawn; results saved before this version are read the same way. + * (Ref: #1621, `functions_workflow_node_results.py`, `functions_workflow_results.py`, `functions_workflow_inspection.py`, `functions_workflow_result_reader.py`, `functions_saved_analysis.py`, `functions_workflow_runner.py`, `content_screening/access.py`, `route_backend_workflows.py`, `WorkflowDefinitionInspector.tsx`, [Workflow Saved Result Source Re-check Fix](fixes/WORKFLOW_SAVED_RESULT_SOURCE_RECHECK_FIX.md)) + +### **(v0.261.230)** + +#### Bug Fixes + +* **Workflow Run History No Longer Fails When A Source Document Changes** + * A workflow's run history no longer returns "Run history is unavailable because source access could not be confirmed." when one of its runs used a document that was later deleted or re-uploaded. The run list, a run's task results, the live run activity view and the last-run preview in the workflow list now take their access from the workflow, not from the documents each run used. + * A task no longer fails right after saving its output with "Saved task output was withheld because its source access could not be confirmed." + * Container checks are unchanged: you still need access to the workflow, group runs still require group membership, and a run from another workflow still answers as not found. + * This is the first step of limiting content screening to uploaded documents. Some per-task previews and structured workflow execution history still re-check sources until the next step. + * (Ref: #1613, #1621, `route_backend_workflows.py`, `functions_workflow_runner.py`, [Workflow Run History Source Re-check Fix](fixes/WORKFLOW_RUN_HISTORY_SOURCE_RECHECK_FIX.md)) + +#### User Interface Enhancements + +* **Approvals Page Screening Notice Only When Screening Is On** + * The content screening notice at the top of **Approval Requests** now appears only while content screening is enabled, and reads in plain language: "To review uploaded documents that content screening is holding, open Content review." + * Each content screening request still has its own **Open content review** link, so documents held before screening was turned off stay reachable. + * (Ref: `templates/approvals.html`, `test_content_screening_classic.py`) + +#### Breaking Changes + +* **Generated Documents Are No Longer Screened** + * Content screening now checks only documents that enter a workspace by a user upload (personal, group or public, including a file attached in chat and saved to a workspace) or File Sync. Files the SimpleChat agent action saves to a workspace and artifacts published into a workspace get no screening marker, are processed normally while screening is on, and are skipped by scan jobs. + * Each generated document version carries a server-managed `screening_exemption` field that is set when the version is created. Clients can't set it: the document APIs reject any `screening` field, no update path accepts one, and responses never include it. A document that already has a screening marker stays screened. + * Publishing an artifact to a group or public workspace no longer reserves a screening scan for its destination, so approval hands it straight to processing. A destination that was held by screening before this version can't be approved; cancel the request and publish the artifact again. + * **Migration**: None. Existing documents keep their screening state, and nothing already screened or held is released. + * (Ref: #1621, `content_screening/contracts.py`, `content_screening/service.py`, `content_screening/jobs.py`, `functions_documents.py`, `functions_simplechat_operations.py`, `functions_artifact_publication.py`, `functions_artifact_publication_readiness.py`, [Upload-Only Screening Intake Fix](fixes/UPLOAD_ONLY_SCREENING_INTAKE_FIX.md)) + +* **Metadata Edits No Longer Hold Screened Documents Again** + * Editing a released document's title, abstract, keywords, authors, classification or tags now applies directly. The document stays available and no new scan starts, so the group and public document metadata APIs return `updated` (200) instead of `queued` (202). + * Metadata the model generates after a document is released also applies directly, without a new hold. + * A rename still can't change a screened file's extension. Tag edits on a screened document update the document and its search chunks but no longer rewrite the released file's blob metadata, so downloads of that file keep working. + * In the V2 Documents explorer, saving a screened document's metadata now shows **Metadata saved.**, and the document stays in the list with the edit shown. The explorer no longer removes it with "Screening is queued". A save is confirmed only by an `updated` 200 receipt; any other receipt, including a `queued` 202 one, keeps the draft. + * (Ref: #1621, `functions_documents.update_document`, `content_screening/access.py`, `functions_group_document_management.py`, `functions_public_document_management.py`, `route_backend_group_documents.py`, `route_backend_public_document_management.py`, `documentOperations.ts`, `DocumentExplorer.tsx`, [Upload-Only Screening Intake Fix](fixes/UPLOAD_ONLY_SCREENING_INTAKE_FIX.md)) + +### **(v0.261.229)** + +#### Breaking Changes + +* **Microsoft 365 Workflow Run As Approval Only When Someone Else Authored The Revision** + * A workflow revision saved by its own Run as user now runs as them without a separate approval, including on its first run. SimpleChat records an approved, audited Run as authorization for that exact revision and connection, sends no notification, and never shows it as pending. Before, every edit asked the Run as user to approve again, even their own. + * Approval is still required when someone else is responsible for what runs: someone else saved the current revision, `modified_by` is missing, or someone else changed an agent or action the workflow uses after the Run as user's save. A new connection or connection generation asks again for a revision someone else saved. + * Conversation audience changes no longer ask for Run as approval again; the authorization still records the audience. Source-sharing approvals and the runtime checks that guard runs and deliveries already in progress are unchanged. + * Explicit decisions still win. A denial or cancellation recorded for a run still stops that run. Revoking a Run as authorization in Profile revokes every approved authorization for that revision, which then waits for approval even if its Run as user saved it. That holds when the revocation overlaps a run recording a new authorization, or is interrupted: a run that finds an authorization the revocation missed revokes it instead of using it. + * Raw edits of workflows, agents and actions in the Data Management Cosmos DB editor now record the administrator as the last author, so an edited record can't run as someone else without asking them. + * The V2 workflow proposal card, the editor's **Saving requires re-approving Run as** note, the AI assistant's `run_as_reapproval` warning, the Run as help text in both editors, and the classic Profile and Approvals labels follow the new rule. + * **Migration**: None. Existing approvals keep working. A workflow its Run as user saved last is authorized automatically the next time it needs a new authorization. To require approval for such a revision, revoke its authorization in Profile. An authorization revoked before this release still withdraws only itself, so other approvals of that revision keep working, but that revision is never authorized automatically. + * (Ref: #1621, `functions_m365_approvals.py`, `functions_m365_execution.py`, `functions_m365_workflow_binding.py`, `functions_m365_runtime.py`, `functions_m365_data_lifecycle.py`, `functions_data_management.py`, `functions_orchestration_workflow_proposals.py`, `functions_workflow_assist.py`, `WorkflowProposalCard.tsx`, `WorkflowChangeTracking.tsx`, [Run as approval for a revision you saved](features/M365_RUN_AS_SELF_AUTHORED_APPROVAL.md)) + +### **(v0.261.228)** + +#### User Interface Enhancements + +* **V2 Workflow Alerts Lead With One Open Button** + * The full workflow alert now shows just what's needed to act: why it came, its summary, and a large green **Open** button next to **Dismiss**. Open goes to the conversation the workflow created, or else to the conversation it posted into, its run or the workflow, and marks the alert read. + * **Mark read** is gone from alerts that have somewhere to open, because Open marks the alert read and takes you there. When one workflow raised several alerts together, Open and Dismiss act on all of them. An alert with nothing to open still offers Mark read. + * The detail, the pills for the alert's enrichments, trigger, runner and agent, and the other ways in (other links, **Open workflow** and **Ask about this**) are under **Show more**. + * Open's green keeps its text at 4.5:1 contrast or better in the light and dark themes. + * (Ref: #1624, `WorkflowAlertCard.tsx`, `workflowAlertStore.ts`, `theme.css`, [V2 Workflow Alert Notices](features/V2_WORKFLOW_ALERT_NOTICES.md)) + +### **(v0.261.227)** + +#### New Features + +* **Workflow results posted back to chat** + * When a chat orchestration plan starts one of the user's saved durable personal workflows, the server can now post the run's terminal outcome back into the same private chat after the run finishes, even hours later. + * The behavior is gated by **Use Workflow Results In Chat** (`enable_chat_workflow_results`), checked when the run starts and again when delivery happens, in addition to the existing Run Workflows From Chat gates. + * Delivery is exactly once per durable runtime generation: one deterministic chat message, one unread mark, and one bell notification. If the chat can no longer receive the post, the server posts nothing and sends one workflow notification with a run link instead. + * A new owner-only status route, `GET /api/v2/orchestration/workflow-runs/status`, gives 6b-2 the batched run-card contract. Retry/Edit on delivery messages are refused with fixed 400 codes. + * (Ref: #1546, #1543, `functions_workflow_chat_delivery.py`, `functions_workflow_chat_delivery_worker.py`, `functions_workflow_chat_delivery_status.py`, `functions_orchestration_workflow_runs.py`, `functions_workflow_runtime.py`, `route_backend_orchestration.py`, `route_backend_conversations.py`, [Workflow result delivery to chat](features/CHAT_WORKFLOW_RESULT_DELIVERY.md)) + +### **(v0.261.226)** + +#### Bug Fixes + +* **V2 New Chat Always Starts A Clean Conversation** + * Clicking **New chat** while chat orchestration was planning or running no longer carries the old turn into the new chat. Before, the new chat kept the previous turn's **Thinking** bubble and a **Stop** button that did nothing. Its empty state never appeared and it wouldn't send until the page was reloaded. + * The previous conversation's plan, run or reply keeps going in the background and is there when you reopen it. If you reopen it while its turn is still running, it shows as working, with **Stop**. + * Clicking **New chat** while the first message of a new chat is still creating its conversation no longer lets that message take over the new chat. The message is still sent, and its conversation appears in the list. Clicking **New chat** while a conversation is loading now shows the empty state instead of loading placeholders. + * **Chat** on selected workspace documents or on a tag, and **Start chatting** on the home page, now always open a brand-new conversation, as the classic interface does. The documents or tag arrive as context in the new chat instead of being added to whichever conversation was last open. Text you've typed but not sent stays in the message box. + * (Ref: #1617, `chatStore.ts`, `orchestrationController.ts`, `DocumentExplorer.tsx`, `TagsSection.tsx`, `HomePage.tsx`, [V2 New Chat Reset Fix](fixes/V2_NEW_CHAT_RESET_FIX.md)) + +### **(v0.261.225)** + +#### New Features + +* **Photos and Facts on V2 Map Points** + * A point an agent puts on a map can now carry a photo of the place and labelled facts about it, such as a reading's time, a transponder ID or a lane. Hovering the point in the V2 chat shows the photo, its caption and the facts with the point's label and description. Clicking keeps them open, and clicking the photo opens it full size in the image viewer. + * A point without a photo shows its facts on their own, so a reading with no camera image still shows when and where it was taken. + * **List what the map shows** includes each point's facts and a thumbnail of its photo, for keyboard and screen reader users. + * The details open on whichever side of the point has room, and pinned details move the map so all of them are visible. + * Agents send `image_url`, `image_caption` and `fields` with each point in `locations_json`. Only `https` photo links are kept; any other link is left out, and the action tells the agent how many it dropped. Photos load without a referrer, and captions and facts are only ever written as text. The classic chat still shows a point's label and description only. + * (Ref: #1609, `azure_maps_openlayers_plugin.py`, `InlineMapCard.tsx`, `lib/inlineMaps.ts`, [V2 Interactive Maps](features/V2_INTERACTIVE_MAPS.md)) + +### **(v0.261.224)** + +#### Bug Fixes + +* **Maps From Agents Load Their Tiles Again** + * Tool results are redacted before they are stored with a reply, and the redaction treated the Azure Maps tile proxy token as a secret. Every stored map lost its token, so maps drew their markers and paths on blank tiles in both the classic and the V2 chat. + * The token in SimpleChat's own tile template is now kept. It is encrypted with the app's secret key, expires, and only works on SimpleChat's signed-in tile proxy. Every other `token=` value and every other secret in a tool result is still redacted. + * Maps stored before this fix keep the redacted token and still show no tiles. Run the request again to get a map with tiles. + * (Ref: #1609, `plugin_invocation_logger.py`, [Azure Maps Tile Token Redaction Fix](fixes/AZURE_MAPS_TILE_TOKEN_REDACTION_FIX.md)) + +{% raw %} +### **(v0.261.223)** + +#### New Features + +* **Interactive Maps in the V2 Chat** + * A reply that used the Azure Maps action now shows its map under the text in the V2 chat, as the classic chat does. It opens fitted to every marker, path and area, and can be panned, zoomed and expanded to full screen. + * Hovering a marker, path or area shows its label and description; clicking keeps them open until you click elsewhere or press Escape. **List what the map shows** lists everything on the map as text for keyboard and screen reader users. + * The mouse wheel zooms only with Ctrl (Cmd on a Mac), so scrolling a conversation never zooms a map by accident. + * OpenLayers 10.6.1, the build the classic chat already uses, is vendored into the V2 app and loaded only when a reply has a map. No CDN assets are added and the Content-Security-Policy is unchanged. + * Map text is only ever written as text, and tiles are only loaded through SimpleChat's tile proxy. + * (Ref: #1609, `InlineMapCard.tsx`, `lib/inlineMaps.ts`, `vendorAssets.ts`, [V2 Interactive Maps](features/V2_INTERACTIVE_MAPS.md)) + +#### Bug Fixes + +* **Maps in Older Replies and Group Conversations Load Their Tiles** + * The message endpoints the V2 chat reads, including the one for group conversations, returned maps with their original tile token, which expires after four hours. Older maps drew their points on blank tiles. + * Both endpoints now reissue the token when the conversation is opened, as the classic route already did. + * (Ref: #1609, `functions_azure_maps.py`, `route_backend_conversations.py`, `route_backend_collaboration.py`) + +* **Legacy Map Blocks Hidden From V2 Replies** + * Older replies that stored a `{{map:...}}` block in their text showed it as raw JSON in the V2 chat. The block is now hidden, as in the classic chat, and the map is drawn from the tool result. + * (Ref: #1609, `stripLegacyMapBlocks` in `lib/inlineMaps.ts`, `MessageList.tsx`) +{% endraw %} + +### **(v0.261.222)** + +#### New Features + +* **Inline Audio and Video Players in the V2 Chat** + * A link to an audio file in a reply, such as a recording an action returned, now plays in place as a compact player. Collapsed, it shows play/pause, the title and elapsed and total time; expanded, it adds a seek slider, stop, volume and mute, playback speed and an open-in-new-tab link. + * A link to a video file renders as a card with the browser's own controls, including fullscreen. Starting one recording or clip pauses any other. + * Media that cannot play, for example an expired signed link or a host the page does not allow, shows a short notice and a link instead of a broken player. + * (Ref: #1611, `InlineAudioPlayer.tsx`, `InlineVideoCard.tsx`, `lib/inlineMedia.ts`, [V2 Inline Media and Agent-Posted Messages](features/V2_INLINE_MEDIA_AND_AGENT_MESSAGES.md)) + +* **External Media Sources in the Content-Security-Policy** + * The new `CSP_MEDIA_SRC_ORIGINS` app setting lets administrators list the `https://` origins whose audio and video may play inline, for example an action's media host. The origins are added to `media-src`. + * Only bare `https://` origins (with an optional leading `*.` label and port) are accepted. Anything else is ignored and logged, so the setting cannot widen the policy beyond media. Empty by default, which keeps media same-origin. + * (Ref: #1611, `csp_media_sources.py`, `config.py`) + +#### User Interface Enhancements + +* **Image Cards in Chat Replies** + * Images in a reply's markdown appear as captioned cards with a bounded height. Selecting one opens the image viewer for the full-size view, saving and opening in a new tab. + * (Ref: #1611, `InlineImageCard.tsx`, `AssistantMarkdown.tsx`) + +* **Agent-Posted Messages Render as Formatted Text** + * A message an agent posts into a personal or shared conversation through the Simple Chat action is now marked as agent-authored markdown. The V2 chat renders it like the agent's own replies, with headings, tables, links, images and players, in the neutral bubble and labeled "posted through an agent". Messages people type are unchanged. + * (Ref: #1611, `functions_simplechat_operations.py`, `MessageList.tsx`, `sharedMessage.ts`) + +### **(v0.261.221)** + +#### Bug Fixes + +* **GPT-6 Sol Agents With Actions Send Reasoning Effort None** + * Agents with actions on `gpt-6-sol` failed unless their Reasoning Effort was set to None by hand. Chat Completions accepts function tools for this model only with `reasoning_effort: none`, and with no effort set the model applied its own default and rejected the tools. + * `gpt-6-sol` is now in the model catalog with a Chat Completions tool rule for Azure and direct OpenAI. An agent with tools and no effort set now sends `none`. Another explicit effort fails before the request with "This model's Chat Completions tools require Reasoning Effort None." instead of a provider error. + * The record also carries the verified limits (1,050,000 context, 922,000 input, 128,000 output, counting reasoning tokens) and the reasoning efforts none, low, medium, high and xhigh. + * (Ref: #1606, `model_capabilities.json`, [GPT-6 Sol Chat Completions Tools Fix](fixes/GPT6_SOL_CHAT_COMPLETIONS_TOOLS_FIX.md)) + +### **(v0.261.220)** + +#### Bug Fixes + +* **GPT-6 Astra Limits And Reasoning Efforts In The Model Catalog** + * `gpt-6-astra` now has verified token limits for Azure and direct OpenAI: 1,050,000 context, 922,000 input and 128,000 output, counting reasoning tokens. File evidence and workflow budgets work without limits entered in Model Endpoints, and limits entered there still take precedence. + * Its reasoning efforts are low, medium, high and xhigh. Chat now sends the selected effort; before, with no policy, it sent no effort and the model used its default. None and other unsupported selections are sent as Low. Agents still send their saved effort as is. + * Azure Chat Completions rejects function tools for this model at every effort it accepts, so agents with actions can't run on it yet. The catalog records this in its notes. #1606 tracks a Responses API path and an early, clear error. + * (Ref: #1606, `model_capabilities.json`, [GPT-6 Astra Model Catalog Fix](fixes/GPT6_ASTRA_MODEL_CATALOG_FIX.md)) + +### **(v0.261.219)** + +#### Bug Fixes + +* **GPT-6 Models Use Reasoning Model Request Parameters** + * Requests to GPT-6 deployments failed whenever a response length was set: "Unsupported parameter: 'max_tokens' is not supported with this model. Use 'max_completion_tokens' instead." Every workflow run on a GPT-6 agent failed on its first model call once token limits were configured. + * GPT-6 is now recognized alongside GPT-5, by name prefix and as a family name anywhere in a deployment or display name. Response length is sent as `max_completion_tokens`, and no temperature is sent. Workflow budgets, chat, prompt variables, content screening and orchestration all follow the shared policy. + * (Ref: #1605, `model_endpoint_clients.py`, [GPT-6 Reasoning Model Parameters Fix](fixes/GPT6_REASONING_MODEL_PARAMETERS_FIX.md)) + +### **(v0.261.218)** + +#### Bug Fixes + +* **Scheduled Workflow Runs Act As Their Owner** + * A scheduled run had no signed-in session, so the runner created one that held only the owner's object ID. Groups and conversation messages the run created showed that ID instead of the owner's name, and the run then wrote the ID over the owner's stored display name. The runner now uses the owner's stored display name and email. A manual run keeps the signed-in session, as before. + * The SimpleChat action's `add_user_to_group` now accepts `user_id`. Given with an email or display name, it adds the user without a directory lookup, as the REST members route already does. Scheduled runs have no delegated token for Microsoft Graph, so they couldn't add members before. + * Group conversation invites first match each identifier against the group's current owner and members, by user ID, email or display name. An identifier that matches no member, or more than one, still goes to the directory lookup. + * (Ref: `functions_workflow_runner.py`, `functions_simplechat_operations.py`, `simplechat_plugin.py`, [Workflow Background Identity Fix](fixes/WORKFLOW_BACKGROUND_IDENTITY_FIX.md), [SimpleChat action](../reference/actions/simplechat.md)) + +### **(v0.261.217)** + +#### New Features + +* **Read Saved Workflow Results From Chat Orchestration** + * Chat orchestration can now read the stored result of one of your own finished personal workflow runs while it answers, for example "what did my Monday email digest workflow say this week?" or "compare yesterday's and today's run of my contract review workflow". A plan step names the workflow and reads its latest finished run, or the newest run that finished on a day you name in your own time zone, optionally only a completed, failed or cancelled one. The workflow isn't re-run, and a plan reads at most two results. + * The new capability, **Read workflow results** (`workflow_results`), is read-only with no approval floor, so a plan that uses it follows the user's approval mode. It's offered only while **Use Workflow Results In Chat** (`enable_chat_workflow_results`) is on, to users who may use personal workflows, in their own private conversations, and only for workflows in the request's workflow catalog. The step checks all of this again when it runs. With the setting off, which is the default, planning is unchanged. + * The planner and the plan name a workflow by a per-request handle, never by its ID. The server picks the run with bounded queries on the user's own runs and reads it through the Phase 6a result reader, which authorizes the run again. At most 24 KB of each run's result, from up to 8 of its outputs, reaches the answer, only as fenced, untrusted notes and never as a cited source. Failed and cancelled runs are reported by their status and time only, and a run still in progress is reported without any of its output. + * The answer adds a server-written **Saved workflow results:** note, with Phase 6a's line for each run it used and the reason for each result it didn't read, and no IDs. The answer stores `workflow_result_contexts`, so Phase 6a's masking withholds it, on the same reads that withhold Follow up answers, once a result it used is no longer available or the chat is no longer the owner's private chat. Each result is authorized again while the answer is written and twice more before it's published. If one changed, the answer is replaced with "A workflow result this answer used changed or is no longer available, so the answer was not saved. Ask again to read the current result." + * Later turns never reuse a read: result aliases skip any run whose plan reads a workflow result, and a new attempt or revised plan runs its reads and the steps built on them again instead of reusing an earlier answer. A plan edit that breaks a results rule is refused, and the previous plan stays. + * Admins can keep it out of plans by clearing **Read workflow results** in the orchestration Capabilities list. Group workflows, structured (version 3) runs and runs that saved an analysis can't be read from a plan, and a plan never waits for a run it starts to finish. + * (Ref: #1546, #1543, `functions_orchestration_workflow_results.py`, `functions_orchestration_registry.py`, `functions_orchestration_workflow_context.py`, `functions_orchestration_planner.py`, `functions_orchestration_schema.py`, `functions_orchestration_execution.py`, `functions_orchestration_executor.py`, `functions_orchestration_composition.py`, `functions_orchestration_services.py`, `functions_orchestration_recovery.py`, `route_backend_orchestration.py`, [Chat Orchestration Workflow Results](features/CHAT_ORCHESTRATION_WORKFLOW_RESULTS.md), [Ask about workflow results](../guides/ask-about-workflow-results.md#ask-orchestrate-about-workflow-results)) + +#### Bug Fixes + +* **Group Document Uploads With Enhanced Citations** + * With Enhanced Citations on, every document uploaded to a group workspace failed with "Document processing failed." That included Word and Markdown documents an agent or workflow saved to a group. The upload's own status update changed the document record's etag between the read and the guarded write, so the write always saw a conflict. + * The status is now posted before the record is read. A change made by anyone else during the upload is still rejected. + * (Ref: `functions_documents.py`, [Group Document Blob Upload Etag Fix](fixes/GROUP_DOCUMENT_BLOB_UPLOAD_ETAG_FIX.md)) + +* **Call Agent Actions Save Again** + * Saving any Call agent action, whether personal, group or global, failed with "Invalid plugin configuration." The manifest check rejected the server-owned `scope_id` field that binding to a collection adds, so agents couldn't delegate to other agents. + * `scope_id` is now accepted. The server always sets it from the authorized collection, and every other extra field is still rejected. + * (Ref: `functions_agent_delegation.py`, [Call Agent Action Save Fix](fixes/CALL_AGENT_ACTION_SAVE_FIX.md)) + +#### User Interface Enhancements + +* **Saved Workflow Results In The V2 Run View** + * A plan step that reads a saved workflow's result shows **Workflow**, the workflow's own name, and **Run**, the run it reads in words, such as **Latest run**, **Latest completed run** or **Run finished on 2025-06-02**, in place of its raw arguments. The name is shown as text, never markup, and a selector the card doesn't recognize shows **Run details unavailable**. + * (Ref: `OrchestrationRunView.tsx`, `orchestrationWorkflowResults.ts`, `orchestrationPlan.ts`, `orchestration.ts`, [Chat controls](../reference/chat-controls.md#saved-workflow-results-in-a-plan)) + +### **(v0.261.216)** + +#### Bug Fixes + +* **Log Injection CodeQL Alerts Fixed** + * Cleared the 19 open `py/log-injection` CodeQL alerts. The search cache logs and the workflow run cancellation error logs now pass identifiers and exception text through `sanitize_log_message`, so an ID containing a line break can no longer start what looks like a separate log entry. Ordinary values are logged exactly as before, and the cancellation errors keep their traceback. + * The admin plugin settings route no longer logs the request body. It was logged before validation, so a field the route then rejects, such as an API key, was written to the log as sent. The route now logs `[PLUGINS] Received plugin settings update request.` with the number of fields. + * `sanitize_log_message` already removed line breaks with a regular expression, which CodeQL doesn't recognize. It now also calls `replace` for them after the regex, which changes nothing at run time but lets CodeQL see that `log_event` output has none. Its output is unchanged. + * (Ref: #1543, `functions_appinsights.py`, `utils_cache.py`, `route_backend_workflows.py`, `route_backend_plugins.py`, [Log Injection CodeQL Alerts Fix](fixes/LOG_INJECTION_CODEQL_ALERTS_FIX.md)) + +### **(v0.261.215)** + +#### Bug Fixes + +* **Draft Workflow Instructions Follows The AI Workflow Assistant Setting** + * Turning off **Enable AI Workflow Assistant** now stops personal AI drafting of workflow task instructions everywhere, not just in the V2 editor. Before, only the V2 editor's **Draft with AI** button was hidden; the classic workspace's **Draft Workflow Instructions** button and direct calls to `/api/workflows/draft-instructions` still drafted instructions. + * The route now refuses a personal request with 403 `workflow_assistant_disabled`, the same answer the **Ask AI** route gives, before any model call. Personal workflows that are off and a missing `WorkflowUser` role keep their existing answers, which still come first. + * The classic workspace's personal workflow modal hides its **Task Brief** box and **Draft Workflow Instructions** button when the assistant isn't available to the signed-in user, using the same role-aware check as the V2 editor. **Task Instructions** stays, so instructions can still be written by hand. + * Group workflows aren't affected. Their **Draft Workflow Instructions** button follows the group workflow settings. + * (Ref: #1548, #1543, `route_backend_workflows.py`, `route_frontend_workspace.py`, `workspace.html`, `is_workflow_assistant_enabled_for_user`, [Workflow Draft Instructions Assistant Gate Fix](fixes/WORKFLOW_DRAFT_INSTRUCTIONS_ASSISTANT_GATE_FIX.md)) + +### **(v0.261.214)** + +#### New Features + +* **Workflow Results In Chat** + * Ask chat about the stored result of one of your finished personal workflow runs without running the workflow again. The answer uses only that run's saved output, with every other source off, and ends with a server-written line that names the run, such as "_This answer uses the stored result of the Weekly digest run of Mon Jun 2, 2025, 9:02 AM CDT. The workflow was not re-run._" Later questions in the same chat keep using the run. + * The run's output is given to the model as untrusted data, never as instructions. An answer sees at most 48 KB of excerpts from up to 8 task outputs, starting with the last task's, and says when it saw only part of the result. An Analyze task's saved analysis is explained the way **Ask about this analysis** explains it. Tools are off, and the question and answer are screened like other chat turns. + * The selection is bound to a SHA-256 digest of the run's stored outputs and checked again on every question, so an answer never comes from a result other than the one selected. A changed result asks the user to select the run again. + * Works only in the owner's private personal chats. In V2, and wherever chat history, search and export already withhold saved analyses, an answer is withheld once its result can't be confirmed for the chat's owner, for example after the run is deleted, after access to one of its sources is lost, or while storage can't be read. Once a chat is shared or converted to a collaboration, its Follow up answers are hidden for everyone, you included. The original chat's stored messages are unchanged. The collaboration's copies are stored with these answers withheld, and the chat's saved summary is cleared on both chats. Single-message Word, PowerPoint and email exports check the result too. Orchestration history skips these answers. A few older reads that only the chat's owner can reach, such as classic chat's message list, still show the owner an answer whose result is no longer available, as they do for saved analyses. + * New admin setting **Use Workflow Results In Chat** (`enable_chat_workflow_results`) in **Workflow** settings, off by default. It needs **Enable Personal Workflows** and follows **Require WorkflowUser App Role**; chat orchestration isn't required. A new requester-only route, `GET /api/user/workflows//runs//result-context`, returns the run's public descriptor. Store references never reach the browser. + * Personal workflows only. Structured workflow runs, named outputs, output formats, Retry and Edit aren't supported yet: the server refuses a retry or edit of a Follow up question or answer. There's no retention setting, so a result stays available until its run is deleted. Letting orchestration read workflow results, the `workflow_results` capability, follows in a later release. + * (Ref: #1546, #1543, `functions_workflow_result_reader.py`, `functions_workflow_result_followup.py`, `functions_workflow_result_masking.py`, `route_backend_chats.py`, `route_backend_workflows.py`, `route_backend_conversations.py`, `route_backend_conversation_export.py`, `functions_saved_analysis.py`, `functions_collaboration.py`, `functions_orchestration_context.py`, `route_backend_v2.py`, [Workflow Results In Chat](features/CHAT_WORKFLOW_RESULTS_FOLLOW_UP.md), [Ask about workflow results](../guides/ask-about-workflow-results.md)) + +#### User Interface Enhancements + +* **Ask In Chat And Ask About This In V2** + * **Ask in chat** on a completed run in a personal workflow's run history, and **Ask about this** in a workflow alert, open a new V2 chat with that run selected. The composer shows "Answering from the Weekly digest run of Mon, Jun 2, 9:02 AM — not re-running the workflow" with a remove button, and turns other sources and **Orchestrate** off. + * A run that can't be asked about as selected removes the notice and says why, rather than falling back to an ordinary chat answer. Choosing a source, uploading a file, turning on **Orchestrate** or switching chats also removes it. + * (Ref: `workflowResults.ts`, `workflowResultFollowUp.ts`, `chatStore.ts`, `Composer.tsx`, `WorkflowResultChip.tsx`, `WorkflowRunAskInChat.tsx`, `workflowAlertNotices.ts`, `conversationUrl.ts`, [Chat controls](../reference/chat-controls.md#workflow-results-in-chat-v2-interface)) + +### **(v0.261.213)** + +#### New Features + +* **Ask AI Tab In The V2 Workflow Editor** + * The V2 workflow editor's side panel has a new **Ask AI** tab, after **Changes**, that edits a personal workflow from a plain-language request, such as "run this at 7 AM on weekdays and only alert me when something is urgent". It calls the AI workflow assistant endpoint added in 0.261.208 and never saves. Each answer is applied to the draft as one AI change, with the same highlights, **Previously:** values, per-field **Revert** and **Changes** list as your own edits, and the first save asks you to review the AI changes. + * Open it with the footer's **Ask AI** toggle, or with a task's **Ask AI** button to ask about that task. Quick actions explain the workflow, tighten task instructions, add a schedule, alert only when urgent, or check what's needed to run. Type `#` to attach up to 20 documents; tags and whole workspaces aren't offered, because a workflow refers to documents. The browser's time zone is sent when the schedule editor offers it, and the 2,000-character limit is counted the way the server counts it, so an emoji counts as one. + * The editor is locked while a request runs, and **Cancel request** stops it with nothing changed. An answer that changes the draft gets a card listing what changed, taken from the editor's own comparison of the draft that was sent with the answer, with **Jump to**, the documents read as context, any warnings, and **Undo this change**, which reverts the turn except fields edited since. Follow-up requests include the earlier completed turns and what became of their changes. + * When the saved workflow changed after the editor opened, the draft is kept and **Reload workflow** is offered. `Retry-After` is honored with "You can send again in N s.", and a failed, cancelled or timed-out request changes nothing and can be retried against the current draft. + * The tab appears only when **Enable AI Workflow Assistant** is on, for a personal workflow the editor can change. Group workflows, read-only editors and workflows with an active run don't offer it. It also works on a workflow proposal opened from chat with **Edit**. + * Replies, errors, warnings, document names and the admin's rate-limit message are shown as plain text, never as HTML or Markdown. The side panel is an ARIA tab list, the thread is a live log, and Escape closes the panel and returns focus to its toggle. + * The conversation lives only in the page. Nothing is stored on the server or in the browser, so reloading starts over. Reopening a saved workflow in the same page shows its earlier turns, marked as coming from an earlier editing session. + * The shared assist thread gained opt-in options for the tab: counting by code points, holding its thread while the editor is open, a custom cancelled message, documents-only `#` references and a reply wrapper. All are off by default, so the chat's image, chart, diagram and plan editors are unchanged. Because the editor holds its thread, using it never clears a chat's idle image or plan editor conversation. + * (Ref: #1548, #1543, `WorkflowAskAiTab.tsx`, `useWorkflowAssist.ts`, `workflowAssist.ts`, `codePoints.ts`, `workflowAssistStore.ts`, `assistThreadStore.ts`, `WorkflowEditorDialog.tsx`, [AI Workflow Assistant](features/WORKFLOW_AI_ASSISTANT.md#the-ask-ai-tab), [V2 Shared Assist Thread](features/V2_SHARED_ASSIST_THREAD.md#options-for-the-workflow-editor), [Create a workflow](../guides/create-a-workflow.md#ask-ai-to-change-a-workflow)) + +* **Draft With AI For Workflow Task Instructions** + * A task with empty instructions in the V2 workflow editor has **Draft with AI**. It drafts instructions from the workflow's name and description and the task's name, adds them as one undoable AI change, and moves focus to them. Instructions longer than the editor's 12,000-character limit aren't added, and the message says so. + * It uses the route behind the classic editor's **Draft Workflow Instructions** button, and it's offered only where **Ask AI** is. The classic button is unchanged. + * (Ref: #1548, `useWorkflowAssist.ts`, `WorkflowTaskFields.tsx`, `/api/workflows/draft-instructions`, [AI Workflow Assistant](features/WORKFLOW_AI_ASSISTANT.md#draft-with-ai)) + +### **(v0.261.212)** + +#### New Features + +* **Saved Workflows Started From Chat** + * With **Run Workflows From Chat** on, a chat orchestration plan can start one of the requester's own saved personal workflows. "Run my weekly digest now" queues the same durable run that **Run** in Workflows does, and the answer lists each workflow as started, already started or not started, with the reason. + * The new `workflow_run` step names the workflow by a request-local handle from the workflow planning context. Workflows the request names are listed first, then those with durable execution on. A plan starts at most three workflows, each once. A step that still breaks the rules after one repair is dropped, and the answer says why. + * A plan that starts a workflow always waits for the user to run it, whatever approval mode they chose. The floor holds on every path that saves a plan, including plan editor revisions and manual edits, and a saved plan that lost it is refused when it's run. + * The plan starts the run and links to it. It never waits for the run or reads its results. The run's id comes from the plan's first attempt and the step, so a retry, a second tab or a crash links the run instead of starting another. + * Only personal workflows with durable execution on can be started, from a conversation that's private to the requester. A workflow waiting for a Microsoft 365 approval or sign-in isn't started again, and a background continuation links a run that already exists but never starts a new one. A storage error fails the step instead of reporting the workflow as deleted, and a failed start is retried only when the user selects **Retry**. + * The executor and recovery now read which capabilities act outside the plan from the capability registry instead of a hard-coded list. The queue and the step share one formula for a durable run's id, `workflow_run_id_for_request`, and a run started from chat records the chat step that started it in `chat_invocation`. + * (Ref: #1551, #1543, `functions_orchestration_workflow_runs.py`, `functions_orchestration_workflow_context.py`, `functions_orchestration_registry.py`, `functions_orchestration_schema.py`, `functions_orchestration_planner.py`, `functions_workflow_runtime.py`, [Chat orchestration workflow runs](features/CHAT_ORCHESTRATION_WORKFLOW_RUNS.md)) + +* **Run Workflows From Chat Setting** + * **Admin Settings > Orchestration > Chat Orchestration > Capabilities** adds **Run Workflows From Chat** (`enable_chat_orchestration_workflow_runs`) to the classic and V2 admin pages. It's off by default, because it lets a conversation start work that acts outside it. It's independent of **Propose Workflows From Chat**, and it also needs Chat Orchestration, personal workflows and, when the capability list is narrowed, **Run workflows**. + * With it off, planning is unchanged. A golden test captured before this change pins the stored planning context, its reads, the planner's messages, the repair message, the degraded plan and the capability resolution, with the setting off and workflow proposals on. + * (Ref: `functions_settings.py`, `admin_settings_fields.py`, `route_frontend_admin_settings.py`, `templates/admin/_panes/chat-orchestration.html`, [Orchestration settings](../admin/orchestration.md)) + +#### User Interface Enhancements + +* **Saved Workflows On The V2 Approval Card** + * A plan that starts saved workflows says on its approval card that it always waits for you to run it, and lists each workflow with its trigger. A paused workflow is marked **Paused**, with a note that starting it here runs it once without turning it back on. Auto approval and the countdown never start such a plan. + * The run view shows a **Workflow** row for each run step, and the recovery dialog says a workflow that already started is linked again, never started twice. + * (Ref: `OrchestrationWorkflowRunNotice.tsx`, `OrchestrationPlanCard.tsx`, `OrchestrationRunView.tsx`, `OrchestrationRecoveryNotice.tsx`, `orchestrationPlan.ts`, `orchestrationController.ts`) + +* **Started Workflows Links In The V2 Chat** + * Under the answer, **Started workflows** lists each run the plan started, with the workflow's name and the run's status when the message loaded. **Open run** opens the workflow in Workflows with its run history open at that run. A deleted workflow, or a run that's no longer in its history, shows as unavailable. + * The links come from a requester-only route that writes nothing. When the requester can no longer start workflows from chat, the conversation isn't private any more, or content review removed the response, each link shows as unavailable without the workflow's name, and no workflow or run is read. + * (Ref: `WorkflowRunLinks.tsx`, `orchestrationWorkflowRuns.ts`, `MessageList.tsx`, `workflowRunLink.ts`, `route_backend_orchestration.py`, `functions_orchestration_workflow_run_links.py`, [Chat controls](../reference/chat-controls.md#workflow-runs-v2-interface), [Trigger a workflow](../guides/trigger-a-workflow.md#run-a-workflow-from-chat)) + +### **(v0.261.210)** + +#### Bug Fixes + +* **Images Without OCR Text Are Now Indexed From Their Vision Description** + * Fixed text-free images, such as a photo of an empty room, ending with no search chunk, no title and nothing chat could use, even though the AI vision model had described them. When OCR finds no text, the vision description is now indexed as the image's chunk, so the image is searchable and final metadata extraction runs as usual. + * Document Intelligence Layout output that is only page-number or page-break annotations, such as a stray `` read from floor tape, no longer counts as image text. Header and footer text still does. Failed or empty vision analyses are never indexed. + * Image chunks are now embedded from the same text that is stored, including the vision description, instead of from the OCR text alone. When the description pushes a page that fits past the limit of a non-legacy embedding model, including an image whose only text is its description, the embedding input is cut to the limit and the stored chunk keeps the whole description. + * With Enhanced extraction, a standalone image that the Content Understanding document analyzer returns nothing for is now described by the Content Understanding image analyzer before falling back to Document Intelligence Layout, for workspace uploads and images uploaded straight into a chat. The image analyzer's `Summary` field is now read, which also replaces the `![image](pages/1)` placeholder that images embedded in DOCX and PPTX files were indexed with. + * The multi-modal vision model is now called through the AI connection that hosts it, instead of always using the primary GPT connection, which returned 404 for a model deployed on another connection. The model's chat and image support are checked on that connection first, and Test Vision Analysis uses the same connection as ingestion. + * (Ref: #1583, `functions_documents.py`, `functions_content.py`, `functions_content_understanding.py`, `functions_model_endpoint_types.py`, `route_backend_settings.py`, `route_frontend_chats.py`, [Image Description Indexing Without OCR Text Fix](fixes/IMAGE_DESCRIPTION_INDEXING_WITHOUT_OCR_TEXT_FIX.md)) + +* **Uploaded Files Show What Was Extracted Instead Of "File content not found"** + * Opening a file uploaded into a conversation, including an image, showed "File content not found" whenever the upload was stored as a workspace document. The preview now shows what ingestion extracted: the engine that read the file and why, the AI vision analysis, the title and summary, and the indexed text that search and chat read. It explains when a file is still processing, when processing failed, or when nothing from it was indexed. The classic file popup shows the same text. + * A cited image in V2 chat now shows **What was extracted from this image**, the passage the assistant was given, beneath the picture. + * (Ref: #1583, `route_backend_documents.py`, `functions_chat_upload_extraction.py`, `ChatUploadExtraction.tsx`, `ChatFilePreview.tsx`, `EnhancedCitationViewer.tsx`, [V2 Chat Upload Extraction and Image Card Fix](fixes/V2_CHAT_UPLOAD_EXTRACTION_AND_IMAGE_CARD_FIX.md)) + +* **Image Generation Uses The gpt-image API Version By Default** + * The default Azure OpenAI image generation API version is now `2025-04-01-preview`, the newest dated Azure OpenAI API version and the one documented for gpt-image-1, gpt-image-1.5 and gpt-image-2. The previous `2024-12-01-preview` default predates gpt-image support. + * A saved legacy image version or AI connection image profile still holding the previous default is upgraded automatically when settings load, and a blank legacy value is filled in. Any other saved version is kept, and APIM image versions are never changed because the gateway defines them. Unmigrated legacy direct Images settings with no version also use the new default; an unmigrated APIM route with no version keeps sending `2024-12-01-preview`. + * (Ref: `functions_settings.py`, `functions_image_api_route.py`, `admin_settings_fields.py`, `azure_openai_image_gen_api_version`, [Image Generation API Version Default Fix](fixes/IMAGE_GENERATION_API_VERSION_DEFAULT_FIX.md)) + +#### User Interface Enhancements + +* **Uploaded Images Match Generated Images In V2 Chat** + * An image uploaded into a V2 conversation is now drawn at its own size in a compact card, the same way a generated image is, instead of filling the message column with the picture letterboxed. + * Its actions appear when you point at or tab to the image. **Show sources** opens a panel with what was extracted from it, alongside **Message details**, **Open file preview**, **Use as reference** and **Edit**. + * (Ref: #1583, `MessageList.tsx`, `MessageInspector.tsx`, `MessageActions.tsx`, [Chat controls](../reference/chat-controls.md)) diff --git a/docs/explanation/release-notes/v0.261-part-3.md b/docs/explanation/release-notes/v0.261-part-3.md new file mode 100644 index 000000000..e615ab11f --- /dev/null +++ b/docs/explanation/release-notes/v0.261-part-3.md @@ -0,0 +1,779 @@ +--- +title: "Release notes 0.261.209 – 0.261.166" +description: "SimpleChat release notes for 0.261.209 – 0.261.166." +section: "Reference" +layout: page +--- + + + +# Release notes 0.261.209 – 0.261.166 + +[Back to release notes index]({{ '/explanation/release_notes/' | relative_url }}) + +### **(v0.261.209)** + +#### Bug Fixes + +* **Orchestrated Web Search And Deep Research Access** + * In V2 chat, orchestrated web search, linked-page and deep research steps failed with "A required retained result is unavailable or changed." Orchestration looked up each user's app roles in Microsoft Graph on every call, which needs the `Directory.Read.All` application permission that deployments don't grant by default. Classic chat never makes that call. + * Orchestration now trusts the app roles in the signed-in session, as classic chat does. Each access still rechecks conversation ownership and Control Center access restrictions. No Graph permission, deployer change or setting is needed, and a `Directory.Read.All` grant made only for orchestration can be revoked. + * Orchestrated deep research also refused to start whenever query planning or linked-page planning was on, which is the default. Both planners now run within the existing Knowledge limits, each planner request still follows a fresh attestation of the planner model, and a planning-setting change during a run stops the step. + * A step that continues in the background, where there is no signed-in session, now says so and asks the user to send the request again instead of showing the generic message. Step, saved-wait, finalization and content-preparation failure events log the refusal code as `sc_authority_reason`. + * (Ref: #1509, `functions_orchestration_external_identity.py`, `functions_orchestration_bootstrap.py`, `functions_orchestration_external_configuration.py`, `functions_source_review.py`, `access_failure`, [Orchestration Web Search and Deep Research Access Fix](fixes/ORCHESTRATION_SESSION_IDENTITY_FIX.md)) + +### **(v0.261.208)** + +#### New Features + +* **AI Workflow Assistant Endpoint** + * The server side of the AI workflow assistant. `POST /api/user/workflows/assist` takes one plain-language instruction, such as "run this at 7 AM on weekdays and only alert me when something is urgent", with the V2 editor's current draft of a personal workflow, and returns a proposed edit for the author to review. The answer is `changed` with the edited draft, `explained` when nothing should change, or `question` when the request is ambiguous. The editor's **Ask AI** tab, which calls it, arrives in a later release, so nothing calls the endpoint yet. + * It can propose a name and description, manual, interval and calendar schedules, alert settings, the runner (only agents and models the author can already use), tasks (add, remove, reorder, instructions and inputs), and documents attached with `#`, placed by intent: shared by every task, used by selected tasks, or one task's document target. When the placement is unclear it asks instead of guessing. + * It never saves anything. It can't turn a workflow on or off, change Run as, approvals, sharing or URL access, or edit identity fields, and it explains rather than editing File Sync triggers or For each and If structure. Group workflows and tag references are refused. Every proposal is checked the way saving checks it, with one chance for the model to correct itself, and a proposal that still fails is discarded. The response warns when saving will need Run as re-approved, when a task the assistant added or changed sends email but isn't run by an agent with the Microsoft 365 action, or when the Microsoft 365 connection is missing. + * The model never sees raw document IDs; each document gets a short handle for the request. The draft, earlier turns and document excerpts are all sent as untrusted material, excerpts are bounded, and telemetry records counts, outcomes and error codes only, never instructions, workflows, replies or document text. + * Requests and the model's replies are read the way the browser reads JSON. `NaN`, `Infinity` and numbers JavaScript can't hold are refused, and a whole number such as `12.0` is checked as the integer the editor would save. + * Each person may have one request running and send 20 requests every 10 minutes, across every app instance. The count is a small per-user document in the settings container, which is the endpoint's only write. If that store is unavailable, the assistant answers 503 rather than running without a limit. + * New admin setting **Enable AI Workflow Assistant** (`enable_workflow_ai_assistant`) in **Workflow** settings, on by default. It has no effect while Enable Personal Workflows is off, and it follows the WorkflowUser role requirement. + * (Ref: #1548, #1543, `route_backend_workflows.py`, `functions_workflow_assist.py`, `functions_workflow_assist_operations.py`, `functions_workflow_assist_editor.py`, `functions_workflow_assist_limits.py`, `functions_workflow_assist_runtime.py`, [AI Workflow Assistant](features/WORKFLOW_AI_ASSISTANT.md), [Workflow settings](../admin/workflow.md#workflow-ai-assistant)) + +### **(v0.261.207)** + +#### New Features + +* **Workflows Proposed From Chat** + * With **Propose Workflows From Chat** on, a chat orchestration plan can handle a recurring request in two parts. It answers the current period once, as a preview, and proposes a personal workflow for the runs to come. "Every Monday at 8, read my email and tell me what I should focus on this week" gets this week's answer and a proposed Monday 08:00 workflow. + * The proposal is a new `workflow_propose` step with a new `workflow` deliverable. The planner describes the workflow as a closed blueprint, naming the requester's agents, File Sync sources, the documents they named in the request and their existing workflows only by request-local handles. The workflow draft service checks the blueprint without writing anything. A blueprint that still breaks the rules after one repair is dropped with everything bound to it, and the rest of the plan runs. + * Nothing is created until the requester decides on the proposal card. Accepting the same proposal twice creates one workflow, a denied proposal can't be accepted, and a proposal expires after 14 days. A workflow the requester deleted comes back only through **Create again**. + * Proposals are personal only and appear only in a conversation that's private to the requester. A task on an agent with Microsoft 365 actions runs as the requester, and accepting never approves Run as. URL Access can't be turned on from chat. + * Four requester-only routes serve the card: status, accept, deny and draft. Accepting records the workflow creation in the activity log, as a save from Workflows does. + * (Ref: #1547, #1543, `functions_orchestration_workflow_context.py`, `functions_orchestration_workflows.py`, `functions_orchestration_workflow_proposals.py`, `route_backend_orchestration.py`, [Chat orchestration workflow proposals](features/CHAT_ORCHESTRATION_WORKFLOW_PROPOSALS.md)) + +* **Propose Workflows From Chat Setting** + * **Admin Settings > Orchestration > Chat Orchestration > Capabilities** adds **Propose Workflows From Chat** (`enable_chat_orchestration_workflows`) to the classic and V2 admin pages. It's off by default, because it lets a conversation lead to standing work that runs later, possibly as the user in Microsoft 365. It also needs Chat Orchestration, personal workflows and, when the capability list is narrowed, **Propose workflows**. + * With it off, planning is unchanged. A golden test captured before this change pins the planner's messages, the deliverable brief and the strict-mode error, with the setting off and with it on while proposals aren't available. + * (Ref: `functions_settings.py`, `admin_settings_fields.py`, `route_frontend_admin_settings.py`, `templates/admin/_panes/chat-orchestration.html`, [Orchestration settings](../admin/orchestration.md)) + +* **Chat Plans Use The Browser's Time Zone** + * The V2 chat sends the browser's time zone with each plan request, so "Monday at 08:00" means 08:00 where the user is. The server accepts only a known zone name. The first plan, regenerate and replan, answers to a question card, plan editor revisions and retries all keep it rather than falling back to UTC. + * When a turn may propose a workflow, its answer is written with the user's local date and time, in the words a calendar workflow's run uses, so the preview and the later runs read "this week" the same way. Every other answer is written exactly as before. + * (Ref: `validated_request_time_zone`, `workflow_answer_time_line`, `functions_orchestration_context.py`, `functions_orchestration_composition.py`, `orchestrationController.ts`) + +* **Personal File Sync In The V2 Workflow Editor** + * The V2 editor for personal workflows can now author the **Monitor File Sync changes** trigger, **Run File Sync before each run** and **Use changed files as Analyze targets**, as the V2 group editor and the classic personal editor already could. A personal workflow whose stored source was deleted can now be fixed in V2. + * The source list says when File Sync isn't enabled for the personal workspace, instead of showing an empty list. + * (Ref: `functions_workflow_file_sync_sources.py`, `route_backend_workflows.py`, `WorkflowEditorDialog.tsx`, [V2 personal workflow File Sync](features/V2_PERSONAL_WORKFLOW_FILE_SYNC.md)) + +#### User Interface Enhancements + +* **Workflow Proposal Card In The V2 Chat** + * A proposed workflow appears as a **Proposed workflow** card under the answer. It shows when the workflow runs, about how often, its alerts, the Microsoft 365 data it uses and whose account it runs as, each task's runner, and each task's full instructions under **Instructions**, as plain text. It also lists similar workflows the user already has. + * **Create & start**, **Create paused** and **Edit** decide it. A manual workflow offers **Create** and says it runs only when started from Workflows. **Edit** opens the proposal in the V2 workflow editor, and Save creates it. **Deny** asks for confirmation, and **Open workflow** goes to the created workflow. + * While the workflow is being created, the card checks back every three seconds for up to two minutes, and waits while the tab is hidden. The plan labels the deliverable **Workflow proposal** and shows a prepared one as **Proposed**. The card is the only place the proposal appears; it's never listed with the answer's files. + * (Ref: `WorkflowProposalCard.tsx`, `workflowProposals.ts`, `orchestrationPlan.ts`, [Chat controls](../reference/chat-controls.md#workflow-proposals-v2-interface), [Create a workflow](../guides/create-a-workflow.md#create-a-workflow-from-chat)) + +### **(v0.261.205)** + +#### Bug Fixes + +* **Workflow Flow Arrows Stay Drawn While The Page Updates** + * The execution arrows in the V2 workflow editor's **Flow** surface disappeared whenever the editor re-rendered without changing the diagram: while saving, after a failed save, or while typing in a field outside the diagram. The read-only Flow view did the same on every runtime poll of a running workflow. The arrows usually came back once React Flow measured the blocks again, but a measurement that arrived between two re-renders could leave them missing, which also made the Flow save-failure UI test flaky. + * The Flow canvas now calls the editor's latest handlers through stable wrappers and keeps each unchanged block's React Flow node between renders, so a re-render that doesn't change the diagram changes nothing on the canvas. A block that does change carries the size React Flow last measured, so its arrows stay drawn while it is measured again, and a block whose selection changes is measured again because its wider border moves its handles. + * Selection, collapse, keyboard navigation, focus requests, dragged positions and change tracking behave as before. + * (Ref: #1573, #1543, `WorkflowFlowCanvas.tsx`, `workflowFlowNodeReuse.ts`, [V2 Workflow Flow Canvas Stable Re-renders Fix](fixes/V2_WORKFLOW_FLOW_CANVAS_STABLE_RERENDERS_FIX.md)) + +### **(v0.261.204)** + +#### User Interface Enhancements + +* **Simpler Orchestration Progress In The V2 Chat** + * While Orchestrate plans a question, the reply now shows just the reasoning step toggle and **Thinking**. The **Orchestration** progress card that sat above them is gone. It showed "Current step: Building a plan", a percentage, a step count and a progress bar, which repeated the reasoning step, stayed at 45% while the planner worked, and disappeared as soon as the plan arrived. + * While a plan runs, the plan card still shows how many steps are done and which one is running, with **Review** for the full list. The planner's steps are still under the reasoning toggle. Expanding a saved answer's reasoning no longer shows the card either. + * Tabular analysis and agent replies keep their progress cards. Nothing changes on the server. + * (Ref: `activityLanes.ts`, `ThoughtsList.tsx`, `showsCard`, [Orchestration Duplicate Progress Card Fix](fixes/ORCHESTRATION_DUPLICATE_PROGRESS_CARD_FIX.md)) + +### **(v0.261.203)** + +#### New Features + +* **Change Tracking In The V2 Workflow Editor** + * The V2 workflow editor now shows every unsaved change where it happened. A changed field is framed and labeled **Edited**, with **Previously** for the value it had when the editor opened (long values behind **Show more**) and its own **Revert**, so color is never the only cue. Until now a long editing session left no record of what differed from the saved workflow. + * A removed task leaves a **Removed task** row in place with **Restore**, which puts the saved version back in its saved position, and removed flow blocks are listed under **Removed blocks** above the canvas. Changes are keyed by stable task, block and reference IDs, never by position, so a reorder is one **Task order** change instead of an edit to every task. An added task or block carries **Added · by you** and one Revert for the whole item. + * **Changes** in the editor footer opens a side panel. It lists every unsaved change with its before and after values as plain text, its author, **Jump**, which focuses the field or selects the block and switches surface when needed, and **Revert**. Below that it lists this session's steps with **Restore to here**. Reverts and restores are added as new history steps, so nothing is deleted. The panel sits beside the editor on wide screens and takes its place on narrower ones. + * Saving your own edits is still one click. When the workflow runs as a Microsoft 365 account and a change touches a field the server fingerprints, the panel notes that saving requires re-approving Run as. The client's copy of the fingerprint fields has a parity test that fails when it drifts from the server's. + * This is the editor side of the AI workflow assistant. History entries now record whether you, AI assist or a restore made them, with the assist turn, and typing never coalesces across either. `WorkflowAuthoringSession.applyAssist` applies an AI candidate as one attributed step through the normal confirmation path, and refuses Run as, enablement, identity and approval changes. `revertTurn` reverts one turn and reports what it skipped, and a save that includes AI changes first asks you to **Confirm and save**. Nothing calls `applyAssist` until the Ask AI tab ships. + * Read-only editors show none of this, and a new workflow points out only AI assist changes. Group workflows are tracked like personal ones. No backend route, setting or package is added. + * (Ref: #1548, #1543, `workflowChangeTracking.ts`, `workflowRunAsFingerprint.ts`, `workflowAuthoringHistory.ts`, `WorkflowAuthoringHistory.tsx`, `WorkflowChangeTracking.tsx`, `WorkflowEditorDialog.tsx`, [Workflow Editor Change Tracking](features/WORKFLOW_EDITOR_CHANGE_TRACKING.md), [Create a workflow](../guides/create-a-workflow.md#review-your-changes)) + +#### Bug Fixes + +* **Structured Workflow Alert And File Sync Edits** + * In a structured workflow, changing the alert settings was rolled back with "Workflow edits cannot change saved identity, scope, revision, or runtime metadata." A group workflow's File Sync settings failed the same way. The editor's list of authored fields had neither, so the edit looked like a change to saved identity, and a restore could not bring those settings back. + * Alert and File Sync settings are now authored fields: they apply, follow Undo and Redo, and are covered by Revert and Restore to here. Identity, scope, revision and runtime fields are still refused. + * (Ref: `WorkflowAuthoringHistory.tsx`, `WORKFLOW_AUTHORED_FIELDS`, [Structured Workflow Alert And File Sync Edits Fix](fixes/STRUCTURED_WORKFLOW_ALERT_FILE_SYNC_EDITS_FIX.md)) + +### **(v0.261.202)** + +#### New Features + +* **Workflow Draft Service For Workflows Proposed From Chat** + * Chat orchestration can now check a proposed workflow exactly as saving it would, without writing anything: no workflow, conversation or notification, and no blob, Key Vault or Microsoft 365 call. The check needs no web request, so orchestration can run it in the background. + * A proposed workflow is a small, closed blueprint: a name, a manual, calendar, interval or File Sync trigger, one to five tasks, alert preferences and a Run as choice. It can't name endpoints, URLs, models, secrets or document ids. Documents, agents and File Sync sources are handles that the server has already chosen for the user, and each is authorized for that user. + * The same blueprint always builds the same workflow. Its results reach the notification bell and never pop up. File Sync workflows wait for the sync and continue only when documents changed. Tasks use the default model unless they name an agent the user can use. + * A refusal carries a stable code, such as `cadence_below_minimum`, `agent_unavailable` or `reference_unknown`, a short message that never repeats the input, and the JSON path to fix, so a model can repair its proposal. + * Accepting a proposal creates its workflow once, paused, even if it's accepted twice. The workflow records where it came from in a server-only `origin` that saves can't set or change, and that notes when the owner first edits the workflow. `origin` isn't part of the Microsoft 365 Run as fingerprint, so it never invalidates an approval. + * Personal and group saves behave exactly as before. The save functions now build a workflow, then store it. + * There's no new screen yet. The V2 workflow assistant (#1548) and orchestration's workflow proposals (#1547) will use the service. + * (Ref: #1545, #1543, `functions_workflow_drafts.py`, `functions_personal_workflows.py`, `functions_group_workflows.py`, `functions_workflow_definitions.py`, `functions_workflow_definition_store.py`, [Workflow draft service](features/WORKFLOW_DRAFT_SERVICE.md)) + +* **Limits For Workflows Created From Chat** + * **Admin Settings > Orchestration > Chat Orchestration > Limits** adds **Workflows Created From Chat Per User**, from 1 to 100 with a default of 20, and **Minimum Schedule Interval For Workflows Created From Chat (seconds)**, from 60 to 86,400 with a default of 3,600 (hourly), in the classic and V2 admin pages. + * The larger of this minimum and **Workflow Minimum Schedule Interval** applies. Calendar schedules always pass. Both limits are checked when a workflow is created from chat; workflows users build themselves are unaffected. + * (Ref: `functions_workflow_limits.py`, `functions_settings.py`, `admin_settings_fields.py`, `route_frontend_admin_settings.py`, `templates/admin/_panes/chat-orchestration.html`, [Orchestration settings](../admin/orchestration.md)) + +* **Calendar Workflow Runs Know The Local Date And Time** + * Each run of a workflow with a calendar schedule now tells the model when it started, in the schedule's time zone, for example "Current date and time: Monday, 28 September 2026, 09:00 (America/New_York)". Instructions such as "list this week's to-dos" now mean the week of the run. + * Scheduled runs, catch-up runs and **Run now** all include it, and so does every task of a multi-task workflow. It's never stored in the workflow or its fingerprint. + * Workflows that run by hand or at a fixed interval keep exactly the prompts they had. + * (Ref: `workflow_run_time_context`, `functions_workflow_schedules.py`, `functions_workflow_runner.py`, [Create a workflow](../guides/create-a-workflow.md)) + +#### Bug Fixes + +* **V2 Editor Keeps A Schedule It Can't Show** + * The V2 editor used to put a schedule of its own in place of a stored one it had no fields for, which only a newer server, a direct write or a record saved before a rule existed can leave. A schedule of another kind showed as every 15 minutes, an unknown calendar frequency such as `fortnightly` as daily, and an unknown interval unit as minutes, so `{unit: 'days', value: 2}` became every 2 minutes. A unit stored as `HOURS` became minutes too, and a value it couldn't read became 15. Saving the workflow for any reason, such as editing its description, stored the substitute. + * Now a workflow opens read-only, says why, and keeps its schedule exactly as stored when the editor can't show that schedule exactly: an unknown kind, frequency, unit or weekly day, a `seconds` or `minutes` unit stored other than exactly (the scheduler runs those as hours), or an interval value that isn't a whole number. + * Only what the server's save canonicalizes changes on save, such as `HOURS` becoming `hours`. A whole-number value the save refuses, such as 0 minutes, is shown as stored and named by validation, which blocks the save until it's corrected. Manual workflows, which don't use a schedule, stay editable. + * (Ref: `workflowScheduleForEditor`, `workflowScheduleSupported`, `normalizeWorkflowSchedule`, `normalizeWorkflowDefinition`, `WorkflowScheduleFields.tsx`, [V2 workflow unsupported schedule fix](fixes/V2_WORKFLOW_UNSUPPORTED_SCHEDULE_FIX.md)) + +#### User Interface Enhancements + +* **Schedule Trigger In The V2 Workflow Editor** + * The V2 workflow editor's **Trigger** option for scheduled workflows is now **Schedule** instead of **Interval**, because it offers calendar schedules as well as fixed intervals. Workflows are stored exactly as before, and the classic editor keeps **Interval Schedule**. + * (Ref: `WorkflowEditorDialog.tsx`, [Create a workflow](../guides/create-a-workflow.md), [Trigger a workflow](../guides/trigger-a-workflow.md)) + +### **(v0.261.201)** + +#### New Features + +* **`#` Documents And Tags In The Plan Editor's Ask Planner** + * **Ask planner** in the V2 plan editor now offers `#` documents and tags and **Add context**, like the main composer. Type `#` and part of a name, or browse, and the pick becomes a chip. The request carries the chips you leave, and your turn in the thread shows them afterwards. + * The server checks every pick for you when the request arrives. Only documents and tags you can read now are accepted: in your personal workspace, in groups you belong to and in public workspaces you can see, from workspace types that are turned on, and within a scope-locked conversation's workspaces. A document must also have finished processing. At most 20 go with one request, and a plan can gather at most 100. + * Accepted picks are added to that revision's plan inputs, as question-card answers are. The planner sees them as selected, a document search that names no documents of its own searches them, and tags filter that search. If the plan searched everything you can read before, the planner's reply says its searches are now limited to what you attached. The plan's existing sources stay available. + * A pick that can't be used, because it was deleted, is unready, is no longer readable by you or its workspace was turned off, fails the request before the planner runs, with a message that names it by the label you picked. The plan and its chat don't change, and **Edit and resend** brings back your text and chips. + * Picks are part of the request's identity. A retry of the same request is answered from what was stored and never merges them twice, and the same submission id with different picks is refused with 409 `submission_conflict`. Uploads, `/` saved prompts, `@` mentions and whole workspaces aren't offered here, and the diagram, chart and image editors, the main composer and the question card are unchanged. + * (Ref: #1556, #1543, `functions_assist_references.py`, `functions_orchestration_plan_editing.py` `resolve_plan_edit_references`, `functions_orchestration_plan_revisions.py`, `route_backend_orchestration.py`, `planReferences.ts`, `OrchestrationPlanEditor.tsx`, `AssistThread.tsx`, [V2 Shared Assist Thread](features/V2_SHARED_ASSIST_THREAD.md), [Chat interface controls](../reference/chat-controls.md), [Review and edit orchestration plans](../guides/review-and-edit-orchestration-plans.md)) + +* **Workspace Reference Authorizer Without A Conversation** + * `resolve_scope_references` authorizes `#` document and tag references for a user with no conversation and no request state, so AI-assist inputs outside the chat, such as the plan editor now and the workflow assistant later, can share it. It refuses chat attachments, whole workspaces and the chat scope, keeps the question card's readiness checks and count bound, reads no document text, and reports a refusal by the label the user picked and a reason code, never by a title read from the server. + * The question card's own check now runs on the same core. A golden test captured from the previous code holds its results and messages unchanged. + * (Ref: `functions_orchestration_context.py` `resolve_scope_references`, `resolve_elicitation_references`, `functional_tests/test_orchestration_reference_authorizer_golden.py`, `functional_tests/test_orchestration_scope_reference_authorizer.py`) + +### **(v0.261.200)** + +#### New Features + +* **Shared AI Assist Thread In The V2 Editors** + * The **Ask AI** tab of the diagram, chart and image editors and the **Ask planner** tab of the plan editor now share one conversation thread. Your message joins it the moment you send it and the input clears, instead of both waiting for the server's answer. + * While a request runs, its reply shows **Working…** with the seconds elapsed and a **Cancel** button. A failed or cancelled request stays in the thread with its error, **Retry** and **Edit and resend**, which puts your text back in the input. + * Cancel in the plan editor discards the pending change on the server. In the other editors it stops waiting; if the server finishes the change anyway, the editor recognises it as yours and shows the latest version rather than reporting a conflict. + * The image editor keeps a transcript of this visit's changes, held on the page only and never saved. **Create image from reference** keeps a separate one. + * A thread lives outside its editor, so closing and reopening the editor keeps a running request and any unsent text. + * (Ref: #1552, #1543, `AssistThread.tsx`, `assistThread.ts`, `assistThreadStore.ts`, `assistLimits.ts`, `DiagramEditor.tsx`, `ChartEditor.tsx`, `ImageEditor.tsx`, `OrchestrationPlanEditor.tsx`, [V2 Shared Assist Thread](features/V2_SHARED_ASSIST_THREAD.md)) + +* **Client Submission Ids For Assist Requests** + * The diagram, chart and image assist routes, personal and shared, accept an optional `submission_id` and store it on both turns of the exchange. A request without one behaves exactly as before. + * A retry whose id is already stored is answered from what was stored, with `"replayed": true`, without calling the model, writing a second revision or notifying a shared conversation twice. The same id sent with a different instruction is refused with 409 `submission_conflict`, and a malformed id with 400. + * Stored ids let the editor match the message it showed early to the stored turn, so a shared conversation's live update arriving before your reply doesn't show the message twice. Ids are never shown to the model. + * A retry sent while the request it repeats is still running is checked again after the model answers: the image routes check the copy they read after the call, and the personal diagram and chart route checks when its write loses to the first request's. The retry is then replayed or refused rather than storing a second version. An image conflict found after the model call returns the versions that won, and a personal diagram or chart write lost to another change returns 409 with the current revisions instead of a 500 when an id was sent. + * The plan edit route holds its existing `submission_id` to the same format and stores it on the plan's edit chat turns. Because it holds an id to the first request it saw, the plan editor's **Retry** reuses an id only for the identical request, and sends a fresh one after Cancel or a step change instead of being refused for good. + * (Ref: `functions_assist_submissions.py`, `functions_image_edit.py`, `functions_message_block_revisions.py`, `functions_message_image_revisions.py`, `functions_orchestration_plan_editing.py`, `functions_orchestration_plan_revisions.py`, `route_backend_chats.py`, `route_backend_collaboration.py`, `route_backend_orchestration.py`, `planSubmissionIds.ts`, `orchestrationController.ts`) + +#### Bug Fixes + +* **Diagram And Chart Editors Closing On Their Own** + * An open diagram or chart editor closed whenever its message changed, for example when a revision was saved. The message's masks were re-parsed on every change, which remounted each diagram and chart, and a diagram returned a different element tree while it re-rendered. Both now keep the editor mounted, so it stays open until you close it. + * (Ref: `AssistantMarkdown.tsx`, `MermaidDiagram.tsx`) + +#### User Interface Enhancements + +* **Keyboard, Limits And Announcements In Assist Inputs** + * Enter sends and Shift+Enter adds a line in all four editors, with Ctrl+Enter and ⌘+Enter kept as aliases. The plan editor previously sent only with Ctrl+Enter or ⌘+Enter. + * A counter shows the length against the 2,000-character limit and says how much to remove past it. Over-limit text is refused rather than silently cut off, as the old inputs' `maxLength` did. + * The thread is announced politely to screen readers as a log. The assist inputs reuse the main composer's editor in a restricted mode without uploads, `/` saved prompts or `@` mentions. + * (Ref: `ComposerEditor.tsx` `restricted`, `AssistThread.tsx`, [Chat interface controls](../reference/chat-controls.md)) + +### **(v0.261.199)** + +#### New Features + +* **V2 Workflow Alert Notices** + * A workflow alert that needs attention now pops up in V2. A small notice hangs from **My Workspace** in the navigation rail, or flies out beside its icon when the rail is collapsed or on a phone. It never takes focus, so typing isn't interrupted. Until now V2 only counted these alerts in the bell, so a critical alert waited there until someone opened the panel. + * Only pop-up alerts appear: alerts on every run, and alert rules whose delivery is **Pop-up alert**, or **Default for severity** at medium, high or critical. **Notification bell only** alerts never pop up. Only alerts from the last 24 hours pop up, so coming back after a few days doesn't bring a pile of them; older ones stay in the bell. + * Each alert pops up in one V2 tab of the browser, however many are open, claimed through `localStorage` under a Web Lock. A notice waits while its tab is hidden, a dialog or the bell's panel is open, or the phone's navigation menu is open. + * Info, low and medium notices tuck into the bell after eight seconds, pausing while pointed at or focused. High and critical notices stay until opened or closed. Closing a notice tucks it into the bell, where it stays unread, and it doesn't pop up again. + * Opening the notice grows it into the full alert card: why you're seeing it and the rules that matched, the summary and detail, what went wrong for a failed run, enrichment chips, the alert's links, and **Open workflow**, which opens the workflow's list in its personal or group workspace on the run that raised the alert, even when that list is already open. **Mark read**, **Dismiss**, "1 of 3" with **Next**, and **Mark all read** handle the alerts. Closing the card leaves them unread. + * A notice goes away only when a successful read shows its alert was read, dismissed or has aged out. A failed read keeps it, and so does a count of zero, which the server also answers when it can't count; the zero is checked with a read first. A count that rises as the reader comes back to the tab is always read, even right after another read. + * Several alerts from one workflow share one notice with a count, such as "Failed 4 times since 9:40 AM", and the most urgent comes first. Priorities use the V2 theme's colors in light and dark, and are always named in words. Motion animates only transform and opacity, and becomes short fades under reduced motion. Screen readers announce each notice once without interrupting, and a critical alert straight away. Alert text is shown as plain text, and links open only pages on this site. + * Developers can try the notice and card with sample alerts at `/v2/dev/alert-lab` on the Vite dev server, which now also answers page loads at `/v2` with the app. The lab is loaded lazily from inside the development-only branch, so production builds never reference it. + * (Ref: #1553, #1543, `WorkflowAlertNotice.tsx`, `WorkflowAlertCard.tsx`, `workflowAlertStore.ts`, `useWorkflowAlertRuntime.ts`, `workflowAlertClaims.ts`, `workflowAlertNotices.ts`, `WorkflowsSection.tsx`, `AlertLabPage.tsx`, `App.tsx`, `vite.config.ts`, [V2 Workflow Alert Notices](features/V2_WORKFLOW_ALERT_NOTICES.md), [Manage notifications](../guides/manage-notifications.md)) + +#### Bug Fixes + +* **Bounded Workflow Alert Pop-up Query** + * `GET /api/notifications/workflow-alerts` read every workflow alert a user had within the 60-day TTL and filtered it in Python, on every check. The unread, not-dismissed and not-notify-only filters now run in one parameterized Cosmos query, limited with `TOP` and kept within the user's partition. Classic's pop-up gets the same alerts at the same cadence. + * The route accepts an optional `since_hours`, a whole number from 1 to 1440, to return only recent alerts; V2 asks for 24. Any other value is refused with `400` before anything is read. The response shape is unchanged. + * With `since_hours`, a read that fails now answers `500` instead of an empty list, so V2 doesn't take a storage error for "nothing unread" and drop the notice it is showing. Classic doesn't pass `since_hours` and still gets the empty list. + * New workflow alerts record whether their workflow is personal or group, and which group (`workflow_scope` and `workflow_group_id`), so V2's **Open workflow** finds the right list. The group isn't recorded as `group_id`, because classic makes that the active group when a notification is opened. + * (Ref: `functions_notifications.py`, `route_backend_notifications.py`, `functions_workflow_runner.py`, [V2 Workflow Alert Notices](features/V2_WORKFLOW_ALERT_NOTICES.md#server-the-bounded-query)) + +### **(v0.261.195)** + +#### New Features + +* **V2 Notification Bell And Panel** + * The V2 navigation rail has a notification bell beside the application's name. It shows the unread count, up to **9+**, or a dot when the rail is collapsed, and opens a panel of your notifications, newest first: workflow alerts, Microsoft 365 approval requests, "AI responded" notices, document processing results, share requests and the rest. Until now V2 users could only see these on the classic Notifications page. + * Each notification can be opened, marked read or dismissed, and **Mark all read** clears the lot. Chat links open the conversation in V2, workspace and profile links open their V2 pages, and approvals, pending Microsoft 365 actions and workflow activity open in classic until V2 has those pages. + * Links are followed only when they stay on this site and name a page. A link whose path would lead to another site once followed, such as `/.//host`, and an API endpoint however its path is escaped are refused, and a classic page's address is checked again just before it opens. Notification text is shown as plain text, because it can quote email or web content. + * Changes hold. A slow answer from the server can't undo a read, dismissal or **Mark all read** you just made, **Load more** doesn't skip a notification after you dismiss one, and a change the server refuses is put back and the count read again. + * The count refreshes when you return to the tab and every 30 seconds while it is visible, backing off to five minutes while nothing changes. It doesn't poll in a hidden tab, and stops once the session has signed out. + * A reply you watch finish is marked read at once, so it doesn't add to the bell. A reply that finishes while you are in another tab or on another V2 page stays unread, with its "AI responded" notice, until you come back to it. + * (Ref: #1554, #1543, `NotificationBell.tsx`, `NotificationPanel.tsx`, `notificationStore.ts`, `notificationLinks.ts`, `chatStore.ts`, [V2 Notification Bell and Desktop Notifications](features/V2_NOTIFICATIONS_BELL.md), [Manage notifications](../guides/manage-notifications.md)) + +* **V2 Desktop Notifications** + * **User Settings > Preferences > Desktop notifications** now works. When an administrator has enabled desktop notifications and the preference is on, V2 raises one operating system notification per reply that finishes while the tab is hidden or unfocused. It names the conversation, never the reply, and clicking it opens the conversation. + * The rules and the preference are shared with classic. Completed orchestrated answers notify too. V2 asks for browser permission when you send, when you turn the preference on, or from **Allow notifications**, and treats the preference as off until it has loaded. + * (Ref: `desktopNotifications.ts`, `PreferencesTab.tsx`, `Composer.tsx`, `orchestrationController.ts`, [Desktop Conversation Notifications](features/DESKTOP_CONVERSATION_NOTIFICATIONS.md)) + +### **(v0.261.194)** + +#### New Features + +* **Documents Record Where They Came From** + * Documents that a workflow or a chat creates now record their origin on each document version: the workflow and run, or the conversation and message, plus any orchestration run and step. The server derives it only from bindings it already holds (the artifact's source, its workflow or orchestration producer, the publication receipt, and chat-upload metadata), never from client input or model output. + * Origins are recorded for generated artifacts published from chats, orchestrations, workflow saved outputs and workflow runs, and for files uploaded in chat and saved to a workspace. A workflow run that publishes through the chat artifact pipeline is recorded as workflow output. Workspace uploads, File Sync, the external API and SimpleChat action uploads record no origin, and a new version uploaded by hand has none. + * Documents a workflow saves get a removable `workflow` tag. In group and public workspaces it's added only when the publisher can manage tags there; the origin is recorded either way. Chat-created documents get no tag. + * The V2 document details pane shows **Created by *workflow* · run *time*** or **Created in chat · *conversation title***, linking to that run in the workflow history or to the conversation. Readers who can't open the origin, or whose origin was deleted, see only "Created by a workflow" or "Created in a chat", with no names, titles or IDs. + * No client can set or edit an origin. Every guarded document mutation route, including the bearer-token external API, refuses origin fields with `document_origin_server_managed`. Responses carry at most `origin_kind`, and single-document reads can ask for an access-checked `origin_summary` with `origin_summary=1`. + * The personal, group and public document list routes accept `origin_workflow_id`, `origin_run_id` and `origin_conversation_id` filters, applied inside each list's existing parameterized, scoped Cosmos query. No search-index or deployer changes are needed. + * (Ref: #1555, #1543, `functions_document_provenance.py`, `functions_documents.py`, `functions_artifact_publication.py`, `content_screening/access.py`, `route_frontend_chats.py`, `DocumentDetailsPane.tsx`, `WorkflowRunHistory.tsx`, [Document Provenance](features/DOCUMENT_PROVENANCE.md), [Uploading and managing documents](../guides/upload-and-manage-documents.md)) + +### **(v0.261.193)** + +#### New Features + +* **Calendar Schedules For Workflows** + * A scheduled personal or group workflow can now run at a local time instead of at a fixed interval: daily, on weekdays, on chosen days of the week, or on a day of the month, in an IANA time zone. For example, "Mondays 08:00 America/New_York" runs at 12:00 UTC in summer and 13:00 UTC in winter. + * Runs keep their local time through daylight saving changes. A time the clocks skip runs later by the amount skipped (02:30 becomes 03:30 in New York), and a time that happens twice runs once, the first time. A monthly day that a month doesn't have, such as the 31st, runs on that month's last day, including 28 or 29 February. + * Group workflows that monitor File Sync can use calendar schedules too. A missed run still runs once when the scheduler catches up, then waits for the next scheduled time. + * Existing interval workflows keep their stored schedules byte for byte, so their Microsoft 365 Run as approvals stay valid. + * The inbound MCP workflow summary includes each workflow's schedule and a readable label. + * (Ref: #1544, #1543, `functions_workflow_schedules.py`, `functions_personal_workflows.py`, `functions_group_workflows.py`, `functions_workflow_editor.py`, `functions_mcp_server_tools.py`, [Workflow calendar schedules](features/WORKFLOW_CALENDAR_SCHEDULES.md), [Create a workflow](../guides/create-a-workflow.md)) + +* **Workflow Minimum Schedule Interval** + * **Admin Settings > Workflow** adds **Workflow Minimum Schedule Interval (seconds)**, from 1 to 86,400, in the classic and V2 admin pages. Its default, 1, allows every interval, as before. + * It's checked only when an interval schedule is created or changed. Workflows already saved on a shorter interval keep running and can be re-saved unchanged. Calendar schedules are never checked. + * A refused save names the minimum, for example "This schedule runs more often than the administrator allows. Choose an interval of at least 5 minutes." + * (Ref: `functions_workflow_limits.py`, `functions_settings.py`, `admin_settings_fields.py`, `route_frontend_admin_settings.py`, `templates/admin/_panes/workflow.html`, [Workflow settings](../admin/workflow.md)) + +#### User Interface Enhancements + +* **Calendar Schedule Fields In The V2 Workflow Editor** + * A new **Repeats** field chooses a fixed interval, daily, weekdays, weekly on chosen days, or monthly on a day of the month, with **Time** and **Time zone** fields. New schedules start in the browser's time zone when the server offers it, otherwise in UTC with a note saying so. + * The editor reads the schedule back, such as "Schedule: Mondays 08:00 America/New_York", and the workflow list shows the same label. The editor checks the server's schedule rules and the administrator's minimum before saving. + * (Ref: `WorkflowScheduleFields.tsx`, `WorkflowEditorDialog.tsx`, `WorkflowsSection.tsx`, `workflowSettings.ts`, `workflowEditor.ts`) + +* **The Classic Editor Sends Calendar Workflows To V2** + * The classic workspace editor edits only fixed intervals, so opening a calendar-scheduled workflow there says "This workflow uses a calendar schedule. Open V2 to edit it without losing its configuration. Run and Cancel remain available here." instead of replacing its schedule. The classic list shows calendar schedules' labels. + * (Ref: `workflowNeedsNativeEditor`, `static/js/workspace/workspace_workflows.js`) + +### **(v0.261.192)** + +#### New Features + +* **Uploaded Images Display Inline In React V2 Chat** + * A PNG, JPG, BMP, or TIFF image uploaded in chat now appears in the thread as a picture with a full-size viewer, instead of a file-name chip. TIFF images are converted to PNG for display; the original upload is unchanged. + * While a workspace upload is still processing, the card shows **Processing image…** and a local preview when the browser can draw one. An image held for review shows **Unavailable pending review**. + * HEIC and HEIF photos are not converted, so they render only in Safari. Other browsers show the file card with a hint to convert the photo to JPG or PNG. + * Shared (collaborative) conversations keep the file chip. + * (Ref: `functions_image_formats.py`, `route_backend_conversations.py`, `route_enhanced_citations.py`, `route_frontend_chats.py`, `MessageList.tsx`, `chatImagePreview.ts`, [Chat Image Uploads and Reference Images](features/CHAT_IMAGE_UPLOADS_AND_REFERENCE_IMAGES.md)) + +* **Reference Images For Image Generation** + * With **Image** on, pictures you attach, paste, drop, or pick from a workspace are sent to the image model as visual input, not only as their extracted text. Requests such as "make a cartoon of my house" or "turn this map into an architectural diagram" can now use the picture itself. + * **Use as reference** adds any uploaded or generated image in the conversation to the next image request. + * The number of references follows the selected image model: up to 10 for GPT Image models (the SimpleChat limit), 10 for FLUX.2 flex, 8 for FLUX.2 pro, and 1 for MAI Image and FLUX Kontext. Generation-only models keep attachments disabled in Image mode. GPT Image 1 and 1.5 are also asked to preserve reference details closely. + * Every reference is authorized and screened before it is sent. Metadata is stripped and the image is resized for the model. HEIC and HEIF are refused with a clear message, and references are never silently dropped. + * (Ref: `functions_image_references.py`, `functions_chat_image_references.py`, `functions_image_adapters.py`, `functions_image_capabilities.py`, `route_backend_chats.py`, `model_capabilities.json`, `Composer.tsx`, `imageReferences.ts`, [Generate images](../guides/generate-images.md)) + +* **Edit An Uploaded Image Into A New Image** + * **Edit** on an uploaded image opens the editor as **Create image from reference**. Describe the change and, when the model supports masking, select a region. **Create new image** adds a new AI image to the conversation and leaves the upload unchanged. + * (Ref: `ImageEditor.tsx`, `chatStore.ts`, `route_backend_chats.py`, [V2 Inline Image Editing](features/V2_INLINE_IMAGE_EDITING.md)) + +* **Reference Images And Requested Styles In Orchestrated Image Tasks** + * With **Orchestrate** and **Image** on, planned **Generate image** tasks can use attached and conversation images as references. Review shows each reference on the task, and you can remove it before approving. References are checked again when the task runs, and the images used are recorded as the task's sources. + * Planned images now follow the style you ask for, including photorealistic images, instead of always being illustrations. Every generated image is still captioned **AI-generated image** and is never presented as a real photograph or as found on the web. + * (Ref: `functions_orchestration_context.py`, `functions_orchestration_images.py`, `functions_orchestration_schema.py`, `functions_orchestration_registry.py`, `functions_orchestration_deliverables.py`, `OrchestrationRunView.tsx`, [Review and edit orchestration plans](../guides/review-and-edit-orchestration-plans.md)) + +* **Vision Chat Models See Images From The Current Message** + * When a vision-capable chat model answers directly, up to four images attached to the current message, or selected as image documents, are sent as pixels alongside their extracted text. Older images stay in the history as text only. + * Agents and models without vision keep the existing text-only behavior. An image that can't be loaded, including HEIC, is skipped without failing the turn. + * (Ref: `functions_chat_vision.py`, `route_backend_chats.py`, [Upload documents in chat](../guides/upload-documents-in-chat.md)) + +### **(v0.261.191)** + +#### Bug Fixes + +* **Comparisons Of Table-Heavy Documents No Longer Stop On Quote Formatting** + * A V2 chat comparison of a document with many tables, such as a narrative PDF compared with a CSV, could end **Partially completed**, with the comparison step failing with "The operation did not produce the complete named results declared by the plan." That step writes the comparison as text and never creates a file. It stopped because Analyze had returned only a partial result for the PDF. + * Analyze rejected correctly quoted evidence whenever the quote's formatting differed from the stored text. Document Intelligence stores tables as HTML and keeps line breaks, while models quote tables as readable text. A quote now also matches when it differs only in line breaks or repeated spaces, table or inline markup, entities, emphasis marks, invisible characters, typographic quotes and dashes, Unicode forms, or letter case. It must still be in the chunk it cites, and start and end on whole words and numbers. Paraphrases, partial words and numbers, and changed numbers or symbols are still rejected, and saved evidence is always the exact source text. + * Analyze now records qualifications, such as a unit the text doesn't state, as caveats on the finding, and observations about a slice as notes. Neither makes the result partial, and both appear in the report, as **Caveat:** lines and under **Analysis notes**. In a comparison, each file's preparation step is now asked only for what its own file contributes, and the comparison step does the comparing. + * When a step can't run because an earlier step returned only a partial result, it now reports `input_partial_not_accepted` instead of `result_invalid`, with a message that says so, and its log names the earlier step's capability. A new `Final findings validated` event gives the counts behind a partial Analyze result, and the window page-range debug log no longer prints `None:None`. + * A run that already failed still fails on retry, because the retry reuses the saved partial result. Ask the question again. + * (Ref: #1540, `functions_document_analysis_results.py`, `functions_document_analysis.py`, `functions_orchestration_planner.py`, `functions_orchestration_result_runtime.py`, `functions_orchestration_executor.py`, `docs/reference/logging-tags.md`, [V2 Comparison Analyze Evidence Matching Fix](fixes/V2_COMPARISON_ANALYZE_EVIDENCE_MATCHING_FIX.md)) + +### **(v0.261.190)** + +#### New Features + +* **Group And Public Workspaces Are Native In V2** + * Group and public workspaces now work in the React V2 interface, on the design of My Workspace. Choosing a workspace on its V2 page selects it, as the classic interface does, and loads its details, your role and the sections you can use without leaving V2. + * Group workspaces cover documents, prompts, actions, agents, connections (identities, file sources and endpoints), workflows, and management: the directory, members, settings, activity and statistics. + * Public workspaces cover the directory, where you choose which workspaces chat uses and save visibility lists, and documents, prompts and management: members, identities and file sources, settings, activity and statistics. Every reader can browse a public workspace's documents, and download them when downloads are enabled; the Owner, Admins and DocumentManagers manage them and approve generated artifacts. + * The workspaces were built in stages from 0.261.127, and each version's entries describe what it added. Deleting a group or public workspace, the legacy document upgrade, managing an inactive group, and chatting with every visible public workspace at once stay in the classic interface. + * (Ref: #1542, #1541, [Use group workspaces in V2](../guides/manage-group-workspaces.md), [Use public workspaces](../guides/use-public-workspaces.md), [V2 Shared Workspace Context and Group Shell](features/V2_SHARED_WORKSPACE_CONTEXT.md)) + +#### Bug Fixes + +* **Tabular Steps Work With Re-uploaded CSV And Workbook Files** + * Fixed orchestration runs, such as a PDF-and-CSV comparison, stalling on the tabular step when the CSV or workbook had been re-uploaded under the same name. The step stopped before reading any rows, the run later reported that the step's saved result could not be confirmed (`result_commit_unconfirmed`), and the step kept showing **Reasoning**. + * The tabular step's replay descriptor carries its own schema version, which was compared with the file's revision even after the source's screening provenance had matched. A matching provenance is now authoritative. Sources without provenance keep their existing checks, and a source that really changed still conflicts. + * Revision conflicts now log `sc_authority_reason`, naming the check that failed, without logging document values. + * Merged from the React V2 branch at `01c728bf`, where it shipped as 0.261.143. This branch had already used 0.261.143 for its group details fix, so here the fix carries the merge's version. Its own documentation and tests keep the React V2 branch's number. + * (Ref: `content_screening/access.py`, `docs/reference/logging-tags.md`, [Re-uploaded Tabular Source Revision Conflict Fix](fixes/REUPLOADED_TABULAR_SOURCE_REVISION_CONFLICT_FIX.md)) + +### **(v0.261.189)** + +Merged from the React V2 branch at `c9faeae0`. These changes shipped there as 0.261.141 and 0.261.142. This branch had already used those numbers for its own changes, so here they carry the merge's version. The version numbers inside their own documentation and tests are the React V2 branch's. + +#### New Features + +* **Native OneNote Workspace Uploads In React V2** + * Upload `.one` sections and `.onepkg` notebooks to personal, group, or public workspaces to search and chat with their typed notes and table contents. + * React v2's document explorer shows the shared supported-format catalog and validates both file selection and drag-and-drop. + * Keeps each notebook package as one workspace document while preserving section names, page titles, and subpage context in searchable text and citations. + * Uses a bounded local extractor, with explicit errors for incomplete, unsafe, unsupported, or oversized files. Handwriting, image OCR, embedded attachments, and direct chat attachment uploads are not included. + * **Deployment requirement:** Rebuild the application container, or build the native extractor when running from source. Existing upload limits still apply; larger notebooks may require a higher configured limit, up to the extractor's 128 MiB input ceiling. Updating this source branch does not deploy the application. + * On this branch the V2 group and public document explorers share the personal explorer, so they also show the supported-format catalog and check file types on selection and drag-and-drop. + * Shipped in the React V2 branch as 0.261.142. + * (Ref: #1525, `config.py`, `functions_onenote.py`, `functions_documents.py`, `route_backend_v2.py`, `DocumentExplorer.tsx`, `native/onenote_extractor/`, `Dockerfile`, [Native OneNote Ingestion](features/ONENOTE_INGESTION.md)) + +#### Bug Fixes + +* **Orchestration Compare, CSV And Word Runs Complete** + * Comparing a PDF with a CSV, creating a CSV from a model's answer, and turning an illustrated report into a Word file each produced a correct plan and then failed while running it. They now complete, and their failures are recorded with their reasons. + * Shipped in the React V2 branch as 0.261.141. + * (Ref: [Orchestration Execution Failures Fix](fixes/ORCHESTRATION_EXECUTION_FAILURES_FIX.md)) + +### **(v0.261.188)** + +#### User Interface Enhancements + +* **Dialogs Keep And Return Keyboard Focus** + * Every V2 dialog now moves focus into itself when it opens, keeps Tab inside it, and returns focus to the control that opened it when it closes, including with Escape. Before, closing a dialog left focus on the page body. + * (Ref: `Modal.tsx`, [V2 Workspace Presentation Fix](fixes/V2_WORKSPACE_PRESENTATION_FIX.md)) + +* **A Failed List Says So, With A Retry** + * In the group and public Prompts, Identities, File sources, Endpoints and Workflows sections, a list that fails to load shows the error with a **Retry** button, announced to screen readers, instead of looking empty with a create button. + * (Ref: `SectionList`, `useSectionResource`, the prompt workbench, [V2 Workspace Presentation Fix](fixes/V2_WORKSPACE_PRESENTATION_FIX.md)) + +* **Readable Tags, Phones And Larger Text** + * Tag chips pick black or white text for contrast with the tag's colour; the prompt list leaves room for its details at 200% text; group Actions and narrow cards stay usable on a phone; locked overview entries are outlined rather than faded. + * Public workspace wording no longer mentions groups, actions or classic browsing where they don't apply, and a link to a deleted public document says it's gone. + * (Ref: [V2 Workspace Presentation Fix](fixes/V2_WORKSPACE_PRESENTATION_FIX.md)) + +### **(v0.261.187)** + +#### Bug Fixes + +* **The V2 Directory No Longer Makes Unavailable Workspaces Visible For Chat** + * An inactive public workspace, or one whose status isn't recognized, shouldn't be searched in chat, but public chat checks only which workspaces you've made visible. The V2 directory's switch, **Show all in chat** and **Use this list** could all make such a workspace visible. + * Now the switch can only turn an unavailable workspace off, and the bulk and saved-list actions leave it hidden and say how many they skipped. The server-side check in public chat is a follow-up. + * (Ref: `PublicDirectoryList.tsx`, `PublicDirectoryPage.tsx`, `publicVisibility.ts`, `publicDirectory.ts`, [V2 Public Directory](features/V2_PUBLIC_DIRECTORY.md)) + +### **(v0.261.186)** + +#### Bug Fixes + +* **Readers Can Download Public Workspace Documents In V2** + * When downloads are turned on for a public workspace, classic lets every reader download its documents, but V2 offered downloads only to its Owner, Admins and DocumentManagers. V2 now follows classic: anyone who can read the workspace can download, individually or in a batch. + * Changing documents stays with the Owner, Admins and DocumentManagers, and a reader never learns of a generated file still awaiting approval. + * (Ref: `functions_public_document_policy.py`, `functions_public_document_access.py`, `functions_workspace_context.py`, [Public Reader Downloads Fix](fixes/PUBLIC_READER_DOWNLOADS_FIX.md)) + +### **(v0.261.185)** + +#### New Features + +* **Public Workspace Settings, Activity And Statistics In V2** + * A public workspace's Owner and Admins now manage its settings in V2, under **Manage**: the Owner edits the name, description, color and logo, and the Owner and Admins edit file downloads and retention. They also read its recent activity, and chart and export its statistics, which DocumentManagers can see too. + * New routes name the workspace in the path, under `/api/public-workspaces//settings` and `/insights`. Writes carry a per-section revision and go through the public workspace guard; a failed read says so instead of returning an empty list. The classic routes are unchanged. + * The group Settings, Activity and Statistics sections are now shared with public workspaces; the group's behaviour and text are unchanged. + * Deleting the workspace still happens on the classic page. Settings explains that it removes only the workspace record, and quotes V2's own count of the workspace's current documents. + * (Ref: `route_backend_public_settings.py`, `functions_public_settings.py`, `functions_public_settings_policy.py`, `functions_public_insights.py`, `WorkspaceSettingsSection.tsx`, [V2 Public Settings](features/V2_PUBLIC_SETTINGS.md), [Public Settings APIs](features/PUBLIC_SETTINGS_APIS.md)) + +#### Bug Fixes + +* **Classic Public Statistics Show Storage Correctly** + * The classic manage public workspace page read "2 undefined" for storage past a terabyte, and "NaN undefined" for a missing size, in its statistics and exported CSV. It now writes B to TB, capped at TB, and "0 B" for no size. + * (Ref: `manage_public_workspace.js`, [Classic Public Stats Export Fix](fixes/CLASSIC_PUBLIC_STATS_EXPORT_FIX.md)) + +* **A Malformed Timestamp No Longer Hides The Group Activity Feed** + * One activity record whose timestamp couldn't be converted to UTC made the whole V2 group Activity feed fail. That record now shows without a time, and the rest of the feed is shown. + * (Ref: `functions_group_insights.py`, [Group Activity Timestamp Overflow Fix](fixes/GROUP_ACTIVITY_TIMESTAMP_OVERFLOW_FIX.md)) + +### **(v0.261.184)** + +#### New Features + +* **Ask To Manage A Public Workspace's Documents From The V2 Directory** + * A signed-in user who doesn't manage a public workspace can ask to manage its documents from the workspace's row in the V2 directory, and cancel the request while it's pending. The Owner and Admins decide in Members, as before. + * New routes `POST` and `DELETE /api/public-workspaces//membership/requests` act only on the caller's own request. The directory row reports `membership: "pending"` for the caller, and nothing about anyone else. + * (Ref: `functions_public_membership.py`, `route_backend_public_membership.py`, `functions_public_directory.py`, `PublicDirectoryList.tsx`, [V2 Public Directory](features/V2_PUBLIC_DIRECTORY.md), [Public Membership APIs](features/PUBLIC_MEMBERSHIP_APIS.md)) + +* **Public Directory Visibility Tools In V2** + * **Show all in chat** and **Hide all from chat** set every workspace at once, and saved lists store and reuse a set of visible workspaces, in the classic directory's settings shape. They cover the whole directory, up to 1,000 workspaces, and report what they changed. + * **Chat with visible (classic)** opens classic chat over the visible workspaces, since V2 chat scopes to one public workspace at a time. + * (Ref: `PublicDirectoryVisibilityTools.tsx`, `publicDirectory.ts`, `publicVisibility.ts`, `PublicDirectoryPage.tsx`, [V2 Public Directory](features/V2_PUBLIC_DIRECTORY.md)) + +### **(v0.261.183)** + +#### Bug Fixes + +* **Unapproved Public Artifacts Are No Longer Shown To Every Reader** + * A generated file waiting for approval to be published into a public workspace was shown to everyone who can read the workspace, which is every signed-in user: V2 listed it with its title, abstract and tags, counted it, and returned its details and its requester; chat's document picker listed it too. + * Now only the workspace's Owner, Admins and DocumentManagers see it before approval, in a held form marked "Awaiting generated artifact approval". To everyone else it doesn't exist until it's approved. + * The chat document list and its tag counts leave pending artifacts out for everyone, since none can be used in chat until it's approved. Classic chat and the classic workspace page's tag filter use the same routes, so they change too. + * (Ref: `functions_public_document_policy.py`, `functions_public_document_reads.py`, `route_backend_public_documents.py`, [Public Pending Artifact Visibility Fix](fixes/PUBLIC_PENDING_ARTIFACT_VISIBILITY_FIX.md)) + +### **(v0.261.182)** + +#### New Features + +* **Manage Public Workspace Identities And File Sources In V2** + * A public workspace's Owner, Admins and DocumentManagers now keep its identities and File Sync sources in V2, in the **Identities** and **Sync** sections, when File Sync is enabled for the workspace. They're the group sections, with public rules: writes only while the workspace is active, read-only in locked and upload-disabled workspaces, and nothing for readers. + * New routes name the workspace in the path, `/api/public-workspaces//identities` and `/file-sources`. Their writes are conditional, stored secrets are never returned, and a source's documents are deleted only when asked, as public documents. The classic routes are unchanged. + * The identity editor offers only the File Sync use, and the source editor says "Use a saved workspace identity". + * (Ref: `route_backend_public_identities_scoped.py`, `route_backend_public_file_sources_scoped.py`, `functions_public_identity_policy.py`, `functions_public_file_source_policy.py`, `identityWorkbench.ts`, `fileSourceWorkbench.ts`, [V2 Public Connections](features/V2_PUBLIC_CONNECTIONS.md), [Public Connection APIs](features/PUBLIC_CONNECTION_APIS.md)) + +### **(v0.261.181)** + +Merged from the React V2 branch at `d243b0ca`. These changes shipped there as 0.261.134, 0.261.137, 0.261.138, 0.261.139 and 0.261.140. This branch had already used those numbers for its own changes, so here they carry the merge's version. The version numbers inside their own documentation and tests are the React V2 branch's. + +#### New Features + +* **Orchestration Plans Deliver What You Asked For** + * A Gather / Reason / Render plan now lists what you asked to receive before its steps: the answer, each file with its format, images with their count, charts, and diagrams. The server checks the list against what it can actually produce. A file must come from a Render task in the same format, so "create a csv of states and capitals" produces the CSV file itself instead of rows in the chat. + * Something that cannot be produced here stays on the list as unavailable, with the server's own reason, such as "This file format is not available for this plan." The planner cannot promise it in a step title, mention it only in an assumption, or call something unavailable that the server can produce. A plan that breaks these rules gets one correction attempt, then fails with a clear message. + * Shipped in the React V2 branch as 0.261.138. + * (Ref: `functions_orchestration_deliverables.py`, `functions_orchestration_planner.py`, `functions_orchestration_schema.py`, [Orchestration Deliverables](features/ORCHESTRATION_DELIVERABLES.md)) + +* **Generated Images In Orchestrated Answers And Files** + * Images you ask for, such as "an image of each president", are generated as planned tasks when the plan runs. Approving the plan is the consent, and in Run automatically mode your request is. Each image appears inline in the answer with the usual viewer and editor, is captioned as an AI-generated illustration, and is embedded in DOCX, PDF, and PowerPoint files. A plan generates at most four images. + * A file can embed only images its own content was prepared from, with bytes checked against what was generated. Images the planner only suggests remain approval cards. + * Shipped in the React V2 branch as 0.261.138. + * (Ref: `functions_orchestration_images.py`, `functions_orchestration_rendering.py`, `functions_orchestration_bootstrap.py`, `functions_image_generation.py`, [Create files with orchestration](../guides/create-files-with-orchestration.md)) + +#### Breaking Changes + +* **Gather / Reason / Render Is The Only Orchestration Plan Contract** + * The **Gather / Reason / Render harness (preview)** admin setting (`enable_chat_orchestration_harness`) and its server-side admission check were removed. Every new plan uses Gather / Reason / Render whenever **Enable Chat Orchestration** is on. A stored value of the old setting is dropped when settings load or save. + * The earlier phase-based contract (Knowledge / Reasoning / Output phases and a final **Answer** step) was deleted. A saved capability list that still names its Answer capability (`respond`) is read as **Prepare content** (`compose`). The stored list is not rewritten, so runs saved before the upgrade can still be continued or retried, and nothing adds **Create a file** or **Generate images** to a narrowed list. + * Plans saved by the earlier contract no longer open or rerun. Opening, editing, retrying, running or cancelling one shows "This plan was created by an earlier orchestration version and can't be opened or rerun. Start a new request." Such runs are left out of the conversation's run list and the planner's history of earlier runs; deleting the conversation still removes their saved data. + * Orchestrated agent steps now always run under the acquisition checks that previously applied only to Gather / Reason / Render plans. They admit Azure AI Foundry (classic) agents without local actions, workspace knowledge or web sources; other agents, including local Semantic Kernel agents, are refused with a safe failure instead of running unchecked. + * **Migration**: Start a new request for any conversation whose earlier orchestration plan no longer opens. Administrators who narrowed the capability list and want files or images in plans should select **Create a file** and **Generate images**. + * Shipped in the React V2 branch as 0.261.139. + * (Ref: `functions_settings.py`, `functions_orchestration_schema.py`, `functions_orchestration_registry.py`, `route_backend_orchestration.py`, `templates/admin/_panes/chat-orchestration.html`, [Deliverable Planning Fix](fixes/ORCHESTRATION_DELIVERABLE_PLANNING_FIX.md), [Orchestration settings](../admin/orchestration.md)) + +#### Bug Fixes + +* **Orchestration Execution And Saved Status Accept Cosmos SDK Responses** + * Fixed valid plans failing before their first step with `context_unavailable`, followed by missing-run responses and repeated **Checking execution status** notices. + * Storage boundaries now normalize Cosmos SDK dictionary responses while retaining ownership checks, conditional ETags, execution leases, and deletion guards. Scheduler recovery can read the saved attempt without resetting its original deadline or silently creating another execution. + * Shipped in the React V2 branch as 0.261.140. + * (Ref: `functions_orchestration_bootstrap.py`, `functions_orchestration_plan_revisions.py`, `functions_orchestration_recovery.py`, `functions_orchestration_output_store.py`, `ORCHESTRATION_COSMOS_RESPONSE_COMPATIBILITY_FIX.md`) + +* **Planner And Execution Failures Have Specific, Safe Diagnostics** + * Planner rejection events now retain the exact deliverables rule and correction-attempt number. Admission and recovery events identify the failing stage, response type, and whether a durable outcome was recorded. + * Application Insights preserves these diagnostic fields and hashed workflow identifiers without logging raw prompts, document content, or provider responses. Required source/output checks and the existing single repair budget remain in place. + * Shipped in the React V2 branch as 0.261.140. + * (Ref: `functions_orchestration_deliverables.py`, `functions_orchestration_planner.py`, `functions_orchestration_execution.py`, `functions_appinsights.py`, `docs/reference/logging-tags.md`) + +* **Writing, Comparison, And File-Only Plans Match Their Deliverable Contract** + * Fixed ambiguous planning guidance that produced answer declarations with both a file format and a quantity. One repair removed the format but then failed on the remaining quantity; the prompt now specifies fields by deliverable kind and asks corrections to check the whole declaration. + * File-only plans can omit `final_response` or use null without inventing an extra answer step. Malformed bindings and declared answers without a valid text producer are still rejected. + * Selected sources now carry server-resolved file types, so a PDF/CSV comparison can use compatible narrative and tabular steps rather than send the CSV to narrative-only comparison. Type mismatches and missing Analyze source bindings are caught during planning and share the existing single correction budget. + * Shipped in the React V2 branch as 0.261.140. + * (Ref: `functions_orchestration_planner.py`, `functions_orchestration_schema.py`, `functions_orchestration_context.py`, `functions_orchestration_plan_editing.py`, `ORCHESTRATION_PLANNER_DELIVERABLE_FIELDS_FIX.md`) + +* **One Planner Carries All Of The Planning Guidance** + * The planner's single prompt now includes the earlier prompt's guidance on choosing agents and actions, searching before analyzing and passing the found sources on, choosing between web search and deep research by cost and need, using conversation, saved memory and earlier runs, writing self-contained step tasks, visuals, and when and how to ask clarifying or file questions. + * A plan the planner writes incorrectly is refused instead of being silently repaired, so an approved plan is exactly the work that runs. The step that writes the chat answer cannot be disabled while editing a plan. + * Shipped in the React V2 branch as 0.261.139. + * (Ref: `functions_orchestration_planner.py`, `functions_orchestration_plan_revisions.py`, [Chat Orchestration](features/CHAT_ORCHESTRATION.md), [Gather / Reason / Render orchestration](features/ORCHESTRATION_GATHER_REASON_RENDER.md)) + +* **Missing Files And Images Are Never Reported As Delivered** + * Fixed orchestrated answers that said "I can't attach a .docx", left "[Insert Image here]" placeholders, or reported success without the requested file. The answer step is told when a later task saves its output as a file, so it writes the finished content. + * A run in which a requested image or file was not produced is reported as incomplete. A deterministic **Delivery notes** list after the answer names what was not delivered or is not available, such as "2 of 3 images were generated." + * A missing image makes the run retryable. The retry reuses the images that were generated, generates the missing one, and writes the answer again with every image. A retry is not offered when it could only send again an image prompt the image service declined; ask again with a different description instead. The chat recovers an attempt that already delivered a file one file at a time, so ask again to regenerate a missing image there. + * Shipped in the React V2 branch as 0.261.138. + * (Ref: `functions_orchestration_composition.py`, `functions_orchestration_execution.py`, `functions_orchestration_executor.py`, `functions_orchestration_recovery.py`, [Deliverable Planning Fix](fixes/ORCHESTRATION_DELIVERABLE_PLANNING_FIX.md)) + +* **A Large Or WEBP Image No Longer Fails The Whole File** + * Fixed a Word, PDF, or PowerPoint file failing because one generated image was over 4 MB, such as a detailed 1536x1024 illustration, or was a WEBP image. The file now embeds a copy the document format accepts, re-encoded and scaled down only as needed, and the chat keeps the original image. + * Shipped in the React V2 branch as 0.261.138. + * (Ref: `functions_orchestration_rendering.py`, `functions_orchestration_result_contracts.py`, [Orchestration Deliverables](features/ORCHESTRATION_DELIVERABLES.md)) + +* **Generated Images Never Offer A Paid Second Approval** + * Fixed a planned image showing as an **Approve** card, which bought a duplicate image when clicked, until the chat loaded it, and fixed an earlier answer losing its images after a retry. Each answer now lists the images it shows, the chat loads them when the run finishes, and a planned image's card shows the image or says it is loading. Approving one through the API returns the saved image instead of generating another. Conversation exports and the classic chat show the same images. + * Shipped in the React V2 branch as 0.261.138. + * (Ref: `functions_orchestration_execution.py`, `functions_orchestration_events.py`, `functions_image_generation.py`, `route_backend_chats.py`, `route_backend_conversation_export.py`, `InlineImageProposal.tsx`, `MessageList.tsx`, `imageProposalSpec.ts`) + +* **Orchestrate Remembers Auto And Pinned Models** + * Fixed the Orchestrate model choice resetting to a specific model whenever you left the chat or opened a new chat, which meant choosing **Auto - choose per step** again on every visit. + * The choice is now saved to your account, so Auto or a pinned model stays selected across chats, reloads, and devices. A model pinned for Orchestrate does not change the model normal chat uses. + * Fixed the normal chat model sometimes snapping back to the model that was selected when the page loaded after you left the chat and returned. + * Shipped in the React V2 branch as 0.261.137. + * (Ref: `Composer.tsx`, `orchestrationModelRouting.ts`, `route_backend_users.py`, [Model Picker Persistence Fix](fixes/ORCHESTRATION_MODEL_PICKER_PERSISTENCE_FIX.md)) + +* **Gather / Reason / Render Answers Match What Orchestration Could Already Do** + * Fixed orchestrated requests quietly falling back to legacy plans when **Auto - choose per step** was selected. Legacy plans cannot create files, so "create a csv" or "create a word file" produced inline text instead. With the Gather / Reason / Render harness enabled, Auto requests now plan that work, and each step runs on its own authorized model, which is checked again before the run. Editing or restoring an Auto plan now keeps Auto routing instead of silently dropping it. + * The answer step now receives saved memory and the relevant earlier messages, so follow-ups such as "put those in a table" work. It also includes the charts, Mermaid diagrams, and image proposal cards the planner asks for, and places charts drawn from an action's exact rows. + * Fixed answers refusing well-known facts ("I don't have source evidence... to provide an accurate CSV") and filling reports with "Verify with a reputable source". The planner now declares what each answer may rely on: general knowledge, gathered sources only, or both. When an optional search fails, the answer is still written and says what could not be checked. If another step also fails, retrying the run from its failed step now searches again and rewrites the answer with the results, instead of stopping with "Saved step inputs changed". + * Shipped in the React V2 branch as 0.261.134. + * (Ref: `functions_orchestration_composition.py`, `functions_orchestration_execution.py`, `functions_orchestration_executor.py`, `functions_orchestration_planner.py`, `functions_orchestration_schema.py`, [Deliverable Planning Fix](fixes/ORCHESTRATION_DELIVERABLE_PLANNING_FIX.md)) + +* **Web Search Failures Explain Themselves, Retry Once, And Link Their Sources** + * A failed web search now reports a specific reason, such as a timeout, an HTTP status, or a service that isn't configured, instead of "This operation could not complete." Read-only gathering retries once after a temporary service error. + * Web search results no longer pass raw citation markers such as 【3:1†source】 into answers. Each marker becomes a numbered link to its source, in both ordinary chat and orchestration. + * Shipped in the React V2 branch as 0.261.134. + * (Ref: `functions_web_search_results.py`, `route_backend_chats.py`, `functions_orchestration_adapters.py`, [Deliverable Planning Fix](fixes/ORCHESTRATION_DELIVERABLE_PLANNING_FIX.md)) + +* **Auto Model Routing Plans Data Analysis, Research, And Actions On Common Deployments** + * Fixed Auto refusing whole plans with "No eligible connected model for Structured data analysis" (or Reasoning) when models such as gpt-4o or gpt-4.1 were connected. Few catalog profiles rate those tasks, so when no connected model is rated for a step's task, the step now runs on the capable model best rated for general answering, and its reason says so. Rated models still win, and a model rated unsuitable for the task, an archived profile, or a model without a required capability such as tool calling is never used. + * Fixed every Auto-routed action and deep research step in a Gather / Reason / Render plan being refused by the external-source check, which rebuilt the step's model from the run's empty Auto selection instead of the step's approved binding. + * The answer is now credited to the model that wrote it. A reply that reuses an earlier turn's result keeps the default model rather than borrowing another step's. An Ask-planner revision that cannot be assigned models now reports that no eligible model is available and keeps the previous plan. + * Shipped in the React V2 branch as 0.261.134. + * (Ref: `functions_orchestration_model_routing.py`, `functions_orchestration_external_metadata.py`, `functions_orchestration_plan_editing.py`, [Choose models for orchestration](../guides/model-catalog-routing.md), [Deliverable Planning Fix](fixes/ORCHESTRATION_DELIVERABLE_PLANNING_FIX.md)) + +* **Whole-Run Retries Of Plans With Files Render Their Own Files** + * Fixed a whole-run retry of a Gather / Reason / Render plan ending as failed whenever the new attempt had to render a file that the previous attempt had already created or admitted. The new attempt listed the previous attempt's withdrawn file as its own, tried to reuse a file that was no longer available, or was refused when rendering the same content again. + * Each attempt now owns its files. A retry renders them again from saved content, without calling a model, and the previous attempt's files are shown as superseded. React V2 still offers **Retry from failed step** only for an attempt without files, and **Retry file** for a single failed file is unchanged. + * Shipped in the React V2 branch as 0.261.134. + * (Ref: `functions_orchestration_recovery.py`, `functions_orchestration_services.py`, [Create files with orchestration](../guides/create-files-with-orchestration.md), [Deliverable Planning Fix](fixes/ORCHESTRATION_DELIVERABLE_PLANNING_FIX.md)) + +#### User Interface Enhancements + +* **Plans Show Only What Runs** + * The plan card, plan panel and run view no longer group steps into Knowledge / Reasoning / Output phases or show an **Always runs** answer step. Steps appear in dependency order with their Gather, Reason or Render role. + * A finished run's steps show their real status, such as **Completed** or **Failed**, instead of **Will run**. Opening a plan saved by the earlier contract shows the message above instead of a spinner. + * A run from the earlier contract that a tab was still following, including one restored after a reload, no longer stays **running** and blocks Stop and retries in its conversation. Its recovery notice shows the message above without offering to check or review it. + * Admin capability help now explains that plans need **Prepare content** to write an answer or a file's content and **Create a file** to deliver files. + * Shipped in the React V2 branch as 0.261.139. + * (Ref: `OrchestrationRunView.tsx`, `OrchestrationPlanCard.tsx`, `OrchestrationMapView.tsx`, `OrchestrationRecoveryNotice.tsx`, `orchestrationPlan.ts`, `orchestrationController.ts`, `orchestrationResume.ts`, `orchestrationErrors.ts`, [Review and edit orchestration plans](../guides/review-and-edit-orchestration-plans.md), [Chat controls](../reference/chat-controls.md)) + +* **You Asked For** + * The plan panel and approval card list what you asked for, each item's state, and the task that produces it. Unavailable items appear in a warning color with their reason. Generate image tasks show their prompt, and the chat loads generated images as soon as the run finishes. + * The Image control's notice now reads "Orchestrate will plan the images you ask for and generate them when the plan runs." + * Shipped in the React V2 branch as 0.261.138. + * (Ref: `OrchestrationDeliverables.tsx`, `OrchestrationRunView.tsx`, `OrchestrationPlanCard.tsx`, `orchestrationPlan.ts`, `Composer.tsx`, [Review and edit orchestration plans](../guides/review-and-edit-orchestration-plans.md), [Chat controls](../reference/chat-controls.md)) + +* **Orchestrate Model Picker Under Manual Controls** + * The Orchestrate model picker now sits in the normal model picker's place under **Manual controls**, instead of appearing above the message box, so the toolbar keeps its shape when Orchestrate is switched on. + * **Auto - choose per step** is the default wherever a connected model has a catalog profile rated for general answering. Auto is not offered when no model qualifies, and when an administrator hides Manual controls, Orchestrate uses Auto without a picker. + * Shipped in the React V2 branch as 0.261.137. + * (Ref: `Composer.tsx`, [Chat controls](../reference/chat-controls.md), [Choose models for orchestration](../guides/model-catalog-routing.md)) + +* **Planner Model Dropdown In Admin Settings** + * Orchestration → Planner model is now one dropdown on both the V2 and classic Admin Settings pages, replacing four text boxes for the deployment name, model ID, endpoint ID, and provider. + * It lists the same models as the default chat model picker, or the classic deployments when AI Connections are off, and writes the four settings for you. **Use the answer model (default)** keeps planning on whichever model answers. + * A saved planner model that is no longer listed stays selected and is labelled as missing rather than being cleared, and V2 refuses saves the runtime could never resolve. + * Shipped in the React V2 branch as 0.261.137. + * (Ref: `OrchestrationPlannerModelPicker.tsx`, `admin_orchestration_planner_model.js`, `chat-orchestration.html`, `admin_settings_fields.py`, [Orchestration settings](../admin/orchestration.md#chat-orchestration-planner-model-section)) + +* **See Who Planned The Work** + * The plan panel and approval card now name the model that wrote the plan and how it was chosen. Each answer task shows its answer basis and requested visuals, and optional inputs are labeled. + * Shipped in the React V2 branch as 0.261.134. + * (Ref: `OrchestrationRunView.tsx`, `OrchestrationPlanCard.tsx`, `orchestrationPlan.ts`, [Review and edit orchestration plans](../guides/review-and-edit-orchestration-plans.md)) + +### **(v0.261.180)** + +#### Bug Fixes + +* **Public Member Import Lists Only Public Roles** + * The V2 public Members section's CSV import said roles could be user, admin or document_manager, and accepted `user` rows that the server then refused one by one. A public workspace assigns only Admins and DocumentManagers. + * The dialog now says "Roles are admin or document_manager.", and a `user` row is refused when the file is read, before anything is sent. The group import is unchanged. + * (Ref: `groupMembership.ts`, `ImportMembersDialog.tsx`, `MembersSection.tsx`, [Public Member CSV Roles Fix](fixes/PUBLIC_MEMBER_CSV_ROLES_FIX.md)) + +### **(v0.261.179)** + +#### New Features + +* **Manage Public Workspace Members In V2** + * A public workspace's Owner and Admins now manage its Admins and DocumentManagers in V2, in a **Members** section under **Manage**. They can add members, change roles, remove members, and decide DocumentManager requests, and the Owner can transfer ownership. It's the group Members section, with public rules. + * New routes name the workspace in the path, `/api/public-workspaces//membership/...`, write conditionally, and record adds, role changes and removals in the activity log. + * The V2 public directory offers **Create** to people allowed to create public workspaces. + * (Ref: `route_backend_public_membership.py`, `functions_public_membership_policy.py`, `MembersSection.tsx`, `PublicMembersSection.tsx`, `functions_public_directory_policy.py`, [V2 Public Members](features/V2_PUBLIC_MEMBERS.md), [Public Membership APIs](features/PUBLIC_MEMBERSHIP_APIS.md)) + +#### Breaking Changes + +* **DocumentManagers No Longer See Members' Emails In V2** + * Classic shows every member's email to every member. In V2's public Members section, the Owner and Admins see emails and a DocumentManager sees names and roles only. The server withholds the emails (decision 18). The classic page is unchanged. + * **Migration**: none. + +### **(v0.261.178)** + +#### User Interface Enhancements + +* **Group Members Can Run Group Workflows In V2** + * Classic, and the server, let every group member run and cancel a group workflow, but V2 offered **Run** and **Cancel** only to workflow managers. Every member of an active group now gets them; creating, editing and deleting stay with the workflow manager roles. + * The workspace context publishes each member's workflow operations from the same policy the routes use, so V2 never offers an action the server would refuse. + * (Ref: `functions_group_workflow_policy.py`, `functions_workspace_context.py`, `WorkflowsSection.tsx`, [V2 Group Workflows](features/V2_GROUP_WORKFLOWS.md)) + +### **(v0.261.177)** + +#### New Features + +* **Public Workspace Prompts In V2** + * A public workspace's prompts now open in the V2 prompt workbench. Every signed-in user can read and use them. Owners, Admins and DocumentManagers can create, edit and delete them in an active workspace, and a conflicting edit keeps your draft. + * New routes name the workspace in the path, `/api/public-workspaces//prompts`, so a change can't land in a different workspace from the one on screen. + * **Use in chat** works for a prompt in a public workspace you've hidden from chat. It fetches just that prompt and changes no setting. + * (Ref: `route_backend_public_prompts_scoped.py`, `functions_public_prompt_policy.py`, `lib/promptWorkbench.ts`, `Composer.tsx`, [V2 Public Prompts](features/V2_PUBLIC_PROMPTS.md), [Public Prompt APIs](features/PUBLIC_PROMPT_APIS.md)) + +#### Breaking Changes + +* **Classic Public Prompt Changes Need An Active Workspace** + * The classic `/api/public_prompts` create, update and delete routes wrote in any workspace status. They now answer 403 "This public workspace is not accepting prompt changes right now." unless the workspace is active, matching the new routes. Reading is unchanged. + * **Migration**: none. Make the workspace active before changing its prompts. + +### **(v0.261.176)** + +#### Bug Fixes + +* **Statistics Date Ranges Are Limited To 366 Days** + * The classic group, public workspace and profile statistics accepted any custom range between 2000 and 9998, and the profile route had no bound at all. A range of millions of days made one request take seconds of processing and over a gigabyte of memory, and return a very large response. + * Every statistics route now refuses a custom range longer than 366 days with "Choose a date range of 366 days or fewer.", as the V2 group statistics already did. The profile statistics also refuse dates outside 2000-01-01 to 9998-12-31. + * (Ref: `functions_stats_windows.py`, `route_backend_groups.py`, `route_backend_public_workspaces.py`, `route_frontend_profile.py`, [Statistics Window Span Limit Fix](fixes/STATS_WINDOW_SPAN_LIMIT_FIX.md)) + +* **Public Workspace Role Changes Check Permission First** + * A public workspace role change or ownership transfer looked the member up in the directory before checking that the caller could make the change. It now checks first. A transfer to an older member entry keeps the member's name and email, and Control Center's ownership approvals keep every member. + * (Ref: `route_backend_public_workspaces.py`, `route_backend_control_center.py`, [Public Workspace Writer Safety Fix](fixes/PUBLIC_WORKSPACE_WRITER_SAFETY_FIX.md)) + +### **(v0.261.175)** + +#### New Features + +* **A Public Workspace Directory In V2** + * **Public Workspaces** in V2 now opens a directory of every public workspace you can discover, with **All** and **My workspaces** views, search, and paging kept in the address. Open a workspace, or choose which ones your public chat searches with **Visible for chat**, the same setting the classic directory uses. + * It reads a new route, `GET /api/public_workspaces/directory`, which the V2 public workspace picker now uses too. Its rows carry no owner email, unlike the classic list route. + * The directory, public workspace pages, the picker and the chat handoffs use the public workspace names an administrator configures. + * (Ref: `route_backend_public_directory.py`, `functions_public_directory.py`, `PublicDirectoryPage.tsx`, `lib/publicVisibility.ts`, `lib/publicWorkspaceLabels.ts`, [V2 Public Workspace Directory](features/V2_PUBLIC_DIRECTORY.md), [Public Directory APIs](features/PUBLIC_DIRECTORY_APIS.md)) + +### **(v0.261.174)** + +#### User Interface Enhancements + +* **Content Screening Scans From A Group's Documents** + * A group's Owner, Admins and DocumentManagers now get **Screening scans** in the V2 group Documents section, the same modal personal documents offer. They can scan the group, follow recent scans with cancel, resume and retry, see the policy additions, and open Content review. + * The server decides who sees it: the workspace context publishes the members the screening routes accept, so a member is never offered a control the server would refuse. + * (Ref: `DocumentsSection.tsx`, `ScreeningWorkspaceControls.tsx`, `functions_workspace_context.py`, [V2 Group Document Management](features/V2_GROUP_DOCUMENT_MANAGEMENT.md)) + +### **(v0.261.173)** + +#### Bug Fixes + +* **Public Workspace Changes No Longer Overwrite Each Other** + * Every classic writer of a public workspace, 27 in all, saved a copy it had read earlier. They covered members, requests, roles, ownership, settings, logo, downloads, retention, tags and seven Control Center actions. So one change could undo another made at the same moment, and a save could bring back a deleted workspace. They now write conditionally, keep unrelated changes, never recreate a deleted workspace, and answer "The public workspace changed while your request was being saved. Try again." when a workspace keeps changing. + * Admins promoted in classic can now see and decide document manager requests, add members and receive ownership. A legacy member entry no longer breaks the members routes, and a role change or ownership transfer keeps the member's name and email. + * Unknown workspace statuses now get the inactive permissions instead of the active ones. Statistics refuse out-of-range dates with a reviewed message instead of failing. The update, download and logo routes answer reviewed messages instead of raw error text, including for an oversized image. Invalid download and retention settings are refused. + * A tag change that loses to a concurrent change answers the coded vocabulary conflict, and metadata edits and bulk tagging save nothing when refused. + * (Ref: `functions_public_workspaces.py`, `route_backend_public_workspaces.py`, `route_backend_public_documents.py`, `route_backend_control_center.py`, `route_backend_retention_policy.py`, [Public Workspace Writer Safety Fix](fixes/PUBLIC_WORKSPACE_WRITER_SAFETY_FIX.md)) + +### **(v0.261.172)** + +#### Bug Fixes + +* **Ignoring A Browsed File Takes Effect** + * In the V2 group file source editor, **Ignore** sent the file's path under the source's root, but the sync engine tracks each file by its full remote path, so the ignore never matched and the file kept syncing. Folders were also offered Ignore, which the engine can't honour. + * Browse now gives every file its full remote path, built the way the engine builds it for SMB, Azure Files, Azure Blob and OneDrive. **Ignore** and **Restore** send it, and appear on files only. + * (Ref: `functions_file_sync.py`, `FileSourceEditorDialog.tsx`, [File Source Browse Ignore Fix](fixes/FILE_SOURCE_BROWSE_IGNORE_FIX.md)) + +### **(v0.261.171)** + +#### New Features + +* **Choose What A Group File Source Syncs, In V2** + * The V2 group file source editor now sets the four remaining classic options: the folders and files to sync under the root (picked with Browse or typed), fixed tags for every synced file (with the group's existing tags as suggestions), how folders become tags, and what happens to the SimpleChat copy when a source file is deleted. + * Editing a source keeps all four unless you change them, and a conflict reload merges them like the other fields. + * (Ref: `FileSourceEditorDialog.tsx`, `lib/fileSourceFields.ts`, [V2 Group File Sources](features/V2_GROUP_FILE_SOURCES.md)) + +#### Bug Fixes + +* **Browsing A Group File Source Works For SMB Sources** + * V2 Browse sent the source's root as the folder to list, which the server resolves under the root, so every SMB source answered an error. Choosing an entry also overwrote the root. + * Browse now starts at the root, opens folders by their path under it, and never changes the root. + * (Ref: `FileSourceEditorDialog.tsx`, [File Source Browse Path Fix](fixes/FILE_SOURCE_BROWSE_PATH_FIX.md)) + +### **(v0.261.170)** + +#### Bug Fixes + +* **Editing A Workspace Identity Keeps Its Managed Identity Client ID** + * Editing a workspace identity that signs in with a user-assigned managed identity erased its client ID, in the classic editor and the V2 group editor, so it silently fell back to the default managed identity. Only identities whose client ID was set through the API were affected, since neither editor can set it. + * The identity details now carry the client ID and a service principal's tenant, both non-secret, and both editors send the client ID back unchanged. The classic editor serves personal, group and public workspaces and the admin global identities, so all of them are fixed. + * (Ref: `functions_workspace_identities.py`, `lib/identityFields.ts`, `workspace-identities.js`, [Identity Credential Round Trip Fix](fixes/IDENTITY_CREDENTIAL_ROUND_TRIP_FIX.md)) + +### **(v0.261.169)** + +#### Bug Fixes + +* **Sharing And Publication Decisions Stay Bound To Their Workspace** + * The V2 document collaboration adapters built their requests for the workspace they were created for, but checked the server's receipt against the caller's scope as it was when the receipt arrived. A caller that changed its scope object in between would see a confirmed decision rejected as unconfirmed. + * Each adapter now keeps one frozen copy of its scope for the request, the receipt check and the public review's workspace name. The V2 Documents sections already created a new scope for each workspace, so nothing changes for users today. + * Found by the full comparison of the React V2 base branch's tests against this branch, which also aligned three tests that had gone stale on this branch. + * (Ref: `lib/documentCollaboration.ts`, `test_v2_group_document_collaboration.mjs`, [Document Collaboration Bound Scope Fix](fixes/DOCUMENT_COLLABORATION_BOUND_SCOPE_FIX.md)) + +### **(v0.261.168)** + +#### Bug Fixes + +* **Refused Group Tag Edits Save Nothing** + * A metadata edit or bulk tag in a group saved the documents before it checked the group's tag definitions. If someone changed the group's tags at that moment, the request was refused with "Refresh and retry", although the documents had already been saved. + * The definitions are now written first, so a refused edit saves nothing, and a refused bulk tag reports one conflict for the whole batch. + * (Ref: `functions_group_document_management.py`, [Group Tag Definitions First Fix](fixes/GROUP_TAG_DEFINITIONS_FIRST_FIX.md)) + +#### User Interface Enhancements + +* **A Refused Upload Says Why** + * A group or public workspace manager who drops files where uploads are disabled, or into a locked workspace, is now told why instead of getting a generic message. + * (Ref: `lib/documentAccessCopy.ts`, `DocumentExplorer.tsx`, [Shared Workspace Document Access Copy Fix](fixes/SHARED_WORKSPACE_DOCUMENT_ACCESS_COPY_FIX.md)) + +### **(v0.261.167)** + +#### Bug Fixes + +* **A Lost Group Tag Change Says What Happened** + * When a group's tags changed while someone was creating, recolouring or renaming a tag, the answer depended on timing. A change caught early said "The group's tags or permissions changed. Refresh and retry." A change that landed during the write said "The resource changed. Refresh and retry the operation.", with no code. + * Both now answer 409 with the first sentence and `error_code: vocabulary_conflict`. The final vocabulary step of a rename or delete, and bulk tagging, report the same sentence and code. + * (Ref: `functions_group_document_management.py`, [Group Tag Vocabulary Conflict Code Fix](fixes/GROUP_TAG_VOCABULARY_CONFLICT_CODE_FIX.md)) + +#### User Interface Enhancements + +* **Shared Workspaces Say Who Can Add Documents** + * An empty group or public Documents section no longer tells a viewer who can't upload to "Use the classic workspace to manage files", which couldn't help them either. It says who can add documents, or why no one can right now: uploads disabled, a locked workspace, or a status that doesn't allow it. + * A viewer with no document operations who tries a change is told the same way, rather than being sent to classic. Public workspaces now say "public workspace" instead of "group". + * (Ref: `lib/documentAccessCopy.ts`, `DocumentExplorer.tsx`, `DocumentsSection.tsx`, [Shared Workspace Document Access Copy Fix](fixes/SHARED_WORKSPACE_DOCUMENT_ACCESS_COPY_FIX.md)) + +### **(v0.261.166)** + +#### User Interface Enhancements + +* **Call Agent Copy Matches What You Can Do** + * In a group's **Actions** section, the **Call agent** introduction promised a choice even when the manager below it was read-only, as it is for members and when group actions are off. It now describes the agents and triggers instead, and offers the choice only when you can make it. + * (Ref: `GroupWorkspacePage.tsx`, [Group Call Agent Read-Only Copy Fix](fixes/GROUP_CALL_AGENT_READ_ONLY_COPY_FIX.md)) diff --git a/docs/explanation/release-notes/v0.261-part-4.md b/docs/explanation/release-notes/v0.261-part-4.md new file mode 100644 index 000000000..f6acca4c0 --- /dev/null +++ b/docs/explanation/release-notes/v0.261-part-4.md @@ -0,0 +1,686 @@ +--- +title: "Release notes 0.261.165 – 0.261.104" +description: "SimpleChat release notes for 0.261.165 – 0.261.104." +section: "Reference" +layout: page +--- + + + +# Release notes 0.261.165 – 0.261.104 + +[Back to release notes index]({{ '/explanation/release_notes/' | relative_url }}) + +### **(v0.261.165)** + +#### New Features + +* **Group Settings, Activity And Statistics In V2** + * Group owners and admins can manage a group's profile, logo, file downloads and retention, read its recent activity, and chart and export its statistics in the V2 group workspace, under **Manage** beside Members. + * The server decides who sees what and which controls work, and a locked control says why. Unsaved edits survive switching browser tabs and conflicting saves, a failed save keeps them, and **Discard changes** puts an editor back to the saved settings. + * Statistics reuse the personal charts and export dialog. The export writes the classic group CSV column for column. + * Deleting a group is still finished on the classic page, from a **Delete this group** section in Settings that shows how many documents remain. + * (Ref: `GroupSettingsSection.tsx`, `GroupActivitySection.tsx`, `GroupStatisticsSection.tsx`, `lib/groupSettings.ts`, `lib/groupStats.ts`, `functions_workspace_context.py`, [V2 Group Settings, Activity and Statistics](features/V2_GROUP_SETTINGS.md)) + +#### User Interface Enhancements + +* **Manage Group (Classic) Only Where It's Still Needed** + * The group header's **Manage group (classic)** button now appears only in an inactive group or one whose status isn't recognized. Everything else it offered is native in V2. + * (Ref: `GroupWorkspacePage.tsx`, [V2 Group Settings, Activity and Statistics](features/V2_GROUP_SETTINGS.md)) + +### **(v0.261.164)** + +#### Bug Fixes + +* **V2 Shows A Failure's Sentence, Not Its Code** + * When a request failed with a code and a sentence, V2 showed the code, for example `document_propagation_incomplete`. It now shows the sentence, such as "The operation changed stored data, but required cleanup or propagation is incomplete. Refresh before retrying." + * This covers document management, File Sync and collaboration refusals, the Microsoft 365 pending actions, the terms-of-use gate and CI bearer authentication. Failures whose message is already a sentence are unchanged. + * (Ref: `lib/apiClient.ts`, [V2 Fixture Parity Findings Fix](fixes/V2_FIXTURE_PARITY_FINDINGS_FIX.md)) + +* **Deleting A Document Names The Conversation That Needs It** + * When a conversation depends on a document, the V2 delete confirmation now names the conversation and links to it in V2 chat, in a new tab. + * Only the conversation's id or a link to this site is used, never a link to another site or to classic chat, and the title is always shown as text. + * (Ref: `lib/documentOperations.ts`, `DocumentDialogs.tsx`, [V2 Fixture Parity Findings Fix](fixes/V2_FIXTURE_PARITY_FINDINGS_FIX.md)) + +* **Batch Downloads Keep The Server's Archive Name** + * Downloading several group or public documents at once saved `documents.zip`. It now saves `group-documents.zip` or `public-documents.zip`, the names the server gives. + * A single document keeps its own file name, and personal downloads are unchanged. + * (Ref: `lib/documentOperations.ts`, `DocumentExplorer.tsx`, [V2 Fixture Parity Findings Fix](fixes/V2_FIXTURE_PARITY_FINDINGS_FIX.md)) + +* **Group Agents Offer Only Group Knowledge** + * The V2 group agent editor offered public knowledge sources, which a group agent never keeps. It now offers only the group's own knowledge. Personal agents are unchanged. + * (Ref: `lib/agentWorkbench.ts`, [V2 Fixture Parity Findings Fix](fixes/V2_FIXTURE_PARITY_FINDINGS_FIX.md)) + +### **(v0.261.163)** + +#### Bug Fixes + +* **Classic Group Statistics Export Downloads Again** + * On the classic **Manage group** page, exporting statistics showed "Failed to export group stats." and downloaded nothing whenever **Storage Usage** was ticked, which it is by default. The export called a byte formatter the page never had. + * The export now downloads its CSV. Storage sizes read like the public workspace export's, for example "1.5 KB", and nothing else in the file changes. + * (Ref: `static/js/group/manage_group.js` `exportGroupStats`, [Classic Group Stats Export Fix](fixes/CLASSIC_GROUP_STATS_EXPORT_FIX.md)) + +### **(v0.261.162)** + +#### Bug Fixes + +* **Searching Your Groups Ignores Case And Matches Descriptions** + * Searching your own groups was case-sensitive and matched names only, so "research" didn't find "Research Group" and a word from a group's description found nothing. It affected the V2 group picker, the V2 Settings **Groups** tab, the classic **My Groups** page and the profile page's **Groups** tab. + * The search now ignores case and matches a group's name or description, as the group directory and the admin group search already did. An empty search still lists all your groups, and paging is unchanged. + * (Ref: `functions_group.py` `search_groups`, `GET /api/groups`, [Group Picker Search Casefold Fix](fixes/GROUP_PICKER_SEARCH_CASEFOLD_FIX.md)) + +### **(v0.261.161)** + +#### Bug Fixes + +* **SimpleChat Agent Tools Explain A Group That Keeps Changing** + * When adding a group member or marking a group inactive couldn't be saved because the group kept changing, the agent got a generic "unexpected" error and the application logged an error with a traceback. + * The tools now answer with the same message every group route uses, "The group changed while your request was being saved. Try again.", and the code `group_write_conflict`. Nothing was saved, so the request can be repeated. The log entry is a warning with no group data. + * (Ref: `simplechat_plugin.py`, [SimpleChat Agent Group Output Fix](fixes/SIMPLECHAT_AGENT_GROUP_OUTPUT_FIX.md)) + +### **(v0.261.160)** + +#### Bug Fixes + +* **The Last Group Writers No Longer Overwrite Concurrent Changes** + * Control Center's activity refresh, group status change, add member and ownership approvals, the classic tag routes, the SimpleChat agent's inactive marker, and the legacy bulk model endpoint save each saved a copy of the group read earlier. Any of them could undo a change made in between, and one that saved after a deletion brought the group back. Control Center's refresh, which runs nightly by default, did this to every group. + * Each now applies its change to the group as it currently is, never recreates a deleted group, and refuses a group that keeps changing with one shared message: "The group changed while your request was being saved. Try again." + * Ownership approvals are re-checked when approved: a request whose owner has since changed is refused, and approving the same request twice no longer marks it failed. An approval that can't be applied because the group keeps changing asks for a new request, since a failed approval can't be approved again. + * The bulk endpoint save deletes superseded Key Vault secrets only after the save commits, and never one the saved endpoints still use. A failed Key Vault delete no longer fails the save. + * (Ref: `route_backend_control_center.py`, `route_backend_group_documents.py`, `functions_documents.py`, `functions_simplechat_operations.py`, `functions_group.py`, `route_backend_models.py`, [Group Residual Writers Write Safety Fix](fixes/GROUP_RESIDUAL_WRITERS_WRITE_SAFETY_FIX.md)) + +* **SimpleChat Agent Tools No Longer Return The Stored Group** + * The create group, add member and mark inactive tools answered the model with the whole stored group, including member emails, pending requests and, with Key Vault storage off, inline model endpoint credentials. + * They now answer only the group's ID, name and status. + * (Ref: `simplechat_plugin.py`, [SimpleChat Agent Group Output Fix](fixes/SIMPLECHAT_AGENT_GROUP_OUTPUT_FIX.md)) + +* **Classic Group Requests Refuse Malformed Input Cleanly** + * The group download setting treated the string "false" as true, a retention save with a non-JSON body answered 500, and a statistics date at the calendar's edge failed the request. + * Each now answers 400 with a clear message and changes nothing. + * (Ref: `route_backend_groups.py`, `route_backend_retention_policy.py`, `functions_stats_windows.py`, [Group Classic Request Gaps Fix](fixes/GROUP_CLASSIC_REQUEST_GAPS_FIX.md)) + +### **(v0.261.159)** + +#### Bug Fixes + +* **Held Documents No Longer Offer Ordinary Actions In Their Details** + * The V2 documents Details pane showed Chat, Download, Tag, Edit, Extract, Share and Delete for a document held by content screening. Most were disabled, but Share was live, so a held file in My Workspace could be shared from the pane. + * A held document's details now explain the hold and offer only the cleanup its workspace allows: Delete where a group or public workspace permits it for that document, and the group sharing review where it's available. A personal held document offers no action; the hold is resolved in Content review. + * Documents that are only blocked from chat, such as a shared group document awaiting approval, keep their other permitted actions. + * (Ref: `DocumentDetailsPane.tsx`, [Held Document Details Actions Fix](fixes/V2_HELD_DOCUMENT_DETAILS_ACTIONS_FIX.md)) + +### **(v0.261.158)** + +#### Bug Fixes + +* **V2 Admin Settings No Longer Wait On The Update Check** + * Fixed V2 Admin Settings showing only loading placeholders, with the version banner reading "Checking for updates...", until the server finished checking GitHub for a newer release. When the daily cached check had expired, the settings response waited for the releases page to download and be parsed, and an unreachable GitHub held the page for at least the full timeout. + * The update check is now its own admin-only request, `GET /api/v2/admin/update-status`, sent at the same time as the settings. Settings appear and can be edited and saved as soon as they load, and only the version banner waits for the check. A failed check still shows "Unable to check for application updates." without affecting the settings. + * Classic Admin Settings is unchanged. + * Merged from the React V2 branch, where it shipped as 0.261.133. This branch had already assigned 0.261.133 to native public document management, so here the fix carries the merge's version. + * (Ref: `route_backend_v2.py`, `AdminSettingsPage.tsx`, `adminFields.ts`, [Administration](../admin/index.md), [Update Check Non-Blocking Fix](fixes/V2_ADMIN_UPDATE_CHECK_NON_BLOCKING_FIX.md)) + +### **(v0.261.157)** + +#### Bug Fixes + +* **Group Profile And Logo Edits Refused When The Group's Status Isn't Recognized** + * The native group settings routes let the owner of a group with an unrecognized status change its name, description, color and logo, although the rest of the workspace treats such a group as unavailable. Those edits are now refused unless the group is active or has uploads disabled, with a message that says the status isn't recognized. + * Downloads, retention and the settings, activity and statistics reads are unchanged. + * (Ref: `functions_group_settings_policy.py`, `functions_group_settings.py`, [Group Settings Unknown Status Fix](fixes/GROUP_SETTINGS_UNKNOWN_STATUS_FIX.md)) + +### **(v0.261.156)** + +#### Bug Fixes + +* **Editing A File Source No Longer Erases Its Tenant** + * Saving a group file source in V2 that signs in with a service principal, even just to rename it, removed its stored tenant ID, so the next sync signed in to the wrong tenant. The editor also showed **Tenant ID** empty. The tenant is now shown and kept. + * Editing a source in either the V2 or the classic editor also removed a managed identity's client ID set through the API. It's now kept too. + * If a source was affected, open it, enter the tenant ID again, and save. + * (Ref: `sanitize_file_sync_source`, `fileSourceFields.ts`, [File Source Credential Round Trip Fix](fixes/FILE_SOURCE_CREDENTIAL_ROUND_TRIP_FIX.md)) + +### **(v0.261.155)** + +#### New Features + +* **V2 Group Members** + * Group workspaces in V2 have a **Members** section, under a new **Manage** group in the navigation. Everyone in the group can see who belongs to it and leave; owners and admins manage it there. + * Owners and admins can add members from the directory, import them from a CSV file in the classic format, change roles, remove members, and approve or reject requests to join. Several members can be changed or removed together, with each member's result shown. The owner can transfer ownership. + * The page offers only what the server allows each person, and shows the server's message when a change is refused. A request someone else already handled is reported as handled, not as an error. + * Members isn't available in inactive groups; **Manage group (classic)** still covers them, and group settings. + * (Ref: `GroupMembersSection.tsx`, `groupMembership.ts`, `sections.members` in `functions_workspace_context.py`, [V2 Group Members](features/V2_GROUP_MEMBERS.md)) + +### **(v0.261.154)** + +#### New Features + +* **Group Settings, Activity and Statistics APIs** + * New server routes under `/api/groups//settings` read and change a named group's name, description, color, logo, download setting and retention periods. Routes under `/api/groups//insights` read its activity feed, statistics and document count. + * Each settings section carries a revision, so a save made from an out-of-date copy is refused with a reload instead of overwriting someone else's change. Every write applies to the group as it currently is. + * One policy decides what the caller may do, matching the classic rules. It is published as `settings_management` in the settings read and in the V2 group context. A locked or inactive group's name, description, color and logo can't be changed, as on the classic page. + * The activity feed shows reviewed summaries and members' display names only, never document titles, file names, emails or error text. The statistics match the classic figures, refuse custom ranges over 366 days, and report a storage failure instead of zeros. + * The V2 Settings, Activity and Statistics views that use these routes arrive in a later release. + * (Ref: `functions_group_settings.py`, `functions_group_settings_policy.py`, `functions_group_insights.py`, `route_backend_group_settings.py`, [Group Settings APIs](features/GROUP_SETTINGS_APIS.md)) + +#### Bug Fixes + +* **Group Settings Saves No Longer Undo Membership Changes** + * Classic renames and the color, logo, download and retention saves, as well as the administrator's retention force push, saved the whole group without a condition. A save landing just after a membership change could undo it, and a late save could recreate a deleted group. Each now applies to the group as it currently is. + * (Ref: `route_backend_groups.py`, `route_backend_retention_policy.py`, [Group Settings Write Safety Fix](fixes/GROUP_SETTINGS_WRITE_SAFETY_FIX.md)) + +* **Delete Group Asks For The Group's Documents To Be Removed First** + * The classic manage page's document count always came back as 0, so **Delete group** never asked the owner to remove the group's documents, and they were left behind. The count is now the group's current documents, as its document list shows them. + * (Ref: `get_group_file_count`, `count_current_group_documents`, [Group Document Count Fix](fixes/GROUP_DOCUMENT_COUNT_FIX.md)) + +* **Group Retention Settings Save Correctly** + * **Using organization default** can be saved again, and saving one retention period keeps the other. Values that aren't a number of days get the existing message instead of being stored as 1 day or failing. + * The route now follows the group workspaces and group retention policy switches. + * (Ref: `update_group_retention_settings`, [Group Retention Settings Fix](fixes/GROUP_RETENTION_SETTINGS_FIX.md)) + +* **Group Settings Errors No Longer Show Internal Details** + * A failed rename, download or logo save showed the raw Cosmos DB or image library message. It now shows a plain message and logs only the error type. A logo that decompresses to an enormous image is refused instead of failing. + * (Ref: `route_backend_groups.py`, [Group Settings Error Text Fix](fixes/GROUP_SETTINGS_ERROR_TEXT_FIX.md)) + +### **(v0.261.153)** + +#### User Interface Enhancements + +* **V2 Group Workspace No Longer Points To Classic For Native Sections** + * Every group workspace section is native, so the navigation and overview no longer label unavailable sections "Classic". An unavailable section shows why it's unavailable instead. + * The Tags classic button and the classic hand-off panel are gone. A section link with an item the section doesn't support, such as `/v2/groups//prompts/`, now opens the section, and a workflow link opens that workflow. + * When group actions are off, **Actions** says so and keeps the Call agent manager, with no classic link. + * Documents' classic link is now **Classic tools**, for upgrading legacy documents, the one classic tool with no V2 equivalent yet. + * (Ref: `GroupWorkspacePage.tsx`, `DocumentsSection.tsx`, `TagsSection.tsx`, [V2 Shared Workspace Context](features/V2_SHARED_WORKSPACE_CONTEXT.md)) + +### **(v0.261.152)** + +#### Bug Fixes + +* **Group Editors Keep Other People's Changes After A Conflict** + * Fixed the group prompt, identity, endpoint and file source editors re-sending your whole draft after a save conflict. That silently undid the other person's changes to fields you never touched. + * Reloading now merges: their changes fill the fields you didn't touch, your edits are kept, and any field you both changed is named, never its value. Secrets are never compared or shown. + * If the item was deleted meanwhile, the editor says so and saves nothing. + * (Ref: `lib/rebaseDraft.ts`, `PromptWorkbench.tsx`, `GroupIdentitiesSection.tsx`, `ModelConnectionsManager.tsx`, `GroupFileSourcesSection.tsx`, [Group Editor Conflict Rebase Fix](fixes/GROUP_EDITOR_CONFLICT_REBASE_FIX.md)) + +### **(v0.261.151)** + +#### New Features + +* **Group Membership APIs** + * New server routes under `/api/groups//membership/` list a group's members, paged and searchable, and add, re-role, remove and let members leave. They also list, approve and reject join requests, and transfer ownership, all for a named group rather than the account's active one. + * The member list says what the caller may do: `membership_management` for the group, and `member_actions` on each member. Both come from one policy module that matches the classic rules. + * The owner's role changes only by transfer, and the owner can't be removed or leave. Members can't be added to locked or inactive groups. A new member is looked up in the directory by ID. + * The V2 Members view that uses these routes arrives in a later release. + * (Ref: `functions_group_membership.py`, `functions_group_membership_policy.py`, `route_backend_group_membership.py`, [Group Membership APIs](features/GROUP_MEMBERSHIP_APIS.md)) + +#### Bug Fixes + +* **Group Membership Changes No Longer Overwrite Each Other** + * Classic join requests, approvals, adds, removals, role changes and ownership transfers saved the whole group without a condition. So two changes at once could lose one, and a late change could recreate a deleted group. Each now applies to the group as it currently is, and a deleted group stays deleted. + * Joining a group with no pending-request list no longer fails. + * (Ref: `update_group_document_with_etag_guard`, `route_backend_groups.py`, [Group Membership Write Safety Fix](fixes/GROUP_MEMBERSHIP_WRITE_SAFETY_FIX.md)) + +* **Classic Membership Edge Cases** + * Approving someone who was already a member no longer adds them twice, and every pending request from a user is settled in one decision. + * The owner's role can no longer be changed except by transfer, and a transfer keeps the old owner's name and email. + * The classic bulk remove now reports its successes, and the members list no longer fails on malformed entries. + * (Ref: `route_backend_groups.py`, `add_group_member_for_current_user`, [Group Membership Edge Cases Fix](fixes/GROUP_MEMBERSHIP_EDGE_CASES_FIX.md)) + +* **User Search Errors No Longer Expose Graph Details** + * `/api/userSearch` returned Microsoft Graph's error body to the browser and had no timeout. Failures now return a fixed message, time out after 20 seconds, and log only the status code. + * (Ref: `route_backend_users.py`, [User Search Error Hardening Fix](fixes/USER_SEARCH_ERROR_HARDENING_FIX.md)) + +### **(v0.261.150)** + +#### New Features + +* **V2 Group Directory** + * Find, join and create groups in V2 at `/v2/groups/directory`, reached from **Browse all groups** in the group workspaces header or from the screen shown when no group is selected. The **All**, **My groups** and **Discover** views, the search and the page stay in the address. + * Each group shows its name, description, member count, owner's name, and your role or pending request. **Open** goes to a group you belong to. **Request to join** and **Cancel request** update the row from the server's answer, and a conflicting change reloads the list rather than guessing. + * **Create group** appears only when the server allows it, including the **CreateGroups** role rule. It checks the name and description limits the way the server counts them, and then opens the new group. + * (Ref: `GroupDirectoryPage.tsx`, `DirectoryList.tsx`, `CreateGroupDialog.tsx`, `groupDirectory.ts`, [V2 Group Directory](features/V2_GROUP_DIRECTORY.md)) + +### **(v0.261.149)** + +#### Bug Fixes + +* **Workflow Saves After A Deletion Keep The Draft** + * Fixed a File Sync source deleted while the V2 editor was open. A group workflow's save failed with a 404, which the editor treated as lost access, losing the draft. A personal workflow's save failed with a server error. Both now get a 400 naming the problem, with the draft kept, and a group editor marks the source so it can be removed. + * Fixed a V2 save of a workflow deleted after the editor opened it. The save silently recreated the workflow; it's now refused with a 409 that writes nothing, and the draft stays open to copy. + * (Ref: `WorkflowSourceUnavailableError`, `WorkflowDeletedConflict`, `refuse_save_of_deleted_workflow`, [Workflow Save After Deletion Fix](fixes/WORKFLOW_SAVE_AFTER_DELETION_FIX.md)) + +* **Workflow Settings Refusals Name The Rule** + * File Sync, schedule and trigger refusals used to share one generic message. Each now has a reviewed message with the code `invalid_workflow_settings`, on both save routes and in the classic editor. + * The V2 editor checks the same rules, with the same messages, before saving personal and group workflows. When File Sync is off for a group, the editor says so, from a new `file_sync_enabled` field on the group File Sync sources route. + * A non-finite schedule value no longer causes a server error. + * (Ref: `lib/workflowSettings.ts`, `WorkflowPublicValidationError`, `route_backend_workflows.py`, [Workflow Settings Reviewed Messages Fix](fixes/WORKFLOW_SETTINGS_REVIEWED_MESSAGES_FIX.md)) + +* **Read-Only Workflow Editors No Longer Request Run As Accounts** + * A group member's read-only workflow editor asked for the run-as accounts, which only workflow managers may list, and showed a load error. It now shows only whether an account is selected, and requests nothing. + * (Ref: `WorkflowMicrosoft365RunAs.tsx`, [Workflow Run As Read-Only View Fix](fixes/WORKFLOW_RUN_AS_READ_ONLY_VIEW_FIX.md)) + +### **(v0.261.148)** + +#### Bug Fixes + +* **Public Publication Decisions Open The Document** + * Fixed the notification a member gets when a public workspace manager approves or rejects their generated file. It linked to `/v2/public-workspaces//documents`, a path the V2 app doesn't serve, so it opened the V2 home page. It now opens `/v2/public//documents` with the document named. + * A new check fails if any backend link points to a V2 path that the V2 router doesn't serve. Notifications sent before the fix keep their old link. + * (Ref: `functions_public_document_publication.py`, `test_public_workspace_notification_links_fix.py`, [Public Document Decision Link Fix](fixes/PUBLIC_DOCUMENT_DECISION_LINK_FIX.md)) + +### **(v0.261.147)** + +#### New Features + +* **Native Group File Sources In V2** + * The group workspace's **File sources** section is native in V2. Owners, Admins and DocumentManagers list the group's SMB, Azure Files and Azure Blob Storage sources, and in an active group create, edit, delete and sync them. + * The editor tests the connection and browses the remote location before saving. It can bind one of the group's reusable identities, and while browsing a saved source it can ignore or restore a path. + * Deleting asks whether to keep the documents the source brought in. The section reports the counts, and says plainly when documents were deleted even though the source couldn't be removed. + * A save made after someone else changed the source keeps your draft. Refusals such as a sync that's already running show the server's reviewed message. + * (Ref: `GroupFileSourcesSection.tsx`, `FileSourceEditorDialog.tsx`, `fileSourceWorkbench.ts`, [V2 Group File Sources](features/V2_GROUP_FILE_SOURCES.md)) + +### **(v0.261.146)** + +#### New Features + +* **Group Directory APIs** + * New server routes back the native V2 group directory: + * `GET /api/groups/directory` lists every group you can discover, paged, sorted and searchable, with your membership in each and whether you may create a group; + * `POST /api/groups/directory` creates a group; + * `POST` and `DELETE /api/groups//join-request` ask to join a group, and cancel that request. Cancelling is new. + * The listing never returns an owner's email or ID, or any member's entry. The membership arrays are reduced to the caller's own entries in the database. + * Group creation is decided by one policy that the directory also reports, so the page can't offer Create to someone the server would refuse. Names are limited to 80 characters and descriptions to 500, with reviewed messages. + * Join requests are written conditionally: a concurrent change is kept, and a group deleted meanwhile is reported as missing, never recreated. Every group status still accepts requests, as before. + * The directory page itself arrives in a later release. The classic Find Group and Create Group flows are unchanged. + * (Ref: `functions_group_directory.py`, `functions_group_directory_policy.py`, `route_backend_group_directory.py`, [Group Directory APIs](features/GROUP_DIRECTORY_APIS.md)) + +#### Bug Fixes + +* **Workspace Notification Links Open The Management Page** + * Fixed six notifications that led to a 404. The group created, member added and role changed notifications linked to `/manage_group/`, and the public workspace member added and role changed notifications linked to `/manage_public_workspace`. Neither path is served. + * They now link to `/groups/` and `/public_workspaces/`. Notifications created before the fix keep their old link. + * (Ref: `_build_group_manage_url`, `route_backend_groups.py`, `route_backend_public_workspaces.py`, [Workspace Notification Links Fix](fixes/WORKSPACE_NOTIFICATION_LINKS_FIX.md)) + +### **(v0.261.145)** + +#### New Features + +* **Native Group Endpoints In V2** + * The group workspace's **Endpoints** section is native in V2. It lists the model connections a group owns for its own agents and workflows. Owners and Admins add, edit, enable or disable, and delete them in an active group, and can discover models and test chat before saving. Everyone else in the group sees a read-only list. + * It reuses the administrator's connection manager through a scope-aware adapter, and talks only to the group's own routes. The administrator-only tests, network policy and notices are not shown in group scope. + * A save made after someone else changed the connection keeps your draft and offers **Reload latest**. A delete refused because an agent or workflow still uses the connection names what uses it. + * Foundry discovery in the group agent editor now works for group-scoped connections, through `POST /api/groups//models/foundry/agents`. + * (Ref: `GroupEndpointsSection.tsx`, `ModelConnectionsManager.tsx`, `createGroupModelConnectionsAdapter`, [V2 Group Endpoints](features/V2_GROUP_ENDPOINTS.md)) + +### **(v0.261.144)** + +#### New Features + +* **Native Workflow Alert Editing** + * Personal and group workflows can have their alerts set up and changed in the V2 workflow editor: when to alert, a pop-up priority, and up to 20 rules. The rules cover seven kinds of condition, each with a scope, a severity and a delivery. + * Before this, a workflow created or saved in V2 could never get alerts, because the classic editor can't open V2 definitions. The group editor's link to the classic alert editor is removed. + * The editor checks the settings with the server's own rules and messages before saving, and a workflow whose alerts you didn't touch is saved with them unchanged. + * (Ref: `WorkflowAlertEditor.tsx`, `workflowAlerts.ts`, `functions_workflow_alerts.py`, [V2 Workflow Alert Editing](features/V2_WORKFLOW_ALERT_EDITING.md)) + +#### Bug Fixes + +* **A Removed Task No Longer Blocks Every Save** + * Fixed V2 refusing every save, with a generic "Invalid workflow settings" message, after a task watched by an alert rule was deleted. The editor now marks the rule and says which one to fix. + * Invalid alert settings now get a specific message naming the rule, on both save routes and in the classic editor. Other invalid settings keep the generic message. + * (Ref: `WorkflowPublicValidationError`, `route_backend_workflows.py`, [Removed-Task Save Fix](fixes/WORKFLOW_ALERT_REMOVED_TASK_SAVE_FIX.md)) + +* **Personal Workflows That Analyze Changed Files Can Be Saved In V2** + * Fixed V2 refusing to save a personal workflow whose Analyze task uses the files File Sync changed, a setup the server accepts. It already worked for group workflows from 0.261.141. + * (Ref: `workflowFileSyncProvidesAnalyzeTargets`, [Analyze Changed Files Fix](fixes/V2_GROUP_WORKFLOW_ANALYZE_CHANGED_FILES_FIX.md)) + +### **(v0.261.143)** + +#### Bug Fixes + +* **Group Details No Longer Expose The Stored Group Document** + * Fixed `GET /api/groups/` returning the stored group document to every member. When Key Vault secret storage is off, that included the plaintext credentials of the group's model endpoints. It also included pending join requests and internal fields. + * The route now returns only what the group management page uses, plus the caller's role. The retention policy is limited to Owners and Admins, as the public workspace details route already does. + * Endpoints, join requests and members remain available from their own routes, with their own permissions. + * (Ref: `route_backend_groups.py`, `build_group_details_payload`, [Group Details Payload Disclosure Fix](fixes/GROUP_DETAILS_PAYLOAD_DISCLOSURE_FIX.md)) + +### **(v0.261.142)** + +#### New Features + +* **Group File Source APIs** + * New routes manage a group's File Sync sources for a named group, at `/api/groups//file-sources`. They cover list, create, edit, delete, connection tests, folder browsing, Sync now, run history and ignored paths, without depending on the account's active group. + * If two managers edit the same source, the second save is refused instead of silently overwriting the first. A sync finishing never counts as a change. + * A delete asks explicitly whether to delete the documents the source produced, and reports how many were deleted. It is refused while a sync is running. If the source changes after its documents were already deleted, the response says so instead of claiming nothing happened. + * New credentials only take effect when a save succeeds, and deleting a source removes its stored secrets. The editor options list only the identities a source can actually use. + * The native V2 editor for group file sources arrives in a later release. Until then, group sources are still managed in the classic group workspace. + * (Ref: `route_backend_group_file_sources_scoped.py`, `functions_group_file_source_access.py`, `functions_group_file_source_policy.py`, `functions_file_sync.py`, [Group File Source APIs](features/GROUP_FILE_SOURCE_APIS.md)) + +#### Bug Fixes + +* **Sync Now Explains Why It Didn't Start** + * **Sync now** used to answer "Verify the source configuration and try again" when a sync was already queued or running, or when the File Sync concurrent-run limit was reached. It now says which of those happened, in the classic and native workspaces. + * (Ref: `queue_file_sync_source_run`, `FileSyncPublicValidationError`) + +### **(v0.261.141)** + +#### New Features + +* **File Sync Triggers In V2 Group Workflows** + * Group workflows can use the **Monitor File Sync changes** trigger in the native V2 editor. On a schedule, it syncs the chosen group File Sync sources and runs only when files changed. A manual or interval workflow can also sync its sources first before each run. + * The editor applies the server's rules before saving: one to ten of the group's own sources, and waiting for the sync before continuing only on changes. It flags a source the group no longer offers so it can be removed. The source list is requested for the page's own group, so another tab changing the active group can't swap it. + * An Analyze task can work on the files each sync changed instead of selected documents. + * (Ref: `WorkflowFileSyncFields.tsx`, `workflowEditor.ts`, `route_backend_workflows.py`, [V2 Group Workflows](features/V2_GROUP_WORKFLOWS.md)) + +#### User Interface Enhancements + +* **Group Workflow Alert Summary** + * Group workflows show their stored alert settings read-only: when to alert, the pop-up priority and the number of rules. Saving in V2 keeps them unchanged. + * A workflow the classic editor can still open links there to edit its alerts, and warns that saving in V2 converts it. Native alert editing arrives in a later release. + * (Ref: `WorkflowAlertSummary.tsx`, [V2 Group Workflows](features/V2_GROUP_WORKFLOWS.md)) + +#### Bug Fixes + +* **Group Workflows That Analyze Changed Files Can Be Saved In V2** + * Fixed V2 refusing to save a group workflow whose Analyze task has no selected documents because File Sync supplies the changed files, a setup the server accepts and the classic editor creates. + * (Ref: `workflowFileSyncProvidesAnalyzeTargets`, `WorkflowTaskFields.tsx`, [Analyze Changed Files Fix](fixes/V2_GROUP_WORKFLOW_ANALYZE_CHANGED_FILES_FIX.md)) + +### **(v0.261.140)** + +#### New Features + +* **Group Model Endpoint APIs** + * New routes manage one group model endpoint at a time for a named group, at `/api/groups//model-endpoints`. They also run model discovery, model tests and Foundry agent discovery for that group. They don't depend on the account's active group, so switching groups in another tab can't redirect a save. + * Saves change only the one endpoint and write the group document conditionally. A membership change made at the same moment is kept rather than overwritten, and a deleted group is never recreated. If two people edit the same endpoint, the second save is refused. + * A delete is refused while a group agent or workflow still uses the endpoint, and the response lists what uses it. Disabling an endpoint is always allowed. + * The native V2 editor for group endpoints arrives in a later release. Until then, group endpoints are still edited in the classic group workspace. + * (Ref: `route_backend_group_endpoints_scoped.py`, `functions_group_endpoint_access.py`, `functions_group_endpoint_policy.py`, `update_group_document_with_etag_guard`, [Group Model Endpoint APIs](features/GROUP_MODEL_ENDPOINT_APIS.md)) + +#### Bug Fixes + +* **Model Endpoint Credentials Stay With Their Own Endpoint** + * Fixed a flaw where a group owner who knew another group's endpoint ID could make their own endpoint use that group's stored key, or overwrite it. Personal endpoints had the same flaw. + * Root cause: these Key Vault secret names are built from the endpoint ID alone, and the save accepted any reference with the right shape. + * A stored reference is now accepted only if it is the endpoint's own, and new keys are always stored under fresh names. Existing keys keep working. + * (Ref: `functions_keyvault.py`, `save_scoped_endpoint_secrets`, [Secret Reference Scope Fix](fixes/MODEL_ENDPOINT_SECRET_REFERENCE_SCOPE_FIX.md)) + +* **Personal And Group Endpoints Can No Longer Redirect The Application's Identity** + * Fixed personal and group model endpoints that use managed identity being able to send the application's own token to any host, for any audience, from any of the application's identities. This applied to model discovery and tests, and to saved endpoints used in chat, agents and workflows. + * The endpoint must now be an Azure AI service host in the deployment's cloud, it uses the deployment's own token audience, authority and identity, and the rule is checked when the endpoint is saved and each time it's used. + * Endpoints that use an API key or a service principal, custom connections, and admin-managed global endpoints are unchanged. + * (Ref: `functions_model_endpoint_app_identity.py`, `functions_azure_endpoint_validation.py`, [Application Identity Fix](fixes/MODEL_ENDPOINT_APPLICATION_IDENTITY_FIX.md)) + +* **Malformed Catalog Profile ID No Longer Causes A Server Error** + * Fixed saving a model endpoint with a malformed catalog profile ID failing with a server error instead of "Choose a valid catalog profile." This affected every endpoint save, including Admin Settings. + * Root cause: the error the check raises was never imported where it's raised. + * (Ref: `functions_settings.py`, [Catalog Profile Validation Fix](fixes/MODEL_ENDPOINT_CATALOG_PROFILE_VALIDATION_FIX.md)) + +#### Breaking Changes + +* **Managed Identity On Personal And Group Endpoints** + * A personal or group endpoint that uses managed identity stops working if any of these apply: + * Its host isn't an Azure AI service host in the deployment's cloud, such as an APIM gateway or a proxy. + * It stores a Foundry scope, custom authority or custom cloud. + * The deployment runs in a custom cloud, where no host qualifies. + * A stored managed identity client ID is ignored, and the deployment's default identity is used. + * **Migration**: switch affected endpoints to an API key or a service principal, or ask an administrator to add a global endpoint. For endpoints that relied on a user-assigned identity, grant the deployment's default identity access instead. + * (Ref: [Configure model endpoint identity](../guides/model-endpoint-identity-setup.md), [Application Identity Fix](fixes/MODEL_ENDPOINT_APPLICATION_IDENTITY_FIX.md)) + +### **(v0.261.139)** + +#### New Features + +* **Native Group Identities** + * Group workspace identities can now be listed, created, edited and deleted from the native group workspace. These are the reusable credentials that group File Sync sources and group actions use. Previously the group Identities section sent users to the classic interface. + * Owners, admins and document managers manage identities while the group is active. Ordinary members don't see them, as in the classic workspace. + * If two managers edit the same identity, the second save is refused and the draft is kept. A delete is refused while a File Sync source or an action still uses the identity, and the editor lists what uses it. + * Stored secrets are never shown. A new secret value only takes effect when the save commits, so a refused save never changes the credential in use. + * The group action editor can now use a reusable group identity. Someone who can't list the group's identities sees an existing binding as kept, without an error. + * (Ref: `route_backend_group_identities_scoped.py`, `functions_group_identity_access.py`, `functions_group_identity_policy.py`, `identityWorkbench.ts`, [V2 Group Identities](features/V2_GROUP_IDENTITIES.md), [Group Identity APIs](features/GROUP_IDENTITY_APIS.md)) + +### **(v0.261.138)** + +#### New Features + +* **Native Group Agents** + * Group agents can now be listed, opened, created, edited and deleted from the native group workspace, in the same collection and editor as personal agents. Previously the group Agents section sent users to the classic interface. + * Owners and admins can change group agents while the group is active; with the owner-only setting, only the owner can. Everyone else in the group sees read-only details and can use the group's agents in chat. + * Everything the editor loads is resolved for the group: the model list (the global connections the member may use, and the group's own when allowed), the group's documents for assigned knowledge, the group's actions, Call agent targets, and instruction drafting. A group page never reads the member's personal agents, endpoints, knowledge or identities. + * "Use in chat" opens the agent in its own group, whichever group the account last selected. A stale link names the group instead of silently choosing another agent. + * Agents created in the classic group workspace keep their stored credentials when edited in V2, and the reverse. + * If two managers edit the same agent, the second save is refused rather than silently overwriting the first, and the editor keeps the draft. + * When group agents are turned off or restricted, the section is left out of the group's navigation, as in the classic workspace. + * The template gallery's submit button on a group agent follows the same rule as the template service, so it is offered only when a submission would be accepted. + * Foundry discovery is not offered for group-scoped Foundry connections yet; global connections discover as before, and project fields can be entered manually. + * (Ref: `route_backend_group_agents_scoped.py`, `functions_group_agent_access.py`, `functions_group_agent_policy.py`, `agentWorkbench.ts`, [V2 Group Agents](features/V2_GROUP_AGENTS.md), [Group Agent APIs](features/GROUP_AGENT_APIS.md)) + +#### Bug Fixes + +* **Native Group Action Changes Were Missing From the Activity Log** + * Creating, editing or deleting a group action from the native V2 group workspace (0.261.137) recorded no activity event, while the classic workspace records one for every change. + * Native group action changes now record the same events, with the group, as the classic routes do. The new native group agent routes do the same. + * (Ref: `functions_workspace_authoring.py` `log_committed_group_editor_change`, `functions_group_action_access.py`, [Group Action APIs](features/GROUP_ACTION_APIS.md)) + +* **File Sync Runs Undid Changes Made While They Were Running** + * Fixed a File Sync run overwriting a manager's changes made while it was running. When the run finished, an edit to the source was lost, a source or schedule that had been turned off was turned back on, a deleted source was recreated and kept syncing, and an ignored path was un-ignored. + * Runs now record only their own results (last run status, counts and the next scheduled time) on the source as it is stored, and the next run follows the schedule as it is now. A deleted source stays deleted, and the run still reports how it went. + * A path ignored while a run was processing it stays ignored, and still records the document the run produced. + * The reverse race is closed too: saving a source no longer erases the result of a run that finished meanwhile, and ignoring a path no longer drops a file's new document, which could make the next run import it again as a duplicate. + * Two managers editing the same source at once are still last-writer-wins. A save that keeps colliding with other writes is refused with a message to reload rather than applied from an outdated copy. + * This affects every File Sync interface, including the classic pages. + * (Ref: `functions_file_sync.py`, `route_backend_file_sync.py`, [File Sync Concurrent Write Fix](fixes/FILE_SYNC_CONCURRENT_WRITE_FIX.md)) + +### **(v0.261.137)** + +#### New Features + +* **Native Group Actions** + * Group actions can now be listed, opened, created, edited, tested and deleted from the native group workspace, in the same collection and editor as personal actions. Previously the group Actions section offered only the Call agent manager and sent every other action to the classic interface. + * Owners and admins can change and test group actions while the group is active; with the owner-only setting, only the owner can. Everyone else in the group sees a read-only collection and read-only details, with each action's configuration fields shown. + * If two managers edit the same action, the second save is refused rather than silently overwriting the first, and the editor keeps the draft. + * Connection tests from a group editor run in that group, never in whichever group the account last selected. + * Provided (global) actions appear read-only when the administrator merges them into workspaces. + * Unsaved drafts are kept per workspace, so a draft started in one group never appears in another group or in My Workspace. + * Reusable group identities cannot be chosen in the V2 editor yet, and saved MCP preconfigurations are not offered for group actions yet. An action already bound to a group identity keeps that binding. + * The group workspace shows the native collection only when group actions are enabled. A tenant with group agents but not group actions keeps the Call agent manager. + * (Ref: `route_backend_group_actions_scoped.py`, `functions_group_action_access.py`, `functions_group_action_policy.py`, `actionWorkbench.ts`, [V2 Group Actions](features/V2_GROUP_ACTIONS.md), [Group Action APIs](features/GROUP_ACTION_APIS.md)) + +#### Bug Fixes + +* **Group Members Could Use a Group's Stored Credentials in Connection Tests** + * Fixed group connection tests and MCP discovery accepting requests from any group member. Testing a saved group action loads its stored credentials, and an unsaved test that names a group identity resolves that identity's secrets. Either way, a member could use the group's stored credentials against a destination they chose. + * These requests now need the same roles as editing a group action: Owner or Admin, or Owner alone under the owner-only setting. The group MCP preconfiguration list shares the same check. + * The classic interface only ever offered these actions inside the action editor, which only those roles can open, so no supported flow changes. + * Scripts or integrations that run group connection tests must use an account with one of those roles. + * (Ref: `route_backend_plugins.py`, [Group Action Test Role Alignment Fix](fixes/GROUP_ACTION_TEST_ROLE_ALIGNMENT_FIX.md)) + +### **(v0.261.136)** + +#### New Features + +* **Native Group Prompts** + * Group prompts can now be read, written, and used from the native group workspace, in the same workbench as personal prompts. The group Prompts section previously sent users to the classic interface. + * Owners, admins, and document managers can create, edit, duplicate, and delete group prompts while the group is active. Everyone else in the group can read prompts, use them in chat, and reword them in the chat composer for a single message; that rewording is never saved to the group's prompt. + * If two managers edit the same prompt, the second save is refused rather than silently overwriting the first, and the editor keeps what was typed. + * "Use in chat" links for group prompts name the group, so a prompt that has been deleted or is no longer accessible is reported clearly instead of silently attaching something else. Existing prompt links keep working. + * Group prompts have no favorites, because a favorite is stored on the prompt and would be shared by everyone in the group. Personal favorites are unchanged. + * (Ref: `route_backend_group_prompts_scoped.py`, `functions_group_prompt_access.py`, `functions_group_prompt_policy.py`, `promptWorkbench.ts`, [V2 Group Prompts](features/V2_GROUP_PROMPTS.md), [Group Prompt APIs](features/GROUP_PROMPT_APIS.md)) + +#### Bug Fixes + +* **Ordinary Group Members Could Change Shared Prompts Through the API** + * Fixed the group prompt create, edit, and delete routes accepting requests from any group member. The classic interface only ever offered those actions to owners, admins, and document managers, but the server did not enforce the same rule, so a member calling the API directly could change or delete the group's shared prompts. + * Those routes now refuse ordinary members, matching the rule public workspace prompts already enforced. Reading and using prompts is unchanged for everyone. + * A refused member is told the change needs an owner, admin, or document manager, rather than being told they are not a member. + * Scripts or integrations that create group prompts must use an account with one of those roles. + * (Ref: `route_backend_group_prompts.py`, [Group Prompt Write Access Fix](fixes/GROUP_PROMPT_WRITE_ACCESS_FIX.md)) + +### **(v0.261.135)** + +#### Bug Fixes + +* **Public Workspace Download Flag Reported Downloads as Disabled** + * Fixed the public workspace document list reporting `file_downloads_enabled: false` for every user, including managers of workspaces where downloads are enabled. The value was fixed at `false` in the read-only browsing release, when there was nothing to download, and was not updated when downloads were added. + * The native explorer was not affected, because it decides whether to offer downloads from the workspace's advertised operations and each document's own action list. Other consumers of the API would have been told downloads were off. + * The flag is now derived from the same policy that decides whether the workspace advertises downloads, so the two cannot disagree. + * (Ref: `route_backend_public_document_reads.py`, `public_document_management_operations`, `test_public_document_read_apis.py`) + +### **(v0.261.134)** + +#### New Features + +* **Native Public Workspace Artifact Approval** + * When someone asks to publish a generated artifact into a public workspace, reviewers can now approve, reject, or withdraw the request in the native explorer instead of switching to the classic page. + * Each decision is tied to the version of the request the reviewer actually saw. If the document changed in the meantime, the reviewer's input is kept and they are asked to refresh before deciding. + * Only the original requester can withdraw a request, and doing so grants no authority over anyone else's. + * Content screening holds are never released early by an approval; approval only starts screening on the new document. + * (Ref: `functions_public_document_publication.py`, `route_backend_public_document_collaboration.py`, `documentCollaboration.ts`, `DocumentCollaborationDialog.tsx`, [V2 Public Artifact Approval](features/V2_PUBLIC_ARTIFACT_APPROVAL.md), [Artifact Approval APIs](features/PUBLIC_DOCUMENT_ARTIFACT_APPROVAL_APIS.md)) + +#### Bug Fixes + +* **Public Workspace Document Actions Were Unavailable** + * Fixed public workspace documents offering no per-document actions against a real backend. Delete, download, metadata editing, extraction, reprocessing, and bulk tagging were all unavailable, although upload and tag management worked. + * The server returned an empty action list left over from the read-only browsing release, and the explorer only offers actions that list contains. It now computes each document's actions from the user's current permissions. + * The failure blocked actions rather than allowing them, so no data was exposed and every operation was still authorized on the server. + * (Ref: `functions_public_document_reads.py`, `get_public_document_actions`, `test_document_action_hint_seam.py`) + +#### Known Limitations + +* **Public Workspace Sharing Is Not Yet Available** + * Public workspace documents cannot yet be shared into other workspaces. The search index used by public workspaces has no field for shared access, so adding it requires an index schema change and a migration for existing deployments. Public workspaces show no share controls until that work is done. + +### **(v0.261.133)** + +#### New Features + +* **Native Public Workspace Document Management** + * Public workspace documents can now be managed in the native explorer, not just browsed: upload, metadata editing, tags and bulk tagging, permitted downloads, metadata extraction, reprocessing, and revision-aware deletion. Previously every one of these required switching to the classic page. + * Each operation names its workspace directly rather than depending on the account's stored selection, so changing workspaces mid-operation cannot redirect an upload, edit, or deletion, and an older server rejects the request instead of acting in the wrong place. + * Every operation receipt is checked against the workspace that was asked for, so a late or misrouted response cannot be shown as a successful change to the wrong workspace. + * Manager roles keep the content-manager boundary, ordinary members retain read access only, and locked or upload-disabled workspaces keep their existing restrictions. Queued work revalidates permission when it runs rather than trusting the moment it was requested. + * Sharing and generated-artifact approval remain on the classic page and arrive natively in a later release. + * (Ref: `functions_public_document_management.py`, `functions_public_document_policy.py`, `route_backend_public_document_management.py`, `documentOperations.ts`, [V2 Public Document Management](features/V2_PUBLIC_DOCUMENT_MANAGEMENT.md), [Management APIs](features/PUBLIC_DOCUMENT_MANAGEMENT_APIS.md)) + +### **(v0.261.132)** + +#### New Features + +* **Native Public Workspace Document Browsing** + * Public workspace documents can be browsed in the same native explorer used for My Workspace and group workspaces, instead of only on the classic page. Filtering, sorting, pagination, facets, tags, document details, and version history all behave the way they already do elsewhere. + * Selecting a public workspace navigates by its identifier, and every document request names that workspace directly rather than depending on the account's stored selection. Returned documents are checked against the workspace that was asked for, so a late or misrouted response cannot populate the wrong workspace. + * The workspace is still recorded as the active selection, because chat document scoping and the classic page both read it. That step is deliberately non-blocking: if it fails, browsing still works, because nothing on the page depends on it. + * This release covers browsing only. Upload, metadata editing, tags, downloads, extraction, reprocessing, deletion, and approval remain on the classic page and arrive natively in later releases. + * (Ref: `functions_public_document_reads.py`, `functions_public_document_access.py`, `route_backend_public_document_reads.py`, `documentReadAdapter.ts`, `PublicWorkspacePage.tsx`, [V2 Public Document Browsing](features/V2_PUBLIC_DOCUMENT_BROWSING.md), [Public Document Read APIs](features/PUBLIC_DOCUMENT_READ_APIS.md)) + +#### Bug Fixes + +* **Charts, Mermaid Diagrams, And Image Proposals In Orchestrated Answers** + * Fixed orchestrated requests such as "Plot BatteryVoltage1 over the last 15 minutes" answering that a plot could not be produced. No orchestration step had a chart tool, the retrieved rows never reached the answer, and the answer step had none of the chart, Mermaid, or image-proposal guidance ordinary chat uses. + * A requested chart is now drawn from the exact rows an action retrieved, by a chart step that can use only the built-in chart tools. Long series are reduced to the 200 points a chart shows while keeping each segment's highest and lowest values, and the chart subtitle states the sampling. + * The answer places those charts, writes Mermaid diagrams, and proposes images when image generation is enabled, including when an image would help but was not requested. Each image is generated only after the user approves its card. + * Saved Instruction memories shape these visuals, so "I don't like charts" stops charts you did not ask for. An explicit request in the current message still wins, and recalled facts never reach the chart step. + * (Ref: `functions_orchestration_visuals.py`, `functions_orchestration_actions.py`, `functions_orchestration_adapters.py`, `functions_chart_operations.py`, [Visual Outputs Fix](fixes/ORCHESTRATION_VISUAL_OUTPUTS_FIX.md)) + +#### User Interface Enhancements + +* **Image In Orchestrate** + * The composer's Image control now works in Orchestrate, where it asks the answer for at least one image proposal card instead of blocking the message. A status line explains that the images wait for approval. + * (Ref: `Composer.tsx`, [Chat controls](../reference/chat-controls.md)) + +### **(v0.261.131)** + +#### New Features + +* **Native Group Document Sharing And Approval Review** + * Share a group document with another group, accept or remove a received share, and approve, reject, or withdraw a pending generated artifact — all inside the native workspace explorer, without switching to the legacy group workspace page. + * A notification opens the exact group and document it refers to, including when that document is not on the current page. A notification that does not match has no navigation or activation side effect, and a link to a denied or deleted document does not silently fall back to another document. + * Decisions are bound to the document revision the reviewer actually saw. A conflicting change keeps the entered target and asks for an explicit refresh instead of resolving itself, and a partial outcome is repaired rather than replayed as a new decision. + * Sharing calls name the group in the URL rather than relying on the account's current selection, so changing groups mid-decision cannot redirect one, and an older server rejects the call instead of acting in the wrong group. + * Approving a generated artifact can start screening on a brand-new destination without that approval being blocked by the destination's own not-yet-started screening reservation. Admission requires recorded proof rather than the absence of evidence, so a scan that ran and was later removed cannot restore eligibility, and the screening hold itself is never released early. + * Removing a share can't be undone by a document update that was already writing the document's search entries. Sharing changes and those updates coordinate on the document itself, and an update carrying an older share list is limited to the document's current shares (0.261.130). + * (Ref: `functions_group_document_collaboration.py`, `functions_group_document_publication.py`, `functions_artifact_publication.py`, `functions_group_document_projection_fence.py`, `route_backend_group_documents.py`, `documentCollaboration.ts`, `DocumentCollaborationDialog.tsx`, [V2 Group Document Collaboration](features/V2_GROUP_DOCUMENT_COLLABORATION.md), [Collaboration APIs](features/GROUP_DOCUMENT_COLLABORATION_APIS.md), [Projection Coordination](features/GROUP_DOCUMENT_PROJECTION_COORDINATION.md), #1542) + +#### Bug Fixes + +* **Document Payload Redaction No Longer Depends On The Screening Toggle** + * Fixed document list and detail responses returning storage internals when content screening was disabled or a document was never enrolled. Affected values included blob paths and containers, SAS and download URLs, canonical source references, screening provenance, and Cosmos system fields. + * The same document was already redacted correctly whenever screening was enabled, so exposure depended on an unrelated administrative setting rather than on the sensitivity of the field. + * Root cause was a duplicated redaction rule: the unscreened path carried a hand-maintained three-field literal that had fallen behind the full private-field set. Both paths now share one predicate, so they cannot drift apart as new private fields are added. + * The generated-artifact request allow-list was duplicated the same way across the payload serializer and the group projection, where drift would silently drop requester attribution from pending artifact reviews. It is now a single shared constant. + * Group collaboration internals — the sharing ledger, the publication receipt id, and the generated artifact source conversation, message, container, and blob path — are redacted for every caller rather than only in the group browser projection. + * No caller loses a field it could rely on, because every newly redacted field was already absent in screening-enabled deployments. Share rosters remain serialized, because the surfaces that render share status and counts read them directly. + * (Ref: `content_screening/access.py`, `functions_group_document_reads.py`, `is_public_document_field`, `PRIVATE_DOCUMENT_FIELDS`, `GENERATED_ARTIFACT_REQUEST_FIELDS`, [Payload Redaction Fix](fixes/DOCUMENT_PAYLOAD_REDACTION_SCREENING_TOGGLE_FIX.md)) + +### **(v0.261.129)** + +#### New Features + +* **Microsoft 365 Mail And Calendar History** + * Read my mail can search by words and received dates, and Read my calendar events can read or search any past or future time range. Long histories continue across calls without repeating items. + * Without a range, calendar reads now return occurrences in the next 30 days instead of the calendar's oldest events and recurring series. + * No new Microsoft Graph permission is required. Reads stay limited to the primary mailbox and default calendar, and items removed by retention can't be returned. + * (Ref: `msgraph_plugin.py`, `functions_msgraph_operations.py`, [Microsoft 365 Email](../reference/actions/m365-email.md), [Microsoft 365 Calendar](../reference/actions/m365-calendar.md), #1523) + +* **Manage Group Documents In V2** + * Group Owners, Admins and DocumentManagers can upload documents and follow their progress, edit metadata and tags, manage the group's tag vocabulary, download when downloads are enabled, extract metadata, reprocess, and delete revisions, in the same explorer. Ordinary members keep read access, without these operations or downloads. + * The server checks each operation against your current role, the group's status, the document's revision and its content screening state. Locked groups are read-only, with downloads controlled separately. Upload-disabled groups allow deletion and reprocessing, but not uploads, edits or metadata extraction. Inactive groups allow none. + * Edits, extraction and reprocessing apply only to a document's current revision, and an edit made against an older copy is refused rather than applied to a newer one. Earlier revisions can only be downloaded or deleted. + * Operations use `/api/groups//documents/...` rather than the account's active group, and queued processing keeps the group it started in. The classic group document routes are unchanged. + * (Ref: `route_backend_group_documents.py`, `functions_group_document_management.py`, `DocumentExplorer.tsx`, [V2 Group Document Management](features/V2_GROUP_DOCUMENT_MANAGEMENT.md), [Group Document Management APIs](features/GROUP_DOCUMENT_MANAGEMENT_APIS.md), #1542) + +#### Bug Fixes + +* **Microsoft 365 Agent Replies Survive Stream Failures** + * Fixed agents that have OneDrive or SharePoint Online actions, or that run in a workflow, ending with "Stream interrupted: Agent streaming failed". Each streamed chunk ran in a new context, so the Microsoft 365 continuation journal failed when it closed. + * A reply that fails after streaming text is now saved as an incomplete message and appears when the conversation is reopened. Approval and sign-in waits still resume as before. + * The error banner says the partial content was saved only when the server saved it. + * (Ref: `agent_delegation_runtime.py`, `route_backend_chats.py`, `chat-streaming.js`, [Stream Context And Persistence Fix](fixes/M365_AGENT_STREAM_CONTEXT_PERSISTENCE_FIX.md), #1523) + +### **(v0.261.128)** + +#### New Features + +* **Browse Group Documents In V2** + * Group Documents uses the same explorer as My Workspace. Search, sort, filter by tag, classification and place, see whole-workspace counts, open a document's details and the revisions you can access, and pass eligible documents to chat, without leaving V2. + * Every read names the group, so the page never shows the documents of a group selected in another tab. Switching groups resets the search and selection, and a slower earlier response can't replace newer details. + * The explorer offers only the sorts, filters and views the server says the group supports. Your display preferences are shared with My Workspace; personal saved views aren't shown as group views. + * The group document list, facets, tags, details and revisions accept one explicit `group_id`. A malformed or repeated one is refused rather than replaced by your active group, a missing group returns 404, and a group you can't view returns 403. Requests without `group_id` behave as before. + * (Ref: `route_backend_group_documents.py`, `functions_group_document_reads.py`, `documentReadAdapter.ts`, `DocumentExplorer.tsx`, [V2 Group Document Browsing](features/V2_GROUP_DOCUMENT_BROWSING.md), [Group Document Read APIs](features/GROUP_DOCUMENT_READ_APIS.md), #1542) + +### **(v0.261.127)** + +#### New Features + +* **Group Workspaces In V2** + * Open your group workspaces in V2 at `/v2/groups`, with the rail and layout of My Workspace. Choosing a group from the picker makes it your active group, as the classic interface does, and loads its details, your role, its status and the sections your role can use. The page opens your active group if you can still view it, and otherwise asks you to choose one. + * Links to a group, or to one of its sections such as `/v2/groups//workflows`, are checked before the page shows that group. The picker keeps your choice while you search or page through your groups. + * Switching groups asks before discarding unsaved edits. A switch you cancel sends nothing, a refused switch keeps the group you had, and an interrupted switch is checked with the server instead of being assumed to have failed. A conversation you already have open keeps its workspace. + * Group workflows and Call agent tools open inside the group page. Sections that weren't native yet linked to their classic pages; every section is native from 0.261.153. + * The page reads its group from `GET /api/v2/workspaces/group/` (0.261.126). It requires current membership in that group (being an application admin isn't enough) and never reads or changes your active group. It returns an allow-listed summary of the group, your role and status, and the sections and operations your role allows, with no settings, membership lists, pending requests, endpoint configurations or credentials. A failure is reported as an error, never as an empty workspace. + * (Ref: `route_backend_v2.py`, `functions_workspace_context.py`, `workspaceContext.ts`, `groupWorkspaceStore.ts`, `GroupWorkspacePage.tsx`, `WorkspaceShell.tsx`, [V2 Shared Workspace Context and Group Shell](features/V2_SHARED_WORKSPACE_CONTEXT.md), #1542) + +### **(v0.261.123)** + +#### New Features + +* **Workflow Authoring Undo And Redo** + * Restore unsaved structured-workflow edits across List and Flow, including common fields, block configuration, unfinished schema text and errors, and Repeat state fields. Redo preserves exact block identities. + * Shared Undo/Redo controls group typing by field visit while text controls retain native undo. Replay that removes blocks or affects references requires fresh confirmation. + * History retains up to 100 actions and 32 MiB of additional accounted data. Old-step eviction is visible; a single oversized edit asks before applying its complete contents and clearing history. + * Failed saves retain the draft, history, and original saved revision. Save success, discard, session changes, and confirmed access loss clear history. Undo never rolls back a saved workflow, execution, or publication. + * (Ref: `WorkflowEditorDialog.tsx`, `WorkflowAuthoringHistory.tsx`, `workflowAuthoringHistory.ts`, [Cross-surface workflow history](features/WORKFLOW_AUTHORING_UNDO_REDO.md)) + +### **(v0.261.104)** + +#### Bug Fixes + +* **Model-Aware Reasoning Across Chat And Orchestration** + * Fixed plan editing and Auto/Review execution failures caused by unsupported reasoning levels. GPT-5.6 Luna's stale Minimal selection becomes Low with a visible adjustment, while supported None remains explicit. + * Both interfaces use canonical per-model capabilities. Narrow provider compatibility recovery reports Model default rather than claiming the rejected effort was honored; model identity and approval safeguards remain unchanged. + * (Ref: `functions_model_capabilities.py`, `model_endpoint_clients.py`, `functions_orchestration_models.py`, `route_backend_chats.py`, [Reasoning Compatibility Fix](fixes/ORCHESTRATION_REASONING_LEVEL_COMPATIBILITY_FIX.md)) + +* **Capability-Aware Planning Without Hidden Shortcuts** + * Selected supported tools and sources are requirements, not a restriction to only those tools. Unchecked controls no longer imply that enabled Web Search or Deep Research is unauthorized. + * Every Orchestrate request reaches the planner, including short questions. Direct answers remain available; no topic rule forces research. Model and discovery failures are surfaced instead of becoming successful answer-only plans. + * Fixed authorized group-agent catalog discovery and preserved original selections separately from model-chosen plan usage. Current capabilities are rechecked before execution. + * Enabled saved memories now inform private-conversation planning, edits, and answers without autosave or embedding backfill. Scope and audience are rechecked before answering, and existing memory citations are preserved. + * (Ref: `functions_orchestration_context.py`, `functions_orchestration_registry.py`, `functions_orchestration_planner.py`, `functions_agent_catalog.py`, [Capability Context Fix](fixes/ORCHESTRATION_CAPABILITY_CONTEXT_FIX.md)) diff --git a/docs/explanation/release-notes/v0.261-part-5.md b/docs/explanation/release-notes/v0.261-part-5.md new file mode 100644 index 000000000..46158eba7 --- /dev/null +++ b/docs/explanation/release-notes/v0.261-part-5.md @@ -0,0 +1,1675 @@ +--- +title: "Release notes 0.261.102 – 0.261.048" +description: "SimpleChat release notes for 0.261.102 – 0.261.048." +section: "Reference" +layout: page +--- + + + +# Release notes 0.261.102 – 0.261.048 + +[Back to release notes index]({{ '/explanation/release_notes/' | relative_url }}) + +### **(v0.261.102)** + +#### New Features + +* **Shared AI Connections Framework** + * Configure a connection and its credentials once, then choose independent chat and image-generation defaults from compatible models on that connection. + * Capability-aware model references and runtime adapters provide extension points for future embeddings, transcription, speech, computer use, and other integrations. Those additional integrations are not enabled by this release. + * (Ref: `functions_ai_connections.py`, `functions_model_capabilities.py`, [AI Connections Framework](features/AI_CONNECTIONS_FRAMEWORK.md), #1436) + +#### User Interface Enhancements + +* **One Image Model Picker Across Shared Connections** + * Classic and V2 Admin Settings now use the shared AI Connections catalog instead of maintaining another image endpoint and credential form. + * Existing image settings are imported automatically, preserving the selected model, authentication, image API version, and APIM routing without enabling multi-endpoint chat. + * Model availability, invalidated defaults, and import recovery are explained alongside the controls. Duplicate connection names and deployment names remain distinct through stable references. + * (Ref: `CapabilityModelPicker.tsx`, `ModelConnectionsManager.tsx`, `functions_ai_connection_migration.py`, [Configure AI Connections](../guides/configure-ai-connections.md)) + +#### Bug Fixes + +* **GPT Image Generation Uses The Correct API** + * Compatible GPT deployments use Azure's v1 Responses image tool rather than the older dated Responses contract. Dedicated image deployments retain the Images API. + * Image generation no longer requires a usable text-model configuration. Inline approvals retain the selected deployment when persisting images, and shared image edits preserve rate-limit and configuration-error status codes. + * Provider-specific image-tool availability and deployment requirements still apply; the application does not provision or guess a backing deployment. + * (Ref: `functions_image_generation.py`, `functions_image_api_route.py`, `route_backend_chats.py`, [GPT Image Generation Fix](fixes/GPT_CHAT_MODEL_IMAGE_GENERATION_FIX.md), #1436) + +* **Connection Changes Preserve Configuration And Capability Boundaries** + * Image-only models are excluded from text-only chat, agent, workflow, and document operations. Unverified model variants do not inherit image-tool support from a name prefix. + * Failed imports and unrelated Classic settings saves retain legacy image settings. Connection duplication preserves image operation profiles, and concurrent imports stage credentials without overwriting another worker's committed value. + * Dated Azure Images requests use API-key authentication independently of a connection's Foundry label, while explicit gateway headers remain supported. + * (Ref: `functions_ai_connection_migration.py`, `functions_keyvault.py`, `route_frontend_admin_settings.py`, `admin_model_endpoints.js`, `functions_model_endpoint_runtime.py`) + +### **(v0.261.100)** + +#### New Features + +* **Composer-Aware Inline Answers** + * V2 orchestration follow-up questions now support `#` file, tag, and workspace references, `/` saved prompts, and uploads through a compact version of the main composer's editor. + * Single- and multiple-choice selections can include an optional explanation, prompt, or additional sources. File suggestions are not an exhaustive list: a different referenced or uploaded file can answer the question on its own. + * This applies to supported chat file types generally, not only tabular files. Existing upload and workspace permissions still apply. + * (Ref: `ComposerEditor.tsx`, `ElicitationCard.tsx`, `composerDraft.ts`, [Chat Orchestration](features/CHAT_ORCHESTRATION.md)) + +#### User Interface Enhancements + +* **Upload Progress And Independent Answer Drafts** + * Inline uploads show processing state and retry/remove controls. Finish waits for selected files to be ready rather than continuing without them. + * Paging and navigation within the current browser session preserve each answer's text, references, prompt edits, and variable values. Failed submissions keep the draft available for correction or retry. + * The main composer remains separate, and inline questions do not duplicate its model, agent, web, or voice toolbar. + * (Ref: `chatUploads.ts`, `orchestrationStore.ts`, `AttachedPromptCard.tsx`, [Upload Documents In Chat](../guides/upload-documents-in-chat.md)) + +#### Bug Fixes + +* **Follow-Up Answers Reach The Resumed Task** + * Fixed a client/server mismatch that could ignore inline answers. Replies are now matched to the stored question and checked against its owner, turn, and revision. + * Accepted explanations, filled prompts, and real source references reach both replanning and execution, while original selections and document filters remain intact. + * Retries cannot apply the same answer twice, and declining or cancelling sends no draft answer context. + * (Ref: `orchestrationController.ts`, `functions_orchestration_schema.py`, `functions_orchestration_context.py`, `functions_orchestration_runs.py`, `route_backend_orchestration.py`) + +### **(v0.261.099)** + +#### Bug Fixes + +* **Orchestration Plans Are No Longer Trapped In One Browser** + * Plans looked like they belonged to the device that made them. Opening the same conversation on a laptop after planning on a phone showed an empty orchestration panel, and simply reloading the page lost the run history too. The answer survived, because that is an ordinary chat message, but the plan behind it, its steps and its results appeared to be gone. + * **Nothing was ever actually lost.** Every run has always been stored against the conversation on the server — the planner already reads earlier runs back to avoid repeating work. The V2 interface just never asked for them, so it could only ever show what the current tab happened to remember. + * **The panel is now rebuilt from the server when you open a conversation.** Earlier runs are listed wherever you open them, and any one of them can be expanded to read the steps and results of work you were not present for. A run opened this way is shown as a record and marked as such: it can be read, but not edited or run again, because it has already happened. + * **A plan still waiting for your approval follows you.** It reappears on the new device as a plan you can approve, edit or discard, rather than leaving you to ask the same question twice. A plan that had a countdown is deliberately restored without one, so nothing starts running on a device where nobody was watching. And if you approved it on the first device in the meantime, approving it again is refused rather than doing the work twice — the conversation reloads to show the answer that already exists. + * **A run whose browser went away mid-flight is now shown as interrupted**, instead of vanishing or appearing to still be working. + * The run listing was also tightened while this was added. It now returns only the fields the panel draws, so plan internals, request seeding and the conversation context are no longer sent to the browser as a side effect of listing a conversation's history. + * (Ref: `route_backend_orchestration.py` run detail route and summary projection, `stores/orchestrationStore.ts` hydration, `lib/orchestrationResume.ts`, [Orchestration Run History Hydration Fix](fixes/ORCHESTRATION_RUN_HISTORY_HYDRATION_FIX.md)) + +### **(v0.261.098)** + +#### New Features + +* **Orchestration Can Use Existing Actions Directly** + * Administrators can enable the default-off **Enable Action Access** setting to let knowledge-collection plans choose an accessible personal, group or global action without loading a configured agent and its unrelated actions. + * Each step can make a bounded sequence of calls to its selected action's functions, preserving existing scope settings, governance, function restrictions and action behavior. **Call agent** remains under **Ask an agent**; no output-phase workflow or new read/write policy is introduced. + * The plan shows the selected action's name and scope. Tool results appear under **Sources > Tool calls**, separately from web references. + * Follow-up action tasks retain the resolved subject and authorized conversation references, alongside the conversation-history and prompt-snapshot improvements from the V2 integration branch. + * (Ref: `functions_action_catalog.py`, `functions_orchestration_actions.py`, `functions_orchestration_registry.py`, `OrchestrationRunView.tsx`, [Chat Orchestration Action Access](features/CHAT_ORCHESTRATION_ACTIONS.md)) + +#### Bug Fixes + +* **Orchestration Preserves Agent Choices And Tool Reporting** + * Available agents now reach plan validation, and retries after an unrenderable clarification retain the caller's capability gates. + * Tool citations use the existing tool-results channel instead of being treated as web sources. Saved usage includes both the answer model and action or agent steps rather than dropping one contribution. + * (Ref: `functions_orchestration_planner.py`, `route_backend_orchestration.py`, `functions_orchestration_events.py`, `chatStore.ts`) + +### **(v0.261.096)** + +#### New Features + +* **Composer-Aware Inline Answers** + * V2 orchestration follow-up questions now support `#` file, tag, and workspace references, `/` saved prompts, and uploads through a compact version of the main composer's editor. + * Single- and multiple-choice selections can include an optional explanation, prompt, or additional sources. File suggestions are not an exhaustive list: a different referenced or uploaded file can answer the question on its own. + * This applies to supported chat file types generally, not only tabular files. Existing upload and workspace permissions still apply. + * (Ref: `ComposerEditor.tsx`, `ElicitationCard.tsx`, `composerDraft.ts`, [Chat Orchestration](features/CHAT_ORCHESTRATION.md)) + +* **Orchestration Can Use Existing Actions Directly** + * Administrators can enable the default-off **Enable Action Access** setting to let knowledge-collection plans choose an accessible personal, group or global action without loading a configured agent and its unrelated actions. + * Each step can make a bounded sequence of calls to its selected action's functions, preserving existing scope settings, governance, function restrictions and action behavior. **Call agent** remains under **Ask an agent**; no output-phase workflow or new read/write policy is introduced. + * The plan shows the selected action's name and scope. Tool results appear under **Sources > Tool calls**, separately from web references. + * (Ref: `functions_action_catalog.py`, `functions_orchestration_actions.py`, `functions_orchestration_registry.py`, `OrchestrationRunView.tsx`, [Chat Orchestration Action Access](features/CHAT_ORCHESTRATION_ACTIONS.md)) + +#### User Interface Enhancements + +* **Upload Progress And Independent Answer Drafts** + * Inline uploads show processing state and retry/remove controls. Finish waits for selected files to be ready rather than continuing without them. + * Paging and navigation within the current browser session preserve each answer's text, references, prompt edits, and variable values. Failed submissions keep the draft available for correction or retry. + * The main composer remains separate, and inline questions do not duplicate its model, agent, web, or voice toolbar. + * (Ref: `chatUploads.ts`, `orchestrationStore.ts`, `AttachedPromptCard.tsx`, [Upload Documents In Chat](../guides/upload-documents-in-chat.md)) + +* **Full-Page Agent And Action Editors In My Workspace** + * Configure personal agents and actions directly in V2, with section navigation instead of popup wizards. Agent authoring includes model connections, knowledge, instructions, capabilities, and templates; action authoring includes the existing connector-specific configuration and authentication workflows. + * **Call agent is a normal action type**, appearing alongside other tools in the Actions collection and the agent's action picker. There is no separate personal delegation section. + * Create an action from an unfinished agent and return to the retained draft. Saving the action does not save the agent prematurely; leaving an unfinished editor asks before discarding changes. + * Provided resources remain read-only, and existing authorized actions can be attached independently of permission to create new actions. Group and administrator management retain their existing interfaces. + * (Ref: `AgentEditorPage.tsx`, `ActionEditorPage.tsx`, `WorkspaceEditorFrame.tsx`, [V2 Workspace Agent And Action Authoring](features/V2_WORKSPACE_AGENTS_ACTIONS.md)) + +#### Bug Fixes + +* **Follow-Up Answers Reach The Resumed Task** + * Fixed a client/server mismatch that could ignore inline answers. Replies are now matched to the stored question and checked against its owner, turn, and revision. + * Accepted explanations, filled prompts, and real source references reach both replanning and execution, while original selections and document filters remain intact. + * Retries cannot apply the same answer twice, and declining or cancelling sends no draft answer context. + * (Ref: `orchestrationController.ts`, `functions_orchestration_schema.py`, `functions_orchestration_context.py`, `functions_orchestration_runs.py`, `route_backend_orchestration.py`) + +* **Orchestration Preserves Agent Choices And Tool Reporting** + * Available agents now reach plan validation, and retries after an unrenderable clarification retain the caller's capability gates. + * Tool citations use the existing tool-results channel instead of being treated as web sources. Saved usage includes both the answer model and action or agent steps rather than dropping one contribution. + * (Ref: `functions_orchestration_planner.py`, `route_backend_orchestration.py`, `functions_orchestration_events.py`, `chatStore.ts`) + +* **Workspace Edits Preserve Existing Configuration** + * Per-record saves retain unedited nested settings, action references, and credentials. Stored secrets stay masked, explicit clearing is distinguished from keeping a value, and conflicting edits are reported rather than silently overwriting another session. + * **Use in chat** refreshes the authorized agent catalogue before starting a new conversation with the chosen agent. Failed refreshes or unavailable agents do not retarget an existing conversation or substitute a manual model. + * (Ref: `functions_workspace_authoring.py`, `workspaceAuthoring.ts`, `workspaceAuthoringApi.ts`, `workspaceEditorDrafts.ts`, `workspaceAgentLaunch.ts`) + +### **(v0.261.095)** + +#### New Features + +* **Agents Can Call Explicitly Selected Specialists** + * Create a **Call agent** action, choose a local or Foundry-backed target, and attach it to a local agent. The caller supplies a task and relevant context, receives the specialist's result, and continues its own answer without automatically sharing the whole conversation. + * Targets retain their own instructions, model configuration, authorized knowledge, and tools. Calls stay in the same workspace or use permitted global agents, with access checked again when each call runs. + * Nested delegation is bounded by **3 levels, 10 delegated attempts per root turn, and 120 seconds per call**. Self-calls and loops are blocked; cancellation preserves completed child citations and observed usage. + * Delegation works in ordinary and streaming chat, agent workflows, and V2 orchestration. Foundry-backed agents are callable targets; their own tools remain configured in Foundry. + * (Ref: `functions_agent_delegation.py`, `agent_delegation_runtime.py`, `semantic_kernel_plugins/agent_plugin.py`, [Agent Delegation Actions](features/AGENT_DELEGATION_ACTION.md)) + +#### User Interface Enhancements + +* **Configure Agent Calls In Classic And V2** + * Both interfaces provide scoped target selection and action attachment for personal, group, and global agents. V2 adds focused controls rather than requiring a trip to classic for Call agent configuration. + * V2 supports confirmed deletion of owned Call agent actions, preserves unrelated bindings and configuration, and reports conflicting edits instead of overwriting them. Selecting a group for configuration does not switch the active workspace. + * (Ref: `plugin_modal_stepper.js`, `agent_modal_stepper.js`, `components/agents/AgentDelegationManager.tsx`, `pages/GroupAgentDelegationPage.tsx`, [Call Another Agent](../guides/call-another-agent.md)) + +#### Bug Fixes + +* **Group Workflow Agents Use The Run Actor's Permissions** + * A manually started group workflow now authorizes agent calls as the authenticated member running it, rather than inheriting the workflow creator's access. Scheduled runs without an interactive actor retain the workflow owner's execution identity. + * (Ref: `functions_workflow_runner.py` workflow execution identity capture and agent execution contexts) + +### **(v0.261.093)** + +#### New Features + +* **Agents Can Call Explicitly Selected Specialists** + * Create a **Call agent** action, choose a local or Foundry-backed target, and attach it to a local agent. The caller supplies a task and relevant context, receives the specialist's result, and continues its own answer without automatically sharing the whole conversation. + * Targets retain their own instructions, model configuration, authorized knowledge, and tools. Calls stay in the same workspace or use permitted global agents, with access checked again when each call runs. + * Nested delegation is bounded by **3 levels, 10 delegated attempts per root turn, and 120 seconds per call**. Self-calls and loops are blocked; cancellation preserves completed child citations and observed usage. + * Delegation works in ordinary and streaming chat, agent workflows, and V2 orchestration. Foundry-backed agents are callable targets; their own tools remain configured in Foundry. + * (Ref: `functions_agent_delegation.py`, `agent_delegation_runtime.py`, `semantic_kernel_plugins/agent_plugin.py`, [Agent Delegation Actions](features/AGENT_DELEGATION_ACTION.md)) + +#### User Interface Enhancements + +* **Configure Agent Calls In Classic And V2** + * Both interfaces provide scoped target selection and action attachment for personal, group, and global agents. V2 adds focused controls rather than requiring a trip to classic for Call agent configuration. + * V2 supports confirmed deletion of owned Call agent actions, preserves unrelated bindings and configuration, and reports conflicting edits instead of overwriting them. Selecting a group for configuration does not switch the active workspace. + * (Ref: `plugin_modal_stepper.js`, `agent_modal_stepper.js`, `components/agents/AgentDelegationManager.tsx`, `pages/GroupAgentDelegationPage.tsx`, [Call Another Agent](../guides/call-another-agent.md)) + +#### Bug Fixes + +* **Group Workflow Agents Use The Run Actor's Permissions** + * A manually started group workflow now authorizes agent calls as the authenticated member running it, rather than inheriting the workflow creator's access. Scheduled runs without an interactive actor retain the workflow owner's execution identity. + * (Ref: `functions_workflow_runner.py` workflow execution identity capture and agent execution contexts) + +{% raw %} +### **(v0.261.092)** + +#### New Features + +* **A Saved Prompt Is Now Attached To Your Message, Not Pasted Into It** + * Picking a prompt in the V2 interface used to paste its text into the message box, where it stopped being a prompt: nothing marked where the standing instructions ended and your own question began, taking it back off meant finding and deleting the right paragraphs, and fixing a variable meant editing prose in the middle of what you were writing. + * **The prompt now sits in a card above the message box**, collapsed to its name, its workspace, and how many variables still need a value. The box below stays yours to type in. + * **Expand it** to fill in variables and read the prompt exactly as it will be sent. **Edit** adjusts the wording for this one message and marks the card *Edited*, with a **Reset** that restores the saved text — the saved prompt is never touched, so a one-off tweak does not change it for everyone else. **Remove** takes it off without disturbing anything you have typed. + * **The prompt is sent first and your message follows it**, which is the order the two are actually written in. A prompt that needs no further input can be sent on its own, so Send now works with an attached prompt and an empty box. + * **Variables are resolved when you send, not when you pick.** `{{composer}}` — "what you have already typed" — used to resolve to nothing, because picking the prompt is the first thing you do. It now means the message you wrote underneath the card. A prompt that positions your text this way is not also sent it a second time. + * **Sent messages show the prompt as a collapsed row** above your own words, expanding to the full text, so a reply can be understood by someone who did not pick the prompt. Copying and exporting still yield the whole message, and older messages render exactly as they did. + * The separate fill-in dialog is gone, folded into the card. Its safety rules came with it: auto-filled values stay badged and clearable, values from the conversation are still only ever offered as chips you click, and nothing is pre-filled at all in a shared conversation. + * (Ref: `components/chat/AttachedPromptCard.tsx`, `lib/usePromptVariableValues.ts`, `lib/promptRequest.ts`, `lib/messagePrompt.ts`, [Prompt Composer Card](features/PROMPT_COMPOSER_CARD.md)) + +* **Orchestration Plans Now Read The Prompt You Chose** + * The planner was told a selected prompt's *name* and nothing else. "Quarterly review" says nothing about whether the work involves reading documents, searching the web, or comparing two things, which is exactly what a plan has to decide. It is now given the prompt's wording, capped so a long saved prompt cannot crowd out the rest of the planner's context. + * A selected prompt also now counts as you having pointed at something, alongside a chosen document or agent, so a request carrying one is no longer triaged as a remark to answer off the cuff. + * (Ref: `functions_orchestration_context.py` `_selected_prompt`, `functions_orchestration_planner.py` `triage_request`, `functions_orchestration_schema.py` `build_plan_inputs`) + +#### Bug Fixes + +* **Using A Saved Prompt In An Ordinary V2 Chat Recorded Nothing** + * The V2 interface sent `prompt_info` only when orchestration was planning the turn. An ordinary message written with a saved prompt therefore left no record that a prompt had been involved at all — nothing in the message's metadata, and nothing for the conversation export to report. Both send paths now report the prompt through one shared builder. + * (Ref: `stores/chatStore.ts` `sendMessage`, `Composer.tsx` `buildOrchestrationSeeds`, `route_backend_chats.py` `prompt_selection`) +{% endraw %} + +### **(v0.261.091)** + +#### New Features + +* **Orchestration Uses The Documents And Tags You Picked** + * The context picker's chips reached a plan only as document ids. A tag chip narrowed a normal chat message and did nothing at all in orchestration, so a plan searched more widely than you had asked it to — the one thing a chip is for. + * **Tags now narrow a plan too, and stay in force for the whole run.** A later step can choose its own search query but cannot widen the shelf you narrowed to. Documents and tags picked together are combined additively, matching the chat path. + * **A document you picked now reaches the planner by name.** It previously arrived as a bare identifier, so the planner could not write "compare the Q3 and Q4 contracts" without being told which document was which, and the plan you were asked to approve listed a row of uuids. The composer sends the names it already had on screen. + * **The plan marks which documents were your choice.** A document you picked and one the planner found are both documents the plan will read, but only the second is a decision worth checking — so the two now look different on the card. + * **A step can act on what an earlier step finds.** A plan could previously only use documents whose ids were known when it was written, so it could not express "search for the relevant contracts, then analyse them" — the planner had to guess which contracts, or not plan it at all. A step can now defer to an earlier searching step instead. The plan says so plainly rather than showing an empty list, and the planner still names documents directly whenever they are already known, because a named document can be approved and a deferred one cannot. + * Names are display only. What a plan may read is still decided from the document ids, so a renamed document is exactly the document it was. Documents that arrive mid-run are re-checked the same way, and still respect the administrator's per-action limit. + * (Ref: `resolve_seeds`, `resolve_candidate_documents`, `RunContext.tags`, `RunContext.step_documents`, `documents_from_step`, `contextDocumentDescriptors`, [Chat Orchestration](features/CHAT_ORCHESTRATION.md), [Chat Context Picker](features/CHAT_CONTEXT_PICKER.md)) + +#### Bug Fixes + +* **Plans No Longer Propose Work The User Cannot Do** + * Whether a capability suits *this person, asking this question* — do they hold the app role, does their message actually contain a link, do they have an agent at all — was described for each capability but never consulted when a plan was made. The planner was shown every capability the deployment allowed, so it could propose reading links in a message containing none, or name an agent the user does not have. The step then failed at the point it ran, having promised something in a plan the user had already approved. + * The per-caller checks now run when a plan is built. Because the same resolution feeds the planner and the validator, this narrows what is offered, what is accepted, and what can reach execution. + * Nobody could reach anything they were not entitled to — each capability re-checks its own permission before doing any work — but a plan could describe it, which is its own kind of wrong. + * (Ref: `plan_request(request_context=...)`, `route_backend_orchestration._capability_request_context`, `functions_orchestration_registry` request gates) + +* **Documents Found During A Run Had No Workspace** + * A document discovered by a search step lost the workspace it came from, because the citation dropped `group_id` and `public_workspace_id` even though the search index returns them. Since context chips are grouped by workspace, a found document had no home to be offered back into. + * (Ref: `_citations_from_search_results`) + +{% raw %} +### **(v0.261.090)** + +#### New Features + +* **A Saved Prompt Is Now Attached To Your Message, Not Pasted Into It** + * Picking a prompt in the V2 interface used to paste its text into the message box, where it stopped being a prompt: nothing marked where the standing instructions ended and your own question began, taking it back off meant finding and deleting the right paragraphs, and fixing a variable meant editing prose in the middle of what you were writing. + * **The prompt now sits in a card above the message box**, collapsed to its name, its workspace, and how many variables still need a value. The box below stays yours to type in. + * **Expand it** to fill in variables and read the prompt exactly as it will be sent. **Edit** adjusts the wording for this one message and marks the card *Edited*, with a **Reset** that restores the saved text — the saved prompt is never touched, so a one-off tweak does not change it for everyone else. **Remove** takes it off without disturbing anything you have typed. + * **The prompt is sent first and your message follows it**, which is the order the two are actually written in. A prompt that needs no further input can be sent on its own, so Send now works with an attached prompt and an empty box. + * **Variables are resolved when you send, not when you pick.** `{{composer}}` — "what you have already typed" — used to resolve to nothing, because picking the prompt is the first thing you do. It now means the message you wrote underneath the card. A prompt that positions your text this way is not also sent it a second time. + * **Sent messages show the prompt as a collapsed row** above your own words, expanding to the full text, so a reply can be understood by someone who did not pick the prompt. Copying and exporting still yield the whole message, and older messages render exactly as they did. + * The separate fill-in dialog is gone, folded into the card. Its safety rules came with it: auto-filled values stay badged and clearable, values from the conversation are still only ever offered as chips you click, and nothing is pre-filled at all in a shared conversation. + * (Ref: `components/chat/AttachedPromptCard.tsx`, `lib/usePromptVariableValues.ts`, `lib/promptRequest.ts`, `lib/messagePrompt.ts`, [Prompt Composer Card](features/PROMPT_COMPOSER_CARD.md)) + +* **Orchestration Plans Now Read The Prompt You Chose** + * The planner was told a selected prompt's *name* and nothing else. "Quarterly review" says nothing about whether the work involves reading documents, searching the web, or comparing two things, which is exactly what a plan has to decide. It is now given the prompt's wording, capped so a long saved prompt cannot crowd out the rest of the planner's context. + * A selected prompt also now counts as you having pointed at something, alongside a chosen document or agent, so a request carrying one is no longer triaged as a remark to answer off the cuff. + * (Ref: `functions_orchestration_context.py` `_selected_prompt`, `functions_orchestration_planner.py` `triage_request`, `functions_orchestration_schema.py` `build_plan_inputs`) + +#### Bug Fixes + +* **Using A Saved Prompt In An Ordinary V2 Chat Recorded Nothing** + * The V2 interface sent `prompt_info` only when orchestration was planning the turn. An ordinary message written with a saved prompt therefore left no record that a prompt had been involved at all — nothing in the message's metadata, and nothing for the conversation export to report. Both send paths now report the prompt through one shared builder. + * (Ref: `stores/chatStore.ts` `sendMessage`, `Composer.tsx` `buildOrchestrationSeeds`, `route_backend_chats.py` `prompt_selection`) +{% endraw %} + +### **(v0.261.089)** + +#### New Features + +* **Choose Which Documents A Message Uses, And See The Choice** + * The **Documents** button was previously a plain on/off: it meant "search my documents", with no way to say *which* ones and nothing on screen describing what a message would actually look at. + * **You can now name them three ways.** Type `#` in the message box and search; open the Documents picker and tick them; or select documents in your workspace and press **Chat**. Tags have a chat action of their own now too. + * **A reference shows up in two places on purpose.** It becomes a removable chip above the message box, grouped by workspace, and it stays inside your message as `#[Q3 Contract.pdf]` — rendered as a chip rather than raw brackets — so "compare `#[Q3 Contract.pdf]` against `#[Q2 Contract.pdf]`" still reads as a sentence after it is sent. Removing the chip removes the text, and editing the text retires the chip. + * **The row condenses as it fills.** Up to five references show by name; beyond that each workspace collapses to a count you can open, so a long selection does not push the message box off the screen. + * **The `#` menu searches everywhere at once** — your personal workspace, every group you belong to, and every visible public workspace — plus tags and whole workspaces. A scope that is briefly unavailable no longer empties the menu of everything else. + * **The Documents picker opens upward and has a search box**, which the V2 dropdowns did not, and groups results by workspace. The original "search all my documents" behaviour is still there as the first row. + * Documents and tags chosen together are now combined additively, so a document chip beside an unrelated tag chip no longer matches nothing. Selecting several tags still requires a document to carry all of them. + * (Ref: `lib/chatContext.ts`, `lib/chatContextTokens.ts`, `lib/contextMentions.ts`, `components/chat/ContextChips.tsx`, `components/chat/DocumentPickerPopover.tsx`, [Chat Context Picker](features/CHAT_CONTEXT_PICKER.md)) + +#### Bug Fixes + +* **Chat From The Workspace Left The V2 Interface** + * Selecting documents in the V2 workspace and pressing **Chat** performed a full page load into the *classic* chat page, quietly moving the user out of V2 by the action most likely to follow choosing a document. It now opens the V2 composer with those documents already referenced. + * (Ref: `DocumentExplorer.onChat`, `lib/chatContextHandoff.ts`) + +* **Retrying A Tag-Filtered Message Searched More Widely Than The Original** + * The streaming chat path — the one the V2 interface uses — recorded which documents a message searched but not which tags, while the non-streaming path recorded both. Retrying or editing such a message therefore replayed a broader search than the one that produced the original answer, which reads as the assistant answering a different question the second time it is asked. + * (Ref: `route_backend_chats.py` `workspace_search` metadata, `route_backend_conversations._build_replayed_document_context`) + +* **Planned Documents Were Listed As Raw Identifiers** + * Each step of an orchestration plan listed the documents it would read as bare uuids. Deciding whether the planner picked the right document is the entire purpose of showing the plan before it runs, and `8f14e45f-ceea-467a-…` does not support that decision. Steps now list documents by name, falling back to the id only when the document can no longer be read. + * (Ref: `lib/documentTitles.ts`, `components/chat/OrchestrationRunView.tsx`) + +### **(v0.261.088)** + +#### New Features + +* **Image Generation Can Now Use A Chat Model Where No Image Model Is Available** + * Image generation previously required a dedicated `gpt-image` or DALL-E deployment. A deployment like that is separately approved and is not offered in every subscription or region, so a tenant without one could not switch image generation on at all — the deployment list was filtered to image models, and nothing appeared to select. + * A chat deployment such as `gpt-5.6` can now be selected instead. It has no image endpoint, so SimpleChat asks it through the Responses API's image generation tool rather than the images endpoint, and works out which of the two applies from the model behind the deployment you chose. There is no new setting to configure. + * **Fetch deployments** now lists chat models alongside image models, and excludes embedding deployments, which can produce an image either way. + * A chat deployment offers whole-image regeneration only. Changing part of an image needs the images API, and the editor says so before you paint a region rather than after. + * Existing configurations are untouched. Every deployment that could be selected before still takes the route it always did, including one whose model name was never recorded and one reached through API Management. + * Worth one test generation after selecting a chat deployment: the tool is served by an image model behind the scenes, so a subscription with no image capability at all may still be refused. + * (Ref: `functions_image_api_route.py`, `functions_image_generation.request_generated_image_source`, `/api/models/image`, [Image generation through Responses-capable chat models](features/IMAGE_GENERATION_RESPONSES_MODELS.md)) + +#### User Interface Enhancements + +* **The Redundant API Management Switch Is Gone From AI Models** + * **Send requests through API Management** has been removed from the AI Models tab. It belongs to the classic single endpoint, and a connection now carries its own API Management configuration, so the switch was a second control for a route connections never take — and the endpoint, deployment and subscription key it depends on were only settable on the classic admin page anyway. + * The setting itself is unchanged and still applies to the classic endpoint. It is edited on the server-rendered admin page. + * (Ref: `admin_settings_fields.ADMIN_SETTINGS_FIELDS['gpt-config']`, `SUPPRESSED_CAPABILITY_KEYS`, [AI Models](../admin/ai-models.md)) + +#### Bug Fixes + +* **Plans No Longer Propose Work The User Cannot Do** + * Whether a capability suits *this person, asking this question* — do they hold the app role, does their message actually contain a link, do they have an agent at all — was described for each capability but never consulted when a plan was made. The planner was shown every capability the deployment allowed, so it could propose reading links in a message containing none, or name an agent the user does not have. The step then failed at the point it ran, having promised something in a plan the user had already approved. + * The per-caller checks now run when a plan is built. Because the same resolution feeds the planner and the validator, this narrows what is offered, what is accepted, and what can reach execution. + * Nobody could reach anything they were not entitled to — each capability re-checks its own permission before doing any work — but a plan could describe it, which is its own kind of wrong. + * (Ref: `plan_request(request_context=...)`, `route_backend_orchestration._capability_request_context`, `functions_orchestration_registry` request gates) + +### **(v0.261.087)** + +#### New Features + +* **Choose Which Documents A Message Uses, And See The Choice** + * The **Documents** button was previously a plain on/off: it meant "search my documents", with no way to say *which* ones and nothing on screen describing what a message would actually look at. + * **You can now name them three ways.** Type `#` in the message box and search; open the Documents picker and tick them; or select documents in your workspace and press **Chat**. Tags have a chat action of their own now too. + * **A reference shows up in two places on purpose.** It becomes a removable chip above the message box, grouped by workspace, and it stays inside your message as `#[Q3 Contract.pdf]` — rendered as a chip rather than raw brackets — so "compare `#[Q3 Contract.pdf]` against `#[Q2 Contract.pdf]`" still reads as a sentence after it is sent. Removing the chip removes the text, and editing the text retires the chip. + * **The row condenses as it fills.** Up to five references show by name; beyond that each workspace collapses to a count you can open, so a long selection does not push the message box off the screen. + * **The `#` menu searches everywhere at once** — your personal workspace, every group you belong to, and every visible public workspace — plus tags and whole workspaces. A scope that is briefly unavailable no longer empties the menu of everything else. + * **The Documents picker opens upward and has a search box**, which the V2 dropdowns did not, and groups results by workspace. The original "search all my documents" behaviour is still there as the first row. + * Documents and tags chosen together are now combined additively, so a document chip beside an unrelated tag chip no longer matches nothing. Selecting several tags still requires a document to carry all of them. + * (Ref: `lib/chatContext.ts`, `lib/chatContextTokens.ts`, `lib/contextMentions.ts`, `components/chat/ContextChips.tsx`, `components/chat/DocumentPickerPopover.tsx`, [Chat Context Picker](features/CHAT_CONTEXT_PICKER.md)) + +* **Image Generation Can Now Use A Chat Model Where No Image Model Is Available** + * Image generation previously required a dedicated `gpt-image` or DALL-E deployment. A deployment like that is separately approved and is not offered in every subscription or region, so a tenant without one could not switch image generation on at all — the deployment list was filtered to image models, and nothing appeared to select. + * A chat deployment such as `gpt-5.6` can now be selected instead. It has no image endpoint, so SimpleChat asks it through the Responses API's image generation tool rather than the images endpoint, and works out which of the two applies from the model behind the deployment you chose. There is no new setting to configure. + * **Fetch deployments** now lists chat models alongside image models, and excludes embedding deployments, which can produce an image either way. + * A chat deployment offers whole-image regeneration only. Changing part of an image needs the images API, and the editor says so before you paint a region rather than after. + * Existing configurations are untouched. Every deployment that could be selected before still takes the route it always did, including one whose model name was never recorded and one reached through API Management. + * Worth one test generation after selecting a chat deployment: the tool is served by an image model behind the scenes, so a subscription with no image capability at all may still be refused. + * (Ref: `functions_image_api_route.py`, `functions_image_generation.request_generated_image_source`, `/api/models/image`, [Image generation through Responses-capable chat models](features/IMAGE_GENERATION_RESPONSES_MODELS.md)) + +* **Orchestration Can Now Reach Agents, Linked Pages And Deep Research** + * Orchestration could plan around a smaller world than you could reach by hand. Three things sitting in the composer as buttons — your agents, reading the links you pasted, and deep research — were invisible to the planner, so asking for them in plain language got you a plan that quietly did something simpler. + * **Your agents can now be part of a plan.** The planner is shown the agents you can reach and picks one where the question calls for it. Picking an agent yourself still wins outright: a plan built around your choice rather than around a guess at it. + * **Linked pages are read when you paste them.** Only links present in your own message are read — never one a model produced. + * **Deep research is available for questions that need several sources reconciled.** It is the most expensive thing a plan can do, so the planner is told plainly that web search is the cheaper answer to an ordinary factual question. + * Deep research and reading links continue to honour the `DeepResearchUser` and `UrlAccessUser` roles, and agents continue to honour your own agent setting. Orchestration still grants nobody access to anything they could not already reach by hand. + * (Ref: `functions_orchestration_registry.py`, `functions_orchestration_adapters.py`, `functions_orchestration_context.resolve_agent_catalog`, [Chat Orchestration](features/CHAT_ORCHESTRATION.md), [Orchestration settings](../admin/orchestration.md)) + +* **A Plan Now Has A Shape: Gather, Then Reason, Then Create** + * Every capability now belongs to an ordered phase — gathering knowledge, reasoning, creating — and a plan moves through them in order. A plan can no longer go looking for something after it has already answered, which used to produce an answer written without the evidence the later step had just found. That was a silently wrong answer rather than a visible failure, which is the worse of the two. + * The third phase is declared but empty. Producing files, placing them in a workspace, sharing and mail come next; they have effects outside the conversation and need an approval story of their own. + * (Ref: `CAPABILITY_PHASES`, `functions_orchestration_schema._enforce_phase_order`) + +* **The Orchestration Settings Are Configurable In The New Admin Interface** + * The new admin interface offered exactly one orchestration setting — the on switch — while the classic interface had sixteen. Approval mode, the countdown, the capability list, every limit and the planner model were all unreachable, so turning orchestration on there meant accepting whatever the defaults happened to be. + * All sixteen are now configurable, grouped as Approval, Capabilities, Limits and Planner Model. + * (Ref: `admin_settings_fields.py`, [Orchestration settings](../admin/orchestration.md)) + +#### Bug Fixes + +* **Chat From The Workspace Left The V2 Interface** + * Selecting documents in the V2 workspace and pressing **Chat** performed a full page load into the *classic* chat page, quietly moving the user out of V2 by the action most likely to follow choosing a document. It now opens the V2 composer with those documents already referenced. + * (Ref: `DocumentExplorer.onChat`, `lib/chatContextHandoff.ts`) + +* **Retrying A Tag-Filtered Message Searched More Widely Than The Original** + * The streaming chat path — the one the V2 interface uses — recorded which documents a message searched but not which tags, while the non-streaming path recorded both. Retrying or editing such a message therefore replayed a broader search than the one that produced the original answer, which reads as the assistant answering a different question the second time it is asked. + * (Ref: `route_backend_chats.py` `workspace_search` metadata, `route_backend_conversations._build_replayed_document_context`) + +* **Planned Documents Were Listed As Raw Identifiers** + * Each step of an orchestration plan listed the documents it would read as bare uuids. Deciding whether the planner picked the right document is the entire purpose of showing the plan before it runs, and `8f14e45f-ceea-467a-…` does not support that decision. Steps now list documents by name, falling back to the id only when the document can no longer be read. + * (Ref: `lib/documentTitles.ts`, `components/chat/OrchestrationRunView.tsx`) + +* **Documents An Orchestrated Answer Used Now Appear In The Documents Tab** + * A plan would search, find the right material, and cite it correctly in its answer — and the Documents tab would still say "No documents used yet". + * Two separate things were missing. The assistant message was saved without its citations, so the reply carried no citation chips. And the conversation's used-document list was never extended, which is what the Documents tab actually reads: an answer can cite a document perfectly and still show nothing there. + * Web sources and document sources are now kept apart, because they are read differently — a citation with no document behind it used to be dropped silently on its way to document tracking. + * (Ref: `route_backend_orchestration._partition_citations`, `merge_cited_documents_into_conversation`, `chatStore.settleOrchestrationTurn`) + +* **The Plan Bar No Longer Lingers After A Run Finishes** + * A collapsed plan summary stayed pinned in the conversation after its run had settled, competing with the answer it had produced. It is now removed once the run is complete; the full plan remains in the side panel, where it can be reopened. + * (Ref: `OrchestrationPlanCard.tsx`) + +#### User Interface Enhancements + +* **The Redundant API Management Switch Is Gone From AI Models** + * **Send requests through API Management** has been removed from the AI Models tab. It belongs to the classic single endpoint, and a connection now carries its own API Management configuration, so the switch was a second control for a route connections never take — and the endpoint, deployment and subscription key it depends on were only settable on the classic admin page anyway. + * The setting itself is unchanged and still applies to the classic endpoint. It is edited on the server-rendered admin page. + * (Ref: `admin_settings_fields.ADMIN_SETTINGS_FIELDS['gpt-config']`, `SUPPRESSED_CAPABILITY_KEYS`, [AI Models](../admin/ai-models.md)) + +### **(v0.261.086)** + +#### Bug Fixes + +* **Orchestrated Answers Failed At The Last Step** + * A plan would research correctly and then fail while writing the answer, reporting only "The answer could not be written". The same fault would have stopped document analysis and document comparison, which is every capability that reasons over what was gathered — web search and document search completed because they never call a model. + * The step that writes the answer asks the model through a small function the request supplies, and SimpleChat has a long-standing convention for how that function is called. The orchestration request was supplying one with a different shape, so the call failed the moment a real step reached it. + * Token usage is now also counted for orchestrated runs. It was previously reported as zero — not because the run was free, but because nothing was adding it up. + * (Ref: `route_backend_orchestration._build_invoke_prompt`, `functions_orchestration_adapters.run_respond`, `functions_workflow_runner.invoke_model_prompt`) + +### **(v0.261.085)** + +#### New Features + +* **Orchestration: Describe What You Want Instead Of Assembling The Request** + * Answering a question well used to depend on choosing correctly before you had seen any results. You had to decide whether to search your documents and which ones, whether to search the web, whether to read the URLs you had pasted, which saved prompt to apply, which agent to use, and which model should answer — all in advance, from a row of controls under the message box. + * **You now just ask.** SimpleChat works out what the question needs, shows you the plan it intends to follow, and runs it. Where an administrator has enabled orchestration the composer opens in it, with the capability toggles and the model, agent and reasoning pickers folded behind **Manual controls** rather than removed; file upload and voice input stay exactly where they are. + * **You decide how much say you want.** A plan can wait for you to read it, run after a countdown you can interrupt, or run immediately. Administrators set the default and can decide whether users may change it. + * **You can narrow a plan before it runs.** Steps can be switched off and documents removed. You cannot add to a plan this way — widening it goes back through planning, so a request never skips the reasoning and the permission check that produced it. + * **Questions are asked in the conversation, as a card.** When the orchestrator genuinely cannot proceed without knowing something, it asks with a short form — choices to pick rather than prose to write — instead of a paragraph you have to answer in the thread. The card speaks the MCP elicitation contract, so an MCP server asking a question later will use the same card. + * **The plan lives in the side panel, next to Contents and Documents.** A Run view shows the current plan with live progress, and a Map view shows every run in the conversation in order, so you can see what has already been searched and produced. Clicking a run jumps the conversation to the turn that produced it. + * **Later turns reuse earlier work.** The planner is shown a bounded summary of what previous turns in the conversation already did, so a follow-up question depends on findings that already exist rather than searching for them again — and you are never asked a question you have already answered. + * Orchestration reaches only capabilities that are already enabled, so it grants no new access to anyone. This release covers document search, document analysis, document comparison, spreadsheet analysis, web search and answering. + * Available in the V2 interface. The classic interface is unchanged. Off by default. + * (Ref: `functions_orchestration_registry.py`, `functions_orchestration_schema.py`, `functions_orchestration_context.py`, `functions_orchestration_planner.py`, `functions_orchestration_executor.py`, `/api/v2/orchestration/plan`, `/api/v2/orchestration/run`, [Chat Orchestration](features/CHAT_ORCHESTRATION.md), [Orchestration settings](../admin/orchestration.md)) + +#### Bug Fixes + +* **Enabling A Feature No Longer Requires Reloading The Chat Tab** + * The new interface read its configuration once, when the page first loaded. An administrator who turned a capability on in Admin Settings — which is a different page, usually a different tab — came back to an open chat that still insisted the capability did not exist, with nothing on screen to explain why. + * The configuration is now re-read whenever the tab comes back to the front, so returning from Admin Settings is enough. This applies to every setting, not just the one that surfaced it. + * (Ref: `bootstrapStore.refresh`, `App.tsx` visibility and focus handling) + +### **(v0.261.084)** + +#### New Features + +* **The Knowledge Settings Are Configurable In The New Admin Interface** + * Knowledge is the largest group in Admin Settings — five tabs and roughly 150 settings covering web search, URL access, deep research, the search index, document extraction, audio and video, and file sync. In the new admin interface it existed only as a couple of dozen bare switches labelled from their internal key names. Every endpoint, key, limit, domain list, model choice and connection test in the group was simply absent, and the page said so with a link back to the classic one. + * All five tabs are now fully configurable there, and each section is laid out to be worked through rather than scanned: **connect, verify, then tune.** + * **Every section leads with its own switch and a status.** A section reports whether it is off, needs configuration, is waiting on something elsewhere, or is ready — so a long page can be skimmed for what still needs attention instead of opened section by section. + * **Detail arrives when it is needed.** Settings cluster into groups that stay collapsed, and the one that needs attention next opens on its own. Searching still reaches everything, including settings inside a collapsed group. + * **Connections can be tested before they are saved**, against the values currently on screen. Both admin interfaces now offer exactly the same set of tests. + * **Prerequisites are stated where they are felt.** File Sync needs Redis Cache, which is configured in a different group; the section now says so, explains the consequence, and links to it, rather than leaving an administrator to turn File Sync on and watch nothing happen. + * (Ref: `admin_settings_fields.py`, `SettingsSection.tsx`, `ConnectionTest.tsx`, `/api/v2/admin/settings/test-connection`) + +* **Image Support Is Read From Real Model Capability Data** + * Whether a model could be used for Multi-Modal Vision Analysis was decided by matching its name against a pattern. That admitted text-only chat variants, said nothing at all about a self-hosted or internally named deployment, and could not be corrected because the rule was in the code. + * Each model on an endpoint now has a **Reads images** setting under **AI Models**, arriving pre-filled from capability data the application already ships. The field says where its answer came from — set here, from the built-in data, or inferred from the name — so the one worth reviewing is obvious. + * The vision model picker offers only models that report image support, and marks one whose support was inferred rather than known. + * (Ref: `functions_model_capabilities.py`, `static/json/model_capabilities.json`, `/api/models/vision-capability`, [AI Models](../admin/ai-models.md)) + +* **File Sync Workspace Assignments Can Now Be Edited** + * File Sync could be restricted to named groups or public workspaces, but the screens for choosing them were never finished, so the lists could not be set from either admin interface. They are now searchable pickers, shown only while the restriction that uses them is turned on. + * (Ref: `file_sync_allowed_group_ids`, `file_sync_allowed_public_workspace_ids`, `AssignmentPicker.tsx`) + +#### Bug Fixes + +* **Admin Credentials Are No Longer Sent To The Browser In Plain Text** + * The classic admin page has always replaced stored keys, connection strings and client secrets with a placeholder before rendering. The settings endpoint behind the new admin interface returned the settings document untouched, so every stored credential was delivered to the browser and visible to anyone who could open the page. + * Credentials are now redacted on the way out and on the way back, and submitting an untouched placeholder is recognised as "no change" rather than being written — so saving an unrelated toggle can no longer overwrite a credential. + * (Ref: `/api/v2/admin/settings`, `admin_settings_fields.py`, `redact_admin_settings_secrets_for_form`) + +* **Two Document Extraction Sections Were Unreachable** + * The **Content Understanding** and **Images Inside Office Files** cards existed but were missing from the settings navigation, so neither admin interface could link to them and neither was documented. Both are now navigable sections in both interfaces. + * (Ref: `admin_settings_nav.py`, [Knowledge settings](../admin/knowledge.md#content-understanding-section)) + +#### User Interface Enhancements + +* **Document Intelligence Is No Longer Inside Out** + * The section opened with a feature toggle and put the endpoint and key that make the feature work at the very bottom, after every option that depends on them. In the new interface the connection comes first, and the settings that need it follow and say so when it is missing. + * (Ref: `document-intelligence-section`) + +* **The Shared Speech Resource Is Explained Before It Is Needed** + * Audio uploads, voice input and voice responses are three separate capabilities that use one Azure Speech resource, and that was explained in a note beneath them. The resource is now configured first, and the long resource ID needed for managed identity can be built from the fields above it instead of typed by hand. + * (Ref: `ai-voice-chat-section`, `ResourceIdBuilder.tsx`) + +* **The Response Completion Sound Moved To Feedback & Alerts** + * It played a local browser sound and required no Azure Speech resource, yet it was the first control in the AI Voice Conversations section. It now sits with the other notification settings under Chat. + * (Ref: `enable_chat_completion_audio_cues`, [Chat settings](../admin/chat.md#desktop-notifications-section)) + +### **(v0.261.083)** + +#### New Features + +* **Embeddings And Image Generation Can Be Configured In The New Admin Interface** + * These were the last two sections of AI Models with nothing behind them. Both fell back to scanning the settings document for `enable_*` booleans, so each showed a switch or two and hid everything that actually makes the route work: the endpoint, the authentication method, the subscription and resource group, the API version, the gateway alternative and the deployment in use. + * **Neither has connections, and the interface no longer pretends otherwise.** Chat can draw from several resources at once; embeddings and image generation each have exactly one, or one API Management gateway in front of one. Both are now configured directly, with the direct and gateway routes presented as the alternatives they are — only the selected one is used, so the fields for the other stay out of the way instead of sitting there looking configured. + * **The deployment in use is chosen from a list rather than typed.** **Fetch deployments** asks the resource what it has, and the choice is saved on its own. A deployment the resource no longer reports is dropped from the selection rather than kept, because a request naming it would fail on the next embedding or image call rather than at save time. The list is a cache of the last answer, which is why fetching is a deliberate action and why the interface says so. + * **Fetching reads the saved endpoint, not the one on screen**, since that is how the underlying discovery has always worked. That caveat is now stated next to the button that trips over it. + * (Ref: `admin_settings_fields.py`, `route_backend_v2.py`, `/api/v2/admin/model-selection`, `ModelSelectionPicker.tsx`, `modelSelection.ts`, `embedding_model`, `image_gen_model`) + +* **Stored Credentials Are No Longer Placed In The Page** + * The new admin interface had no way to describe a secret, so a credential declared in a section would have been drawn by the ordinary text control — a live Azure OpenAI key sitting in a readable box, offered to every password manager and form-restore cache on the machine. Secrets are now a field type of their own: masked, excluded from browser autofill, and revealed only on request. + * **The box starts empty whatever is stored, and leaving it empty keeps the stored value.** The new interface saves a section at a time, so an untouched credential still travels with whatever else was edited. Had an empty box meant "empty string", saving an API version would have wiped a working key, and the damage would only have surfaced the next time the service was called. Removal is now its own action, which is the only thing that clears a stored secret. + * (Ref: `admin_settings_fields.py`, `fields.tsx`, `AdminSettingsPage.tsx`, `azure_openai_embedding_key`, `azure_openai_image_gen_key`) + +#### Bug Fixes + +* **A Document Extraction Setting Was Appearing Under Image Generation** + * Settings the new admin interface has no description for are placed by matching words in their name against section names. **Analyze images embedded in DOCX and PPTX files** shares the word "image" with Image Generation, so it was filed there — as a bare switch, with no explanation, among settings about producing pictures rather than reading them out of Word and PowerPoint files. + * It now appears under Knowledge, in Document Extraction, alongside the rest of Document Intelligence, and carries the explanation the classic page has always had. + * (Ref: `admin_settings_fields.py`, `enable_office_embedded_image_analysis`, `document-intelligence-section`) + +#### User Interface Enhancements + +* **Conditional Settings Follow The Choice They Depend On** + * A setting could previously be shown or hidden by a switch, but not by a choice — the condition was read as true or false, and every non-empty option is true. The API key field, which belongs to key authentication, would therefore have stayed on screen after switching a route to managed identity, next to a note saying it was not being used. + * Conditions now compare the actual value, and a control that sits inside two nested blocks can name both. The Azure OpenAI keys for embeddings and image generation are gated on the direct-connection route *and* on key authentication, so switching either section to API Management now takes the whole direct connection with it, including its key, rather than leaving parts of it behind. + * (Ref: `adminFields.ts`, `isFieldVisible`, `admin_settings_fields.py`, `docs/admin/ai-models.md`) + +### **(v0.261.082)** + +#### New Features + +* **Embeddings And Image Generation Can Be Configured In The New Admin Interface** + * These were the last two sections of AI Models with nothing behind them. Both fell back to scanning the settings document for `enable_*` booleans, so each showed a switch or two and hid everything that actually makes the route work: the endpoint, the authentication method, the subscription and resource group, the API version, the gateway alternative and the deployment in use. + * **Neither has connections, and the interface no longer pretends otherwise.** Chat can draw from several resources at once; embeddings and image generation each have exactly one, or one API Management gateway in front of one. Both are now configured directly, with the direct and gateway routes presented as the alternatives they are — only the selected one is used, so the fields for the other stay out of the way instead of sitting there looking configured. + * **The deployment in use is chosen from a list rather than typed.** **Fetch deployments** asks the resource what it has, and the choice is saved on its own. A deployment the resource no longer reports is dropped from the selection rather than kept, because a request naming it would fail on the next embedding or image call rather than at save time. The list is a cache of the last answer, which is why fetching is a deliberate action and why the interface says so. + * **Fetching reads the saved endpoint, not the one on screen**, since that is how the underlying discovery has always worked. That caveat is now stated next to the button that trips over it. + * (Ref: `admin_settings_fields.py`, `route_backend_v2.py`, `/api/v2/admin/model-selection`, `ModelSelectionPicker.tsx`, `modelSelection.ts`, `embedding_model`, `image_gen_model`) + +* **Stored Credentials Are No Longer Placed In The Page** + * The new admin interface had no way to describe a secret, so a credential declared in a section would have been drawn by the ordinary text control — a live Azure OpenAI key sitting in a readable box, offered to every password manager and form-restore cache on the machine. Secrets are now a field type of their own: masked, excluded from browser autofill, and revealed only on request. + * **The box starts empty whatever is stored, and leaving it empty keeps the stored value.** The new interface saves a section at a time, so an untouched credential still travels with whatever else was edited. Had an empty box meant "empty string", saving an API version would have wiped a working key, and the damage would only have surfaced the next time the service was called. Removal is now its own action, which is the only thing that clears a stored secret. + * (Ref: `admin_settings_fields.py`, `fields.tsx`, `AdminSettingsPage.tsx`, `azure_openai_embedding_key`, `azure_openai_image_gen_key`) + +#### Bug Fixes + +* **A Document Extraction Setting Was Appearing Under Image Generation** + * Settings the new admin interface has no description for are placed by matching words in their name against section names. **Analyze images embedded in DOCX and PPTX files** shares the word "image" with Image Generation, so it was filed there — as a bare switch, with no explanation, among settings about producing pictures rather than reading them out of Word and PowerPoint files. + * It now appears under Knowledge, in Document Extraction, alongside the rest of Document Intelligence, and carries the explanation the classic page has always had. + * (Ref: `admin_settings_fields.py`, `enable_office_embedded_image_analysis`, `document-intelligence-section`) + +#### User Interface Enhancements + +* **Conditional Settings Follow The Choice They Depend On** + * A setting could previously be shown or hidden by a switch, but not by a choice — the condition was read as true or false, and every non-empty option is true. The API key field, which belongs to key authentication, would therefore have stayed on screen after switching a route to managed identity, next to a note saying it was not being used. + * Conditions now compare the actual value, and a setting whose condition depends on something already hidden is hidden with it. Switching an embedding or image route to API Management now takes the whole direct connection with it, including its key, rather than leaving parts of it behind. + * (Ref: `adminFields.ts`, `isFieldVisible`, `admin_settings_fields.py`, `docs/admin/ai-models.md`) + +### **(v0.261.081)** + +#### New Features + +* **The Default Chat Model Can Be Chosen In The New Admin Interface** + * The Chat section of AI Models had nothing in it but a single switch. The default model — the one chat uses when nobody has picked anything, such as on a brand new conversation — could only be set on the classic admin page, even though the connections it selects from had just moved. + * **Only models that can actually serve are offered.** The list is built from enabled models on enabled connections, because a reference to anything else is cleared the moment connections are next saved. Offering a disabled model would have let an administrator choose a value that quietly reverted, with nothing to explain why. + * **A choice that no longer resolves is refused rather than accepted.** Naming a connection that has been deleted, or a model that has been switched off, now comes back with a message saying so. Previously the two failure modes — storing a dangling reference, and silently emptying it — both ended up looking identical: the field simply read as having no default. + * **The saved default is shown for what it currently means.** If the connection or model it named has since gone, the interface says that rather than presenting an empty field as though nothing had ever been set. Opening the page does not rewrite the stored value, so the evidence of what went missing survives. + * The reference is validated by the same rule the classic form uses, so the two interfaces cannot disagree about whether a default is still valid. + * (Ref: `route_backend_v2.py`, `/api/v2/admin/default-model`, `ChatDefaultModel.tsx`, `modelConnections.ts`, `admin_settings_fields.py`, `resolve_default_model_selection`) + +#### User Interface Enhancements + +* **Chat Now Says Which Endpoint Is Actually Serving It** + * SimpleChat has two ways to reach a chat model — the connections list, or a single classic endpoint — and only one is in force at a time. Nothing on screen distinguished them. An administrator could add a connection, test it, watch it save, and still be served by the classic endpoint, with no error and nothing to suggest the two were different things. + * The Chat section now names the live route. When it is the classic single endpoint, it links to the page where that endpoint is configured, since those fields are deliberately not duplicated in the new interface. + * It also warns before the switch is thrown: turning connections on is one-way, because the setting is stored as "already on or newly on" and cannot be turned back off afterwards. + * Connections and the settings that depend on them are edited side by side, so they now stay in step with each other. Enabling connections, adding one, or switching a model off is reflected immediately in the model list and in the notice, rather than leaving adjacent parts of one screen describing different states until the page is reloaded. + * **The API Management switch is no longer anonymous.** Settings the new interface has not described are placed by matching their name against section names, and that guess had dropped this one into the Chat section as a bare switch reading "Gpt apim". Beneath a notice explaining which endpoint chat uses, it looked like part of the same explanation — while turning it on actually reaches for an APIM endpoint, deployment and subscription key that can only be set on the classic admin page, so requests would have had nowhere to go. It now says what it does and where the rest of it lives. + * (Ref: `ChatModeNotice.tsx`, `ModelConnectionsManager.tsx`, `modelConnectionsStore.ts`, `enable_multi_model_endpoints`, `enable_gpt_apim`, `docs/admin/ai-models.md`) + +### **(v0.261.074)** + +#### New Features + +* **Agent Settings Are Now Actually Present In The New Admin Interface** + * The new Admin Settings surface built its Agents tab by scanning the settings document for on/off switches and guessing where each one belonged. Agents are configured with far more than switches, so the result was close to empty: the Agents Configuration section rendered nothing at all, **Enable Agents** was filed under "Other capabilities" because nothing in its name matched the section, and not one of Workspace Mode, the workspace permissions, or the eleven Agents page settings appeared anywhere. + * The Agents tab is now described properly and renders every one of those settings, split into four sections instead of a single crowded card: **Agent Runtime**, **Workspace Agent Permissions**, **Agents Page**, and **Agent Template Approvals**. + * **The settings now say what depends on what.** Enable Agents gates the whole tab. Workspace Mode only appears once agents are on, and the merge behaviour only once Workspace Mode is on — a chain, so a control cannot reappear because an intermediate gate happens to be off. Workspace Agent Permissions is hidden entirely outside Workspace Mode, matching the classic interface. + * **Workspace Mode explains itself.** It reads as a choice between one shared set of agents that administrators curate, and a separate collection for each user and group, rather than as an unexplained switch. + * **The Agents page settings now follow the page they customise.** That page is served behind Enable Agents, so its hero, guidance text and promotion settings are hidden while agents are off instead of offering edits that could not take effect. + * **Promoted agents are chosen from a list rather than edited as JSON.** Agents already promoted are not offered again, and each promotion's time window is set per row. + * Rarely-changed settings are grouped and collapsed rather than laid out flat, and a search opens any collapsed group so a match is never hidden behind it. + * (Ref: `admin_settings_fields.py`, `admin_settings_nav.py`, `adminAgents.ts`, `PromotedAgentsEditor.tsx`, `AdminSettingsPage.tsx`) + +* **Action Settings Are Now Present In The New Admin Interface** + * The Actions tab in the new Admin Settings surface showed a single toggle. The Analyze and Document Comparison limits were invisible because they are stored as one nested object rather than as separate settings, and the built-in action toggles had been scattered by the same guesswork that emptied the Agents tab — **Enable Text Action** had ended up under Appearance › Branding because its name contains "text". + * The Actions tab now renders as four sections: **Document Actions**, **Workspace Action Permissions**, **Built-in Actions**, and **Global Actions**. + * **Document action limits are editable again**, with the ranges the application actually enforces (2–300 for chat, 2–1000 for a workflow run) and the workflow limit separated from the chat one. Saving one limit no longer risks the other five, and out-of-range values are clamped by the same code the classic interface uses. + * **Built-in actions are collapsed rather than laid out flat.** They default on and are rarely changed, so they no longer take up the tab; the documentation now says which one is worth a decision — HTTP, the only built-in action that reaches outside the deployment. + * All six built-in action toggles now live together in one section. **Enable Text Action** previously sat alone in the Actions Configuration section, so that section no longer declares any settings and the schema does not describe it. Restoring it would declare `enable_text_plugin` in two places at once. What belongs there is the global actions table, which is still authored in the classic interface. + +* **Inbound MCP Is Fully Configurable In The New Admin Interface** + * The Inbound MCP tab in the new Admin Settings surface showed exactly two switches — **Enable inbound MCP server** and **Enable tool throttles** — and nothing else. The delegated scope, the required Entra roles, the request and throttle limits, and all three allowlists were invisible, which read as "disabled, but throttles are on" with no explanation for either. + * The tab now renders the whole configuration, grouped as **Runtime gate**, **Request limits** and **Allowlists**. + * **Allowlists are edited as rows rather than raw JSON.** Each row pairs the identifier the runtime matches with a description of who it belongs to, and a repeated value is flagged as you type rather than disappearing on save. + * **The lists the runtime actually reads are derived on save**, exactly as the classic interface derives them. Editing an allowlist in the new interface now takes effect; previously the schema had no way to express that one setting produces several, which for an access control list is the worst kind of failure — the screen would show the new restriction while the runtime applied the old one. + * **Turning off "Allow additional tenant IDs" no longer discards the tenants you listed.** It stops admitting them, and turning it back on restores them. + * (Ref: `admin_settings_fields.py`, `functions_mcp_server_config.py`, `EntryListEditor.tsx`, `adminEntries.ts`) + +#### Bug Fixes + +* **Tabular Processing Is No Longer Shown As A Toggle That Does Nothing** + * The new interface showed **Tabular Processing** as a live switch under Chat › Processing Thoughts, purely because the word "processing" matched that section. The application recomputes this setting from **Enhanced Citations** on every settings read, so switching it had no effect and the value silently reverted. + * It now appears where it belongs, under Built-in Actions, as a read-only entry that reports its state and names Enhanced Citations as its source. Attempting to set it directly is refused rather than accepted and discarded. + * (Ref: `enable_tabular_processing_plugin`, `is_tabular_processing_enabled`, `admin_settings_fields.py`) + +* **Fact Memory Is Listed With The Actions It Affects Without Moving Its Control** + * Fact memory is a chat capability, but it also decides whether agents get a memory action, so it was missing from any list an administrator would consult when working out what an agent can do. + * It is now mirrored under Built-in Actions as a read-only entry that links to where it is set, while the control that actually sets it stays in Chat. + * (Ref: `enable_fact_memory_plugin`, `fields.tsx`) + +* **Multi-Agent Orchestration Is No Longer Shown As A Switch Under AI Models** + * **Multi-agent orchestration** appeared in the new Admin Settings surface as a live toggle in the AI Models group, which it has nothing to do with. It is written by the orchestration settings API from the chosen orchestration mode, so setting it there did nothing and the value reverted. + * It now sits with the agent runtime settings as a read-only entry that reports the current mode. + * (Ref: `enable_multi_agent_orchestration`, `admin_settings_fields.py`) + +* **Defects Found While Combining The Admin Settings Groups** + * Six efforts described different admin settings groups at the same time, all writing into the same registries. Combining them surfaced four faults that produced code which parsed, imported and ran — three would have shipped silently. + * **Nine settings sections were declared twice.** Python keeps the last definition of a duplicate key, so nothing failed; the new registry integrity test caught it on its first run. + * **A section emptied earlier had quietly gained a new setting.** The Actions Configuration section was emptied when the Text action moved in with the other built-in actions, and the Default Embedding Model action was moved into it afterwards. A check now confirms that emptying a section never takes a setting with it. + * **Fact Memory was declared as editable in two places at once.** It is a chat capability that also decides whether agents get a memory action, so it appears in both surfaces; only the Chat declaration may be the editable one, or which control governs saving depends on declaration order. + * **A test crashed once a duplicate was resolved.** A workflow check read a setting's dependency as a single condition rather than a chain. The chained setting had existed for some time but was masked by a duplicate section declaration; fixing the duplicate made it visible. + * (Ref: [Admin Settings Registry Merge Fixes](fixes/ADMIN_SETTINGS_REGISTRY_MERGE_FIXES.md)) + +#### User Interface Enhancements + +* **Agent Orchestration No Longer Shows A Choice That Does Not Exist** + * Agent Orchestration offered an Orchestration Type dropdown and a Max Rounds Per Agent field, but the application ships a single orchestration mode; the multi-agent modes are not built into this release. The dropdown therefore had exactly one option and Max Rounds could never be reached. + * The new interface asks the server which modes exist and shows the card only when there is a genuine choice. It will reappear on its own if multi-agent orchestration ships, without another change here. + * (Ref: `OrchestrationCard.tsx`, `/api/orchestration_types`, `get_agent_orchestration_types`) + +* **Agents & Actions Documentation Describes Behaviour Instead Of Restating Labels** + * The Agents entries in the Agents & Actions administration page mostly read "Defines behavior for the related admin workflow", which told an administrator nothing. They now describe what each setting does, what it depends on, and why you would change it — including that the Agents catalog page is unavailable while agents are off, and why promoting an agent exists at all. + * (Ref: `docs/admin/agents-actions.md`) + +* **The Inbound MCP Tab Explains Why It Is Empty** + * Inbound MCP is a preview whose settings stay hidden until an App Service application setting is added. Because that flag is not part of the settings document, the new interface could not see it, and simply rendered two unexplained switches. + * The settings API now reports it, and the tab shows what Inbound MCP is, the `ENABLE_MCP_UI` setting that reveals it, and that revealing it does not open the endpoint. + * (Ref: `runtime_flags`, `is_mcp_ui_enabled`, `InboundMcpNotice.tsx`) + +* **Inbound MCP Documentation Describes The Access Model** + * The administration page now states the five checks a request passes before it is served, explains that a source id is a client-supplied header that identifies rather than authenticates, and notes that throttles being on does not mean the endpoint is reachable. + * (Ref: `docs/admin/agents-actions.md`) + +### **(v0.261.071)** + +#### New Features + +* **Chat Settings Are Fully Editable In The New Admin Interface** + * The new admin interface builds each settings section from a description of what it contains. Chat had no description, so it fell back to scanning for on/off flags — which meant it could draw switches and nothing else. Every chat setting that was not a switch was simply absent: the conversation history limit, the default system prompt, and the whole Enhanced Citations configuration. + * All eleven Chat sections now render the same controls the classic interface does, across Chat Experience, Feedback & Alerts and Citations. + * **Two switches were invisible even though they are switches**, because their setting names do not begin with `enable_`. Workspace Scope Lock and the ChatFileUploadUser role requirement are now both present. + * **Enhanced Citations came across in full**, including the storage authentication type, the storage credentials, the tabular preview size cap, the large-run confirmation thresholds, and the chunk processing model — plus a **Test storage connection** button. Startup deliberately skips storage checks so an outage cannot stop the application booting, which previously meant a wrong credential was only discovered when a citation failed to open. + * (Ref: `admin_settings_fields.py`, `EnhancedCitationsStorageTest.tsx`, `AdminSettingsPage.tsx`, [V2 Admin Chat Settings](features/V2_ADMIN_CHAT_SETTINGS.md)) + +* **Conversation History Summarization Can Be Configured** + * Three settings that control what happens to conversation history — summarizing messages that fall outside the history limit, summarizing recent turns into the document-search query, and how many messages that summary reads — have always been acted on by the chat backend but have never had a control anywhere. + * All three are now editable in both the classic and the new admin interface, under Conversation History. + * (Ref: `chat-experience.html`, `conversation-history-section`, [Chat settings](../../admin/chat/)) + +#### Bug Fixes + +* **Admin Saves No Longer Reset The History Summarization Settings** + * Because the three settings above had no input anywhere, every save of Admin Settings wrote them back as off and 10 — whatever they had been set to. An administrator who configured them directly in the database lost the values the next time anyone saved the page for any unrelated reason. + * Adding the controls fixes this. The conversation history limit and the message count are also parsed defensively now, so an empty value keeps the current setting instead of raising an error or snapping to a default. + * (Ref: `route_frontend_admin_settings.py`, [Chat History Summarize Settings Reset Fix](fixes/CHAT_HISTORY_SUMMARIZE_SETTINGS_RESET_FIX.md)) + +* **Enhanced Citations File Downloads No Longer Break After An Admin Save** + * The storage account key used to sign Enhanced Citations file links has no input on the admin page, but the save handler wrote it from the form anyway — so every admin save, for any unrelated reason, replaced it with an empty value and citation downloads started returning a server error. + * The key is now preserved when the form does not carry it, along with the equivalent video and audio storage keys. + * (Ref: `route_frontend_admin_settings.py`, `office_docs_key`, [Enhanced Citations Storage Key Reset Fix](fixes/ENHANCED_CITATIONS_STORAGE_KEY_RESET_FIX.md)) + +* **Stored Credentials Are No Longer Sent To The Admin Browser** + * The new admin interface's settings endpoint returned the settings document unchanged, so every stored credential in it — model keys, search and Document Intelligence keys, the Redis key, storage connection strings, and every configured model endpoint's API key and client secret — was delivered to the administrator's browser in cleartext. The classic admin page has always masked these. + * Credentials are now masked the same way in the new interface. A configured value shows as saved and hidden, and can be replaced or cleared but not read back. An untouched field keeps its stored value on save rather than being overwritten by the mask, and erasing one warns before it is removed. + * **Three storage account keys were unmasked on both interfaces.** The key used to sign citation file access links is a live credential and is now masked everywhere. + * (Ref: `admin_settings_secret_utils.py`, `route_backend_v2.py`, `SecretField.tsx`, [V2 Admin Settings Secret Exposure Fix](fixes/V2_ADMIN_SETTINGS_SECRET_EXPOSURE_FIX.md)) + +#### User Interface Enhancements + +* **Settings From Other Areas No Longer Appear Under Chat** + * Undescribed settings are placed by matching their name against section names, and six landed in the wrong group: audio and video file support and chat completion audio cues (Knowledge > Audio & Video), enhanced extraction (Knowledge > Document Extraction), and the embedding model and tabular processing actions (Agents & Actions). Each now appears where it belongs. + * **Four switches that did nothing have been removed.** Tabular processing is derived from Enhanced Citations rather than stored, so toggling it appeared to save and then reverted; the Enhanced Citations mount and two mixed-source rollout flags have no administrator control at all. + * (Ref: `SUPPRESSED_CAPABILITY_KEYS`, `buildCapabilityIndex`, `test_v2_admin_capability_placement.py`) + +* **Settings No Longer Linger After Their Feature Is Switched Off** + * A setting shown only under certain conditions was checked against one other setting, which was not always enough. The Latest Features documentation links toggle stayed on screen with the Support menu switched off, because the condition it was checked against remained satisfied on its own. + * Conditions can now be combined, so a setting disappears with the feature that owns it as well as with the option that selects it. + * (Ref: `depends_on`, `isFieldVisible`, `admin_settings_fields.py`) + +#### Fixes Inherited From The Admin Settings Merge + +These three defects came from the Workflow and Workspaces admin settings work and +were found while merging it. They are recorded separately because they are not +part of the Chat group feature work. + +* **Enable Chat File Uploads Was Missing From The Admin Page** + * The chat file uploads section was registered twice. Registrations are applied in order and the last one wins, so a later registration holding only the app role requirement replaced the full one — and the **Enable Chat File Uploads** switch disappeared from the page entirely, with nothing reporting an error. + * The two registrations are now one, holding both settings. + * (Ref: `chat-file-uploads-section`, `ADMIN_SETTINGS_FIELDS`) + +* **Six Workflow Settings Were One Reordering Away From Vanishing** + * The Workflow section was also registered twice, with different contents. The complete registration happened to come last, so the page was correct — but the shorter one was dead code, and any change to the order would have silently dropped six Workflow settings. + * (Ref: `workflow-settings-section`, `ADMIN_SETTINGS_FIELDS`) + +* **Group Assignment Pickers Stayed Visible For Disabled Features** + * Three pickers — group workspace file downloads, public workspace file downloads and group workflows — were only checked against the toggle that turns assignment on, not against the capability that owns it. Switching the capability back off left the picker on screen, still offering to assign access to a feature that was no longer available. + * (Ref: `file_download_allowed_group_ids`, `file_download_allowed_public_workspace_ids`, `group_workflow_allowed_group_ids`) + +* **A Duplicate Registration Now Fails The Build** + * Both duplications above were invisible because a repeated registration is not an error — it just replaces the earlier one. A new test reads the settings registry directly and fails on a repeated section, a repeated setting, or an empty section, so the next merge into this file reports the problem instead of quietly losing settings. + * (Ref: `functional_tests/test_admin_settings_fields_registry_integrity.py`) + +### **(v0.261.070)** + +#### Bug Fixes + +* **Turning Connections Off Said It Worked, And Did Not** + * Enabling connections for chat is one-way — the stored flag is coerced to "already on, or newly requested", so once it is true it stays true. The classic page reflects that by hiding the checkbox after it is enabled. The new admin interface showed an ordinary switch, so turning it off reported a successful save, moved the switch, and changed nothing; chat carried on routing through connections and only a page reload revealed it. The attempt is now refused with an explanation. + * **Enabling connections from the new interface also used to strand a deployment.** The classic page carries the existing single chat endpoint over as the first connection when the flag is first switched on. The new interface skipped that step, so a deployment that enabled connections there was left with an empty model catalog — and, because the flag cannot be turned back off, no way to recover. Both paths now migrate through the same code. + * (Ref: `admin_settings_fields.py`, `build_migrated_model_endpoints_from_legacy`, `_seed_connections_on_first_enable`) + +* **Deleting A Connection Could Break Document Metadata Extraction** + * Metadata extraction can be pointed at a specific model, stored as a reference to a connection and a model within it. Deleting or disabling that connection left the reference naming something that no longer existed, and unlike the chat default — which quietly falls back — extraction raises instead, and its caller only writes the failure to the processing log. Metadata extraction would therefore fail for every subsequently ingested document with nothing visible to say why. That reference is now re-checked whenever connections change, exactly as the chat default already was. + * (Ref: `resolve_metadata_extraction_model_selection`, `_persist_global_model_endpoints`) + +* **A Failed Settings Write Reported Success After The Secrets Were Already Gone** + * Saving a connection removes superseded secrets from Key Vault before writing the settings document. The settings write reports failure by returning a value rather than raising, and that value was not checked — so if the write failed, the interface said the change had been saved while the connection was left referencing a secret that had already been deleted. A failed write is now reported as an error. + * (Ref: `_persist_global_model_endpoints`, `update_settings`) + +* **A Reserved Identity Header Name Is Now Refused Rather Than Silently Dropped** + * The new admin interface accepted any identity header name. A name that collides with a header the model call already sets — `authorization` or `api-key`, for instance — was normalized away to nothing on read, which turned the header off without saying so. Such a name is now rejected at save time with an explanation. + * (Ref: `admin_settings_fields.py`, `normalize_model_endpoint_identity_header_name`) + +#### New Features + +* **Model Connections Can Be Managed In The New Admin Interface** + * The AI Models group in the new interface previously showed nothing but a few switches. That was structural rather than cosmetic: no section in the group had a described field schema, so the page fell back to scanning the settings document for `enable_*` booleans — and endpoints, keys, API versions, authentication and model selection are none of those. All of it was invisible. + * **Model endpoints are now presented as connections.** A connection is one Azure OpenAI or Azure AI Foundry resource: where it is, how SimpleChat authenticates to it, and which of its deployed models may be used. The wording and the editing model changed; what is stored did not, so a connection configured in either interface is the same record. + * **Adding a connection now actually saves it.** In the classic interface, adding or editing a model endpoint changed an in-memory list that was serialized into a hidden form field, so nothing was stored until the whole admin settings page was submitted — and a half-filled endpoint looked identical to a saved one. Each connection is now its own resource with its own Save, and the editor says which state it is in. + * **The editor only shows the fields the connection actually uses.** Provider and authentication method together decide what is relevant, derived in one place rather than from several listeners toggling visibility independently. Choosing an API key, for example, hides the subscription and resource group, because an API key cannot reach Azure Resource Manager and those fields would serve no purpose. + * **Validation names the field that is wrong**, next to the control, instead of raising a message that described the problem but not its location. + * **Discovery and testing are part of editing.** *Test connection* checks the credentials resolve, *Discover models* lists the resource's deployments, and each model can be tested individually. Discovered models arrive switched off, because finding a deployment is not the same as choosing to publish it, and re-running discovery does not duplicate a model or overwrite a display name edited by hand. + * **Stored secrets survive an edit.** Keys and client secrets are never returned to the browser; a field whose secret is already stored shows that it exists and stays empty, and leaving it empty keeps the stored value. Deleting a connection removes the secrets it owned from Key Vault. + * Deleting or disabling a connection no longer leaves the default model pointing at something that no longer resolves — chat would otherwise fall back to a different model with no indication that it had. The rule that decides this is now shared with the classic form, so the two interfaces cannot disagree about it. + * (Ref: `route_backend_v2.py`, `/api/v2/admin/model-endpoints`, `modelConnections.ts`, `ModelConnectionsManager.tsx`, `admin_settings_fields.py`, `resolve_default_model_selection`) + +#### User Interface Enhancements + +* **AI Models Reads As Connections And Roles** + * The AI Models group conflated two different things: a place models live, and the job a model does. The Model Endpoints tab is now **Connections**, and the Chat Model section is now **Chat**, separating the resource being connected to from the purpose it serves. + * (Ref: `admin_settings_nav.py`, `docs/admin/ai-models.md`) + +### **(v0.261.065)** + +#### New Features + +* **The Knowledge Settings Are Configurable In The New Admin Interface** + * Knowledge is the largest group in Admin Settings — five tabs and roughly 150 settings covering web search, URL access, deep research, the search index, document extraction, audio and video, and file sync. In the new admin interface it existed only as a couple of dozen bare switches labelled from their internal key names. Every endpoint, key, limit, domain list, model choice and connection test in the group was simply absent, and the page said so with a link back to the classic one. + * All five tabs are now fully configurable there, and each section is laid out to be worked through rather than scanned: **connect, verify, then tune.** + * **Every section leads with its own switch and a status.** A section reports whether it is off, needs configuration, is waiting on something elsewhere, or is ready — so a long page can be skimmed for what still needs attention instead of opened section by section. + * **Detail arrives when it is needed.** Settings cluster into groups that stay collapsed, and the one that needs attention next opens on its own. Searching still reaches everything, including settings inside a collapsed group. + * **Connections can be tested before they are saved**, against the values currently on screen. Both admin interfaces now offer exactly the same set of tests. + * **Prerequisites are stated where they are felt.** File Sync needs Redis Cache, which is configured in a different group; the section now says so, explains the consequence, and links to it, rather than leaving an administrator to turn File Sync on and watch nothing happen. + * (Ref: `admin_settings_fields.py`, `SettingsSection.tsx`, `ConnectionTest.tsx`, `/api/v2/admin/settings/test-connection`) + +* **Image Support Is Read From Real Model Capability Data** + * Whether a model could be used for Multi-Modal Vision Analysis was decided by matching its name against a pattern. That admitted text-only chat variants, said nothing at all about a self-hosted or internally named deployment, and could not be corrected because the rule was in the code. + * Each model on an endpoint now has a **Reads images** setting under **AI Models**, arriving pre-filled from capability data the application already ships. The field says where its answer came from — set here, from the built-in data, or inferred from the name — so the one worth reviewing is obvious. + * The vision model picker offers only models that report image support, and marks one whose support was inferred rather than known. + * (Ref: `functions_model_capabilities.py`, `static/json/model_capabilities.json`, `/api/models/vision-capability`, [AI Models](../admin/ai-models.md)) + +* **File Sync Workspace Assignments Can Now Be Edited** + * File Sync could be restricted to named groups or public workspaces, but the screens for choosing them were never finished, so the lists could not be set from either admin interface. They are now searchable pickers, shown only while the restriction that uses them is turned on. + * (Ref: `file_sync_allowed_group_ids`, `file_sync_allowed_public_workspace_ids`, `AssignmentPicker.tsx`) + +* **Agent Settings Are Now Actually Present In The New Admin Interface** + * The new Admin Settings surface built its Agents tab by scanning the settings document for on/off switches and guessing where each one belonged. Agents are configured with far more than switches, so the result was close to empty: the Agents Configuration section rendered nothing at all, **Enable Agents** was filed under "Other capabilities" because nothing in its name matched the section, and not one of Workspace Mode, the workspace permissions, or the eleven Agents page settings appeared anywhere. + * The Agents tab is now described properly and renders every one of those settings, split into four sections instead of a single crowded card: **Agent Runtime**, **Workspace Agent Permissions**, **Agents Page**, and **Agent Template Approvals**. + * **The settings now say what depends on what.** Enable Agents gates the whole tab. Workspace Mode only appears once agents are on, and the merge behaviour only once Workspace Mode is on — a chain, so a control cannot reappear because an intermediate gate happens to be off. Workspace Agent Permissions is hidden entirely outside Workspace Mode, matching the classic interface. + * **Workspace Mode explains itself.** It reads as a choice between one shared set of agents that administrators curate, and a separate collection for each user and group, rather than as an unexplained switch. + * **The Agents page settings now follow the page they customise.** That page is served behind Enable Agents, so its hero, guidance text and promotion settings are hidden while agents are off instead of offering edits that could not take effect. + * **Promoted agents are chosen from a list rather than edited as JSON.** Agents already promoted are not offered again, and each promotion's time window is set per row. + * Rarely-changed settings are grouped and collapsed rather than laid out flat, and a search opens any collapsed group so a match is never hidden behind it. + * (Ref: `admin_settings_fields.py`, `admin_settings_nav.py`, `adminAgents.ts`, `PromotedAgentsEditor.tsx`, `AdminSettingsPage.tsx`) + +* **Action Settings Are Now Present In The New Admin Interface** + * The Actions tab in the new Admin Settings surface showed a single toggle. The Analyze and Document Comparison limits were invisible because they are stored as one nested object rather than as separate settings, and the built-in action toggles had been scattered by the same guesswork that emptied the Agents tab — **Enable Text Action** had ended up under Appearance › Branding because its name contains "text". + * The Actions tab now renders as four sections: **Document Actions**, **Workspace Action Permissions**, **Built-in Actions**, and **Global Actions**. + * **Document action limits are editable again**, with the ranges the application actually enforces (2–300 for chat, 2–1000 for a workflow run) and the workflow limit separated from the chat one. Saving one limit no longer risks the other five, and out-of-range values are clamped by the same code the classic interface uses. + * **Built-in actions are collapsed rather than laid out flat.** They default on and are rarely changed, so they no longer take up the tab; the documentation now says which one is worth a decision — HTTP, the only built-in action that reaches outside the deployment. + +* **Inbound MCP Is Fully Configurable In The New Admin Interface** + * The Inbound MCP tab in the new Admin Settings surface showed exactly two switches — **Enable inbound MCP server** and **Enable tool throttles** — and nothing else. The delegated scope, the required Entra roles, the request and throttle limits, and all three allowlists were invisible, which read as "disabled, but throttles are on" with no explanation for either. + * The tab now renders the whole configuration, grouped as **Runtime gate**, **Request limits** and **Allowlists**. + * **Allowlists are edited as rows rather than raw JSON.** Each row pairs the identifier the runtime matches with a description of who it belongs to, and a repeated value is flagged as you type rather than disappearing on save. + * **The lists the runtime actually reads are derived on save**, exactly as the classic interface derives them. Editing an allowlist in the new interface now takes effect; previously the schema had no way to express that one setting produces several, which for an access control list is the worst kind of failure — the screen would show the new restriction while the runtime applied the old one. + * **Turning off "Allow additional tenant IDs" no longer discards the tenants you listed.** It stops admitting them, and turning it back on restores them. + * (Ref: `admin_settings_fields.py`, `functions_mcp_server_config.py`, `EntryListEditor.tsx`, `adminEntries.ts`) + +#### Bug Fixes + +* **Admin Credentials Are No Longer Sent To The Browser In Plain Text** + * The classic admin page has always replaced stored keys, connection strings and client secrets with a placeholder before rendering. The settings endpoint behind the new admin interface returned the settings document untouched, so every stored credential was delivered to the browser and visible to anyone who could open the page. + * Credentials are now redacted on the way out and on the way back, and submitting an untouched placeholder is recognised as "no change" rather than being written — so saving an unrelated toggle can no longer overwrite a credential. + * (Ref: `/api/v2/admin/settings`, `admin_settings_fields.py`, `redact_admin_settings_secrets_for_form`) + +* **Two Document Extraction Sections Were Unreachable** + * The **Content Understanding** and **Images Inside Office Files** cards existed but were missing from the settings navigation, so neither admin interface could link to them and neither was documented. Both are now navigable sections in both interfaces. + * (Ref: `admin_settings_nav.py`, [Knowledge settings](../admin/knowledge.md#content-understanding-section)) + +* **Tabular Processing Is No Longer Shown As A Toggle That Does Nothing** + * The new interface showed **Tabular Processing** as a live switch under Chat › Processing Thoughts, purely because the word "processing" matched that section. The application recomputes this setting from **Enhanced Citations** on every settings read, so switching it had no effect and the value silently reverted. + * It now appears where it belongs, under Built-in Actions, as a read-only entry that reports its state and names Enhanced Citations as its source. Attempting to set it directly is refused rather than accepted and discarded. + * (Ref: `enable_tabular_processing_plugin`, `is_tabular_processing_enabled`, `admin_settings_fields.py`) + +* **Fact Memory Is Listed With The Actions It Affects Without Moving Its Control** + * Fact memory is a chat capability, but it also decides whether agents get a memory action, so it was missing from any list an administrator would consult when working out what an agent can do. + * It is now mirrored under Built-in Actions as a read-only entry that links to where it is set, while the control that actually sets it stays in Chat. + * (Ref: `enable_fact_memory_plugin`, `fields.tsx`) + +* **Multi-Agent Orchestration Is No Longer Shown As A Switch Under AI Models** + * **Multi-agent orchestration** appeared in the new Admin Settings surface as a live toggle in the AI Models group, which it has nothing to do with. It is written by the orchestration settings API from the chosen orchestration mode, so setting it there did nothing and the value reverted. + * It now sits with the agent runtime settings as a read-only entry that reports the current mode. + * (Ref: `enable_multi_agent_orchestration`, `admin_settings_fields.py`) + +* **Defects Found While Combining The Admin Settings Groups** + * Six efforts described different admin settings groups at the same time, all writing into the same registries. Combining them surfaced four faults that produced code which parsed, imported and ran — three would have shipped silently. + * **Nine settings sections were declared twice.** Python keeps the last definition of a duplicate key, so nothing failed; the new registry integrity test caught it on its first run. + * **A section emptied earlier had quietly gained a new setting.** The Actions Configuration section was emptied when the Text action moved in with the other built-in actions, and the Default Embedding Model action was moved into it afterwards. A check now confirms that emptying a section never takes a setting with it. + * **Fact Memory was declared as editable in two places at once.** It is a chat capability that also decides whether agents get a memory action, so it appears in both surfaces; only the Chat declaration may be the editable one, or which control governs saving depends on declaration order. + * **A test crashed once a duplicate was resolved.** A workflow check read a setting's dependency as a single condition rather than a chain. The chained setting had existed for some time but was masked by a duplicate section declaration; fixing the duplicate made it visible. + * (Ref: [Admin Settings Registry Merge Fixes](fixes/ADMIN_SETTINGS_REGISTRY_MERGE_FIXES.md)) + +#### User Interface Enhancements + +* **Document Intelligence Is No Longer Inside Out** + * The section opened with a feature toggle and put the endpoint and key that make the feature work at the very bottom, after every option that depends on them. In the new interface the connection comes first, and the settings that need it follow and say so when it is missing. + * (Ref: `document-intelligence-section`) + +* **The Shared Speech Resource Is Explained Before It Is Needed** + * Audio uploads, voice input and voice responses are three separate capabilities that use one Azure Speech resource, and that was explained in a note beneath them. The resource is now configured first, and the long resource ID needed for managed identity can be built from the fields above it instead of typed by hand. + * (Ref: `ai-voice-chat-section`, `ResourceIdBuilder.tsx`) + +* **The Response Completion Sound Moved To Feedback & Alerts** + * It played a local browser sound and required no Azure Speech resource, yet it was the first control in the AI Voice Conversations section. It now sits with the other notification settings under Chat. + * (Ref: `enable_chat_completion_audio_cues`, [Chat settings](../admin/chat.md#desktop-notifications-section)) + +* **Agent Orchestration No Longer Shows A Choice That Does Not Exist** + * Agent Orchestration offered an Orchestration Type dropdown and a Max Rounds Per Agent field, but the application ships a single orchestration mode; the multi-agent modes are not built into this release. The dropdown therefore had exactly one option and Max Rounds could never be reached. + * The new interface asks the server which modes exist and shows the card only when there is a genuine choice. It will reappear on its own if multi-agent orchestration ships, without another change here. + * (Ref: `OrchestrationCard.tsx`, `/api/orchestration_types`, `get_agent_orchestration_types`) + +* **Agents & Actions Documentation Describes Behaviour Instead Of Restating Labels** + * The Agents entries in the Agents & Actions administration page mostly read "Defines behavior for the related admin workflow", which told an administrator nothing. They now describe what each setting does, what it depends on, and why you would change it — including that the Agents catalog page is unavailable while agents are off, and why promoting an agent exists at all. + * (Ref: `docs/admin/agents-actions.md`) + +* **The Inbound MCP Tab Explains Why It Is Empty** + * Inbound MCP is a preview whose settings stay hidden until an App Service application setting is added. Because that flag is not part of the settings document, the new interface could not see it, and simply rendered two unexplained switches. + * The settings API now reports it, and the tab shows what Inbound MCP is, the `ENABLE_MCP_UI` setting that reveals it, and that revealing it does not open the endpoint. + * (Ref: `runtime_flags`, `is_mcp_ui_enabled`, `InboundMcpNotice.tsx`) + +* **Inbound MCP Documentation Describes The Access Model** + * The administration page now states the five checks a request passes before it is served, explains that a source id is a client-supplied header that identifies rather than authenticates, and notes that throttles being on does not mean the endpoint is reachable. + * (Ref: `docs/admin/agents-actions.md`) + +### **(v0.261.064)** + +#### New Features + +* **Chat Settings Are Fully Editable In The New Admin Interface** + * The new admin interface builds each settings section from a description of what it contains. Chat had no description, so it fell back to scanning for on/off flags — which meant it could draw switches and nothing else. Every chat setting that was not a switch was simply absent: the conversation history limit, the default system prompt, and the whole Enhanced Citations configuration. + * All eleven Chat sections now render the same controls the classic interface does, across Chat Experience, Feedback & Alerts and Citations. + * **Two switches were invisible even though they are switches**, because their setting names do not begin with `enable_`. Workspace Scope Lock and the ChatFileUploadUser role requirement are now both present. + * **Enhanced Citations came across in full**, including the storage authentication type, the storage credentials, the tabular preview size cap, the large-run confirmation thresholds, and the chunk processing model — plus a **Test storage connection** button. Startup deliberately skips storage checks so an outage cannot stop the application booting, which previously meant a wrong credential was only discovered when a citation failed to open. + * (Ref: `admin_settings_fields.py`, `EnhancedCitationsStorageTest.tsx`, `AdminSettingsPage.tsx`, [V2 Admin Chat Settings](features/V2_ADMIN_CHAT_SETTINGS.md)) + +* **Conversation History Summarization Can Be Configured** + * Three settings that control what happens to conversation history — summarizing messages that fall outside the history limit, summarizing recent turns into the document-search query, and how many messages that summary reads — have always been acted on by the chat backend but have never had a control anywhere. + * All three are now editable in both the classic and the new admin interface, under Conversation History. + * (Ref: `chat-experience.html`, `conversation-history-section`, [Chat settings](../../admin/chat/)) + +#### Bug Fixes + +* **Admin Saves No Longer Reset The History Summarization Settings** + * Because the three settings above had no input anywhere, every save of Admin Settings wrote them back as off and 10 — whatever they had been set to. An administrator who configured them directly in the database lost the values the next time anyone saved the page for any unrelated reason. + * Adding the controls fixes this. The conversation history limit and the message count are also parsed defensively now, so an empty value keeps the current setting instead of raising an error or snapping to a default. + * (Ref: `route_frontend_admin_settings.py`, [Chat History Summarize Settings Reset Fix](fixes/CHAT_HISTORY_SUMMARIZE_SETTINGS_RESET_FIX.md)) + +* **Enhanced Citations File Downloads No Longer Break After An Admin Save** + * The storage account key used to sign Enhanced Citations file links has no input on the admin page, but the save handler wrote it from the form anyway — so every admin save, for any unrelated reason, replaced it with an empty value and citation downloads started returning a server error. + * The key is now preserved when the form does not carry it, along with the equivalent video and audio storage keys. + * (Ref: `route_frontend_admin_settings.py`, `office_docs_key`, [Enhanced Citations Storage Key Reset Fix](fixes/ENHANCED_CITATIONS_STORAGE_KEY_RESET_FIX.md)) + +* **Stored Credentials Are No Longer Sent To The Admin Browser** + * The new admin interface's settings endpoint returned the settings document unchanged, so every stored credential in it — model keys, search and Document Intelligence keys, the Redis key, storage connection strings, and every configured model endpoint's API key and client secret — was delivered to the administrator's browser in cleartext. The classic admin page has always masked these. + * Credentials are now masked the same way in the new interface. A configured value shows as saved and hidden, and can be replaced or cleared but not read back. An untouched field keeps its stored value on save rather than being overwritten by the mask, and erasing one warns before it is removed. + * **Three storage account keys were unmasked on both interfaces.** The key used to sign citation file access links is a live credential and is now masked everywhere. + * (Ref: `admin_settings_secret_utils.py`, `route_backend_v2.py`, `SecretField.tsx`, [V2 Admin Settings Secret Exposure Fix](fixes/V2_ADMIN_SETTINGS_SECRET_EXPOSURE_FIX.md)) + +#### User Interface Enhancements + +* **Settings From Other Areas No Longer Appear Under Chat** + * Undescribed settings are placed by matching their name against section names, and six landed in the wrong group: audio and video file support and chat completion audio cues (Knowledge > Audio & Video), enhanced extraction (Knowledge > Document Extraction), and the embedding model and tabular processing actions (Agents & Actions). Each now appears where it belongs. + * **Four switches that did nothing have been removed.** Tabular processing is derived from Enhanced Citations rather than stored, so toggling it appeared to save and then reverted; the Enhanced Citations mount and two mixed-source rollout flags have no administrator control at all. + * (Ref: `SUPPRESSED_CAPABILITY_KEYS`, `buildCapabilityIndex`, `test_v2_admin_capability_placement.py`) + +* **Settings No Longer Linger After Their Feature Is Switched Off** + * A setting shown only under certain conditions was checked against one other setting, which was not always enough. The Latest Features documentation links toggle stayed on screen with the Support menu switched off, because the condition it was checked against remained satisfied on its own. + * Conditions can now be combined, so a setting disappears with the feature that owns it as well as with the option that selects it. + * The same problem affected three group assignment pickers — group and public workspace file downloads, and group workflows. Turning the capability off left the picker on screen assigning access for a disabled feature. + * (Ref: `depends_on`, `isFieldVisible`, `admin_settings_fields.py`) + +* **Workflow Settings Were Declared Twice** + * The Workflow section was registered twice, and only the second registration took effect. The two were not identical, so the first was silently dead — and a reordering would have swapped which one applied, dropping six settings from the page. + * The same duplication had begun to affect chat file uploads, where the second registration was overriding the first and hiding the **Enable Chat File Uploads** switch. + * (Ref: `ADMIN_SETTINGS_FIELDS`, `workflow-settings-section`, `chat-file-uploads-section`) + +### **(v0.261.063)** + +#### New Features + +* **The Whole Security Group Now Works In The New Admin Interface** + * The new Admin Settings surface renders from a description of each setting. Security had no description, so it fell back to scanning for on/off switches — which meant it could draw switches and nothing else. The Key Vault name, the Content Safety endpoint and key, the idle timeout values, the Front Door URL and the access denied message were all unreachable, and three sections — Permissions, Access Denied Message and Key Vault — rendered as nothing at all, because a section with no switches and nothing described is skipped entirely. + * All six Security tabs are now described in full: **Access & Roles**, **Secrets**, **Content Safety**, **Session**, **Network** and **Rate Limiting**. Every field the classic page submits has a control, and a test keeps the two from drifting apart again. + * (Ref: `admin_settings_fields.py`, `AdminSettingsPage.tsx`, `test_v2_admin_security_parity.py`, [Security settings](../admin/security.md)) + +* **App Role Requirements Reads As A Policy, Not A List Of Switches** + * Role requirements are decided on the tab that owns each feature, which keeps each decision in context but scatters the access policy across seven tabs. The old roster gathered the switches but said nothing about them: a row was a label and a toggle, with no indication of which Entra role to assign or what changed when you flipped it. + * Each row now names the exact app role value to assign — copyable, so it does not get retyped wrong into Entra — and states both halves of the decision: what enforcing it restricts, and who keeps access when it is left off. A count at the top reads the posture at a glance, and the list can be filtered. + * **A requirement that is doing nothing now says so.** Enforcing a role for a feature that is switched off looks like protection and is not, so those rows are marked. + * **One requirement was missing entirely.** The old roster was built by scanning the page for checkboxes named `require_member_of_*`, so `file_sync_personal_require_app_role` never appeared in it. The catalog is built from a declared registry instead, and a test fails if a new role requirement is not registered. + * (Ref: `admin_app_roles.py`, `AppRoleRequirements.tsx`, `test_v2_admin_app_role_registry.py`) + +* **Sections Say Whether They Are Actually Working** + * "Enabled" and "working" are not the same thing for an integration. Content Safety can be switched on with no endpoint, and Key Vault with no vault name, and in both cases the feature silently does nothing — which you would only discover by opening the section and reading every field. + * Section headers now carry **Off**, **Needs configuration** or **On**, so the security posture can be read without opening anything. Content Safety is judged against whichever endpoint its routing choice actually uses. + +* **Key Vault Expiration Reminders And The Tracked Secret Inventory** + * Key Vault secret names written by SimpleChat are content hashes, so an expiry alert from Azure names something like `sc-a1b2c3` and nothing an operator can act on. The reminder settings and the inventory that maps that name back to its owner, source action and field are now available in the new interface, including the on-demand sweep. + +* **Test Buttons And Front Door Redirect URIs** + * Content Safety and Key Vault both have inline connection tests again, run against what is currently on screen rather than what was last saved, so a mistake is caught before Save. A broken Content Safety connection blocks chat rather than failing quietly, which makes testing first worth the click. + * The Front Door section derives and shows the two redirect URIs that must be registered on the Entra app registration, each copyable. An unregistered URI is the usual cause of sign-in completing at Microsoft and then failing on the way back. + +#### Bug Fixes + +* **Secrets Are No Longer Sent To The Browser By The New Admin Interface** + * The classic admin page replaces stored credentials with a placeholder before rendering, and restores them on save. The new interface's settings endpoint returned the settings document as-is, so opening Admin Settings put every stored key into the page payload — including the Content Safety key, the APIM subscription key, and the Azure Storage account key used to sign document SAS URLs, which no admin template renders as a secret at all. + * Secrets are now redacted, against a wider list than the classic form uses, because this endpoint returns the whole settings document rather than the subset a form draws. A credential field reports whether a value is stored and offers **Replace**; the stored value is never sent, so it cannot be read back out of the browser. Saving without touching a secret leaves it intact, and the connection tests resolve the placeholder on the server. + * **Replace does not stage a deletion.** It only opens the field for entry — leaving it blank keeps what is stored, and a secret is removed only by clearing a value you typed. + * (Ref: `route_backend_v2.py`, `redact_admin_settings_secrets_for_api`, `resolve_admin_settings_secret_value`, `test_v2_admin_settings_secret_redaction.py`) + +* **The Content Safety Connection Test Now Tests The Path Actually In Use** + * The new interface sent the authentication mode under the wrong name, so the endpoint never saw it and always took the key path. On a deployment authenticating with a managed identity, the test reported a failure for a working configuration — or, if a stale key happened to be stored, quietly validated a path that is not the one in use. + * (Ref: `ConnectionTest.tsx`, `_test_safety_connection`) + +* **"Startup App Maintenance" No Longer Appears Under Security** + * `enable_app_maintenance` and `enable_startup_app_maintenance` were showing up in **Security > App Role Requirements**, beside Entra role switches they have nothing to do with. The fallback scan files an undescribed setting by matching word stems, and both matched "app" in `app-role-requirements-section`. + * They are Cosmos maintenance switches — the recurring job that checks index policies, reconciles the document access index and clears stale caches — and now live under **Scale > Cosmos > Cosmos Maintenance** with labels and help text that say what they do. Neither had a control on the classic page at all. + * `enable_key_vault_secret_storage` was likewise being filed under Backup & Recovery by matching "storage", and `enable_key_vault_secret_expiration_reminders` matched nothing and fell into "Other capabilities". Both are now in the Key Vault section where they belong. + * (Ref: `admin_settings_fields.py`, `test_v2_admin_capability_placement.py`) + +* **The Idle Warning Can No Longer Be Set After The Sign-Out It Warns About** + * The classic page silently lowers a warning time that exceeds the sign-out time. The new interface's save now does the same and says so, instead of storing a warning that could never appear. + +#### User Interface Enhancements + +* **Long Settings Sections Are Broken Into Labelled Parts** + * Key Vault has eleven controls and a table; as one flat list, its connection settings and its expiration reminder settings read as a single undifferentiated decision. Sections can now declare sub-headings, so Key Vault reads as **Vault connection**, **Expiration reminders** and **Tracked secrets**, and Content Safety as **Connection** and **When a message is blocked**. + +* **Standing Warnings Sit Next To The Control They Apply To** + * Consequences that no label can hold — enabling Key Vault is effectively one-way, and SimpleChat records expiry reminders rather than emailing them — are now shown as callouts beside the switch rather than hidden behind a tooltip or left unsaid. + +#### Documentation + +* **Security And Cosmos Maintenance Documentation Rewritten** + * The Security page's section bodies were placeholder text, and several setting descriptions were wrong: the Key Vault name, lead days and scan interval were all described as "the secret credential used when the selected authentication mode requires one". Every section now explains what the capability does and why it matters, and the common tasks and troubleshooting tables cover the failures these settings actually produce. + * (Ref: `docs/admin/security.md`, `docs/admin/scale.md`) + +### **(v0.261.062)** + +#### Bug Fixes + +* **Multi-Agent Orchestration Is No Longer Shown As A Switch Under AI Models** + * **Multi-agent orchestration** appeared in the new Admin Settings surface as a live toggle in the AI Models group, which it has nothing to do with. It is written by the orchestration settings API from the chosen orchestration mode, so setting it there did nothing and the value reverted. + * It now sits with the agent runtime settings as a read-only entry that reports the current mode. + * (Ref: `enable_multi_agent_orchestration`, `admin_settings_fields.py`) + +### **(v0.261.061)** + +#### Bug Fixes + +* **Enabling A Feature No Longer Requires Reloading The Chat Tab** + * The new interface read its configuration once, when the page first loaded. An administrator who turned a capability on in Admin Settings — which is a different page, usually a different tab — came back to an open chat that still insisted the capability did not exist, with nothing on screen to explain why. + * The configuration is now re-read whenever the tab comes back to the front, so returning from Admin Settings is enough. This applies to every setting, not just the one that surfaced it. + * (Ref: `bootstrapStore.refresh`, `App.tsx` visibility and focus handling) + +* **Chart Smoothing, Fill And Data Table Settings Now Do Something** + * The chart format has always accepted `smooth`, `fill` and `showDataTable`, and every renderer read them and then ignored them. A chart asking for straight line segments was drawn curved, one asking to be shaded was not, and one asking to keep its numbers private still offered them. + * All three now take effect in the new interface, the classic interface and exported images alike. + * (Ref: `inlineChartSpec.ts`, `chat-inline-charts.js`, `functions_chart_export.py`) + +* **Horizontal Bar Charts Scaled The Wrong Axis** + * A bar chart laid on its side draws its values along the bottom, but "start at zero" was applied to the axis carrying the category names instead — so the setting did nothing on exactly the charts where a truncated scale is most misleading. + * (Ref: `inlineChartSpec.ts`, `chat-inline-charts.js`, `functions_chart_export.py`) + +#### User Interface Enhancements + +* **Orchestration Is On By Default Where It Is Enabled** + * Orchestration previously had to be switched on in the composer before it did anything, which meant a deployment that had enabled it still opened on a row of capability buttons inviting exactly the decisions the planner exists to make. + * Where an administrator has enabled orchestration, the composer now opens in it, with the document, web, model, agent and reasoning controls folded behind **Manual controls** rather than removed. Turning orchestration off restores the classic composer, and anything chosen under the disclosure still constrains the plan. + * (Ref: `Composer.tsx`, `chat_orchestration_show_manual_controls`) + +* **The Inbound MCP Tab Explains Why It Is Empty** + * Inbound MCP is a preview whose settings stay hidden until an App Service application setting is added. Because that flag is not part of the settings document, the new interface could not see it, and simply rendered two unexplained switches. + * The settings API now reports it, and the tab shows what Inbound MCP is, the `ENABLE_MCP_UI` setting that reveals it, and that revealing it does not open the endpoint. + * (Ref: `runtime_flags`, `is_mcp_ui_enabled`, `InboundMcpNotice.tsx`) + +* **Inbound MCP Documentation Describes The Access Model** + * The administration page now states the five checks a request passes before it is served, explains that a source id is a client-supplied header that identifies rather than authenticates, and notes that throttles being on does not mean the endpoint is reachable. + * (Ref: `docs/admin/agents-actions.md`) + +* **Chat Now Says Which Endpoint Is Actually Serving It** + * SimpleChat has two ways to reach a chat model — the connections list, or a single classic endpoint — and only one is in force at a time. Nothing on screen distinguished them. An administrator could add a connection, test it, watch it save, and still be served by the classic endpoint, with no error and nothing to suggest the two were different things. + * The Chat section now names the live route. When it is the classic single endpoint, it links to the page where that endpoint is configured, since those fields are deliberately not duplicated in the new interface. + * It also warns before the switch is thrown: turning connections on is one-way, because the setting is stored as "already on or newly on" and cannot be turned back off afterwards. + * Connections and the settings that depend on them are edited side by side, so they now stay in step with each other. Enabling connections, adding one, or switching a model off is reflected immediately in the model list and in the notice, rather than leaving adjacent parts of one screen describing different states until the page is reloaded. + * **The API Management switch is no longer anonymous.** Settings the new interface has not described are placed by matching their name against section names, and that guess had dropped this one into the Chat section as a bare switch reading "Gpt apim". Beneath a notice explaining which endpoint chat uses, it looked like part of the same explanation — while turning it on actually reaches for an APIM endpoint, deployment and subscription key that can only be set on the classic admin page, so requests would have had nowhere to go. It now says what it does and where the rest of it lives. + * (Ref: `ChatModeNotice.tsx`, `ModelConnectionsManager.tsx`, `modelConnectionsStore.ts`, `enable_multi_model_endpoints`, `enable_gpt_apim`, `docs/admin/ai-models.md`) + +#### New Features + +* **Charts Can Be Changed Where They Are, Instead Of Regenerated** + * A generated chart used to be final. If the model picked a pie where a bar was wanted, put a scale on the axis that flattened the whole story, left the axes unnamed, or got a single number wrong, the only remedy was to ask again — which left a second near-duplicate chart sitting below the first with nothing to say which one was current. + * Every chart now has an **Edit** button. The editor opens beside a live preview with six tabs: **Data**, **Design**, **Axes**, **Source**, **Ask AI** and **History**. + * **The numbers are editable.** A grid of the chart's own labels and series: change any value, rename or add a series, add and remove rows. An emptied cell is a gap rather than a zero, so a line is drawn straight past it instead of dropping to the axis. Scatter and bubble charts get a list of their x/y pairs instead. + * **The chart type can be changed**, but only to a type the data can actually be read as. A scatter chart is not offered a bar chart, and pie, doughnut and polar area appear once a chart has a single series — with the reason given, rather than the option silently missing. + * **The axes can be scaled and named.** An explicit minimum and maximum, start-at-zero, a logarithmic scale for values that span orders of magnitude, and an angle and a limit for category labels too crowded to read. On a horizontal bar chart these correctly apply to the axis that carries the values, which runs along the bottom. + * **Bar width, line thickness, point size, the doughnut hole, gridlines, the legend and its position, stacking and orientation** are all adjustable, along with the chart's title, subtitle and caption. + * **Six changes make one entry in the history, not six.** The whole panel edits a draft that the preview follows, and one save records the lot with a note naming what changed — "Bar width, Value axis" rather than "Edited". + * **Ask AI changes the chart in front of it** without adding anything to the conversation, and is told not to invent numbers: if an instruction asks for values the chart does not have and cannot derive, the data is left alone. A reply that is not a chart is refused rather than stored. + * **Nothing is deleted.** History keeps every version with who made it and what it was; restoring moves a pointer rather than discarding newer ones, and the chart the model originally produced is always kept. + * Exported and emailed charts, and the classic interface, all show the current version with every setting applied — so a conversation looks the same wherever it is read. + * (Ref: `chartEdits.ts`, `ChartEditor.tsx`, `ChartDataGrid.tsx`, `ChartCanvas.tsx`, `functions_message_block_revisions.py`, `functions_block_revision_assist.py`, `functions_chart_export.py`, `chat-block-revisions.js`, [V2 Inline Chart Editing](features/V2_INLINE_CHART_EDITING.md)) + +* **Inbound MCP Is Fully Configurable In The New Admin Interface** + * The Inbound MCP tab in the new Admin Settings surface showed exactly two switches — **Enable inbound MCP server** and **Enable tool throttles** — and nothing else. The delegated scope, the required Entra roles, the request and throttle limits, and all three allowlists were invisible, which read as "disabled, but throttles are on" with no explanation for either. + * The tab now renders the whole configuration, grouped as **Runtime gate**, **Request limits** and **Allowlists**. + * **Allowlists are edited as rows rather than raw JSON.** Each row pairs the identifier the runtime matches with a description of who it belongs to, and a repeated value is flagged as you type rather than disappearing on save. + * **The lists the runtime actually reads are derived on save**, exactly as the classic interface derives them. Editing an allowlist in the new interface now takes effect; previously the schema had no way to express that one setting produces several, which for an access control list is the worst kind of failure — the screen would show the new restriction while the runtime applied the old one. + * **Turning off "Allow additional tenant IDs" no longer discards the tenants you listed.** It stops admitting them, and turning it back on restores them. + * (Ref: `admin_settings_fields.py`, `functions_mcp_server_config.py`, `EntryListEditor.tsx`, `adminEntries.ts`) + +* **The Default Chat Model Can Be Chosen In The New Admin Interface** + * The Chat section of AI Models had nothing in it but a single switch. The default model — the one chat uses when nobody has picked anything, such as on a brand new conversation — could only be set on the classic admin page, even though the connections it selects from had just moved. + * **Only models that can actually serve are offered.** The list is built from enabled models on enabled connections, because a reference to anything else is cleared the moment connections are next saved. Offering a disabled model would have let an administrator choose a value that quietly reverted, with nothing to explain why. + * **A choice that no longer resolves is refused rather than accepted.** Naming a connection that has been deleted, or a model that has been switched off, now comes back with a message saying so. Previously the two failure modes — storing a dangling reference, and silently emptying it — both ended up looking identical: the field simply read as having no default. + * **The saved default is shown for what it currently means.** If the connection or model it named has since gone, the interface says that rather than presenting an empty field as though nothing had ever been set. Opening the page does not rewrite the stored value, so the evidence of what went missing survives. + * The reference is validated by the same rule the classic form uses, so the two interfaces cannot disagree about whether a default is still valid. + * (Ref: `route_backend_v2.py`, `/api/v2/admin/default-model`, `ChatDefaultModel.tsx`, `modelConnections.ts`, `admin_settings_fields.py`, `resolve_default_model_selection`) + +### **(v0.261.060)** + +#### New Features + +* **Orchestration: Describe What You Want Instead Of Assembling The Request** + * Answering a question well used to depend on choosing correctly before you had seen any results. You had to decide whether to search your documents and which ones, whether to search the web, whether to read the URLs you had pasted, which saved prompt to apply, which agent to use, and which model should answer — all in advance, from a row of controls under the message box. + * **You now just ask.** SimpleChat works out what the question needs, shows you the plan it intends to follow, and runs it. Turning orchestration on hides the capability toggles and the model, agent and reasoning pickers behind a disclosure; file upload and voice input stay exactly where they are. + * **You decide how much say you want.** A plan can wait for you to read it, run after a countdown you can interrupt, or run immediately. Administrators set the default and can decide whether users may change it. + * **You can narrow a plan before it runs.** Steps can be switched off and documents removed. You cannot add to a plan this way — widening it goes back through planning, so a request never skips the reasoning and the permission check that produced it. + * **Questions are asked in the conversation, as a card.** When the orchestrator genuinely cannot proceed without knowing something, it asks with a short form — choices to pick rather than prose to write — instead of a paragraph you have to answer in the thread. The card speaks the MCP elicitation contract, so an MCP server asking a question later will use the same card. + * **The plan lives in the side panel, next to Contents and Documents.** A Run view shows the current plan with live progress, and a Map view shows every run in the conversation in order, so you can see what has already been searched and produced. Clicking a run jumps the conversation to the turn that produced it. + * **Later turns reuse earlier work.** The planner is shown a bounded summary of what previous turns in the conversation already did, so a follow-up question depends on findings that already exist rather than searching for them again — and you are never asked a question you have already answered. + * Orchestration reaches only capabilities that are already enabled, so it grants no new access to anyone. This release covers document search, document analysis, document comparison, spreadsheet analysis, web search and answering. + * Available in the V2 interface. The classic interface is unchanged. Off by default. + * (Ref: `functions_orchestration_registry.py`, `functions_orchestration_schema.py`, `functions_orchestration_context.py`, `functions_orchestration_planner.py`, `functions_orchestration_executor.py`, `/api/v2/orchestration/plan`, `/api/v2/orchestration/run`, [Chat Orchestration](features/CHAT_ORCHESTRATION.md), [Orchestration settings](../admin/orchestration.md)) + +* **The Whole Workspaces Group Is Now Editable In The New Interface** + * The new admin page draws real controls from a description of each settings section. Workspaces had almost no description, so it fell back to scanning for on/off flags — which meant it could show switches and nothing else. Thirteen Workspaces settings had no control anywhere in the new interface, and the Global Identities tab rendered as a blank page. + * **Everything that was missing is now there**: personal, group and public workspace downloads, both "require assignment" rules and the group and public workspace pickers that go with them, the public workspace end-user display name, shared conversation file approvals, the CreateGroups and CreatePublicWorkspaces role requirements, and the owner-only restriction on group agents and actions. + * **The four toggles that were already visible now explain themselves.** They were previously drawn from the flag name alone — "Group creation", with no help text and no indication that it does nothing while group workspaces are off. Each now carries a written explanation of what it does and when you would want it. + * **Settings that do nothing are hidden rather than shown as inert.** Requiring a group assignment only appears once group downloads are enabled, and the group picker only appears once the assignment is required, so what is on screen is what currently has an effect. + * (Ref: `admin_settings_fields.py`, `AssignmentPicker.tsx`, `AdminSettingsPage.tsx`, [V2 Workspaces Admin Settings](features/V2_WORKSPACES_ADMIN_SETTINGS.md)) + +* **Every App Role Requirement, Readable In One Place** + * Ten settings in SimpleChat can demand an Entra app role, spread across six tabs. None of them appeared in the new interface at all, because the fallback scan that was drawing those pages could only see on/off capability flags. + * All ten are now on the tab that owns them, and **Security > App Role Requirements** additionally gathers them into one list. Each switch there is the same value as the one on its own tab, not a copy, so changing it in either place is the same change. + * Each entry names the group, tab and section the setting really lives on, and the list reports how many of the ten are currently being enforced — so the deployment's access policy can be read as a whole rather than reconstructed tab by tab. + * (Ref: `collectAppRoleEntries`, `AppRoleRoster.tsx`, `admin_access_roles_roster.js`) + +* **Global Identities Explain What They Are, And Sit Somewhere Sensible** + * Global Identities are saved credentials for the systems SimpleChat connects out to, used by File Sync sources and actions. They were filed under Workspaces, which owns neither, and the tab in the new interface was empty. + * The tab has moved to **Security**, next to Key Vault — which is where each identity's secret is actually stored — and now lists what exists with its sign-in type, alongside a link to the classic page for adding or editing one. Existing links to the tab still work. + * (Ref: `admin_settings_nav.py`, `GlobalIdentitiesList.tsx`) + +* **Action Settings Are Now Present In The New Admin Interface** + * The Actions tab in the new Admin Settings surface showed a single toggle. The Analyze and Document Comparison limits were invisible because they are stored as one nested object rather than as separate settings, and the built-in action toggles had been scattered by the same guesswork that emptied the Agents tab — **Enable Text Action** had ended up under Appearance › Branding because its name contains "text". + * The Actions tab now renders as four sections: **Document Actions**, **Workspace Action Permissions**, **Built-in Actions**, and **Global Actions**. + * **Document action limits are editable again**, with the ranges the application actually enforces (2–300 for chat, 2–1000 for a workflow run) and the workflow limit separated from the chat one. Saving one limit no longer risks the other five, and out-of-range values are clamped by the same code the classic interface uses. + * **Built-in actions are collapsed rather than laid out flat.** They default on and are rarely changed, so they no longer take up the tab; the documentation now says which one is worth a decision — HTTP, the only built-in action that reaches outside the deployment. + +#### User Interface Enhancements + +* **Maximum File Size Moved To Knowledge** + * The upload ceiling applies to chat attachments as well as workspace documents, and it is checked before extraction runs, so Workspaces only ever described half of what it does. It now sits in **Knowledge > Document Extraction** beside Chunk Sizes, and its description says plainly that it covers both upload paths. + * (Ref: `max_file_size_mb`, `functions_documents.py`, `route_frontend_chats.py`) + +* **Group Creation Reads The Right Way Round** + * The classic page asks you to tick "Disable Group Creation" to prevent it — a double negative over a setting that is stored as *enable* group creation. The new interface presents it as **Allow Users to Create Groups**, where on means users can create groups. + * The wording also now says what was previously undocumented: switching it off freezes the group list without disabling existing groups, and overrides the CreateGroups role requirement entirely. + * The classic page is unchanged, so nothing about an existing deployment moves. + * (Ref: `enable_group_creation`, `LEGACY_FIELD_NAMES`) + +* **Workspaces, Knowledge And Security Documentation Rewritten** + * The Workspaces admin page described its sections with a repeated sentence that said only which tab they belonged to. It now explains what each workspace type means for who can read a document, how the three group-creation controls stack, and why downloads default to off, with troubleshooting for the cases where a setting appears to have no effect. + * (Ref: `docs/admin/workspaces.md`, `docs/admin/knowledge.md`, `docs/admin/security.md`) + +#### Bug Fixes + +* **Turning Connections Off Said It Worked, And Did Not** + * Enabling connections for chat is one-way — the stored flag is coerced to "already on, or newly requested", so once it is true it stays true. The classic page reflects that by hiding the checkbox after it is enabled. The new admin interface showed an ordinary switch, so turning it off reported a successful save, moved the switch, and changed nothing; chat carried on routing through connections and only a page reload revealed it. The attempt is now refused with an explanation. + * **Enabling connections from the new interface also used to strand a deployment.** The classic page carries the existing single chat endpoint over as the first connection when the flag is first switched on. The new interface skipped that step, so a deployment that enabled connections there was left with an empty model catalog — and, because the flag cannot be turned back off, no way to recover. Both paths now migrate through the same code. + * (Ref: `admin_settings_fields.py`, `build_migrated_model_endpoints_from_legacy`, `_seed_connections_on_first_enable`) + +* **Deleting A Connection Could Break Document Metadata Extraction** + * Metadata extraction can be pointed at a specific model, stored as a reference to a connection and a model within it. Deleting or disabling that connection left the reference naming something that no longer existed, and unlike the chat default — which quietly falls back — extraction raises instead, and its caller only writes the failure to the processing log. Metadata extraction would therefore fail for every subsequently ingested document with nothing visible to say why. That reference is now re-checked whenever connections change, exactly as the chat default already was. + * (Ref: `resolve_metadata_extraction_model_selection`, `_persist_global_model_endpoints`) + +* **A Failed Settings Write Reported Success After The Secrets Were Already Gone** + * Saving a connection removes superseded secrets from Key Vault before writing the settings document. The settings write reports failure by returning a value rather than raising, and that value was not checked — so if the write failed, the interface said the change had been saved while the connection was left referencing a secret that had already been deleted. A failed write is now reported as an error. + * (Ref: `_persist_global_model_endpoints`, `update_settings`) + +* **Tabular Processing Is No Longer Shown As A Toggle That Does Nothing** + * The new interface showed **Tabular Processing** as a live switch under Chat › Processing Thoughts, purely because the word "processing" matched that section. The application recomputes this setting from **Enhanced Citations** on every settings read, so switching it had no effect and the value silently reverted. + * It now appears where it belongs, under Built-in Actions, as a read-only entry that reports its state and names Enhanced Citations as its source. Attempting to set it directly is refused rather than accepted and discarded. + * (Ref: `enable_tabular_processing_plugin`, `is_tabular_processing_enabled`, `admin_settings_fields.py`) + +* **Fact Memory Is Listed With The Actions It Affects Without Moving Its Control** + * Fact memory is a chat capability, but it also decides whether agents get a memory action, so it was missing from any list an administrator would consult when working out what an agent can do. + * It is now mirrored under Built-in Actions as a read-only entry that links to where it is set, while the control that actually sets it stays in Chat. + * (Ref: `enable_fact_memory_plugin`, `fields.tsx`) + +### **(v0.261.059)** + +#### New Features + +* **The Whole Security Group Now Works In The New Admin Interface** + * The new Admin Settings surface renders from a description of each setting. Security had no description, so it fell back to scanning for on/off switches — which meant it could draw switches and nothing else. The Key Vault name, the Content Safety endpoint and key, the idle timeout values, the Front Door URL and the access denied message were all unreachable, and three sections — Permissions, Access Denied Message and Key Vault — rendered as nothing at all, because a section with no switches and nothing described is skipped entirely. + * All six Security tabs are now described in full: **Access & Roles**, **Secrets**, **Content Safety**, **Session**, **Network** and **Rate Limiting**. Every field the classic page submits has a control, and a test keeps the two from drifting apart again. + * (Ref: `admin_settings_fields.py`, `AdminSettingsPage.tsx`, `test_v2_admin_security_parity.py`, [Security settings](../admin/security.md)) + +* **App Role Requirements Reads As A Policy, Not A List Of Switches** + * Role requirements are decided on the tab that owns each feature, which keeps each decision in context but scatters the access policy across seven tabs. The old roster gathered the switches but said nothing about them: a row was a label and a toggle, with no indication of which Entra role to assign or what changed when you flipped it. + * Each row now names the exact app role value to assign — copyable, so it does not get retyped wrong into Entra — and states both halves of the decision: what enforcing it restricts, and who keeps access when it is left off. A count at the top reads the posture at a glance, and the list can be filtered. + * **A requirement that is doing nothing now says so.** Enforcing a role for a feature that is switched off looks like protection and is not, so those rows are marked. + * **One requirement was missing entirely.** The old roster was built by scanning the page for checkboxes named `require_member_of_*`, so `file_sync_personal_require_app_role` never appeared in it. The catalog is built from a declared registry instead, and a test fails if a new role requirement is not registered. + * (Ref: `admin_app_roles.py`, `AppRoleRequirements.tsx`, `test_v2_admin_app_role_registry.py`) + +* **Sections Say Whether They Are Actually Working** + * "Enabled" and "working" are not the same thing for an integration. Content Safety can be switched on with no endpoint, and Key Vault with no vault name, and in both cases the feature silently does nothing — which you would only discover by opening the section and reading every field. + * Section headers now carry **Off**, **Needs configuration** or **On**, so the security posture can be read without opening anything. Content Safety is judged against whichever endpoint its routing choice actually uses. + +* **Key Vault Expiration Reminders And The Tracked Secret Inventory** + * Key Vault secret names written by SimpleChat are content hashes, so an expiry alert from Azure names something like `sc-a1b2c3` and nothing an operator can act on. The reminder settings and the inventory that maps that name back to its owner, source action and field are now available in the new interface, including the on-demand sweep. + +* **Test Buttons And Front Door Redirect URIs** + * Content Safety and Key Vault both have inline connection tests again, run against what is currently on screen rather than what was last saved, so a mistake is caught before Save. A broken Content Safety connection blocks chat rather than failing quietly, which makes testing first worth the click. + * The Front Door section derives and shows the two redirect URIs that must be registered on the Entra app registration, each copyable. An unregistered URI is the usual cause of sign-in completing at Microsoft and then failing on the way back. + +* **The Whole Workspaces Group Is Now Editable In The New Interface** + * The new admin page draws real controls from a description of each settings section. Workspaces had almost no description, so it fell back to scanning for on/off flags — which meant it could show switches and nothing else. Thirteen Workspaces settings had no control anywhere in the new interface, and the Global Identities tab rendered as a blank page. + * **Everything that was missing is now there**: personal, group and public workspace downloads, both "require assignment" rules and the group and public workspace pickers that go with them, the public workspace end-user display name, shared conversation file approvals, the CreateGroups and CreatePublicWorkspaces role requirements, and the owner-only restriction on group agents and actions. + * **The four toggles that were already visible now explain themselves.** They were previously drawn from the flag name alone — "Group creation", with no help text and no indication that it does nothing while group workspaces are off. Each now carries a written explanation of what it does and when you would want it. + * **Settings that do nothing are hidden rather than shown as inert.** Requiring a group assignment only appears once group downloads are enabled, and the group picker only appears once the assignment is required, so what is on screen is what currently has an effect. + * (Ref: `admin_settings_fields.py`, `AssignmentPicker.tsx`, `AdminSettingsPage.tsx`, [V2 Workspaces Admin Settings](features/V2_WORKSPACES_ADMIN_SETTINGS.md)) + +* **Every App Role Requirement, Readable In One Place** + * Ten settings in SimpleChat can demand an Entra app role, spread across six tabs. None of them appeared in the new interface at all, because the fallback scan that was drawing those pages could only see on/off capability flags. + * All ten are now on the tab that owns them, and **Security > App Role Requirements** additionally gathers them into one list. Each switch there is the same value as the one on its own tab, not a copy, so changing it in either place is the same change. + * Each entry names the group, tab and section the setting really lives on, and the list reports how many of the ten are currently being enforced — so the deployment's access policy can be read as a whole rather than reconstructed tab by tab. + * (Ref: `collectAppRoleEntries`, `AppRoleRoster.tsx`, `admin_access_roles_roster.js`) + +* **Global Identities Explain What They Are, And Sit Somewhere Sensible** + * Global Identities are saved credentials for the systems SimpleChat connects out to, used by File Sync sources and actions. They were filed under Workspaces, which owns neither, and the tab in the new interface was empty. + * The tab has moved to **Security**, next to Key Vault — which is where each identity's secret is actually stored — and now lists what exists with its sign-in type, alongside a link to the classic page for adding or editing one. Existing links to the tab still work. + * (Ref: `admin_settings_nav.py`, `GlobalIdentitiesList.tsx`) + +* **Charts Can Be Changed Where They Are, Instead Of Regenerated** + * A generated chart used to be final. If the model picked a pie where a bar was wanted, put a scale on the axis that flattened the whole story, left the axes unnamed, or got a single number wrong, the only remedy was to ask again — which left a second near-duplicate chart sitting below the first with nothing to say which one was current. + * Every chart now has an **Edit** button. The editor opens beside a live preview with six tabs: **Data**, **Design**, **Axes**, **Source**, **Ask AI** and **History**. + * **The numbers are editable.** A grid of the chart's own labels and series: change any value, rename or add a series, add and remove rows. An emptied cell is a gap rather than a zero, so a line is drawn straight past it instead of dropping to the axis. Scatter and bubble charts get a list of their x/y pairs instead. + * **The chart type can be changed**, but only to a type the data can actually be read as. A scatter chart is not offered a bar chart, and pie, doughnut and polar area appear once a chart has a single series — with the reason given, rather than the option silently missing. + * **The axes can be scaled and named.** An explicit minimum and maximum, start-at-zero, a logarithmic scale for values that span orders of magnitude, and an angle and a limit for category labels too crowded to read. On a horizontal bar chart these correctly apply to the axis that carries the values, which runs along the bottom. + * **Bar width, line thickness, point size, the doughnut hole, gridlines, the legend and its position, stacking and orientation** are all adjustable, along with the chart's title, subtitle and caption. + * **Six changes make one entry in the history, not six.** The whole panel edits a draft that the preview follows, and one save records the lot with a note naming what changed — "Bar width, Value axis" rather than "Edited". + * **Ask AI changes the chart in front of it** without adding anything to the conversation, and is told not to invent numbers: if an instruction asks for values the chart does not have and cannot derive, the data is left alone. A reply that is not a chart is refused rather than stored. + * **Nothing is deleted.** History keeps every version with who made it and what it was; restoring moves a pointer rather than discarding newer ones, and the chart the model originally produced is always kept. + * Exported and emailed charts, and the classic interface, all show the current version with every setting applied — so a conversation looks the same wherever it is read. + * (Ref: `chartEdits.ts`, `ChartEditor.tsx`, `ChartDataGrid.tsx`, `ChartCanvas.tsx`, `functions_message_block_revisions.py`, `functions_block_revision_assist.py`, `functions_chart_export.py`, `chat-block-revisions.js`, [V2 Inline Chart Editing](features/V2_INLINE_CHART_EDITING.md)) + +* **Chat Settings Are Fully Editable In The New Admin Interface** + * The new admin interface builds each settings section from a description of what it contains. Chat had no description, so it fell back to scanning for on/off flags — which meant it could draw switches and nothing else. Every chat setting that was not a switch was simply absent: the conversation history limit, the default system prompt, and the whole Enhanced Citations configuration. + * All eleven Chat sections now render the same controls the classic interface does, across Chat Experience, Feedback & Alerts and Citations. + * **Two switches were invisible even though they are switches**, because their setting names do not begin with `enable_`. Workspace Scope Lock and the ChatFileUploadUser role requirement are now both present. + * **Enhanced Citations came across in full**, including the storage authentication type, the storage credentials, the tabular preview size cap, the large-run confirmation thresholds, and the chunk processing model — plus a **Test storage connection** button. Startup deliberately skips storage checks so an outage cannot stop the application booting, which previously meant a wrong credential was only discovered when a citation failed to open. + * (Ref: `admin_settings_fields.py`, `EnhancedCitationsStorageTest.tsx`, `AdminSettingsPage.tsx`, [V2 Admin Chat Settings](features/V2_ADMIN_CHAT_SETTINGS.md)) + +* **Conversation History Summarization Can Be Configured** + * Three settings that control what happens to conversation history — summarizing messages that fall outside the history limit, summarizing recent turns into the document-search query, and how many messages that summary reads — have always been acted on by the chat backend but have never had a control anywhere. + * All three are now editable in both the classic and the new admin interface, under Conversation History. + * (Ref: `chat-experience.html`, `conversation-history-section`, [Chat settings](../../admin/chat/)) + +* **Model Connections Can Be Managed In The New Admin Interface** + * The AI Models group in the new interface previously showed nothing but a few switches. That was structural rather than cosmetic: no section in the group had a described field schema, so the page fell back to scanning the settings document for `enable_*` booleans — and endpoints, keys, API versions, authentication and model selection are none of those. All of it was invisible. + * **Model endpoints are now presented as connections.** A connection is one Azure OpenAI or Azure AI Foundry resource: where it is, how SimpleChat authenticates to it, and which of its deployed models may be used. The wording and the editing model changed; what is stored did not, so a connection configured in either interface is the same record. + * **Adding a connection now actually saves it.** In the classic interface, adding or editing a model endpoint changed an in-memory list that was serialized into a hidden form field, so nothing was stored until the whole admin settings page was submitted — and a half-filled endpoint looked identical to a saved one. Each connection is now its own resource with its own Save, and the editor says which state it is in. + * **The editor only shows the fields the connection actually uses.** Provider and authentication method together decide what is relevant, derived in one place rather than from several listeners toggling visibility independently. Choosing an API key, for example, hides the subscription and resource group, because an API key cannot reach Azure Resource Manager and those fields would serve no purpose. + * **Validation names the field that is wrong**, next to the control, instead of raising a message that described the problem but not its location. + * **Discovery and testing are part of editing.** *Test connection* checks the credentials resolve, *Discover models* lists the resource's deployments, and each model can be tested individually. Discovered models arrive switched off, because finding a deployment is not the same as choosing to publish it, and re-running discovery does not duplicate a model or overwrite a display name edited by hand. + * **Stored secrets survive an edit.** Keys and client secrets are never returned to the browser; a field whose secret is already stored shows that it exists and stays empty, and leaving it empty keeps the stored value. Deleting a connection removes the secrets it owned from Key Vault. + * Deleting or disabling a connection no longer leaves the default model pointing at something that no longer resolves — chat would otherwise fall back to a different model with no indication that it had. The rule that decides this is now shared with the classic form, so the two interfaces cannot disagree about it. + * (Ref: `route_backend_v2.py`, `/api/v2/admin/model-endpoints`, `modelConnections.ts`, `ModelConnectionsManager.tsx`, `admin_settings_fields.py`, `resolve_default_model_selection`) + +* **Agent Settings Are Now Actually Present In The New Admin Interface** + * The new Admin Settings surface built its Agents tab by scanning the settings document for on/off switches and guessing where each one belonged. Agents are configured with far more than switches, so the result was close to empty: the Agents Configuration section rendered nothing at all, **Enable Agents** was filed under "Other capabilities" because nothing in its name matched the section, and not one of Workspace Mode, the workspace permissions, or the eleven Agents page settings appeared anywhere. + * The Agents tab is now described properly and renders every one of those settings, split into four sections instead of a single crowded card: **Agent Runtime**, **Workspace Agent Permissions**, **Agents Page**, and **Agent Template Approvals**. + * **The settings now say what depends on what.** Enable Agents gates the whole tab. Workspace Mode only appears once agents are on, and the merge behaviour only once Workspace Mode is on — a chain, so a control cannot reappear because an intermediate gate happens to be off. Workspace Agent Permissions is hidden entirely outside Workspace Mode, matching the classic interface. + * **Workspace Mode explains itself.** It reads as a choice between one shared set of agents that administrators curate, and a separate collection for each user and group, rather than as an unexplained switch. + * **The Agents page settings now follow the page they customise.** That page is served behind Enable Agents, so its hero, guidance text and promotion settings are hidden while agents are off instead of offering edits that could not take effect. + * **Promoted agents are chosen from a list rather than edited as JSON.** Agents already promoted are not offered again, and each promotion's time window is set per row. + * Rarely-changed settings are grouped and collapsed rather than laid out flat, and a search opens any collapsed group so a match is never hidden behind it. + * (Ref: `admin_settings_fields.py`, `admin_settings_nav.py`, `adminAgents.ts`, `PromotedAgentsEditor.tsx`, `AdminSettingsPage.tsx`) + +#### Bug Fixes + +* **Secrets Are No Longer Sent To The Browser By The New Admin Interface** + * The classic admin page replaces stored credentials with a placeholder before rendering, and restores them on save. The new interface's settings endpoint returned the settings document as-is, so opening Admin Settings put every stored key into the page payload — including the Content Safety key, the APIM subscription key, and the Azure Storage account key used to sign document SAS URLs, which no admin template renders as a secret at all. + * Secrets are now redacted, against a wider list than the classic form uses, because this endpoint returns the whole settings document rather than the subset a form draws. A credential field reports whether a value is stored and offers **Replace**; the stored value is never sent, so it cannot be read back out of the browser. Saving without touching a secret leaves it intact, and the connection tests resolve the placeholder on the server. + * **Replace does not stage a deletion.** It only opens the field for entry — leaving it blank keeps what is stored, and a secret is removed only by clearing a value you typed. + * (Ref: `route_backend_v2.py`, `redact_admin_settings_secrets_for_api`, `resolve_admin_settings_secret_value`, `test_v2_admin_settings_secret_redaction.py`) + +* **The Content Safety Connection Test Now Tests The Path Actually In Use** + * The new interface sent the authentication mode under the wrong name, so the endpoint never saw it and always took the key path. On a deployment authenticating with a managed identity, the test reported a failure for a working configuration — or, if a stale key happened to be stored, quietly validated a path that is not the one in use. + * (Ref: `ConnectionTest.tsx`, `_test_safety_connection`) + +* **"Startup App Maintenance" No Longer Appears Under Security** + * `enable_app_maintenance` and `enable_startup_app_maintenance` were showing up in **Security > App Role Requirements**, beside Entra role switches they have nothing to do with. The fallback scan files an undescribed setting by matching word stems, and both matched "app" in `app-role-requirements-section`. + * They are Cosmos maintenance switches — the recurring job that checks index policies, reconciles the document access index and clears stale caches — and now live under **Scale > Cosmos > Cosmos Maintenance** with labels and help text that say what they do. Neither had a control on the classic page at all. + * `enable_key_vault_secret_storage` was likewise being filed under Backup & Recovery by matching "storage", and `enable_key_vault_secret_expiration_reminders` matched nothing and fell into "Other capabilities". Both are now in the Key Vault section where they belong. + * (Ref: `admin_settings_fields.py`, `test_v2_admin_capability_placement.py`) + +* **The Idle Warning Can No Longer Be Set After The Sign-Out It Warns About** + * The classic page silently lowers a warning time that exceeds the sign-out time. The new interface's save now does the same and says so, instead of storing a warning that could never appear. + +* **Workflow Settings Are Reachable In The New Admin Interface** + * The **Workflow** group in the new admin interface was empty. Selecting it showed no controls at all — not a missing toggle here or there, but the whole group. Enabling workflows meant going back to the classic admin page. + * The cause was one thing, not seven. The new admin interface draws a section either from a description of its controls, or by scanning for settings whose names begin with `enable_`. Workflow had no description, and not one of its settings is named that way — they are `allow_user_workflows`, `allow_group_workflows`, `workflow_max_tasks` and so on — so the scan found nothing and the section was dropped for being empty. + * All seven settings are now present: **Enable Personal Workflows**, **Require WorkflowUser App Role**, **Enable Group Workflows**, **Require Group Assignment to Use Workflow**, **Assigned Groups**, **Workflow Agent Action Limit** and **Workflow Task Limit**. + * **Sub-settings stay out of the way until they apply.** The `WorkflowUser` role requirement appears once personal workflows are on, and the group allow list appears once group assignment is required. The two run limits are always shown, because they bound personal and group runs alike. + * (Ref: `admin_settings_fields.py`, `AdminSettingsPage.tsx`, [V2 Admin Workflow Settings Parity](fixes/V2_ADMIN_WORKFLOW_SETTINGS_PARITY_FIX.md)) + +* **Chart Smoothing, Fill And Data Table Settings Now Do Something** + * The chart format has always accepted `smooth`, `fill` and `showDataTable`, and every renderer read them and then ignored them. A chart asking for straight line segments was drawn curved, one asking to be shaded was not, and one asking to keep its numbers private still offered them. + * All three now take effect in the new interface, the classic interface and exported images alike. + * (Ref: `inlineChartSpec.ts`, `chat-inline-charts.js`, `functions_chart_export.py`) + +* **Horizontal Bar Charts Scaled The Wrong Axis** + * A bar chart laid on its side draws its values along the bottom, but "start at zero" was applied to the axis carrying the category names instead — so the setting did nothing on exactly the charts where a truncated scale is most misleading. + * (Ref: `inlineChartSpec.ts`, `chat-inline-charts.js`, `functions_chart_export.py`) + +* **Admin Saves No Longer Reset The History Summarization Settings** + * Because the three settings above had no input anywhere, every save of Admin Settings wrote them back as off and 10 — whatever they had been set to. An administrator who configured them directly in the database lost the values the next time anyone saved the page for any unrelated reason. + * Adding the controls fixes this. The conversation history limit and the message count are also parsed defensively now, so an empty value keeps the current setting instead of raising an error or snapping to a default. + * (Ref: `route_frontend_admin_settings.py`, [Chat History Summarize Settings Reset Fix](fixes/CHAT_HISTORY_SUMMARIZE_SETTINGS_RESET_FIX.md)) + +* **Enhanced Citations File Downloads No Longer Break After An Admin Save** + * The storage account key used to sign Enhanced Citations file links has no input on the admin page, but the save handler wrote it from the form anyway — so every admin save, for any unrelated reason, replaced it with an empty value and citation downloads started returning a server error. + * The key is now preserved when the form does not carry it, along with the equivalent video and audio storage keys. + * (Ref: `route_frontend_admin_settings.py`, `office_docs_key`, [Enhanced Citations Storage Key Reset Fix](fixes/ENHANCED_CITATIONS_STORAGE_KEY_RESET_FIX.md)) + +* **Stored Credentials Are No Longer Sent To The Admin Browser** + * The new admin interface's settings endpoint returned the settings document unchanged, so every stored credential in it — model keys, search and Document Intelligence keys, the Redis key, storage connection strings, and every configured model endpoint's API key and client secret — was delivered to the administrator's browser in cleartext. The classic admin page has always masked these. + * Credentials are now masked the same way in the new interface. A configured value shows as saved and hidden, and can be replaced or cleared but not read back. An untouched field keeps its stored value on save rather than being overwritten by the mask, and erasing one warns before it is removed. + * **Three storage account keys were unmasked on both interfaces.** The key used to sign citation file access links is a live credential and is now masked everywhere. + * (Ref: `admin_settings_secret_utils.py`, `route_backend_v2.py`, `SecretField.tsx`, [V2 Admin Settings Secret Exposure Fix](fixes/V2_ADMIN_SETTINGS_SECRET_EXPOSURE_FIX.md)) + +* **A Reserved Identity Header Name Is Now Refused Rather Than Silently Dropped** + * The new admin interface accepted any identity header name. A name that collides with a header the model call already sets — `authorization` or `api-key`, for instance — was normalized away to nothing on read, which turned the header off without saying so. Such a name is now rejected at save time with an explanation. + * (Ref: `admin_settings_fields.py`, `normalize_model_endpoint_identity_header_name`) + +#### User Interface Enhancements + +* **Long Settings Sections Are Broken Into Labelled Parts** + * Key Vault has eleven controls and a table; as one flat list, its connection settings and its expiration reminder settings read as a single undifferentiated decision. Sections can now declare sub-headings, so Key Vault reads as **Vault connection**, **Expiration reminders** and **Tracked secrets**, and Content Safety as **Connection** and **When a message is blocked**. + +* **Standing Warnings Sit Next To The Control They Apply To** + * Consequences that no label can hold — enabling Key Vault is effectively one-way — are now shown as callouts beside the switch rather than hidden behind a tooltip or left unsaid. + +* **Maximum File Size Moved To Knowledge** + * The upload ceiling applies to chat attachments as well as workspace documents, and it is checked before extraction runs, so Workspaces only ever described half of what it does. It now sits in **Knowledge > Document Extraction** beside Chunk Sizes, and its description says plainly that it covers both upload paths. + * (Ref: `max_file_size_mb`, `functions_documents.py`, `route_frontend_chats.py`) + +* **Group Creation Reads The Right Way Round** + * The classic page asks you to tick "Disable Group Creation" to prevent it — a double negative over a setting that is stored as *enable* group creation. The new interface presents it as **Allow Users to Create Groups**, where on means users can create groups. + * The wording also now says what was previously undocumented: switching it off freezes the group list without disabling existing groups, and overrides the CreateGroups role requirement entirely. + * The classic page is unchanged, so nothing about an existing deployment moves. + * (Ref: `enable_group_creation`, `LEGACY_FIELD_NAMES`) + +* **Workspaces, Knowledge And Security Documentation Rewritten** + * The Workspaces admin page described its sections with a repeated sentence that said only which tab they belonged to. It now explains what each workspace type means for who can read a document, how the three group-creation controls stack, and why downloads default to off, with troubleshooting for the cases where a setting appears to have no effect. + * (Ref: `docs/admin/workspaces.md`, `docs/admin/knowledge.md`, `docs/admin/security.md`) + +* **Assigned Groups Are Shown By Name Instead Of Counted** + * The classic admin page could only report "3 groups assigned". Finding out *which* three meant opening a modal and searching for them again, and a group that had since been deleted left an id in the list that nothing on screen ever mentioned. + * In the new interface the assignment is visible: each assigned group appears as a named chip you can remove, search is inline and filters as you type rather than waiting behind a Search button, and an assignment pointing at a group that no longer exists is marked **Not found** so it can be cleared out. + * The list saves with the toggle that gates it, so requiring assignment and choosing the groups is one save rather than two. + * (Ref: `GroupAssignmentField.tsx`, `/api/v2/admin/groups`) + +* **Settings From Other Areas No Longer Appear Under Chat** + * Undescribed settings are placed by matching their name against section names, and six landed in the wrong group: audio and video file support and chat completion audio cues (Knowledge > Audio & Video), enhanced extraction (Knowledge > Document Extraction), and the embedding model and tabular processing actions (Agents & Actions). Each now appears where it belongs. + * **Four switches that did nothing have been removed.** Tabular processing is derived from Enhanced Citations rather than stored, so toggling it appeared to save and then reverted; the Enhanced Citations mount and two mixed-source rollout flags have no administrator control at all. + * (Ref: `SUPPRESSED_CAPABILITY_KEYS`, `buildCapabilityIndex`, `test_v2_admin_capability_placement.py`) + +* **Settings No Longer Linger After Their Feature Is Switched Off** + * A setting shown only under certain conditions was checked against one other setting, which was not always enough. The Latest Features documentation links toggle stayed on screen with the Support menu switched off, because the condition it was checked against remained satisfied on its own. + * Conditions can now be combined, so a setting disappears with the feature that owns it as well as with the option that selects it. + * (Ref: `depends_on`, `isFieldVisible`, `admin_settings_fields.py`) + +* **AI Models Reads As Connections And Roles** + * The AI Models group conflated two different things: a place models live, and the job a model does. The Model Endpoints tab is now **Connections**, and the Chat Model section is now **Chat**, separating the resource being connected to from the purpose it serves. + * (Ref: `admin_settings_nav.py`, `docs/admin/ai-models.md`) + +* **Agent Orchestration No Longer Shows A Choice That Does Not Exist** + * Agent Orchestration offered an Orchestration Type dropdown and a Max Rounds Per Agent field, but the application ships a single orchestration mode; the multi-agent modes are not built into this release. The dropdown therefore had exactly one option and Max Rounds could never be reached. + * The new interface asks the server which modes exist and shows the card only when there is a genuine choice. It will reappear on its own if multi-agent orchestration ships, without another change here. + * (Ref: `OrchestrationCard.tsx`, `/api/orchestration_types`, `get_agent_orchestration_types`) + +* **Agents & Actions Documentation Describes Behaviour Instead Of Restating Labels** + * The Agents entries in the Agents & Actions administration page mostly read "Defines behavior for the related admin workflow", which told an administrator nothing. They now describe what each setting does, what it depends on, and why you would change it — including that the Agents catalog page is unavailable while agents are off, and why promoting an agent exists at all. + * (Ref: `docs/admin/agents-actions.md`) + +#### Documentation + +* **Security And Cosmos Maintenance Documentation Rewritten** + * The Security page's section bodies were placeholder text, and several setting descriptions were wrong: the Key Vault name, lead days and scan interval were all described as "the secret credential used when the selected authentication mode requires one". Every section now explains what the capability does and why it matters, and the common tasks and troubleshooting tables cover the failures these settings actually produce. + * (Ref: `docs/admin/security.md`, `docs/admin/scale.md`) + +### **(v0.261.058)** + +#### New Features + +* **Generated Images Can Be Changed Where They Are, Instead Of Regenerated** + * A generated image used to be final. Changing one meant asking again, which cost another paid generation and added another image to the thread — so refining an image a few times left the conversation full of near-duplicates with no way to tell which was current. + * Every generated image now has an **Edit** button, in the thread, in the full-size viewer, and on an approved image proposal card. The editor opens with four tabs: **Ask AI** for describing a change, **Prompt** for the wording that produced the image, **Controls** for shape, quality and background, and **History** for every version it has had. + * **You can point at the part you want changed.** Select a region with a box or a freehand brush and the change is applied there, leaving the rest of the image alone. The editor reports how much of the image is selected, and says plainly that the selection guides the model rather than fixing every pixel outside it. + * **Masking is not mouse-only.** A nine-region grid selects the same areas from the keyboard, producing exactly the shapes a drag would. + * **Nothing is ever deleted.** History shows every version as a thumbnail with who made it and why, holding **compare** reveals the previous one, and restoring an older version moves a pointer rather than discarding newer ones. The image the model originally produced is always kept. + * **Each change builds on the version you are looking at**, so successive edits accumulate instead of each one starting from the original. + * **If your image model cannot edit, the editor says so.** Region editing needs a `gpt-image` deployment; DALL·E 3 has no editing capability at all. On a model that cannot, the selection tools are hidden and the panel names the model or the API version setting responsible, rather than failing after you have selected a region and waited. + * Works in shared conversations as well as personal ones. Any participant can change an image, each change is attributed, and the edit is written through to the underlying image so the owner and exports see the same version; the other participants see it change as it happens. + * The classic interface shows whichever version is current, so a conversation read in either place shows the same image. + * (Ref: `functions_message_image_revisions.py`, `functions_image_edit.py`, `ImageEditor.tsx`, `ImageMaskCanvas.tsx`, `/api/message//image-revision`, `/api/collaboration/conversations//messages//image-revision`, [V2 Inline Image Editing](features/V2_INLINE_IMAGE_EDITING.md)) + +#### Bug Fixes + +* **Images In Shared Conversations Now Display In The New Interface** + * An image in a shared conversation is served from a different address than one in a personal conversation, and the new interface only recognised the personal form. A shared image therefore rendered as "Image unavailable" rather than as a picture. + * (Ref: `images.ts`, `resolveImageSource`, collaboration image URLs) + +### **(v0.261.057)** + +#### New Features + +* **Generated Files Are Reachable In The New Interface** + * Tabular analysis has always *run* in the new interface — asking a question about a spreadsheet gave a real answer, because the analysis happens on the server. What was missing was everything the analysis produced. A request for an export created the file, said it had created it, and then offered nothing to open it with. The file existed in storage the whole time; nothing in the interface led to it. + * Every file an assistant turn produces now appears as a card beneath the reply, with its row count, where it was saved, what it came from, a preview and a download. + * **This covers more than spreadsheets.** The same cards surface Analyze summaries, document-comparison workbooks and Deep Research ledgers, all of which were equally unreachable. + * A finished row-level export gets a compact card — its name and row count say what it is — with a **View** control that opens the full preview when you actually want it. Longer prose artifacts show their preview inline, and Analyze and comparison outputs keep theirs collapsed so they do not push the reply off screen. + * (Ref: `generatedArtifacts.ts`, `GeneratedArtifactCard.tsx`, `/api/chat_artifacts/download`, [V2 Tabular Analysis](features/V2_TABULAR_ANALYSIS.md)) + +* **Long Exports Report Their Progress And Can Be Stopped** + * A large export runs in the background over minutes, checkpointing as it goes. Previously the new interface showed none of this: the run was invisible, and there was no way to restart one the worker had dropped or stop one started by mistake. + * A running export now shows its status, a progress bar, what it is currently doing, throughput and an estimated time remaining, with **Continue** and **Cancel** where the server says those are possible. When it finishes, the progress card is replaced by the files it produced. + * (Ref: `TabularRunStatus.tsx`, `/api/tabular/generated-output/runs/`) + +* **Staged Work Shows How Far Along It Is** + * Reasoning steps were drawn as a flat list of sentences. For a tabular analysis — one workbook tool call at a time, for minutes — that is a growing wall of text that reads the same whether the run is working or stuck. + * Work that happens in countable stages now gets a progress card above the steps, naming the current stage and counting completed against total. It stays visible while the run is live without anything needing to be expanded. + * The progress bar never runs backwards, so a new stage starting is never mistaken for lost work. + * (Ref: `activityLanes.ts`, `ThoughtsList.tsx`) + +* **A Confirmation Before An Expensive Row-Level Run** + * Asking for output for every one of several thousand rows is a long, expensive run, and it is as easy to ask for by accident as on purpose. The new interface now says what it is about to do — the row count and the estimated number of batches — and offers to narrow the prompt instead. Declining leaves what you typed exactly where it was. + * Only fires when a prompt asks for row-level output *and* a file *and* names a count over the administrator's thresholds, so ordinary questions are never interrupted. + * (Ref: `tabularRunEstimate.ts`, `enable_tabular_durable_run_confirmation`) + +* **Uploaded Files Can Be Opened** + * A file attached to a conversation showed only its name. Since an attachment is usually the subject of everything said after it, that meant checking the assistant's answers against something you could not see. Clicking the name now opens it — spreadsheets as a table — with **Download original** when the original file is still in storage. + * Personal conversations only, and only where the user workspace is enabled; the name stays inert elsewhere rather than offering a control that cannot work. + * (Ref: `ChatFilePreview.tsx`, `/api/get_file_content`) + +* **Files Awaiting Approval Say So** + * A file generated in a shared conversation is withheld until the conversation's owner releases it. The card now explains that instead of showing a download button that fails, and offers **Approve** and **Deny** to whoever can make the decision. + * (Ref: `GeneratedArtifactCard.tsx`, generated file approvals) + +* **Large Tool Results Are Readable** + * A tabular tool call that matched four thousand rows returned four thousand rows into the message inspector. The result now states how many matched and shows the first few, with controls for twenty-five or all of them. + * (Ref: `agentCitationRows.ts`, `MessageInspector.tsx`) + +#### Bug Fixes + +* **Live Reasoning Steps Were All Labelled "Thinking"** + * The new interface read a step's title from a field the server does not send, so every step of a live response was labelled identically until the page was reloaded. Steps now carry the name the server gives them as they arrive. + * (Ref: `chatStore.ts`, thought stream frames) + +### **(v0.261.056)** + +#### New Features + +* **Generated Images Can Be Changed Where They Are, Instead Of Regenerated** + * A generated image used to be final. Changing one meant asking again, which cost another paid generation and added another image to the thread — so refining an image a few times left the conversation full of near-duplicates with no way to tell which was current. + * Every generated image now has an **Edit** button, in the thread, in the full-size viewer, and on an approved image proposal card. The editor opens with four tabs: **Ask AI** for describing a change, **Prompt** for the wording that produced the image, **Controls** for shape, quality and background, and **History** for every version it has had. + * **You can point at the part you want changed.** Select a region with a box or a freehand brush and the change is applied there, leaving the rest of the image alone. The editor reports how much of the image is selected, and says plainly that the selection guides the model rather than fixing every pixel outside it. + * **Masking is not mouse-only.** A nine-region grid selects the same areas from the keyboard, producing exactly the shapes a drag would. + * **Nothing is ever deleted.** History shows every version as a thumbnail with who made it and why, holding **compare** reveals the previous one, and restoring an older version moves a pointer rather than discarding newer ones. The image the model originally produced is always kept. + * **Each change builds on the version you are looking at**, so successive edits accumulate instead of each one starting from the original. + * **If your image model cannot edit, the editor says so.** Region editing needs a `gpt-image` deployment; DALL·E 3 has no editing capability at all. On a model that cannot, the selection tools are hidden and the panel names the model or the API version setting responsible, rather than failing after you have selected a region and waited. + * Works in shared conversations as well as personal ones. Any participant can change an image, each change is attributed, and the edit is written through to the underlying image so the owner and exports see the same version; the other participants see it change as it happens. + * The classic interface shows whichever version is current, so a conversation read in either place shows the same image. + * (Ref: `functions_message_image_revisions.py`, `functions_image_edit.py`, `ImageEditor.tsx`, `ImageMaskCanvas.tsx`, `/api/message//image-revision`, `/api/collaboration/conversations//messages//image-revision`, [V2 Inline Image Editing](features/V2_INLINE_IMAGE_EDITING.md)) + +* **Act On Several Conversations At Once In The Sidebar** + * Hovering a conversation in the chat sidebar now reveals a checkbox at its left edge. Ticking it starts a selection and a compact bar appears above the list offering **Pin**, **Hide**, **Export** and **Delete** for everything selected. Previously the only thing a selection could do was export, and pinning, hiding or deleting several conversations meant opening the same menu once per row. + * **Ctrl-click adds a conversation and Shift-click selects a run of them**, matching the workspace documents explorer — the two lists now share one implementation of those rules, so they cannot drift apart. + * **A plain click still opens a conversation.** Modifier keys select; an unmodified click opens and drops the selection. Once anything is selected every row shows its checkbox, so the state is visible before a click discards it. + * **The permanent "Select" button is gone**, so the sidebar is a line shorter at rest than it was before this feature was added. Checkboxes appear on touch devices permanently, since there is no hover to reveal them with. + * **Pin adapts to what is selected**: it offers Unpin only when everything selected is already pinned, and otherwise pins. Hiding stays one-way, matching the single-row action. + * **Shared conversations are handled correctly rather than skipped.** The bulk endpoints only cover personal conversations, so a mixed selection is split — and removing a shared conversation is a deletion for its owner and a departure for everybody else, chosen per conversation rather than assumed. + * `Escape` clears the selection, and `Ctrl+A` inside the sidebar selects everything loaded. + * (Ref: `ConversationRail.tsx`, `listSelection.ts`, `conversationSelection.ts`, `POST /api/delete_multiple_conversations`, `POST /api/conversations/bulk-pin`, `POST /api/conversations/bulk-hide`, [V2 Conversation Multi-Select](features/V2_CONVERSATION_MULTI_SELECT.md)) + +#### Bug Fixes + +* **Images In Shared Conversations Now Display In The New Interface** + * An image in a shared conversation is served from a different address than one in a personal conversation, and the new interface only recognised the personal form. A shared image therefore rendered as "Image unavailable" rather than as a picture. + * (Ref: `images.ts`, `resolveImageSource`, collaboration image URLs) + +* **Deleting A Conversation Now Asks First** + * Deleting a conversation from the sidebar used to happen on a single click, with no confirmation and no way back — one mis-click on a menu destroyed a conversation and every message in it. + * Both the single-row delete and the new bulk delete now confirm, and the confirmation says what will really happen: a shared conversation you can only step out of is described as being left rather than deleted, and a mixed selection reports the two counts separately. + * (Ref: conversation deletion, `ConfirmDialog.tsx`, `ConversationRail.tsx`) + +* **A Sidebar Selection Can No Longer Outlive The Rows It Named** + * Searching or paging the conversation list left a stale selection behind, so a bulk action taken afterwards could apply to conversations that were no longer on screen. + * The selection is now pruned whenever the list reloads, whenever a conversation is removed or hidden, and when a shared conversation is taken away by its owner while you have it selected. Every bulk action also resolves its selection against the visible list before sending anything. + * (Ref: conversation list paging, selection pruning, `chatStore.ts`) + +* **Leaving A Shared Conversation Could Delete It For Everybody** + * The sidebar decided whether removing a shared conversation meant *leaving* it or *deleting* it from two copies of your permissions that were refreshed independently: opening a conversation and role-change events updated one, while the sidebar row kept the value it was loaded with. + * If you were promoted to owner while the conversation was open, the menu still offered **Leave** — but confirming posted a delete, destroying the conversation and all of its messages for every participant. A demoted owner hit the reverse, being told they had deleted something that still existed. + * Both copies are now kept in step, and the decision is made once and carried through to the request, so the wording you confirm is always the action performed. + * (Ref: shared conversation removal, `can_delete_conversation`, `conversationSelection.ts`, `chatStore.ts`) + +### **(v0.261.055)** + +#### User Interface Enhancements + +* **Admin Settings Has Moved Into The Account Menu In The V2 Interface** + * In the V2 interface, an administrator's left-hand menu listed **Admin Settings** alongside Chats, Agents and the workspaces, mixing the places you work with the place you administer. + * It now lives in the account menu at the bottom of the rail, under your name — the same place the classic interface keeps **App Settings**. The primary list is once again only the places you work. + * **Settings** in that menu is now **User Settings**, so the two entries a line apart say which is which, and the page it opens is titled to match. + * (Ref: V2 interface, left-hand menu, account menu, Admin Settings, personal settings) + +* **Your Profile Picture Now Appears In The V2 Interface** + * Your Microsoft 365 profile photo is already retrieved and stored when you first sign in, and the classic interface has always shown it. The V2 interface drew your initials instead. + * It now appears on the account control at the bottom of the left-hand menu and in the header of your **User Settings** page. Initials are still used when you have no photo. + * (Ref: V2 interface, account menu, User Settings, profile photo) + +#### Bug Fixes + +* **Custom Pages And External Links Now Collapse And Stay That Way In The V2 Interface** + * The **Custom Pages** and **External Links** groups in the V2 left-hand menu only became collapsible once a group held three or more links, and even then reopened on the next page load. A group put away never stayed away. + * Every group now collapses from its heading, whatever the link count, and the choice is remembered against your account. It is the same preference the classic interface uses, so a group you put away in one interface stays away in the other. + * Because every group in the V2 menu is now collapsible, **Force Menu Display** in Admin Settings no longer changes it. The setting continues to work in the classic interface. + * (Ref: V2 interface, left-hand menu, Custom Pages, External Links, `sidebarMenuState`, [V2 Sidebar Navigation And Account Menu](fixes/V2_SIDEBAR_NAV_AND_ACCOUNT_MENU_FIX.md)) + +* **The Account Menu Could Not Be Opened From The Collapsed V2 Menu** + * With the V2 left-hand menu collapsed to icons, clicking your account picture did nothing at all. Your settings, the way back to the classic interface and **Sign out** could only be reached by expanding the menu first, and nothing said so. + * The menu now opens beside the icon strip and names the account it belongs to. It also closes when you press Escape or click elsewhere, which it previously did not. + * (Ref: V2 interface, left-hand menu, collapsed rail, account menu) + +### **(v0.261.054)** + +#### Bug Fixes + +* **Image Generation No Longer Goes Quiet When You Look Away** + * Approving inline image proposals and then opening another conversation used to leave the cards looking untouched when you came back — no progress, **Approve** enabled again, **Approve all** back — even though those images were still being generated and did arrive a moment later. Pressing **Approve** again would have paid for the same image twice. + * **Approvals now keep reporting themselves wherever you go.** Leave the conversation, visit My Workspace, come back an hour later: the cards still show their queue position and "Generating image…", because that state no longer belongs to the view you left. + * **Reloading the page no longer loses track of them either.** Generation runs on the server and finishes whether or not your browser is still connected, so the only thing a reload ever really lost was the knowledge that it was coming. Restored cards say "Still generating from before the page reloaded…" and fill in as each image lands. + * **You can tell from anywhere in the app.** A conversation still generating images shows a spinner and a count on its row in the conversation list, and a single notice reports images being generated whose cards are not on screen — whether you have moved to another conversation or left chat entirely. The notice is deliberately absent while you are looking at those cards, where it would only repeat what they already say. + * **An approval that never finishes is written off rather than left spinning.** After ten minutes the card explains that the page reloaded while it was generating and offers to approve it again. + * This is per browser tab. An image approved in one tab is not tracked in another, though the image itself is stored and appears there as soon as the conversation is read. + * (Ref: `imageProposalStore.ts`, `imageProposalTracking.ts`, `imageProposalResume.ts`, `GET /api/chat/image-proposals/status/`, [V2 Inline Image Proposal Resume](fixes/V2_INLINE_IMAGE_PROPOSAL_RESUME_FIX.md)) + +{% raw %} +### **(v0.261.053)** + +#### New Features + +* **Prompts In My Workspace Is Now A Workbench** + * Editing a prompt used to open a form **above the list**. Clicking Edit on a prompt part-way down the page moved the editor to the top, out of view, with nothing tying it to the row you clicked — so the first thing you did after pressing Edit was scroll to find the editor. The layout now matches the documents explorer: the list on the left, the prompt rendered on the right, and writing done in a dialog. + * **You can read a prompt without editing it.** The details pane shows the full body rendered as markdown, with its variables, its description and its actions. Previously the only way to see past the first 140 characters was to open an editor you then had to cancel out of. + * **The editor shows the source and the rendering side by side**, with a small formatting toolbar, becoming tabs on a narrow screen. Closing it with unsaved changes asks first. + * **Prompts now have a description and can be favourited.** Favourites sort to the top of the list and of the composer's `/` menu. + * Duplicate, copy and "Use in chat" are available from the details pane, and search now covers the body of a prompt as well as its name — so you can find the one that mentions a particular system without remembering what you called it. + * (Ref: V2 My Workspace, Prompts, `PromptWorkbench.tsx`, `GET/POST/PATCH /api/prompts`, [V2 Prompts Workbench](features/V2_PROMPTS_WORKBENCH.md)) + +* **Prompts Can Ask You To Fill In The Blanks** + * Write `{{customer}}` or `{{region}}` in a prompt and you are asked for those values when you use it, instead of editing the same three words every time. A default can be written as `{{tone|formal}}`. + * **Values you entered last time are offered back**, remembered per prompt so "name" in one prompt never pre-fills "name" in another. Anything filled in for you is badged as reused and cleared with one click, and the dialog always appears rather than inserting silently. + * **Eight values the app already knows are filled in for you**: today's date, the current time, your name, the conversation title, the documents in scope, the last reply, your last message, and what you have already typed. + * **Values from the conversation are one click each, never automatic.** The last reply can be quoting an uploaded document, and document text becoming part of your next instruction is how prompt injection gets a foothold — so you choose which field gets it. + * **Nothing is pre-filled in a shared conversation**, where a value remembered from a private chat would become visible to everyone the moment you send. + * **The values stay in your browser and are never sent to the server.** Anything matching an obvious key or token shape is not stored at all, and each prompt offers a "Forget saved values" control. + * A prompt that documents a templating language is safe: `{{ }}` inside a code block is left alone, and `\{{` escapes a single occurrence. + * (Ref: `promptVariables.ts`, `promptVariableMemory.ts`, `PromptVariablesDialog.tsx`) + +* **Saving A Prompt From A Conversation** + * Any message now offers **Save as prompt** in its overflow menu, and the composer offers the same for wording you have drafted but not sent. Both open the editor prefilled with a suggested name, so you can turn the specifics into `{{variables}}` before saving. + * A prompt saved this way is selectable in the composer immediately, without a reload. + * (Ref: `MessageActions.tsx`, `Composer.tsx`, `bootstrapStore.upsertPromptInCatalog`) + +* **A `/` Shortcut For Prompts In The Composer** + * Type `/` at the start of a word to search your prompts and insert one without reaching for the picker. Favourites come first, and matching covers the name, description and workspace. + * A slash in the middle of a word is left alone, so `and/or` and `https://` do not trigger it. + * (Ref: `promptSlash.ts`, `PromptSlashMenu.tsx`) + +#### Bug Fixes + +* **Picking A Prompt No Longer Discards What You Had Typed** + * Choosing a prompt from the composer's picker replaced the entire message box, so reaching for a prompt part-way through writing threw away the half-sentence you reached for it from. Prompts are now inserted at the cursor, or over the selection, with sensible spacing. + * (Ref: composer prompt picker, `insertPromptText`) + +* **"Use As Prompt" Renamed To "Copy To Composer"** + * The message menu entry called "Use as prompt" only ever copied the message text into the composer — it never saved a prompt. It now says what it does, and a real **Save as prompt** sits beside it. + * (Ref: `MessageActions.tsx`) + +* **Prompt Search Now Actually Reaches The Server** + * The workspace sent its prompt search as `search_term` while the API reads `search`, so server-side search had never once run and the list was filtered in the browser over the first page of results only. + * (Ref: `fetchPrompts`, `list_prompts`) + +* **One Validator For Personal, Group And Public Prompts** + * The three prompt APIs each carried their own copy of the same field checks. Adding a field to one and not the others produces a value that saves on a personal prompt and is silently dropped on a group one, which reads as a save that did not work. All three now share a single validator, and creates and updates return the same fields. + * (Ref: `build_prompt_updates`, `serialize_prompt_summary`, `functions_prompts.py`) +{% endraw %} + +### **(v0.261.052)** + +#### User Interface Enhancements + +* **The Logo Is The Way Home In The New Interface** + * Clicking the logo or the application title at the top of the left rail now opens the home page. Previously it was not a link at all, and a separate **Home** row sat directly underneath it — two controls for one destination, only one of which worked. + * That **Home** row has been removed. The rail is one item shorter, and the place people already click is the place that works. + * The link is announced to screen readers as the application name followed by "home", so it is still identifiable when the rail is collapsed to just a logo or a single letter. + * (Ref: V2 navigation rail, brand mark, [V2 Brand Mark Home Link Fix](fixes/V2_BRAND_MARK_HOME_LINK_FIX.md)) + +#### Bug Fixes + +* **The Application Title No Longer Appears Twice In The Left Rail** + * With no custom logo uploaded, the new interface showed a coloured square holding the first letter of the application title *and* the full title next to it — "**S** SimpleChat". + * The lettered square is a stand-in for a logo, so it now appears only where the title itself cannot: when the left rail is collapsed, or when **Hide Application Title** is turned on. An expanded rail shows the title on its own. + * Deployments with a custom logo are unaffected. + * (Ref: V2 navigation rail, Appearance > Branding, [V2 Brand Mark Home Link Fix](fixes/V2_BRAND_MARK_HOME_LINK_FIX.md)) + +### **(v0.261.051)** + +#### Bug Fixes + +* **Clicking Away Just After Starting A New Chat No Longer Bounces You Back** + * The first message in a brand-new chat has to create the conversation before it can answer, and clicking a different conversation during that moment used to undo the click and drop you back into the chat you had just left. + * In the unluckier version of the same timing, the conversation you clicked would have its messages shown underneath the new chat, so a thread appeared to contain a question that was asked somewhere else. + * Your click now wins. The message is still sent and its answer still generated and saved — the new chat appears in the list with its title and unread marker — but the interface stays where you put it. + * Nothing was ever lost or filed against the wrong conversation; the effect was limited to what was on screen. + * (Ref: `chatStore.ts` `sendMessage`, `runChatStream`, [V2 New Conversation Send Race](fixes/V2_NEW_CONVERSATION_SEND_RACE_FIX.md)) + +### **(v0.261.050)** + +#### Bug Fixes + +* **Leaving A Chat No Longer Destroys The Answer Being Written** + * In the new interface, sending a message and then opening a different conversation before the reply finished ended the reply. Coming back showed the question with no answer, and nothing to reconnect to. + * It looked intermittent because the timing changed the symptom. Leaving during the "thinking" phase, before the assistant had produced its first word, saved no reply at all. Leaving later left a truncated one. + * The cause was that switching conversations, and starting a new chat, asked the server to *cancel* the generation rather than simply stopping reading it. Those are different things, and only the Stop button should mean the second one. Answers are generated in the background and deliberately outlive the connection carrying them, so dropping the connection was always safe; cancelling was not. + * Leaving a conversation now detaches. The answer finishes and is saved, reopening the thread picks the reply back up mid-flow or shows it complete, and several conversations can be generating at once — matching how the classic interface has always behaved. + * Stop is unchanged and still cancels. Leaving a shared conversation also no longer cancels a reply other participants are waiting for. + * (Ref: `chatStore.ts` `detachActiveStream`, `selectConversation`, `startNewConversation`, `stopStreaming`, `/api/chat/stream/cancel`, [V2 Stream Cancelled On Conversation Leave](fixes/V2_STREAM_CANCELLED_ON_CONVERSATION_LEAVE_FIX.md)) + +### **(v0.261.049)** + +#### New Features + +* **Diagrams Can Be Edited Where They Are, Instead Of Regenerated** + * A Mermaid diagram in a reply used to be final. Changing one meant asking again in the thread, which produced another message with another diagram — so refining a diagram a few times left the conversation full of near-duplicates, all of them still being sent to the model. + * Every rendered diagram now has an **Edit** button. The editor opens with a live preview and four tabs: **Source** for editing the Mermaid directly, **Layout** for flow direction and spacing, **Ask AI** for describing a change in words, and **History** for every version it has had. + * **Ask AI is a conversation with the diagram, not with the thread.** The model is given the diagram's current source, that diagram's own earlier instructions, and the request the diagram originally came from — not the conversation. Its replies never appear in the thread. + * **Only the version on your screen is used as context.** The revision history and the editing conversation behind it are stored with the diagram and are never sent to the model, so refining a diagram ten times costs the same context as generating it once. + * **Nothing is ever deleted.** History records who changed what and why, restoring an older version moves a pointer rather than discarding newer ones, and the version the model originally produced is always kept. + * **Boxes still cannot be dragged**, and the interface says so rather than pretending otherwise. Mermaid computes node positions and has no syntax for placing one, so what the Layout tab offers instead is direction, spacing, and — through the source editor — grouping and reordering. + * Works in shared conversations as well as personal ones. Any participant can edit a diagram, matching who can already recolour one, and each change is attributed. An edit is written through to the underlying message, so the AI, exports and the conversation's owner all see the version you edited; the other participants see it change as it happens. + * The classic interface shows whichever version is current, so a conversation read in either place shows the same diagram, and exports carry the current version too. + * (Ref: `functions_message_block_revisions.py`, `functions_block_revision_assist.py`, `DiagramEditor.tsx`, `/api/message//block-revision`, `/api/collaboration/conversations//messages//block-revision`, [V2 Inline Diagram Editing](features/V2_INLINE_DIAGRAM_EDITING.md)) + +### **(v0.261.048)** + +#### New Features + +* **Documents In My Workspace Is Now A File Explorer** + * The documents page in the new interface was one flat list that could upload a file, delete a file, and filter by one tag. It is now laid out like a file manager: a command bar across the top, a navigation rail down the left, the list in the middle, a details pane on the right and a status bar along the bottom. + * **Everything the server could already do is now reachable.** The page previously asked for a thousand documents at once and sorted and searched them in the browser, ignoring the paging, searching, sorting, filtering, downloading, metadata editing, re-extraction and sharing the API has supported all along. All of it is now used. + * **Tags live in the left rail instead of behind a view mode.** The classic workspace shows tags as folder cards you switch into a separate mode to see; here they are always visible, with their colour and a live count, and clicking one filters the list. Ctrl-clicking a second tag narrows to documents carrying both. + * **Drag documents onto a tag to file them.** The confirmation offers an **Undo**, so tagging a batch never needs a dialog first. + * **Saved views** pin a combination of search, tags, classification and status to the rail under your own name for it — the closest thing to a folder that a workspace organised by tags can honestly offer. + * **Multi-select works the way it does everywhere else.** There is no longer a mode to switch on before checkboxes appear: click, Ctrl-click, Shift-click, Ctrl+A, Escape, and the arrow keys all behave as expected, and the toolbar's Download, Tag, Chat, Extract and Delete act on whatever is selected. + * **Titles lead, file names follow.** A file called `MSA_v2_FINAL(3).docx` now shows its extracted title on the first line with the file name beneath it, and its tags as coloured chips in their own column rather than run together into one line of text. + * **The details pane** shows a document's tags, classification, size, pages, version, dates, authors, keywords, abstract and sharing, and lets you edit them. With several documents selected it reports how many, their combined size and the tags they have in common, so a bulk action states what it is about to touch. + * **Two views instead of four.** Details (a sortable table) and Tiles (cards). The two folder-shaped modes are gone, replaced by the rail. + * **Active filters are visible.** Whatever is narrowing the list appears as removable chips above it, with a Clear all — previously there was no way to tell why documents seemed to be missing. + * (Ref: V2 My Workspace, Documents, `GET /api/documents`, `GET /api/documents/facets`, `POST /api/documents/bulk-delete`, `POST /api/documents/bulk-tag`) + +* **A Tags Section In My Workspace** + * Tags now have a home of their own under Knowledge, where they can be created, renamed, recoloured, merged and deleted, with a count of how many documents use each one. + * **Renaming a tag onto one that already exists merges the two**, and the page says so before you commit to it. + * Deleting a tag says how many documents it will be removed from, and never deletes the documents themselves. + * Applying tags stays on the Documents page. Choosing what the tags *are* and browsing *by* them are different jobs, and the classic workspace does both from the same toolbar. + * (Ref: V2 My Workspace, Tags, `PATCH /api/documents/tags/`, tag colours) + +* **Deleting Several Documents At Once** + * Removing a selection was previously one request per document. It is now a single request that reports on each document individually. + * **A document the server refuses to delete is named, with the reason.** Files uploaded through chat and files managed by file sync are protected because deleting them silently damages something else; those are now listed by name with an option to go ahead anyway, instead of one protected file blocking everything selected with it. + * (Ref: `POST /api/documents/bulk-delete`, conversation-linked documents, file sync) + +#### Bug Fixes + +* **Typing In The Documents Search Box Dropped Characters** + * The search box searched on every keystroke and lost letters if you typed at anything like normal speed, so a search term had to be entered slowly and checked. + * The box was displaying the *searched* term rather than what you had typed, so each keystroke was undone until the search caught up. It now shows what you type, waits for you to stop before searching, and searches immediately if you press **Enter**. **Escape** clears it. + * (Ref: V2 My Workspace, Documents, search) + +* **Tagging Several Documents Appeared To Hang** + * Applying tags to a multi-document selection showed a spinner labelled "Working…" that never went away and never refreshed the list. The tags had actually been applied — a page refresh showed them — but nothing on screen said so. + * Tagging a document is expensive on the server: it rewrites the document and every one of its search index entries. Sending one request for the whole selection meant a single request that ran for a very long time behind a spinner that could not distinguish slow from stuck. + * Tagging and deleting are now sent in small batches with **a real progress bar** showing how many documents of the total are done, and the list refreshes when it finishes. A batch that fails now reports itself instead of leaving the progress indicator up indefinitely. + * (Ref: V2 My Workspace, Documents, bulk tagging, bulk delete, progress reporting) + +* **Sorting Documents By Size Or Page Count Would Have Crashed The Listing** + * The document sort compared a missing value against a present one as different types, which raises an error in Python. It was harmless while documents could only be sorted by name, title and date, and would have failed the moment any numeric column became sortable. + * Sort fields are now declared as numeric or textual, so a document that has never had a size, a page count or a version still sorts correctly alongside ones that do. + * (Ref: `sort_documents`, `ALLOWED_DOCUMENT_SORT_FIELDS`, personal, group and public document lists) + +* **The "Shared With Me" Filter Never Did Anything** + * The classic workspace sends a `shared_only` filter that the document listing has never read, so ticking it changed nothing. The listing now supports filtering by document state — recent, shared with you, processing, needs attention, or untagged — and the new interface's rail uses it. + * (Ref: `GET /api/documents`, `place` parameter) + +#### User Interface Enhancements + +* **Re-Extraction Now Says Which Mode A Document Is Already On** + * The details pane offered **Standard** and **Enhanced** as two identical buttons without indicating which one had been used, so the only way to change a document's extraction was to guess and then check. + * It now states the current mode, marks that option as **(current)**, and presents the other as **Switch to Standard** or **Switch to Enhanced**. A selection spanning both reads **Mixed**; one where no mode was ever recorded reads **Not recorded**. + * The choice is hidden for files it does not apply to — only PDFs and images are processed this way — and in a mixed selection those files are named and left alone rather than silently included. + * **Enhanced** is disabled with an explanation when your administrator has not enabled enhanced extraction, instead of being offered and then refused. + * (Ref: V2 My Workspace, Documents, details pane, `enable_enhanced_extraction`) + +* **The My Workspace Section Menu Can Be Collapsed** + * The list of workspace sections down the left can now be collapsed to icons, giving the documents explorer noticeably more room. The choice is remembered. + * It collapses independently of the main application sidebar, so you can narrow either one without affecting the other. Collapsed entries keep their names as tooltips and for screen readers. + * (Ref: V2 My Workspace, section navigation) diff --git a/docs/explanation/release-notes/v0.261-part-6.md b/docs/explanation/release-notes/v0.261-part-6.md new file mode 100644 index 000000000..45e2620c6 --- /dev/null +++ b/docs/explanation/release-notes/v0.261-part-6.md @@ -0,0 +1,945 @@ +--- +title: "Release notes 0.261.047 – 0.261.008" +description: "SimpleChat release notes for 0.261.047 – 0.261.008." +section: "Reference" +layout: page +--- + + + +# Release notes 0.261.047 – 0.261.008 + +[Back to release notes index]({{ '/explanation/release_notes/' | relative_url }}) + +### **(v0.261.047)** + +#### New Features + +* **A Home Page In The V2 Interface** + * The new interface opened straight into chat and had no landing page, which meant three Appearance settings — **Landing Page Text**, **Markdown Alignment** and **Main Page Logo Size** — configured a page that only existed in the classic interface. Editing them appeared to do nothing. + * `/v2` now opens a home page carrying your logo, your landing copy and a **Start chatting** button, and **Home** has been added to the left-hand menu. The logo is sized by **Main Page Logo Size** exactly as the classic home page sizes it. + * Clearing the landing copy leaves the home page without it, rather than restoring the default wording you deleted. + * (Ref: V2 interface, home page, `landing_page_text`, `landing_page_alignment`, `landing_page_logo_scale_percent`) + +* **Custom Pages And External Links In The V2 Left-Hand Menu** + * Both are configured under **Appearance > Pages & Links** and neither appeared in the new interface at all, so a deployment that had set them up saw a menu with none of its own links in it. + * They now appear, following the same rule as the classic navigation: one or two entries sit inline as ordinary menu items, and three or more — or any number when **Force Menu Display** is on — collapse into a group under the menu name you configured, with a count. + * External links open in a new tab so they cannot take an in-progress conversation with them. Custom pages honour their own "open in new tab" setting, and are still filtered per page against your roles. + * (Ref: V2 interface, left-hand menu, Custom Pages, External Links, `/api/v2/bootstrap`) + +#### Bug Fixes + +* **Custom Favicon Was Never Shown In The V2 Interface** + * An uploaded favicon replaced the browser tab icon in the classic interface but never in the new one, which also always showed "SimpleChat" as the tab title regardless of the configured application title. + * The favicon file keeps the same name every time it is replaced, so the address it is served from has to change or the browser keeps showing the copy it already has. The classic interface has always done this; the new interface is compiled ahead of time and could not know your branding. The page it serves is now stamped with the current favicon and application title on the way out. + * (Ref: V2 interface, favicon, application title) + +* **Custom Logo Was Squashed In The V2 Left-Hand Menu** + * The logo was drawn into a fixed square, so any logo that is not square — which is most of them — was compressed. It is now sized by height with its own proportions kept, as in the classic navigation. + * (Ref: V2 interface, left-hand menu, custom logo) + +* **Unsafe External Link URLs Are No Longer Rendered In The V2 Left-Hand Menu** + * The new interface validates external link addresses when you save them, but the classic Admin Settings form does not, so a link saved through the classic form — or already stored from an earlier version — could carry an address that is not a web page. + * Addresses that are neither a local path nor `http`/`https` are now removed before the menu is built, so a link that the save path would have rejected can no longer reach the menu by another route. + * (Ref: V2 interface, left-hand menu, External Links, `EXTERNAL_LINK_ALLOWED_SCHEMES`) + +* **Four Settings Appeared Under Appearance Instead Of Their Own Tab** + * **Enable Personal Workspaces** appeared under **Appearance > Notices & Agreements > User Agreement**, and the two external health check endpoints and **Show Simple Chat Documentation Guide Links** appeared under **Appearance > Pages & Links > External Links**. A fifth, **Enable Text Action**, appeared under **Appearance > Branding**. + * Settings that had not yet been described to the new admin page were placed by matching their name against the section names, and these five matched the wrong section — "external" matched External Links rather than Health Check, and "user" matched User Agreement rather than Personal Workspaces. + * They are now described properly and appear where they belong: the health check endpoints under **Operations > Logging & Health > Health Check**, the documentation guide links under **Help > User-Facing Latest Features**, personal workspaces under **Workspaces > Workspace Types**, and the text action under **Agents & Actions > Actions**. Each also gained the explanatory text the classic page has always shown. + * (Ref: V2 Admin Settings, `admin_settings_fields.py`, capability placement) + +### **(v0.261.046)** + +#### Bug Fixes + +* **Admin Settings Changes Did Not Appear Until The Page Was Reloaded (New Interface)** + * Enabling the classification banner in the new interface and saving appeared to do nothing. The banner was saved correctly, but it only showed up after reloading the browser. + * The banner was not the only thing affected. The new interface reads the application title, the sidebar logo, the classification banner and the list of enabled capabilities once, when the page first loads, and had no way of being told that any of them had changed. Every one of those went stale the moment you saved. + * Saving in **Admin Settings** now refreshes what the interface knows about itself, so the banner appears — or disappears — straight away, along with a changed title or a newly enabled capability. Uploading a logo or favicon updates the sidebar immediately too, rather than waiting for the next reload. + * Your unsaved edits are unaffected: the refresh happens quietly in the background and never interrupts the page you are working on. + * (Ref: V2 interface, Admin Settings, classification banner, branding, `/api/v2/bootstrap`) + +### **(v0.261.045)** + +#### Bug Fixes + +* **Approving Several Images At Once Looked Like It Had Stopped** + * In the new interface, when a reply proposed several images and you pressed **Approve all**, every card correctly greyed out its Approve button and showed **Generating image…**. The moment the first image appeared, the remaining cards lost their status, their Approve buttons came back, and the **Approve all** button reappeared — so it looked as though nothing was happening and you needed to approve again. The images were in fact still being generated and did arrive shortly after. + * The cards are now told what is happening by the message they belong to rather than keeping it to themselves, so a card that is queued or generating keeps saying so until its image arrives, whatever else happens in the conversation. Editing a proposal's prompt, or dismissing a proposal, now survives a refresh of the reply too. + * The underlying cause was that the whole message was being rebuilt from scratch every time anything about it changed. Diagrams and charts were being thrown away and redrawn for the same reason, and no longer are. + * (Ref: V2 chat, inline image proposals, approve all, assistant markdown rendering) + +* **Generated Image Cards Kept Repeating The Proposal's Description** + * After an image had been generated, its card still showed the labels from the original proposal — the kind of visual, the slide it referred to, and the context it was drawn from. Those describe an image that does not exist yet, and say nothing once you can see it. + * A generated card now shows the title, the image, and which model produced it. Cards still waiting for a decision are unchanged. + * (Ref: V2 chat, inline image proposals, generated image card) + +### **(v0.261.044)** + +#### Bug Fixes + +* **The New Interface's New Chat Button Now Works, And Buttons Look Clickable** + * **New chat** appeared in the sidebar on every page but only ever did anything on the chat page. Clicking it from **My Workspace**, **Admin Settings** or **Settings** produced no visible response at all — it reset chat state that was not on screen and left you where you were. It is now shown only on the chat page, where it has something to act on. + * **Chats** in the sidebar covers what that leaves behind. Clicking it from anywhere else now starts a fresh chat, which is what makes a new chat reachable from the rest of the application. Two exceptions: clicking **Chats** while already on the chat page still does nothing, so a stray click cannot discard what you are reading, and a conversation still streaming a reply is returned to rather than reset, so a response in progress is never thrown away. + * Previously, leaving the chat page and coming back silently reopened whatever conversation you last had. That conversation is still one click away in the conversation list. + * No button in the new interface showed a hand cursor — not **New chat**, not the chat header icons, the conversation list, the composer or the admin controls. Everything rendered with the arrow cursor browsers use for text you cannot click. The whole interface now indicates what is clickable. + * Starting a new chat no longer leaves the conversation drawer stranded open and empty after its **Contents** and **Documents** buttons have disappeared, and the conversation details panel no longer outlives the conversation it was describing. + * (Ref: V2 navigation rail, New chat, conversation drawer, button cursor) + +### **(v0.261.043)** + +#### Bug Fixes + +* **Chart And Diagram Colours Could Not Be Saved In A Shared Conversation** + * Picking a colour for a chart or a diagram in a shared conversation changed it on screen and then reported "That change could not be saved", with a 404 in the browser console. Only shared conversations were affected; personal ones saved as normal. + * Shared conversations are stored separately from personal ones, and the endpoint that saves a block's colours could only find personal conversations, so it refused the write before it ever reached the message. Colours and sizes now save in shared conversations too. + * Because a shared conversation is shared, the colours you choose are stored on the message and are what everybody in the conversation sees — the same way masking already works — and they change on the other participants' screens as you make them rather than on their next visit. Changing them needs the same access as posting a message, so a read-only viewer sees the colours others chose without being able to alter them for everybody. + * (Ref: V2 chat, shared conversations, diagram and chart colours, `/api/collaboration/conversations/.../visual-style`) + +* **Opening A Link To A Shared Conversation Logged A Console Error** + * Following a link to a shared conversation worked, but always left a 404 in the browser console. The interface was working out whether the link pointed at a personal or a shared conversation by asking about it as a personal one first and reading the failure as its answer. + * It now asks once and gets a straight answer, so opening a shared conversation from a link is a single successful request and the console stays clean. + * (Ref: V2 chat, shared conversations, conversation deep links) + +### **(v0.261.042)** + +#### New Features + +* **My Workspace In The V2 Interface** + * The personal workspace in the new interface previously listed documents and nothing else. It now covers all eight areas the classic workspace does — documents, file sources, prompts, agents, actions, workflows, identities and endpoints. + * **The sections are grouped by what they are for**, rather than presented as eight equal tabs. **Knowledge** is what your assistant can draw on, **Automation** is what it can do, and **Connections** is the shared setup the other two reuse. Identities and endpoints are plumbing that other sections consume, and no longer sit alongside documents as though they were the same kind of thing. + * **An overview page explains how the pieces fit together.** It lists each group with a count of what is in it, one line on what each section is for, and states the relationships plainly: identities are used by file sources and actions, file sources feed documents, documents and actions and endpoints are used by agents, and agents are used by workflows. + * **Sections your administrator has not enabled are named, with the reason.** They appear on the overview greyed out rather than silently vanishing, so a capability that is switched off can no longer be mistaken for one that is broken or missing. + * **Sync is now called File sources**, which is what people go there to set up, and identities are described as saved sign-ins for other systems rather than as your own account. + * Each section is its own address — `/workspace/agents`, `/workspace/prompts` — so a link to one can be bookmarked or shared. + * Prompts support full create, edit and delete. Agents can be created, edited and deleted. Workflows can be run, cancelled and inspected. File sources can be synced on demand with their run history. Connector configuration, the workflow designer and endpoint connection details are still done in the classic workspace, and each section links to it. + * (Ref: V2 interface, My Workspace, `/api/v2/bootstrap`, `functions_workspace_sections.py`) + +* **Per-Item Editing For Personal Agents, Actions And Endpoints** + * Saving or deleting one of these used to rewrite the entire collection. Two browser tabs open on the workspace could overwrite each other's work, and any item a client did not know about was deleted along the way. + * Each now supports editing and removing a single item. The old whole-collection save still works and the classic interface is unaffected. + * Editing a model endpoint no longer risks blanking its stored credentials: only the fields that changed are sent, and the secrets — which are never given to the browser in the first place — are merged in on the server. + * (Ref: `/api/user/agents`, `/api/user/plugins`, `/api/user/model-endpoints`) + +#### Bug Fixes + +* **Deleting A Personal Agent Reported An Error After It Had Worked** + * The delete endpoint removed the agent and then answered with an error, so the caller was told the operation failed when it had already succeeded. This happened on any delete that left at least one agent behind, in any deployment without a global agent configured. + * The check that caused it now runs before the delete, so a refusal means nothing was removed. + * (Ref: personal agents, `/api/user/agents`) + +* **Deleting A Personal Agent Skipped Its Governance Check** + * Saving agents checked whether governance policy allowed it; deleting them did not, so a user who had been denied access to personal agents could still delete them. The check is now applied before the agent is removed. + * (Ref: personal agents, governance policy) + +### **(v0.261.041)** + +#### New Features + +* **Full Activity Stats In The V2 Interface** + * The **Stats** tab under Settings in the new interface now reports everything the classic profile page's stats tab reports, instead of the four small sparklines it showed before. + * **Your lifetime totals** — conversations, messages, documents and sign-ins — are shown as cards, together with when they were last worked out and when you last signed in. These are cached figures rather than live ones, and the tab now says so, so they are not mistaken for the totals for the period you are looking at. + * **Pick a period.** The last 7, 30 or 90 days, or a start and end date of your own. A range with the dates the wrong way round, or with one date missing, is now explained before the request is sent rather than failing silently. + * **Four activity charts and a storage breakdown.** Sign-ins and token usage as lines, conversations and documents as paired bars showing created against deleted and uploaded against deleted, and storage split between AI Search and blob storage. These are the same charts, shapes and colours as the classic page. + * **Export to CSV.** Choose which of summary totals, sign-ins, conversations, documents and token usage to include, and the period to cover — which can differ from the one on screen. The file matches the classic export column for column. + * **Your account** — name, email address and user id — is shown at the foot of the tab. + * (Ref: V2 settings, Stats tab, `/api/user/activity-trends`, `/api/user/settings`, vendored Chart.js) + +#### User Interface Enhancements + +* **One Destination For Personal Settings In The V2 Interface** + * The account menu in the left rail offered both **Settings** and **Profile**, the second of which left the new interface for the classic profile page. With the activity stats now rebuilt under Settings, the Profile entry led nowhere the new interface does not already cover, so it has been removed. **Settings** is the single destination. + * The "open in the classic interface" links inside the Groups and Public tabs are unchanged — those capabilities have not been rebuilt yet, and the links remain the way to reach them. + * (Ref: V2 sidebar, account menu, settings navigation) + +### **(v0.261.040)** + +#### New Features + +* **Export Conversations From The New Interface** + * The new interface could save a single reply as Word, PowerPoint or an email, but there was no way to export a whole conversation — you had to switch back to the classic interface for that. You can now export one conversation, or several at once, as **JSON**, **Markdown** or **PDF**. + * Export is offered in three places: the **⋯** menu on any conversation in the sidebar, the **Export** button in **Conversation details**, and a new **Select** mode above the conversation list for picking several at once. + * Several conversations can be combined into one file or bundled as a ZIP with one file per conversation. Picking more than one defaults to the ZIP, which is almost always what is wanted. + * Optionally, a short AI-written intro summary can be placed above each transcript, using whichever chat model you choose. + * Diagrams are included as pictures in Markdown and PDF exports, drawn in your browser so they match what you were looking at. A diagram that will not draw is left to the server rather than breaking the export. JSON exports keep the original text, diagrams included as source. + * Exported files are identical to the ones the classic interface produces, down to the filename. + * (Ref: V2 chat, conversation export, export wizard, conversation multi-select) + +#### Bug Fixes + +* **Exported And Downloaded Diagrams Were Squashed** + * A diagram saved as a PNG, or embedded in an exported file, came out compressed into a narrow strip roughly 100 pixels wide regardless of its real size — a 1094×541 flowchart was rasterized at 100×541. + * The cause was the diagram's width being read as the number `100` from the value `100%`. The real dimensions were sitting in the diagram alongside it and are now used instead. + * (Ref: diagram PNG download, conversation export, SVG rasterizing) + +### **(v0.261.039)** + +#### New Features + +* **The New Admin Settings Page Now Has The Rest Of The Appearance Settings** + * The new interface's admin page could only ever show on/off switches, because it worked out what to display by looking for settings that happened to be true or false. Everything else was invisible — so the whole Appearance group amounted to a handful of switches and a note telling you to go back to the classic page. + * Appearance is now complete. **Branding** has the application title, the home page logo size slider, and uploads for the light logo, dark logo and favicon, each showing the image you currently have. **Home Page Text** has the alignment control and the landing page editor, with a live preview that follows the alignment you pick. **Notices & Agreements** has the classification banner with both colour pickers and a preview strip, the AI notice, the full Terms of Use configuration, and the user agreement with its four apply-to options, a word counter and a **Test preview** button. **Pages & Links** has the custom pages settings, the full static page designer, the developer guide, and an external links editor you can add to, remove from and reorder. + * (Ref: V2 admin settings, Appearance group, `admin_settings_fields.py`) + +* **Changes Are Saved Together, When You Say So** + * Switches used to save the instant you clicked them, which does not translate to typing into a text box. Edits now collect in a bar at the bottom of the page that tells you how many changes are waiting, with **Save changes** and **Discard**. `Ctrl`/`Cmd`+`S` saves, and closing the tab with unsaved edits asks first. + * This also fixes something subtler: the Terms of Use and AI notice re-prompt every user whenever their wording changes. Saving on every keystroke would have re-prompted everyone once per character typed. + * (Ref: V2 admin settings, save bar) + +* **Rejected Settings Now Explain Themselves** + * Values are checked before anything is written, and a rejected save comes back with the reason attached to the control that caused it — a colour that is not valid hex, a link that is not an http or https address, a cancel redirect that would leave the site unsafely. Nothing is saved unless everything in the batch is valid, so a save can no longer land half-applied. + * The checks reuse the same code the classic page uses, so the two interfaces agree about what a valid value is. + * (Ref: V2 admin settings, settings validation) + +#### Bug Fixes + +* **Navigation Links Can No Longer Be Given An Unsafe Address** + * External navigation links saved through the new admin page are now restricted to local paths and http or https addresses. Previously any text was accepted and placed directly into the link, which allowed a `javascript:` address into the navigation bar on every page. + * (Ref: external links, navigation) + +* **Logo And Favicon Handling Now Lives In One Place** + * Image conversion was written into the classic settings page. Both admin pages now share one implementation, so a logo is stored identically no matter where it was uploaded from, and the PNG/JPEG-only restriction that keeps unexpected image formats away from the image library applies to every upload path. + * (Ref: `functions_branding_images.py`, logo and favicon uploads) + +* **Three Tests That Could Not Fail Correctly** + * The Pillow security test pinned an exact dependency version and the custom logo test looked for help text in a file it had moved out of, so both reported problems that were not real while no longer checking the thing they were written for. They now assert a minimum version and read the composed template. + * (Ref: functional tests, version assertions) + +### **(v0.261.038)** + +#### Bug Fixes + +* **Shared Conversations Opened Empty In The V2 Interface** + * Clicking a shared conversation in the new interface showed a thread with no messages in it. The interface loaded every conversation's messages from the personal chat API, which does not hold shared conversations and answers with an empty list rather than an error, so the conversation opened successfully with nothing in it — and remained the target of the next message sent. + * Shared conversations are now read from the collaboration API they actually live in, in both the personal and the group case. + * (Ref: V2 chat, `/api/get_messages`, `/api/collaboration/conversations`) + +#### New Features + +* **Shared Conversations In The V2 Interface** + * The new interface now supports shared conversations in full, matching the classic one. Both shared personal and shared group conversations can be read, replied to and managed there. + * **Messages say who wrote them.** Another participant's message appears on the left with their name above it, and your own on the right as before. Uploads shared into the conversation are shown as named attachments, and a message that asked the assistant is labelled as such. + * **The assistant only answers when asked.** As in the classic interface, a message in a shared conversation goes to the other people in it and not to the model — unless it `@`-mentions a model or an agent, or you have turned on an assistant tool such as document search, web search, image generation, deep research, reading URLs, an agent or a saved prompt. Tagging a model or agent uses that one for the message, whatever the pickers hold. + * **Type `@` to mention somebody.** The menu offers the people already in the conversation, the models and agents you can address, and — if you can manage members — people you could add. Choosing one of the last group invites them. + * **The conversation stays live.** Messages other people write appear as they are sent, along with a "typing" indicator, and deletions and masks applied by others are reflected straight away. You are told when somebody mentions you. + * **Reply to a specific message** with the reply button on any message; the reply shows what it is answering. + * **Share an existing conversation** from the new **people** button in the chat header, or **Share** in the conversation's menu in the left rail. The same panel manages who is in the conversation, promotes members to admin, removes people, and lets you leave it or delete it for everyone. What you are offered follows what you are actually allowed to do. + * **Invitations** are shown above the conversation with **Join** and **Decline**. You can read an invited conversation before joining, so you can see what you are being invited to. + * **Files the assistant generates** in a shared conversation are held back until approved, and anything waiting on your decision is listed above the thread with Approve and Deny. + * Retry, edit, attempt navigation and fork are not offered in a shared conversation, matching the classic interface — those actions have no equivalent there. + * Requires **Collaborative conversations** to be enabled in admin settings. With it off, none of these controls appear. + * (Ref: V2 chat, collaboration API, participants panel, mentions, live updates) + +#### User Interface Enhancements + +* **Mentioning Somebody No Longer Notifies The Wrong Person** + * Where one person's display name is the start of another's — "Ada" and "Ada Lovelace" — writing "@Ada Lovelace" also counted as mentioning "Ada", and notified them. The longer name now claims its own text, so only the person actually named is notified. Naming both people in one message still notifies both. + * (Ref: shared conversations, mentions) + +### **(v0.261.037)** + +#### Bug Fixes + +* **Diagrams No Longer Render Too Small To Read** + * A diagram in the new interface was drawn at roughly a quarter of its natural size — and clicking **Colors** made it suddenly readable. Both were the same bug: the message bubble sizes itself to its contents, and a diagram was telling it "I'll take whatever width you have", so the bubble shrank to the width of the diagram's own buttons. Opening the colour menu added something that *did* have a width, which is why the diagram grew. + * A diagram now sizes its own panel. Measured on the Azure hierarchy diagram from the report, it went from 300 pixels wide to 966, and opening **Colors** no longer changes anything. + * Long labels also wrap at a more sensible width, so a diagram with wordy boxes is no longer squeezed into tall, narrow columns of text. + * (Ref: V2 chat, Mermaid diagrams, message layout) + +* **A Long Diagram No Longer Breaks Scrolling** + * A tall diagram used to be dropped into the conversation at full height — a large flowchart can be tens of thousands of pixels tall — which made scrolling stutter or lock up. A diagram now sits in a panel of its own that scrolls internally. + * The bottom of a conversation was also unreachable after a diagram appeared: diagrams draw a moment after the message does, so the chat had already scrolled before the diagram grew. It now follows content that arrives late. + * Scrolling and typing in a long conversation are noticeably lighter, because the messages you are not looking at are no longer redrawn on every scroll and every word of a streaming reply. + * (Ref: V2 chat, Mermaid diagrams, message list performance) + +* **A Diagram That Will Not Draw Now Says Why** + * "Diagram could not be rendered" was all you got, and nothing was written to the browser console either, so there was nothing to report and nothing to look into. The reason is now shown behind **Show details**, logged to the console, and the source can be copied with one click. + * More usefully, most of those diagrams now just work. When a diagram fails, SimpleChat repairs the common mistakes and draws it again: reserved words such as `end` used as a box name, a `subgraph` left unclosed, `End` instead of `end`, placeholders like `` carried over from pasted text, stray quotes and braces inside a label, and several more. Fourteen distinct failures were reproduced and all fourteen now render. + * Repairs only happen after a diagram has already failed, so a diagram that draws correctly today is never altered. + * A diagram is also given a time limit and a size limit, so a broken one can no longer leave "Rendering diagram…" on screen indefinitely. + * (Ref: V2 chat, Mermaid diagrams, diagram source repair) + +#### User Interface Enhancements + +* **Make A Diagram Bigger** + * Diagrams now have **−**, **+** and a fit button to scale them, and an **Expand** button that opens the diagram full screen with its own zoom and PNG download. + * The bar along the bottom edge of a diagram can be dragged to make the panel taller or shorter. It also works from the keyboard — arrow keys to resize, **Home** to go back to the automatic height. + * The size you leave a diagram at is remembered with the conversation, so it is still there when you come back. Resizing a diagram does not disturb its colours, and changing its colours does not resize it. + * (Ref: V2 chat, Mermaid diagrams, diagram zoom and resize) + +* **Better Diagrams From The Assistant** + * The assistant is now told which words break a diagram, to close every `subgraph`, and to keep box labels to a short phrase rather than packing a dozen lines into one box — which is what made the reported diagrams both unreadable and unrenderable. + * When you paste text or ASCII art and ask for a diagram, it is told to summarise the detail rather than copying placeholders and punctuation straight into the boxes. + * (Ref: diagram prompt guidance) + +### **(v0.261.036)** + +#### Bug Fixes + +* **Your Reasoning Level Now Sticks In The New Interface** + * Choosing a reasoning level in the V2 chat used to be forgotten the moment you left the page and came back, and switching models either carried the level over or lost it entirely. It is now remembered per model, so each model keeps the level you chose for it. + * The level is shared with the classic interface, which has always remembered it: set **Medium** for a model in one interface and it is Medium in the other. + * Switching to a model that has no reasoning — `gpt-4o`, for example — no longer leaves the previous model's level attached to your request. The setting disappeared from the toolbar but was still being sent. + * The model you pick is now remembered too. It previously only appeared to stick, because the page fell back to whichever model the server considered your default. + * A model you have not chosen a level for shows its usual default rather than a blank control, matching the classic interface. + * (Ref: V2 chat, reasoning effort, model picker, user preferences) + +### **(v0.261.035)** + +#### Bug Fixes + +* **Exported Diagrams Are No Longer Empty Boxes** + * Exporting a message with a diagram to Word or PowerPoint produced the right shapes, arrows and layout, but **no text inside any of the boxes**, even though the same diagram read correctly on screen. + * The application image ships no scalable font, so when the server drew a diagram itself the browser it uses had nothing to write with: it measured every label as zero-width, fell back to the smallest box that would fit nothing, and painted no lettering. The tell-tale sign was every box coming out the same width, while on screen they varied with their labels. + * The renderer now carries its own font instead of relying on whatever the host happens to have, so a diagram exports the same way on every deployment. Real fonts were also added to the image for everything else that draws in it. + * Diagrams are now captured directly from the page they are drawn on, rather than being redrawn from a copy — a step that quietly discarded anything it could not reproduce. + * (Ref: message export, Mermaid diagrams, server-side rendering, container fonts) + +* **Exports Say When They Are Working** + * Exporting to Word, PowerPoint or email closed the menu and then appeared to do nothing, sometimes for a long time. A PowerPoint export in particular waits on the model planning your slides, and long enough that it was reasonably read as having hung or failed when it was still going. + * All three now put up a notice as soon as you click, which stays until the file arrives and is then replaced by the result. The menu entry spins and greys out while it runs, so an impatient second click cannot start a duplicate export. + * Both the classic and new interfaces behave the same way. + * (Ref: message export, notifications, chat message menu) + +#### User Interface Enhancements + +* **Faster Exports That Keep Your Diagram Colours** + * The new interface now sends the diagram it has already drawn along with the export, so the server no longer starts a browser to draw it again. Exports containing diagrams finish noticeably sooner. + * Because the exported picture is the one on your screen, any colours you picked for that diagram now come with it. + * (Ref: V2 chat, message export, Mermaid diagrams) + +### **(v0.261.034)** + +#### Bug Fixes + +* **Picking An Agent No Longer Leaves A Model And Reasoning Level Pretending To Apply** + * In the new interface, choosing an agent left the **Model** picker still showing a selected model and the **Reasoning** picker still offering a level, even though an agent can act on neither — an agent answers with its own model, and reasoning levels only reach a directly chosen model. + * Worse, the request sent all three together, and the server reads a model sent alongside an agent as a deliberate instruction to override it. So an agent could quietly answer through the wrong model rather than the one it is configured with. + * Selecting an agent now dims the **Model** picker back to the plain word "Model" and hides **Reasoning**. The model you had chosen is remembered, not thrown away, and comes straight back when you clear the agent. + * The model picker stays clickable while it is dimmed: choosing a model is how you switch back, and doing so clears the agent for you. Its menu still shows a tick beside the model you had, so you can see what returns. + * Hovering the dimmed picker names the agent that is supplying the model. + * Reasoning is now also hidden while generating an image, matching the classic interface. + * (Ref: V2 chat, agent picker, model picker, reasoning effort, `chatRequestSelection.ts`) + +### **(v0.261.033)** + +#### New Features + +* **Save A Diagram As A Picture** + * Diagrams in the new interface now have a **PNG** button, matching the one charts have always had. The image is captured from the diagram exactly as it appears on screen, so whatever colours and background you have chosen come with it. + * The picture is saved on a solid background rather than a transparent one, so it stays readable wherever you paste it. + * (Ref: V2 chat, Mermaid diagrams, PNG download) + +* **Choose The Colours For Diagrams And Charts** + * Every diagram and chart in the new interface now has a **Colors** button offering five palettes — Default, Calm, Vivid, Warm and Contrast — the same five the classic interface already used for charts. + * Charts additionally let you set each series or slice individually, and both diagrams and charts let you set a background colour. Leaving the background on **Match theme** means it keeps following light and dark mode, as before. + * Set your own defaults under Settings → Preferences, in the new **Diagrams** and **Charts** sections. Those apply everywhere you have not chosen something specific. + * Colours you choose on an individual diagram or chart are saved with the conversation, so they are still there when you come back to it. Recolouring one chart never changes the others — three charts in a reply keep three independent sets of colours. + * Anything you have not touched looks exactly as it did before. + * (Ref: V2 chat, Mermaid diagrams, inline charts, user preferences) + +### **(v0.261.032)** + +#### New Features + +* **Links To A Conversation Now Work In The V2 Interface** + * The V2 chat page never put the open conversation in the address bar, so copying the URL shared nothing and reloading the page dropped you into an empty chat. It now names the conversation you are reading, exactly as the classic interface has since v0.237.001. + * A link such as `/v2/chat?conversationId=` opens that conversation. Both spellings the application produces are accepted — `conversationId` from notifications and workflow runs, `conversation_id` from chat responses and workspace document rows — so an existing link opens either way. + * The address bar keeps up on its own: it follows conversations you open from the list, the one created when you send your first message, and forks. Starting a new chat or deleting the open conversation clears it. Opening several conversations does not fill the back button with an entry for each. + * A linked conversation that is older than the loaded list, or hidden, now appears in the rail with its real title instead of leaving the header reading "New chat". + * A link to a conversation that was deleted, or that belongs to somebody else, says so and returns you to an empty chat, rather than leaving an error that reappeared on every refresh. + * **Back to classic UI** in the account menu carries the conversation across, so switching interfaces keeps your place. + * (Ref: V2 chat, conversation deep linking, conversation list) + +### **(v0.261.031)** + +#### New Features + +* **Ask For A Diagram, Get A Diagram** + * Asking for a diagram, flowchart, sequence, architecture or data flow used to come back as ASCII box art in a plain code block — unreadable, impossible to export as a picture, and meaningless to a screen reader. The assistant was never told that SimpleChat can draw real diagrams, so it fell back on drawing with keyboard characters. + * Diagram requests now get an answer the app actually draws. Chart requests are unaffected: "plot the sales trend" is still a chart, "draw the request flow" is now a diagram. + * The assistant is also steered toward diagram syntax that renders on the first attempt, and toward a diagram rather than a generated image for structural content, so the result stays selectable, searchable and accessible. + * (Ref: chat prompt guidance, Mermaid diagrams, inline charts) + +* **Diagrams Now Render In The Classic Chat Interface** + * The classic interface could turn a diagram into a picture when you exported it, but showed the same diagram as a block of code in the conversation itself. Diagrams now render on screen where you are reading them. + * Diagrams follow light and dark mode, appear as they finish streaming rather than flickering through half-written source, and a diagram that cannot be drawn shows its source instead of vanishing. + * Copying a message and exporting one both still produce the original diagram code, so nothing downstream changes. + * The diagram library is only downloaded the first time a conversation shows a diagram. + * (Ref: classic chat rendering, Mermaid diagrams, dark mode) + +#### Bug Fixes + +* **Chart Guidance No Longer Suppresses Genuine Diagrams** + * Guidance intended to stop the assistant from answering a chart request with diagram code was worded broadly enough to discourage diagrams entirely, including in answers where a process or architecture diagram was the right thing to show. + * It now rules out diagram code only as a substitute for a data chart, and explicitly allows a diagram alongside charts in the same answer. + * (Ref: chart guidance, Mermaid diagrams) + +* **Spoken And Preview Text No Longer Read Out Internal Placeholders** + * Reply previews and read-aloud could include internal placeholder text left behind where a chart, diagram or image card sits in a message. Those placeholders are now removed before the text is previewed or spoken. + * (Ref: reply preview, text-to-speech, inline visuals) + +### **(v0.261.030)** + +#### Bug Fixes + +* **Cross-Cloud Account Selection for Access-Denied Users** + * Added a **Sign in with another account** action for authenticated users whose selected Microsoft Entra identity does not have the required SimpleChat app role. + * The alternate flow requests the Entra account picker only through the controlled `/login?select_account=1` path; ordinary sign-in remains prompt-free, and arbitrary OAuth prompt values are ignored. + * The access-denied state now identifies the current account using safe session claims while suppressing resource-tenant `#EXT#` UPNs, helping users distinguish native Azure Government and synchronized commercial identities. + * Improved the action's light- and dark-theme contrast without changing tenant authority, app-role authorization, Easy Auth configuration, or login-hint behavior. + * (Ref: `route_frontend_authentication.py`, `functions_authentication.py`, access-denied landing page, cross-cloud Microsoft Entra B2B sign-in) + +* **ANSI-Encoded CSV Files Are Now Read Correctly** + * CSV metadata extraction, indexing, citations, row searches, and durable tabular replay now support UTF-8, UTF-8 with BOM, Windows-1252, and Latin-1 files, preserving characters that previously made uploaded content appear unreadable. + * Tabular analysis retains the broader automatic-invocation budget for complex questions while detecting repeated equivalent failures and routing the next model pass to a different call shape instead of repeating the same error. + * Repeated tabular tool failures now emit an explicit retry lifecycle thought and server-side diagnostic event, making the failure visible while recovery continues. + * (Ref: `functions_tabular_csv_query.py`, `functions_documents.py`, `tabular_processing_plugin.py`, `route_backend_chats.py`, [Tabular CSV ANSI Encoding and Retry Fix](fixes/TABULAR_CSV_ANSI_ENCODING_AND_RETRY_FIX.md)) + +* **Group Chat Uploads Now Recognize Workspace Owners** + * Fixed group-scoped chat uploads incorrectly reporting that no group workspace was available when the signed-in user was the group owner. + * Chat bootstrap data now includes each group's resolved user role, allowing owners, admins, and document managers to use the existing group upload permissions while preserving server-side authorization checks. + * (Ref: `route_frontend_chats.py`, `chat-input-actions.js`, [Chat Group Upload Owner Role Fix](fixes/CHAT_GROUP_UPLOAD_OWNER_ROLE_FIX.md)) + +### **(v0.261.029)** + +#### New Features + +* **Image Suggestions Can Now Be Approved In The New Interface** + * When a reply would be clearer with a picture — a timeline, a slide visual, a diagram — the assistant can suggest one instead of producing it unasked. The new interface previously showed those suggestions as a block of raw JSON, so there was no way to act on them. + * Each suggestion now appears as a card in the reply, exactly where the assistant put it, with **Approve** to generate the image, **Edit** to reword the prompt first, and **Cancel** to dismiss it. Nothing is generated until you ask for it. + * A reply containing more than two suggestions gets an **Approve all** button, and approvals are run one at a time with their place in the queue shown, rather than starting every image at once. + * An approved image appears inside its own card rather than at the end of the conversation, so it stays next to the paragraph it illustrates — including after the conversation is reopened. Clicking it opens the same full-size viewer as any other image. + * Approval is refused in shared conversations, which the card now tells you rather than failing silently. A suggestion can only be approved once the reply has finished arriving. + * (Ref: `simpleimage` proposals, V2 chat interface, `/api/chat/image-proposals/generate`, image generation) + +### **(v0.261.028)** + +Tracking: [#1489](https://github.com/microsoft/simplechat/issues/1489); implementation: [PR #1488](https://github.com/microsoft/simplechat/pull/1488). + +#### Bug Fixes + +* **Your Configured Chat Notices Now Appear In The V2 Interface** + * Two notices administrators can configure — the data-handling notice shown when web search is used, and the AI notice shown under the message box — appeared in the classic interface but never in V2. Anyone who switched interfaces stopped seeing them. + * The web search notice now appears above the message box while web search is turned on, using your configured wording, and can be dismissed for the rest of the browser session. It requires the same three settings as before, including the consent acknowledgement. + * The AI notice now appears below the message box and respects all four display behaviours: always visible, dismissible once per session, once per day, or once per message version. Editing the notice text still brings it back for everyone who had dismissed the previous wording. + * A dismissal now carries across both interfaces in the same browser session, rather than reappearing when you switch. + * Dismissing the notice waits for the change to save, so it no longer disappears and then return on the next page load. A failed save is now reported instead of looking like a dead button. + * **Behaviour change**: V2 previously showed its own fixed line, "AI responses can be inaccurate. Verify important information.", regardless of your settings. That line has been removed. If you want a notice under the message box, enable the AI notice in Admin Settings → Notices & Agreements → Chat AI Notice and set your own wording; if the AI notice is turned off, V2 now shows nothing there, matching the classic interface. + * (Ref: V2 chat composer, web search user notice, chat AI notice, `/api/v2/bootstrap`) + +* **Credential-Aware Cosmos Deployment Checks** + * Managed-identity deployments now use the deployment runner's tenant-scoped Entra credential instead of requiring Cosmos keys. Key-mode deployments report when local authentication is disabled. + * Authentication and network failures no longer trigger automatic firewall changes or misleading propagation waits. Windows environment lookups and POSIX role operations target the selected deployment explicitly. + * **Deployment requirement:** The runner must already have the required data-plane permissions and network access. Post-configuration does not open firewalls, enable key authentication, or create policy exemptions. + * (Ref: [PR #1488](https://github.com/microsoft/simplechat/pull/1488), `deployers/azure.yaml`, `deployment_cosmos.py`, `cosmosDb-postDeployPerms.sh`) + +* **Safe Post-Deployment Settings Publication** + * Post-configuration uses conflict-checked Cosmos writes and the application's shared Redis publication protocol, preserving concurrent unrelated settings changes and reporting publication failures instead of claiming success. + * Consolidated duplicate Redis configuration blocks and preserved external Redis settings when no cache is provisioned. Equivalent connection settings can be normalized; changing an active cache endpoint or authentication mode requires a planned migration. + * **Deployment requirement:** When Redis is enabled, the runner needs Redis data-plane access as well as network connectivity. The service restarts only after post-configuration succeeds. + * (Ref: [PR #1488](https://github.com/microsoft/simplechat/pull/1488), `postconfig.py`, `deployment_configuration.py`, `app_settings_store.py`) + +#### New Features + +* **Automatic Missing Search Index Creation** + * Post-configuration creates missing personal, group, and public Search indexes from the same JSON schemas used by the admin setup controls. + * Existing indexes and documents are left unchanged. Authorization and service failures stop initialization rather than being treated as missing indexes; concurrent creation is verified before continuing. + * Included in deployer version **1.0.31**. Existing-index schema upgrades remain administrator-managed operations. + * (Ref: [PR #1488](https://github.com/microsoft/simplechat/pull/1488), `deployment_configuration.py`, `application/single_app/static/json/ai_search-index-*.json`, [deployment prerequisites](../reference/deploy/azd-cli_deploy.md#post-provision-access)) + +### **(v0.261.027)** + +#### New Features + +* **Diagrams In Exports No Longer Need The Browser You Exported From** + * Diagrams are drawn by your browser when you export from one that can draw them. Anything else — an export started from an interface that does not, or one the server starts on its own — previously fell back to showing the diagram's code. + * Those diagrams are now drawn on the server, using the Chromium the app already installs. No new dependency was added, nothing is fetched from the internet, and a diagram looks the same whichever way it was drawn. + * A diagram your browser already drew is never drawn twice, and an export containing no diagrams does no extra work. + * Deployments built without the optional Chromium component are unaffected and keep showing the diagram's code, as before. + * (Ref: conversation and message exports, Mermaid diagrams, server-side rendering) + +#### Bug Fixes + +* **Settings Cache Bootstrap Import Boundaries** + * Removed reverse imports from cache helpers into application configuration, preventing the import cycle identified during review. + * Web and scheduler startup now configure the cache from the supplied settings object, with storage handles, the Redis factory, and logging callbacks passed separately. + * Preserves shared settings reads and fail-closed Redis writes without restoring worker-local settings snapshots. + * Added real-module cold-start probes for normal and optimized Python, moved state-changing test operations outside assertions, and strengthened repository instructions for dependency and startup validation. + * (Ref: [#1477](https://github.com/microsoft/simplechat/issues/1477), [PR #1478](https://github.com/microsoft/simplechat/pull/1478), `app_settings_cache.py`, `functions_settings.py`, `test_app_settings_import_boundaries.py`) + +### **(v0.261.026)** + +#### New Features + +* **Diagrams And Formulas Now Become Pictures In Exports** + * Assistant replies often contain Mermaid diagrams and LaTeX formulas. Until now these arrived in an exported file as raw code — a wall of `graph TD` lines or `\frac{a}{b}` — which was unreadable for anyone the file was sent to. + * Word, PowerPoint, PDF, Markdown and email exports now render them as images, the same way inline charts have always been handled, so an exported file matches what the conversation shows. + * Recognised formula syntax is ` ```math `, ` ```latex ` and ` ```tex ` blocks, `$$…$$`, and `\[…\]`. Single-dollar text is deliberately left alone so a sentence like "costs $100 to $200" is never mistaken for a formula, and a ` ```text ` block is never mistaken for one either. + * **Open in Email** treats them like charts: each diagram and formula downloads as a PNG file with a matching reference in the draft body, because a `mailto:` draft cannot carry inline images. + * Anything that cannot be drawn — an unusual diagram, or a formula using an environment such as `align` or `matrix` — keeps its original code block rather than failing the export. + * (Ref: conversation and message exports, Mermaid diagrams, TeX math, inline export visuals) + +#### Bug Fixes + +* **Redis Explorer Shared Settings Compatibility** + * Fixed key browsing and previews failing after removal of the worker-local settings cache. + * Explorer now recognizes the current shared settings record and labels leftover settings payload/version keys as legacy, without restoring worker caching. + * Preserves credential and Cosmos session-token redaction in previews. + * Added offline coverage for Azure Managed Redis and Azure Cache for Redis, including service-specific ports, key/managed-identity authentication, and app-cache/session clients. + * (Ref: [#1477](https://github.com/microsoft/simplechat/issues/1477), `functions_redis_monitoring.py`, `test_cosmos_wave5a3_redis_monitoring.py`) + +### **(v0.261.025)** + +#### User Interface Enhancements + +* **Generated Images Now Open In A Viewer Instead Of A New Tab** + * In the V2 interface, clicking an image the assistant generated used to open the raw file in a new browser tab, dropping you out of your conversation. It now opens in a viewer over the page, matching how the classic interface has always behaved. + * The viewer scales the image to fit and can switch to actual size, where a large image scrolls so its edges stay reachable. Clicking the image toggles between the two. + * Saving the image and opening the raw file are both available from the viewer, so nothing is lost by staying in the app. Close it with the close button, the Escape key, or by clicking outside it. + * Smaller images previously did nothing at all when clicked, because browsers refuse to open the inline format they arrive in. Those images now open correctly, and can be saved. + * Images you upload benefit in the same way, since they are shown through the same message type. + * (Ref: V2 chat image messages, image lightbox, image download) + +#### Bug Fixes + +* **Delegated Action-Type Policies Now Override Broad Action Access** + * Fixed a governance gap where an explicit delegated item policy for a personal, group, or global action type could still be bypassed by a broader feature-level allow. + * Action-type governance now treats explicit item policies as authoritative once they exist, so a targeted policy such as `personal_action_type = azure_maps` can block that action type even when the broader action feature remains enabled. + * This resolves cases where action types such as Azure Maps continued to appear in action creation flows after admins saved a delegated item policy intended to block them. + * (Ref: delegated item governance, action-type enforcement, `functions_governance.py`) + +* **Admin Settings Consistency Across Workers** + * Removed worker-local admin settings snapshots so reloads read shared Redis settings, or Cosmos directly when Redis is disabled. + * Added conflict-checked writes and coordinated cache publication to prevent stale metadata updates, worker startup, and interrupted saves from restoring older settings. + * Stale admin forms now require a reload. Saves are rejected when configured Redis is unavailable; unconfirmed saves prompt verification rather than reporting success. + * Reads retain Cosmos fallback without serving an old worker snapshot. Deploy all web workers and the scheduler together; Cosmos fallback remains subject to Session consistency. + * (Ref: [#1477](https://github.com/microsoft/simplechat/issues/1477), `app_settings_store.py`, `app_settings_cache.py`, `functions_settings.py`, admin settings form, auxiliary settings writers, `docs/admin/scale.md`) + +### **(v0.261.024)** + +#### New Features + +* **Equations, Diagrams And Charts Render In The V2 Chat** + * Answers containing mathematics now show it set as mathematics rather than as raw TeX. `$$…$$`, `\[…\]` and `\(…\)` are recognised. A single `$` is deliberately left alone, so an answer about pricing still reads "costs $5 to $10 per user" rather than turning the sentence into an equation. + * Mermaid diagrams render as diagrams. These have been reaching the chat for some time without anyone being able to see them: documents processed with Content Understanding have their figures extracted as Mermaid, so a document containing a flowchart was arriving as diagram source in the middle of an answer. + * Charts produced by the built-in chart action now draw in V2 as they already do in the classic interface, instead of showing the underlying data as a block of JSON. + * A diagram or chart that cannot be read falls back to showing its source rather than disappearing, and one that is still being written shows a placeholder until it is complete. + * (Ref: V2 chat rendering, KaTeX, Mermaid, Chart.js, `AssistantMarkdown.tsx`) + +* **Chart Data And Image Download** + * Each chart carries its title, subtitle and description, plus a **Data** control that reveals the numbers behind it and a **PNG** control that downloads the image. The data table starts closed, so the chart stays the answer, and the download is composited onto a solid background so it stays readable wherever it is pasted. + * Charts and diagrams follow the light and dark theme rather than staying fixed to whichever was active when they first drew. + * (Ref: V2 inline charts, chart data table, PNG export) + +#### User Interface Enhancements + +* **Copying A Message Containing A Chart** + * Copying an answer with a chart in it used to paste several kilobytes of chart data into the middle of the text. The chart's title and its numbers are now pasted as a small table instead. Equation and diagram sources are left as they are, since those are still readable as text. + * (Ref: V2 message copy, `messageText.ts`) + +#### Breaking Changes + +* **None.** No settings were added, and the Content-Security-Policy is unchanged. The libraries behind this work are committed to the repository under `application/v2_ui/public/vendor/` and served from SimpleChat itself, so nothing is fetched from a public CDN at run time or from a package registry at build time. Pinning them this way means any future change to that third-party code is a reviewable commit rather than a silent substitution. + +### **(v0.261.023)** + +#### User Interface Enhancements + +* **V2 Appearance Preferences Now Follow You** + * Light and dark mode, the collapsed state of the left navigation, and the chat width were previously remembered only by the browser you set them in, so they reset on a different machine. + * They are now saved to your account. Light and dark mode in particular is shared with the classic interface, so choosing dark in either place applies to both. + * Your browser still remembers them as well, which is what stops the wrong theme appearing for a moment while your preferences load. + * (Ref: V2 theme, navigation and chat width preferences) + +#### New Features + +* **XSD Ingestion and Schema-Validated XML Generation** + * Added `.xsd` ingestion for personal, group, public, chat-upload, generated-artifact promotion, and File Sync paths when Enhanced Citations storage is available. + * Preserves exact schema bytes in immutable Blob locations and indexes one bounded metadata summary instead of fragmenting the schema as narrative content. + * Resolves same-workspace includes and imports, tracks schema readiness, and revalidates dependent schemas when current revisions change. + * Uses an explicitly selected ready XSD as the authoritative contract for XML generated by Chat, Analyze, agents, and Analyze workflows, with whole-document validation before publication. + * Implements the fail-closed `simplechat-xsd10-subset-profile/1`; unsupported XSD 1.1 and other excluded constructs are retained with diagnostics but cannot govern generation. + * (Ref: #1212, XSD ingestion, mixed-source orchestration, schema-bound XML artifacts, `functions_xsd_schema.py`, `functions_documents.py`) + +#### Bug Fixes + +* **XSD Upload Errors Use Allowlisted Browser Responses** + * Replaced exception-derived chat-upload responses with an explicit allowlist of stable XSD error codes, messages, and HTTP statuses. + * Unknown XSD ingestion failures now return a generic error without exposing exception details. + * (Ref: `route_frontend_chats.py`, XSD upload error handling) + +### **(v0.261.022)** + +#### New Features + +* **The V2 Settings Page Is Now Complete** + * All six sections of the personal settings page are built in the V2 interface: Preferences, Stats, Groups, Public, Feedback and Violations. They no longer link out to the classic page. + * **Groups** and **Public** list the workspaces you belong to with search and paging, show your role in each, and let you switch which one is active. If a switch is refused — because you are not a member, or the workspace no longer exists — the reason is shown rather than the change quietly failing. + * **Feedback** shows the ratings you have given assistant replies, whether each has been reviewed, and any reply from a reviewer, with filters and a CSV export. + * **Violations** shows anything flagged on your account with its status and the categories that triggered it. You can add your own notes to a record; status and reviewer notes remain an administrator's to set. + * **Stats** charts your conversations, uploads, token use and sign-ins over 7, 30 or 90 days. + * (Ref: V2 settings tabs, group and public workspace selection, feedback, safety violations, activity trends) + +### **(v0.261.021)** + +#### New Features + +* **Text Size And Spoken Reply Voice In V2 Settings** + * The V2 Settings page now offers a text size choice that scales the whole interface, using the same sizes as the classic interface so a size chosen in one applies to the other. + * When text-to-speech is enabled, you can choose which voice reads assistant replies aloud. Replies are also now read as clean prose: citation markers and masked text are no longer spoken. + * Sections for capabilities your administrator has turned off do not appear, and preferences that only affect the classic interface are deliberately left there rather than shown as controls that would do nothing here. + * (Ref: V2 settings, text size, text-to-speech voice) + +#### Bug Fixes + +* **OAuth2 Token Endpoints Are Now Validated When The Token Is Fetched** + * A Custom endpoint's OAuth2 token URL was checked when the endpoint was saved, but not when the token was actually requested. Validating only at save time leaves the request itself unguarded, since settings can be written by another path, restored from backup, or changed after validation. Code scanning correctly identified this as a server-side request forgery. + * The token URL is now revalidated at request time against the same outbound policy as the inference endpoint, and the request runs on the same pinned transport, so its addresses are validated at connection time and redirects are refused. + * Refusing redirects is safe for this grant: redirects belong to the browser-based authorization-code flow, whereas a client-credentials token endpoint answers a server-to-server POST with a JSON body. The previous code allowed them based on an incorrect assumption. + * (Ref: `functions_model_endpoint_auth.py`, [#1437](https://github.com/microsoft/simplechat/pull/1437)) + +* **Endpoint URL Version Matching No Longer Backtracks** + * The pattern recognising a version path segment allowed its optional suffix to begin with a digit, making it ambiguous with the preceding digits and quadratic on a long run of them. A 8,000-character segment took roughly 0.19 seconds to reject; it now takes 0.0003 seconds. + * The suffix must now begin with a letter, which removes the ambiguity while matching exactly the same version segments. + * (Ref: `model_endpoint_clients.py`, [#1437](https://github.com/microsoft/simplechat/pull/1437)) + +### **(v0.261.020)** + +#### New Features + +* **Workspace Tags In The Conversation List** + * The conversation list now labels each conversation with the workspace it belongs to — the group or public workspace name, or a note that it is shared with other people. Personal conversations stay unlabelled, so the label means something when it appears. + * Previously the only way to tell a group conversation from a personal one was to open it and read the badge beside its title, which made a long list of conversations hard to navigate for anyone working across several groups. + * The label is drawn from information the conversation list already receives, so it costs no extra loading time. + * (Ref: conversation list, workspace badges, V2 interface) + +* **Personal Settings In The V2 Interface** + * A new **Settings** page, reachable from the account menu, gives the V2 interface its own home for personal preferences. It opens on Preferences and keeps the familiar sections from the classic profile page — Stats, Groups, Public, Feedback and Violations — each appearing only when the relevant capability is enabled. + * Preferences save as you change them rather than behind a Save button, and a preference that fails to save is put back to its stored value rather than left showing something that was never recorded. + * The first preferences available are the new workspace tags toggle and the conversation contents drawer. The remaining sections currently link through to the classic page while they are rebuilt. + * (Ref: V2 settings page, `/api/user/settings`, personal preferences) + +* **Custom Model Endpoints Support Bearer Tokens, OAuth2, And Client Certificates** + * Custom endpoints accepted one authentication scheme: an API key sent in whichever header the built-in providers happened to use. That covers OpenAI and Anthropic and nothing else, so a gateway expecting `x-goog-api-key`, a corporate gateway issuing short-lived tokens, and an appliance requiring a client certificate were all unreachable. + * **The API key header name and value prefix are now configurable**, so a single scheme covers `Authorization: Bearer`, Anthropic's `x-api-key`, Google's `x-goog-api-key`, and any bespoke gateway header. + * **Added static bearer token authentication.** + * **Added OAuth2 client credentials**, with token caching and refresh ahead of expiry so a token cannot lapse mid-request. The token endpoint is validated against the same outbound policy as the inference endpoint, so it cannot become an unchecked request target, and a failing token response is sanitized before it reaches the browser. + * **Added mTLS client certificates.** Certificates are referenced by file path so a private key is mounted into the deployment and never written to the configuration database. + * (Ref: `functions_model_endpoint_auth.py`, `functions_model_endpoint_providers.py`, `functions_model_endpoint_validation.py`, [#1228](https://github.com/microsoft/simplechat/pull/1228)) + +### **(v0.261.019)** + +#### User Interface Enhancements + +* **A Recovered Response No Longer Looks Stalled** + * After a dropped connection was picked back up, the message kept saying "Reconnecting" and kept a "Reconnected to the response still being generated" banner in place for the rest of the answer. Because both stayed put while the response was actually arriving, it read as though nothing was happening. + * The two moments are now told apart. While the connection is being re-established the message says so plainly, alongside a spinner, and it does this even when part of the answer is already on screen — so a response that stopped mid-sentence no longer just looks frozen. Once content starts arriving again the wording changes to a brief "Reconnected." note, the activity indicator goes back to the usual "Thinking", and the note clears itself a few seconds later so the rest of the answer reads like any other. + * (Ref: V2 streaming recovery, reconnect states) + +#### New Features + +* **On-Premises Custom Model Endpoints Now Work** + * The administrator gate named "allow private Custom endpoint hosts" did not actually permit the two most common on-premises address forms. An IP address such as `https://10.20.30.40/v1` and a short host name such as `https://llm-gateway/v1` were both rejected even with the gate enabled, and both were refused with a message claiming the URL was an IP address, which was wrong for the short host name. + * With the gate enabled, IP addresses, short host names, and hosts resolving to private ranges are now accepted. Loopback, link-local, and cloud metadata addresses remain rejected regardless of any setting, and every address is still revalidated at connection time. + * **Added a CA bundle setting.** Custom endpoints trust only public certificate authorities and deliberately ignore ambient environment variables, so an on-premises gateway using an internally issued certificate previously could not be trusted at all. An administrator can now name a PEM bundle. A bundle that cannot be loaded fails loudly rather than silently falling back to weaker trust. + * **Added a separate plaintext HTTP gate** for isolated networks where TLS cannot be terminated. It requires the private-hosts gate as well, and is labelled with its consequence: prompts and API keys travel unencrypted. + * Saving an endpoint no longer requires the host name to resolve from the application tier, so configuration can be seeded or restored from backup ahead of connectivity. Policy violations are still refused at save time, and the connection-time check is unchanged. + * (Ref: `functions_model_endpoint_validation.py`, `model_endpoint_clients.py`, `allow_insecure_custom_model_endpoints`, `custom_model_endpoint_ca_bundle_path`, [#1228](https://github.com/microsoft/simplechat/pull/1228)) + +### **(v0.261.018)** + +#### Bug Fixes + +* **Copying A Message Pasted Unusable Citation Text In V2** + * Copying an answer put the raw citation markers on the clipboard, so a pasted paragraph read `...up to 0.2 mm/s at full load. (Source: NanoPZ.pdf, Page: 13) [#0d4d4eb0-fbbb-4821-a809-8bbd649be6ef_13]` — unusable in an email or a document. + * Copy now produces the answer as a person would want to read it: the citation markers are removed, along with the space in front of them so no stray gaps or double spaces are left behind, while bold, lists, headings and paragraph breaks are kept intact. + * The same conversion is used when saving a message as Markdown and when reusing one as a prompt, so neither carries the markers either. + * (Ref: V2 message copy, citation markers, `messageText.ts`) + +* **Copying A Message Could Reveal Masked Text In V2** + * A masked span hides text from other readers, but copying the message wrote the underlying content to the clipboard, so the hidden text could be recovered by pasting it. Saving the message as Markdown had the same problem. + * Redactions now survive leaving the app: masked spans are replaced with `[masked]`, and a message masked in its entirety is withheld rather than partially copied. + * (Ref: V2 message masking, clipboard, message download) + +* **Tool-Calling Responses Now Actually Stream** + * SimpleChat only supports streaming responses, but tool calling cannot stream, because a tool call has to arrive whole. The completed answer was delivered through the streaming interface as a single chunk, so the user saw nothing at all and then the entire response at once, which reads as a hang. Because agents and plugins rely on tool calling, this was a common path rather than an edge case. + * A completed answer is now split into chunks at word boundaries and delivered progressively, so it reads like a real stream. Chunking is lossless — the reassembled text is byte-for-byte identical. + * Tool calls still arrive whole, on the final chunk, alongside the finish reason and usage metadata. Emitting metadata once means token usage is no longer at risk of being counted per chunk. + * **Streaming responses can report token usage again.** `stream_options` was stripped from every OpenAI-compatible request, which suppressed usage reporting for all of them. It is now dropped only for surfaces that reject it. + * (Ref: `model_endpoint_clients.py`, `functions_model_endpoint_providers.py`, [#1228](https://github.com/microsoft/simplechat/pull/1228)) + +#### User Interface Enhancements + +* **Copy With Sources** + * Removing citations from a copy also removes the attribution, which is sometimes the reason for copying in the first place. The message overflow menu now offers **Copy with sources**, which appends the cited documents and pages as a short numbered list under the answer instead of interleaving them with the prose. + * Saved Markdown files include the same reference list, since a file is more likely to be read later by someone who wants to check where a claim came from. + * (Ref: V2 message actions, citation references) + +### **(v0.261.017)** + +#### Bug Fixes + +* **Lists And Line Breaks Disappeared After A Citation In V2** + * In the V2 chat page, a bullet list, heading or new paragraph that followed a citation was pulled up onto the citation's own line, so answers that cited a source lost their formatting from that point on. + * The citation pattern deliberately absorbs the spacing after a citation marker while it removes the marker, and V2 was discarding that spacing instead of putting it back. The blank line that separates one markdown block from the next went with it, so the following block was no longer recognised as a block. + * Answers now keep their structure after a citation. Citations that land inside a heading, list item, table cell or quote are also rendered properly rather than leaving a stray placeholder behind. + * (Ref: V2 citation rendering, markdown blocks, `chat-citations.js` parity) + +* **A Dropped Connection Lost The Answer In V2** + * If the connection carrying a response was interrupted — a sleeping laptop, a flaky network, a proxy timeout — V2 showed "The response ended unexpectedly" and the answer was lost, even though the server was still writing it. + * Responses are generated on the server and outlive the connection that requested them. V2 now checks whether the response is still being produced and picks it back up where the classic interface does, replacing what was on screen with the replayed answer so nothing is duplicated. Opening a conversation whose answer is still being written also joins it in progress rather than showing a message that stops mid-sentence. + * A short "Reconnected to the response still being generated" note makes it clear what happened. A reconnect is attempted once; if it fails the error is reported as before. + * (Ref: V2 streaming recovery, `/api/chat/stream/status`, `/api/chat/stream/reattach`) + +* **Custom Endpoint URLs Are No Longer Rewritten Into 404s** + * SimpleChat appended `/v1` to every Custom OpenAI-compatible endpoint, even when the configured URL already said where the API lived. A gateway at `https://apim.example.com/inference/chat/completions` was called at `https://apim.example.com/inference/v1/`, and any base carrying its own version segment, such as `/v1beta` or `/v2`, was broken the same way. + * `/v1` is now appended only when the URL does not already name the API surface. A path whose last segment is a version is left alone, and a full operation URL is treated as stating the base exactly. + * Added a **Use this URL exactly as entered** option for gateways that serve the API at a path SimpleChat cannot infer. + * **Test Connection now reports the URL that was actually called.** URL normalization rewrites the configured endpoint, and that rewrite was previously invisible, so a misdirected request looked identical to a correct one. + * (Ref: `model_endpoint_clients.py`, `route_backend_models.py`, `_multiendpoint_modal.html`, `admin_model_endpoints.js`, `workspace_model_endpoints.js`, [#1228](https://github.com/microsoft/simplechat/pull/1228)) + +### **(v0.261.016)** + +#### Bug Fixes + +* **Document Search Failed In V2** + * Turning on **Documents** in the V2 chat page ended with "Something went wrong while streaming the response", while the same search worked in the classic interface. + * Two request problems were behind it. First, V2 identified the chosen model by its deployment name alone, but when multiple model endpoints are configured a name is not unique — the server could not resolve the selection and quietly fell back to a different endpoint than the one shown in the picker. Second, the document scope was fixed to "all workspaces" but the workspace identifiers were never sent, so the search was asked to cover workspaces it was given no way to look in. + * V2 now sends the full model identity with every message and retry, so the model you pick is the model that answers. The document scope is worked out from the workspaces actually in use — personal only, or personal plus the group or public workspace you are working in — and the matching identifiers travel with it. + * (Ref: V2 model selection, document search scope, `/api/chat/stream`) + +* **Custom Model Endpoint Failures Are Now Diagnosable** + * Every Custom endpoint failure produced the same sentence — "Custom model request failed." — with the underlying cause discarded before it reached the log. A wrong path, a wrong API key, a wrong model name, a TLS failure, and a blocked address were indistinguishable, and nothing anywhere explained which had happened. + * The browser message stays sanitized, because an upstream error body can echo back a URL, a header, or an API key. The real cause is now recorded server-side with the API type, the resolved request URL, the upstream status code, and the upstream error body. + * Both the message and the log entry now carry a short reference id, so an administrator can join the message a user reports to the log entry that explains it. + * Credentials are redacted before anything is written to the log, covering API keys, bearer tokens, `x-api-key`, `x-goog-api-key`, and key query parameters. A logging failure never replaces the original error. + * The resolved request URL is included deliberately: URL normalization can rewrite what the administrator typed, and that rewrite was previously invisible. + * (Ref: `functions_model_endpoint_diagnostics.py`, `model_endpoint_clients.py`, `functions_model_endpoint_runtime.py`, [#1228](https://github.com/microsoft/simplechat/pull/1228)) + +### **(v0.261.015)** + +#### New Features + +* **V2 Conversation Details Rebuilt** + * The details panel previously listed every tag as one undifferentiated row of chips, which mixed documents in with model names, participants and topics, and reduced source documents to a bare count. + * Each kind of information now has its own section: **Source documents**, **Workspaces**, **Models and agents**, **Participants**, **Topics** and **Web sources**. + * **Source documents** are listed properly — file name, the pages or sheets used, how many excerpts, workspace and classification — and marked as **cited** when a response actually referenced them. The list is paged, since a long conversation can draw on dozens of documents. + * Conversations that predate citation tracking say so, rather than showing every document as though it went unused. + * A conversation **summary** can now be generated on demand from the panel, and regenerated later, showing the model used and when it was produced. + * (Ref: V2 conversation details, `/api/conversations//metadata`, `/api/conversations//summary`) + +* **Custom Model Endpoints Now Support Google Gemini, Through A Provider Registry** + * Custom endpoints supported exactly three API types, and each one was hard-coded in five separate places: the allowlist, the request-model resolver, the protocol inference chain, the admin template's option list, and the admin JavaScript. Adding a provider meant editing all five and hoping none were missed. + * An API type is now a single declarative registry entry that carries its wire protocol, which field names the model, how its URL is built, which authentication types it accepts, and which version field applies. The admin API Type list, the model identifier label, and the version fields all render from that registry. + * **Google Gemini is now selectable as a Custom endpoint API type**, reached through its OpenAI-compatible surface so it still runs on SimpleChat's validated-DNS pinned transport. + * Fixed URL handling for endpoints that already carry a version segment. SimpleChat appended `/v1` unconditionally, which turned Gemini's `…/v1beta/openai` base into `…/v1beta/openai/v1` and produced a 404. URL construction is now per-provider, so `/v1` is appended only where it belongs. + * The three existing API types are unchanged, and an unregistered API type is still refused. + * (Ref: `functions_model_endpoint_providers.py`, `functions_model_endpoint_types.py`, `model_endpoint_clients.py`, `admin_model_endpoints.js`, `workspace_model_endpoints.js`, `_multiendpoint_modal.html`, [#1228](https://github.com/microsoft/simplechat/pull/1228)) + +#### User Interface Enhancements + +* **Composer Controls Appear When They Are Relevant** + * **Read URLs** now appears only once the message you are writing actually contains a URL, and **Deep research** only when there is something for it to research — web search being on, or URLs in the message. + * Turning on **Image generation** now disables the controls it is incompatible with and hides the model picker, since the request goes to an image endpoint that ignores them. + * A control that disappears also clears its setting, so a request never carries a capability you can no longer see you enabled. + * Together this removes the permanently crowded row of buttons in the composer. + * (Ref: V2 composer gating) + +* **Chat Width Can Be Changed** + * A control in the chat header switches between a comfortable reading width and using the full width of the pane. The choice is remembered. + * The message thread and the composer widen together, so the composer controls get the extra room too. + * (Ref: V2 chat width) + +### **(v0.261.014)** + +#### New Features + +* **Message Masking In V2** + * You can now mask content in the V2 chat page, which the classic interface has always supported. Masked content is withheld from the AI model when it builds conversation history, so it is a real protection rather than a display setting. + * Select any text in a message and a small **Mask selection** control appears over it. The selected text is replaced with a redaction marker. + * The hover row also offers masking the whole message, and clearing masks once any exist. + * Hovering a redaction shows who applied it and when, so in a shared conversation it is clear whose masking you are looking at. + * Masked text is removed before the message is rendered rather than hidden with styling, so it is never present in the page. + * If a selection cannot be matched back to the stored message — which can happen when it spans citations or formatting — the interface says so instead of appearing to succeed. + * (Ref: V2 message masking, `/api/message//mask`) + +* **Model Capabilities Now Come From The Model Catalog Instead Of The Model's Name** + * SimpleChat previously worked out what a model could do by pattern-matching its name, so a model the catalog did not know about — an on-premises or customer-supplied model reached through a Custom endpoint — silently received wrong answers for vision, tool calling, streaming, and reasoning. A model named `corp-llm-v2` was treated as having no capabilities at all. + * Capability answers now resolve through a precedence chain: a per-model override, then an endpoint-level override, then the shipped model catalog, then the original name heuristics. Administrators can describe a model the catalog has never heard of without waiting for a catalog update, and models absent from the catalog behave exactly as before. + * The catalog gained `supportsStreaming` and `reasoning` flags for every model, and now covers Google Gemini, which had no entries at all. Claude, Llama 4, and Phi-4 multimodal models are correctly recognised as vision-capable for the first time; the `-chat` variants of the GPT-5.x families are correctly recognised as not vision-capable. + * The catalog is now validated against a published JSON schema, so a malformed or incomplete model record fails a test rather than silently degrading capability answers at runtime. + * (Ref: `functions_model_capabilities.py`, `model_capabilities.json`, `model_capabilities.schema.json`, `MODEL_CAPABILITY_CATALOG.md`, [#1228](https://github.com/microsoft/simplechat/pull/1228)) + +### **(v0.261.013)** + +#### New Features + +* **Message Details, Sources And Reasoning In V2** + * Hovering a message now offers three new controls that open a panel beneath it, so you can see what a response was built from without leaving the conversation. + * **Sources** lists the documents a response cited and where in each one, the web results it used, and any tools it called with their arguments and results. + * **Reasoning** shows the steps taken to produce a response *after the fact*, not only while it is being generated. It is presented exactly as it appears live, so the same information does not look like a different feature once the response has finished. + * **Details** covers the model or agent used, the reasoning effort, and which capabilities were available versus actually exercised — a response where web search was enabled but never used now says so. + * Details also explains how the conversation history was assembled: how many earlier messages were kept, summarised, or skipped because they were an inactive retry attempt or were masked. This is often the answer to "why didn't it know that?". + * User messages get their own details panel covering the retry thread and the capabilities that were active when the message was sent. + * (Ref: V2 message inspector, `/api/message//metadata`, `/api/conversations//messages//thoughts`) + +* **Custom Model Endpoint Provider** + * Added manually configured Custom endpoints for OpenAI API, Azure OpenAI API, and Anthropic chat models across global, personal, and group scopes. + * Added type-specific model identifiers, API-key authentication, connection testing, response-length controls, and Anthropic Version support without model discovery. + * Enforced HTTPS, DNS/address safety with connection-time address pinning, runtime URL revalidation, redirect refusal, Key Vault secret handling, and an administrator-controlled private-host policy. + * (Ref: #1222, Custom model endpoints, `functions_model_endpoint_runtime.py`, `_multiendpoint_modal.html`, `CUSTOM_MODEL_ENDPOINT_PROVIDER.md`) + +#### User Interface Enhancements + +* **V2 Message Actions Grouped** + * The hover row beneath each message is now spaced into related groups — retry attempts, working with the message, feedback, inspecting it, and branching — rather than one long strip of undifferentiated icons. + * (Ref: V2 message actions) + +### **(v0.261.012)** + +#### Bug Fixes + +* **V2 Chat Exports Now Work** + * Export to Word, Export to PowerPoint and Open as email did nothing in the V2 chat page. The request was submitted as a web form, but these endpoints only accept a JSON body, so every attempt was rejected before it started. + * All three now work. Word and PowerPoint download the document, and Open as email opens your mail client with the subject and body filled in, saving any charts or images from the response so you can attach them. + * Failures are now reported instead of appearing to do nothing. + * (Ref: V2 message export, `/api/message/export-word`, `/api/message/export-powerpoint`, `/api/message/export-email-draft`) + +* **Generated Images Display In V2** + * Images produced in a conversation showed their internal address as a line of text instead of the picture. V2 now renders the image, and selecting it opens the full-size version. + * An image that cannot be loaded now says so and shows the prompt that produced it, rather than leaving a broken image. + * (Ref: V2 image messages, `/api/image/`) + +* **Message Retry Counter Was Always Wrong** + * Every message in the V2 chat page claimed to be attempt "2 of 2", including messages that had never been retried. + * The counter is now correct and only appears once a message actually has more than one attempt. + * (Ref: V2 message actions, retry attempts) + +* **Conversation Tag Was The Same On Every Conversation** + * The badge beside the conversation title showed your currently selected group rather than anything about the conversation you were reading, so it never changed. + * It now shows what the conversation itself is working in: the group or public workspace name, a "shared" marker for shared conversations, nothing for personal ones, plus classification labels and whether the workspace scope is locked. + * (Ref: V2 chat header, conversation metadata) + +* **Selecting An Agent Had No Effect In V2** + * Choosing an agent in the V2 composer appeared to work but was ignored, because the selection was sent in a form the server does not read. + * Agent selection now applies to both new messages and retries. + * (Ref: V2 composer, agent selection) + +* **Line Breaks Lost In V2 Responses** + * A response written as several short lines was run together into one paragraph, while the same text in your own message kept its line breaks. The two now render the same way, and a single line break is preserved. + * (Ref: V2 message rendering) + +* **Conversation Summary Never Displayed In V2** + * The summary section of V2's conversation details read a field that does not exist, so a generated summary was never shown. + * (Ref: V2 conversation details, conversation summary) + +#### New Features + +* **Yamcs Actions Can Reach Servers Behind An Authenticating Proxy** + * Ground segments commonly publish Yamcs through a reverse proxy, such as Apache, that challenges every request with HTTP Basic authentication against a directory before the request reaches Yamcs. Yamcs behind that proxy often has no authentication of its own. Until now a Yamcs action could not answer that challenge, so such a server was unreachable even when the Yamcs settings were correct. + * A new **Reverse Proxy Authentication** option on the Yamcs action sends an HTTP Basic `Authorization` header on every request. It is off by default, so a Yamcs server reached directly, such as a local simulator, is unaffected. + * The proxy credential can be typed on the action, with the password stored in Key Vault, or supplied by a reusable **username and password identity**. Where a directory issues temporary passwords, the identity is rotated once under **Workspace → Identities** and every action that references it picks up the new password without being edited. + * The proxy credential has its own identity reference, separate from the Yamcs credential, so one action can use both. + * Proxy authentication combines with the **No Authentication** and **API Key** Yamcs methods. It cannot combine with **Username and Password** or **Access Token**, because only one `Authorization` header can be sent and the Yamcs token request would itself be refused by the proxy. The conflict is reported when saving the action and when running **Test Yamcs Connection** rather than failing later at run time. + * **Test Yamcs Connection** exercises the proxy credential and distinguishes a proxy rejection from a Yamcs rejection. + * (Ref: `functions_yamcs_operations.py`, `yamcs_plugin.py`, `functions_workspace_identities.py`, `plugin_health_checker.py`, `route_backend_plugins.py`, `_plugin_modal.html`, `plugin_modal_stepper.js`, `test_yamcs_basic_auth.py`, [Yamcs Action](features/YAMCS_ACTION.md), [#1435](https://github.com/microsoft/simplechat/issues/1435)) + +### **(v0.261.011)** + +#### Bug Fixes + +* **V2 Composer Menus Opened Off-Screen** + * The Model, Agent, Prompt and Reasoning pickers in the V2 chat page dropped their menus downward from a composer that sits at the bottom of the window, so the options rendered below the bottom edge of the browser and could not be seen or clicked. + * These menus now open upward when there is not enough room below, and size themselves to the space actually available so they stay on screen in short browser windows. Menus still open downward wherever there is room for them. + * The Reasoning picker was the most affected, since it sits on the composer's lower row. + * (Ref: V2 composer pickers, shared dropdown placement) + +* **Redis Connection Test No Longer Returns Credential Errors To The Browser** + * Restored the hardening that a refactor had dropped: when the admin Redis connection test fails while resolving credentials, the details are logged under `[REDIS_TEST]` and the browser receives a generic message instead of the raw exception, which could carry Key Vault secret names, vault URIs, or token details. + * Validation problems such as a missing host name or access key are still returned directly, because those messages are generated by SimpleChat and are what the admin needs to fix the form. + * (Ref: `route_backend_settings.py`, `test_redis_client_factory.py`) + +* **Deployer Redis Kind Detection Matches The Full Host Name Suffix** + * The postprovision fallback that infers the Redis offering from a host name used a substring check, so a host name that merely contained `.redis.azure.net` anywhere could be misread as Azure Managed Redis and configured with the wrong port. + * It now matches the full suffix, consistent with the application's own detection. + * (Ref: `deployers/bicep/postconfig.py`) + +### **(v0.261.010)** + +#### New Features + +* **V2 Enhanced Citations** + * Selecting a citation in the V2 chat page now opens the source itself rather than only the extracted passage. PDFs open at the cited page, images open in a zoomable viewer, video and audio play from the cited moment, spreadsheets open as a browsable table with a sheet switcher, and Visio drawings render page by page. + * Each viewer offers a download of the original file, so the source can be opened in its own application when the in-browser view is not enough. + * Documents can still be opted out individually, and file types with no viewer — plain text or Word, for example — continue to show the cited passage as before. + * When a source cannot be loaded, the passage is shown instead and the panel says that it fell back, so a misconfigured deployment is visible rather than presenting an empty viewer. + * PDFs are rendered by the browser's own viewer, so no PDF engine or other third-party browser asset was added. + * (Ref: V2 enhanced citations, `/api/enhanced_citations/*`, `X-Sub-PDF-Page`, `enable_enhanced_citations`) + +* **Azure Managed Redis Support** + * SimpleChat now connects to Azure Managed Redis as well as Azure Cache for Redis, ahead of the September 30, 2028 retirement of the Azure Cache for Redis Basic, Standard, and Premium tiers. + * The two services listen on different TLS ports, so SimpleChat resolves the port from the host name suffix: `*..redis.azure.net` connects on port 10000 and `*.redis.cache.windows.net` (plus the Azure Government and 21Vianet equivalents) on port 6380. + * New **Redis Service** and **Redis Port** settings let an administrator state the service explicitly when a custom DNS name or private endpoint hides the Azure suffix. Existing deployments are unaffected: unrecognized host names keep the previous Azure Cache for Redis behavior. + * The Redis Metrics panel now reports which service and port were resolved, and whether that came from detection or an explicit setting. + * Both services are supported because Azure Managed Redis is not available in Azure Government or Azure operated by 21Vianet. + * (Ref: `functions_redis_client.py`, `app_settings_cache.py`, `app.py`, `redis-caching.html`, [Azure Managed Redis Support](features/AZURE_MANAGED_REDIS_SUPPORT.md)) + +* **Redis Deployment Uses Azure Managed Redis** + * The Bicep deployer now provisions Azure Managed Redis `Balanced_B0` with high availability enabled, Microsoft's documented replacement for the Azure Cache for Redis Standard C0 it previously deployed. That is twice the memory for less cost. + * A new `redisCacheKind` parameter still deploys classic Azure Cache for Redis for sovereign clouds where Azure Managed Redis is unavailable. + * The database is created with the `NoCluster` clustering policy. The service default is `OSSCluster`, which requires a cluster-aware Redis client that SimpleChat does not use. + * Managed identity deployments disable access keys and grant the web app the built-in `default` access policy on the Azure Managed Redis database. + * (Ref: `deployers/bicep/modules/redisCache.bicep`, `setPermissions.bicep`, `setNativeWebAppPermissions.bicep`, `postconfig.py`, `deployers/version.txt`) + +#### Bug Fixes + +* **Redis Managed Identity Tokens Refresh On Open Connections** + * Redis managed identity authentication now uses the `redis-entraid` streaming credential provider, which renews the Microsoft Entra token in the background and re-issues `AUTH` on connections that are already open. Previously credentials were supplied only at connect time, so a long-lived pooled connection relied on the server dropping it once the token expired. + * The shared application cache and Flask session storage each get their own provider instance, because the provider holds a single re-authentication callback slot and sharing one would leave the first client's connections unrefreshed. + * The admin "Test Redis Connection" button uses a connect-time-only provider, so repeated clicks no longer accumulate background threads, event loops, and recurring token requests. + * The package is imported defensively, falling back to the previous in-repo provider if it is missing, so an application updated without reinstalling requirements still starts. + * (Ref: `functions_redis_client.py`, `requirements.txt`, `test_redis_entra_token_auth.py`) + +* **Redis Connection Test Uses The Same Token Scope As The Application** + * The admin Redis connection test acquired its managed identity token from the legacy cache infrastructure endpoint while the running application used the `https://redis.azure.com/.default` scope, so a passing test did not prove the application could connect. + * The test now builds its client through the same factory as the application, including service and port resolution. + * (Ref: `route_backend_settings.py`) + +### **(v0.261.009)** + +#### New Features + +* **V2 Research, Reasoning And Voice Controls** + * **Deep research** and **Read URLs** are now working controls in the V2 composer rather than placeholders, each appearing only when the corresponding capability is enabled for you. + * **Reasoning effort** can be set per message, and the control only appears for models that actually support it — offering the levels that model allows rather than a fixed list. Models with no reasoning support show no control at all. + * **Voice input** records from your microphone and inserts the transcription into the composer, with an explicit send or cancel so a misheard phrase can be discarded rather than sent. + * **Read aloud** plays any response using the configured speech service. + * Every composer control is now wired; none remain marked as previews. + * (Ref: V2 composer, `/api/speech/transcribe-chat`, `/api/chat/tts`, deep research and URL access request fields) + +#### Bug Fixes + +* **Shared Workspace File Approvals Are Visible To Approvers Again** + * Fixed document access index candidate selection for workspace scope projections so pending-approval records are considered alongside already granted records. + * Shared files staged for approval are intentionally not granted yet, so filtering only on `access_granted = true` could hide those files from approval experiences even though they were eligible for review. + * The projection query now includes `approval_status = not_approved` rows while still requiring current-version projection records. + * (Ref: `functions_document_access_index.py`, [Workspace Shared File Approval Visibility Fix](fixes/WORKSPACE_SHARED_FILE_APPROVAL_VISIBILITY_FIX.md)) + +* **Distroless Runtime Copy No Longer Fails On `/usr/lib64` Overlay Conflicts** + * Fixed Docker BuildKit failures where `COPY --from=builder /odbc-runtime/ /` or `COPY --from=builder /playwright-runtime/ /` could abort with `cannot copy to non-directory ... /usr/lib64` when the distroless base exposes `/usr/lib64` as a non-directory entry. + * Updated runtime staging to copy native shared libraries into `/odbc-runtime/usr/lib` and `/playwright-runtime/usr/lib` while continuing to source candidates from both `/usr/lib64` and `/usr/lib` in the builder stage. + * This preserves SQL ODBC and Playwright Chromium runtime packaging while avoiding path-type collisions against evolving base-image filesystem layouts. + * (Ref: `Dockerfile`, `test_sql_container_odbc_runtime.py`, `test_deep_research_chromium_build_opt_out.py`, [Distroless Runtime Overlay Path Fix](fixes/DISTROLESS_RUNTIME_OVERLAY_PATH_FIX.md)) + +### **(v0.261.008)** + +#### New Features + +* **V2 Citations** + * Responses in the V2 chat page now show citations as compact inline chips instead of the raw `(Source: ...)` text the model emits. Each chip names the file and the page, sheet, or location it came from. + * Selecting a chip opens the passage that was actually cited, along with its file name and location. + * Citations that point at a web page render as an outbound link instead, opening in a new tab, since there is no stored passage to show for them. + * Answers without citations are unaffected, and tables, code blocks, and other formatting continue to render normally around the chips. + * (Ref: V2 citations, `/api/get_citation`, citation marker format) diff --git a/docs/explanation/release-notes/v0.261.md b/docs/explanation/release-notes/v0.261-part-7.md similarity index 96% rename from docs/explanation/release-notes/v0.261.md rename to docs/explanation/release-notes/v0.261-part-7.md index 79a29078a..a9ccc7f0b 100644 --- a/docs/explanation/release-notes/v0.261.md +++ b/docs/explanation/release-notes/v0.261-part-7.md @@ -1,66 +1,27 @@ --- -title: "Release notes 0.261 series" -description: "SimpleChat release notes for 0.261.010 – 0.261.001." +title: "Release notes 0.261.007 – 0.261.001" +description: "SimpleChat release notes for 0.261.007 – 0.261.001." section: "Reference" layout: page --- -# Release notes 0.261 series +# Release notes 0.261.007 – 0.261.001 [Back to release notes index]({{ '/explanation/release_notes/' | relative_url }}) -### **(v0.261.010)** +### **(v0.261.007)** #### New Features -* **Azure Managed Redis Support** - * SimpleChat now connects to Azure Managed Redis as well as Azure Cache for Redis, ahead of the September 30, 2028 retirement of the Azure Cache for Redis Basic, Standard, and Premium tiers. - * The two services listen on different TLS ports, so SimpleChat resolves the port from the host name suffix: `*..redis.azure.net` connects on port 10000 and `*.redis.cache.windows.net` (plus the Azure Government and 21Vianet equivalents) on port 6380. - * New **Redis Service** and **Redis Port** settings let an administrator state the service explicitly when a custom DNS name or private endpoint hides the Azure suffix. Existing deployments are unaffected: unrecognized host names keep the previous Azure Cache for Redis behavior. - * The Redis Metrics panel now reports which service and port were resolved, and whether that came from detection or an explicit setting. - * Both services are supported because Azure Managed Redis is not available in Azure Government or Azure operated by 21Vianet. - * (Ref: `functions_redis_client.py`, `app_settings_cache.py`, `app.py`, `redis-caching.html`, [Azure Managed Redis Support](features/AZURE_MANAGED_REDIS_SUPPORT.md)) - -* **Redis Deployment Uses Azure Managed Redis** - * The Bicep deployer now provisions Azure Managed Redis `Balanced_B0` with high availability enabled, Microsoft's documented replacement for the Azure Cache for Redis Standard C0 it previously deployed. That is twice the memory for less cost. - * A new `redisCacheKind` parameter still deploys classic Azure Cache for Redis for sovereign clouds where Azure Managed Redis is unavailable. - * The database is created with the `NoCluster` clustering policy. The service default is `OSSCluster`, which requires a cluster-aware Redis client that SimpleChat does not use. - * Managed identity deployments disable access keys and grant the web app the built-in `default` access policy on the Azure Managed Redis database. - * (Ref: `deployers/bicep/modules/redisCache.bicep`, `setPermissions.bicep`, `setNativeWebAppPermissions.bicep`, `postconfig.py`, `deployers/version.txt`) - -#### Bug Fixes - -* **Redis Managed Identity Tokens Refresh On Open Connections** - * Redis managed identity authentication now uses the `redis-entraid` streaming credential provider, which renews the Microsoft Entra token in the background and re-issues `AUTH` on connections that are already open. Previously credentials were supplied only at connect time, so a long-lived pooled connection relied on the server dropping it once the token expired. - * The shared application cache and Flask session storage each get their own provider instance, because the provider holds a single re-authentication callback slot and sharing one would leave the first client's connections unrefreshed. - * The admin "Test Redis Connection" button uses a connect-time-only provider, so repeated clicks no longer accumulate background threads, event loops, and recurring token requests. - * The package is imported defensively, falling back to the previous in-repo provider if it is missing, so an application updated without reinstalling requirements still starts. - * (Ref: `functions_redis_client.py`, `requirements.txt`, `test_redis_entra_token_auth.py`) - -* **Redis Connection Test Uses The Same Token Scope As The Application** - * The admin Redis connection test acquired its managed identity token from the legacy cache infrastructure endpoint while the running application used the `https://redis.azure.com/.default` scope, so a passing test did not prove the application could connect. - * The test now builds its client through the same factory as the application, including service and port resolution. - * (Ref: `route_backend_settings.py`) - -### **(v0.261.009)** - -#### Bug Fixes - -* **Shared Workspace File Approvals Are Visible To Approvers Again** - * Fixed document access index candidate selection for workspace scope projections so pending-approval records are considered alongside already granted records. - * Shared files staged for approval are intentionally not granted yet, so filtering only on `access_granted = true` could hide those files from approval experiences even though they were eligible for review. - * The projection query now includes `approval_status = not_approved` rows while still requiring current-version projection records. - * (Ref: `functions_document_access_index.py`, [Workspace Shared File Approval Visibility Fix](fixes/WORKSPACE_SHARED_FILE_APPROVAL_VISIBILITY_FIX.md)) - -* **Distroless Runtime Copy No Longer Fails On `/usr/lib64` Overlay Conflicts** - * Fixed Docker BuildKit failures where `COPY --from=builder /odbc-runtime/ /` or `COPY --from=builder /playwright-runtime/ /` could abort with `cannot copy to non-directory ... /usr/lib64` when the distroless base exposes `/usr/lib64` as a non-directory entry. - * Updated runtime staging to copy native shared libraries into `/odbc-runtime/usr/lib` and `/playwright-runtime/usr/lib` while continuing to source candidates from both `/usr/lib64` and `/usr/lib` in the builder stage. - * This preserves SQL ODBC and Playwright Chromium runtime packaging while avoiding path-type collisions against evolving base-image filesystem layouts. - * (Ref: `Dockerfile`, `test_sql_container_odbc_runtime.py`, `test_deep_research_chromium_build_opt_out.py`, [Distroless Runtime Overlay Path Fix](fixes/DISTROLESS_RUNTIME_OVERLAY_PATH_FIX.md)) - -### **(v0.261.007)** +* **V2 Per-Message Actions** + * Added an action row to every message in the V2 chat page, appearing on hover and reachable by keyboard. + * **Both roles**: copy, retry, use as prompt, download as Markdown, export to Word or PowerPoint, open as an email draft, and delete. + * **Your messages**: edit in place and resend, which creates a new attempt rather than overwriting the original. + * **Responses**: rate with thumbs up or down (a thumbs down asks for an optional reason), and fork the conversation from that point into a new one, which then opens. + * **Attempt navigation**: when a message has been retried or edited, arrows appear showing which attempt you are viewing and let you page between them. + * (Ref: V2 message actions, `/api/message/` retry, edit, switch-attempt, delete, `/feedback/submit`, conversation fork) #### Bug Fixes @@ -71,6 +32,21 @@ layout: page ### **(v0.261.006)** +#### New Features + +* **V2 Conversation Details** + * Added a conversation details view to the V2 chat page, opened from an info button in the header. It shows the title, identifier, last updated time, conversation type, pinned/hidden/scope-locked state, classification, tags, how many documents were used, and the generated summary along with which model produced it and when. + * The conversation can be renamed inline from this view. Renaming to the same title does nothing rather than issuing a pointless save. + * Only fields the server actually returns are displayed, so the panel never shows permanently blank rows for data that does not exist. + * (Ref: V2 conversation details, `/api/conversations//metadata`) + +#### User Interface Enhancements + +* **V2 Dialogs And Menus Are Now Readable Over Content** + * Dialogs, dropdown menus, and the account menu in the V2 interface used the same translucent glass treatment as the fixed page furniture, which left chat text legible straight through them and made the panel itself hard to read. + * Overlays now use a dedicated near-opaque surface in both light and dark themes, while keeping the glass look. Users who ask their system for reduced transparency continue to get fully solid panels. + * (Ref: V2 design tokens, dialog and popover surfaces) + #### Bug Fixes * **Markdown Uploads Retry Transient OrderedDict Parser Failures** @@ -80,6 +56,15 @@ layout: page ### **(v0.261.005)** +#### New Features + +* **V2 Chat Conversation Drawer** + * Added a right-hand panel to the V2 chat page with two modes, matching the drawer in the current interface. + * **Contents** lists your questions in order so you can jump straight to a point in a long conversation; selecting one scrolls the thread there and briefly highlights it. It respects the same **Conversation Contents Drawer** admin setting and per-user preference the current interface uses, so turning it off hides it in both. + * **Documents** lists the documents behind a conversation, marking which were actually **Cited**, where the citations landed (pages or sheets), which workspace each came from, and its classification. The toggle carries a count badge. + * Documents are gathered from all three places the server records them, de-duplicated, so files attached directly to a chat appear alongside ones found through search. + * (Ref: V2 conversation drawer, `/api/conversations//metadata`, `build_used_documents`) + #### User Interface Enhancements * **Workspace Upload Progress Now Separates Request Status From Document Processing Status** @@ -91,6 +76,26 @@ layout: page #### Bug Fixes +* **V2 Workspace Page Crashed To A Blank Screen** + * Opening **My Workspace** in the V2 interface blanked the page, leaving only a minified React error in the browser console. + * Workspace tags are returned as objects carrying a name, a usage count and a colour, but the V2 tag filter rendered the object itself, which React cannot display. The failure then unmounted the whole interface rather than just the tag row. + * Tags are now read correctly in every shape the API uses, and the chips show their document counts. + * (Ref: `/api/documents/tags`, `build_workspace_tags_from_counts`, V2 workspace page) + +* **V2 Chat Logged A 404 When Opening A Conversation** + * Selecting a conversation in the V2 interface fired a "mark as read" request every time, which failed with a 404 for shared conversations because those are stored separately and have their own endpoint. + * The request is now sent only when a conversation actually has an unread response, and shared conversations use the shared endpoint — matching how the existing interface behaves. + * (Ref: conversation feed, mark-read endpoints, V2 conversation rail) + +* **V2 Pin And Unread Indicators Never Appeared** + * Pinned conversations showed no pin icon and unread conversations showed no dot, because the V2 rail read the wrong field names from the conversation feed. The underlying pin and hide actions worked, so only the indicators were affected. + * Pin and hide are also server-side toggles rather than settable states, so V2 now applies whatever the server reports back instead of assuming the result. + * (Ref: `is_pinned`, `is_hidden`, `has_unread_assistant_response`, V2 conversation rail) + +* **A Single View Failure No Longer Takes Down The V2 Interface** + * Added an error boundary around the V2 content area. An unexpected render failure now shows a contained message with the error and a retry button while the navigation rail keeps working, instead of blanking the entire application. + * (Ref: V2 error boundary, `App.tsx`) + * **Large Workspace Uploads No Longer Fail On Search Write Gate Contention** * Fixed partial failures when uploading many small Markdown, JSON, or YAML files to personal, group, or public workspaces at once. Document processing could fail with a message that the Data Management Search write gate changed too often to reserve a write slot. * The shared write gate now waits within the existing request timeout budget, briefly backs off after transient Cosmos ETag conflicts, and serializes Search writes inside each worker process. This prevents local upload threads from stampeding the same gate document while preserving the migration freeze protection for Azure AI Search writes. @@ -100,6 +105,30 @@ layout: page ### **(v0.261.003)** +#### New Features + +* **React V2 Interface (Preview)** + * Added an optional React interface at **/v2**, running alongside the existing UI rather than replacing it. Both surfaces share one login, so you can move between `/chats` and `/v2` freely and compare them directly. + * Built with React 18, TypeScript, Vite and Tailwind on a custom glassmorphism design system, with full dark and light modes. The theme is remembered per browser and applied before first paint, so switching to dark mode never flashes white. + * Keeps the current layout language: logo in the upper left, a single collapsible left rail carrying navigation, conversations, theme and account, and no top bar. All content lives in the right-hand pane. + * Chat is wired to the existing APIs — conversation list with paging and search, rename, pin, hide and delete, streaming responses with a collapsible reasoning panel, stop generation, model, agent and prompt pickers, document and web search toggles, and file upload. Controls that are part of the design but not yet connected are visibly marked **Preview** rather than silently doing nothing. + * Personal workspace documents can be listed, searched, filtered by tag, uploaded and deleted. + * Nothing in the existing interface changed. No current route, template, or JavaScript module was modified. + * (Ref: `application/v2_ui/`, `route_frontend_v2.py`, `route_backend_v2.py`, `docs/explanation/features/REACT_V2_UI.md`) + +* **Search-First Admin Settings In V2** + * Reimagined Admin Settings for the V2 interface. The classic page nests 14 groups into 46 tabs into 96 sections, so reaching one toggle can take several clicks through two levels of tabs. + * V2 flattens it: a category rail, one scrollable pane, and a search box that matches across every section, tab, group and capability key at once. Press `/` to focus search from anywhere on the page — searching either `retention` or `data lifecycle` finds the retention settings. + * Toggles save individually and roll back visually if the save fails, so the switch never claims a change that did not persist. + * The structure is still generated from `admin_settings_nav.py`, so it cannot drift from the classic page. Settings needing more than a switch — endpoints, keys, prompts, connection tests — remain on the classic page, which is linked from the V2 page. + * (Ref: `admin_settings_nav.py`, `/api/v2/admin/settings`, V2 admin surface) + +* **Optional Standalone Hosting For The V2 Interface** + * The V2 interface is served by the existing App Service at `/v2` by default, so it needs no new infrastructure and inherits the current sign-in, CSRF protection and content security policy unchanged. + * For deployments that want the front end deployed and scaled on its own, a new opt-in `deployV2FrontendAppService` parameter provisions a dedicated App Service for it. Setting `V2_UI_ALLOWED_ORIGIN` on the API app is the single switch that enables the cross-origin configuration this requires. + * Both settings are off by default and have no effect on existing deployments. + * (Ref: `deployers/bicep/modules/v2FrontendAppService.bicep`, `main.bicep`, `deployers/azure.yaml`, `V2_UI_ALLOWED_ORIGIN`) + #### Bug Fixes * **Broken Documentation Links Repaired** @@ -797,6 +826,7 @@ layout: page * Previously the modal only collected a connection string even though the backend accepted other methods, so managed identity was not reachable through the UI. * The endpoint field is validated against the Azure Blob hostname allowlist before the action is saved. * (Ref: `_plugin_modal.html`, `plugin_modal_stepper.js`, `blob_storage.definition.json`) + * **Shared Conversations No Longer Fail With "Stream interrupted: Forbidden"** * Fixed invited participants being unable to invoke the AI at all in a shared conversation. Any explicit AI request returned `Forbidden` with no content. * Root cause was the hidden source conversation behind every shared conversation being owned by its creator, so participants failed a plain ownership comparison in the chat streaming route even though they are legitimate members. diff --git a/docs/explanation/release_notes.md b/docs/explanation/release_notes.md index 0bd86786d..b92968011 100644 --- a/docs/explanation/release_notes.md +++ b/docs/explanation/release_notes.md @@ -2,6 +2,31 @@ For feature-focused and fix-focused drill-downs by version, see [Features by Version](https://github.com/microsoft/simplechat/tree/main/docs/explanation/features) and [Fixes by Version](https://github.com/microsoft/simplechat/tree/main/docs/explanation/fixes). +### **(v0.261.333)** + +#### New Features + +* **Optional Private Monitoring In Commercial And Government** + * Deployment administrators can opt into Azure Monitor Private Link Scope (AMPLS) for Application Insights and Log Analytics in either cloud. The option defaults off and requires private networking; when enabled, monitoring ingestion and queries use private-only access. + * Bicep/AZD, Terraform, and Azure CLI support cloud-specific monitoring DNS and existing-zone reuse. AMPLS reuses Blob DNS, and Government's additional ADX zone is handled separately from the five-zone private endpoint DNS group. + * (Ref: `enableAzureMonitorPrivateLink`, `ENABLE_AZURE_MONITOR_PRIVATE_LINK`, `azureMonitorPrivateNetworking.bicep`, [Azure Monitor Private Link](features/AZURE_MONITOR_PRIVATE_LINK.md), [#1765](https://github.com/microsoft/simplechat/issues/1765)) + +#### Bug Fixes + +* **Redis Private Endpoints And Cloud DNS Corrections** + * Private Bicep/AZD deployments now create the appropriate Managed or classic Redis private endpoint. Redis remains reachable during bootstrap, and the final `azd up` hook disables public access after configuration succeeds. Separate provision/deploy workflows must perform equivalent final lockdown. + * Commercial Video Indexer uses `privatelink.api.videoindexer.ai`, while unsupported Government private endpoint creation remains disabled. Government Key Vault now uses `privatelink.vaultcore.usgovcloudapi.net` across all three deployers. + * (Ref: `privateNetworking.bicep`, `redisCache.bicep`, `privateDNSZones.json`, `deployers/azure.yaml`, [Private Endpoint And DNS Fix](fixes/DEPLOYER_PRIVATE_ENDPOINT_DNS_FIX.md), [#1765](https://github.com/microsoft/simplechat/issues/1765)) + +#### Documentation And Architecture + +* **Expanded Visio Architectures And Deployment Network References** + * Updated `artifacts/architecture.vsdx` and `artifacts/private_endpoints.vsdx` with Azure Container Registry, Key Vault, platform networking, compact subnet sizing, monitoring DNS, and Commercial/Government service distinctions, including Managed versus classic Redis. + * Cloud-specific tables distinguish fresh script-created networks from a compact existing-network reference, showing CIDRs, total and Azure-usable addresses, delegation, service placement, and growth space. The compact segmented reference is not automatically created by existing-VNet reuse. + * Added a Visio companion and deployment guidance covering AMPLS capacity, shared-DNS impacts, bootstrap lockdown, and cloud limitations. Diagram labels do not override the documented service/private-endpoint availability requirements. + * Deployer version: **1.0.34**. Live-cloud connectivity and monitoring validation remain deployment checks. + * (Ref: `artifacts/architecture.vsdx`, `artifacts/private_endpoints.vsdx`, [Diagram Network Reference](../../artifacts/PRIVATE_ENDPOINTS_NETWORK_REFERENCE.md), [Enterprise Networking](../guides/enterprise-networking.md), [#1765](https://github.com/microsoft/simplechat/issues/1765)) + ### **(v0.261.315)** #### New Features diff --git a/docs/guides/enterprise-networking.md b/docs/guides/enterprise-networking.md index 4a0d6fddd..a9c9aa927 100644 --- a/docs/guides/enterprise-networking.md +++ b/docs/guides/enterprise-networking.md @@ -56,10 +56,71 @@ Enterprise networking for Simple Chat involves: ![Architecture with Private Endpoints in Azure Commercial](../images/architecture-private-endpoints-commercial.png) +**Fresh deployment defaults:** With private networking enabled and no existing VNet or subnet IDs supplied, Bicep/AZD, Terraform, and the Azure CLI script create the following network. Terraform and CLI address ranges can be overridden; these are their defaults. + +| Network/Subnet | Address space | Addresses | Azure-usable | Delegation / Notes | +|---|---|---:|---:|---| +| **VNet** | **`10.0.0.0/21`** | **2048** | - | Fresh deployment; no existing network supplied | +| `AppServiceIntegration` | `10.0.0.0/24` | 256 | 251 | `Microsoft.Web/serverFarms`; outbound only | +| `PrivateEndpoints` | `10.0.2.0/24` | 256 | 251 | All created private endpoints, including app inbound and optional AMPLS | +| **Unallocated** | **`10.0.1.0/24`, `10.0.3.0/24`, `10.0.4.0/22`** | **1536** | - | Reserved for future subnets | + +**Compact existing-network reference:** This is a segmented design for an enterprise-managed network, not the automatic output of the scripts. + +| Network/Subnet | Address space | Addresses | Azure-usable | Delegation / Notes | +|---|---|---:|---:|---| +| **VNet** | **`10.42.0.0/24`** | **256** | - | Example nonoverlapping enterprise allocation | +| `snet-app-outbound` | `10.42.0.0/26` | 64 | 59 | `Microsoft.Web/serverFarms`; App Service integration | +| `snet-platform-pe` | `10.42.0.64/26` | 64 | 59 | ACR, Key Vault, optional AMPLS | +| `snet-data-pe` | `10.42.0.128/27` | 32 | 27 | Cosmos DB, Blob Storage, optional Managed Redis | +| `snet-ai-pe` | `10.42.0.160/27` | 32 | 27 | OpenAI, Search, Document Intelligence; optional Content Safety, Speech, Video Indexer | +| `snet-app-inbound-pe` | `10.42.0.192/28` | 16 | 11 | Primary App Service private endpoint | +| **Unallocated** | **`10.42.0.208-10.42.0.255`** | **48** | - | Future growth; divisible into `/28` and `/27` blocks | + ### Networking in Azure Government ![Architecture with Private Endpoints in Azure Government](../images/architecture-private-endpoints-government.png) +**Fresh deployment defaults:** With private networking enabled and no existing VNet or subnet IDs supplied, all three deployers create the same network shape as Commercial. DNS zones and optional service availability differ. + +| Network/Subnet | Address space | Addresses | Azure-usable | Delegation / Notes | +|---|---|---:|---:|---| +| **VNet** | **`10.0.0.0/21`** | **2048** | - | Fresh deployment; no existing network supplied | +| `AppServiceIntegration` | `10.0.0.0/24` | 256 | 251 | `Microsoft.Web/serverFarms`; outbound only | +| `PrivateEndpoints` | `10.0.2.0/24` | 256 | 251 | All supported private endpoints, including optional Government AMPLS | +| **Unallocated** | **`10.0.1.0/24`, `10.0.3.0/24`, `10.0.4.0/22`** | **1536** | - | Reserved for future subnets | + +**Compact existing-network reference:** Use classic Azure Cache for Redis instead of Managed Redis, and do not assume a Government Video Indexer private endpoint exists. + +| Network/Subnet | Address space | Addresses | Azure-usable | Delegation / Notes | +|---|---|---:|---:|---| +| **VNet** | **`10.42.0.0/24`** | **256** | - | Example nonoverlapping enterprise allocation | +| `snet-app-outbound` | `10.42.0.0/26` | 64 | 59 | `Microsoft.Web/serverFarms`; App Service integration | +| `snet-platform-pe` | `10.42.0.64/26` | 64 | 59 | ACR, Key Vault, optional Government AMPLS | +| `snet-data-pe` | `10.42.0.128/27` | 32 | 27 | Cosmos DB, Blob Storage, optional classic Redis | +| `snet-ai-pe` | `10.42.0.160/27` | 32 | 27 | OpenAI, Search, Document Intelligence; Content Safety/Speech where available | +| `snet-app-inbound-pe` | `10.42.0.192/28` | 16 | 11 | Primary App Service private endpoint | +| **Unallocated** | **`10.42.0.208-10.42.0.255`** | **48** | - | Future growth; divisible into `/28` and `/27` blocks | + +Azure reserves five addresses **per subnet**, not per VNet. Unallocated counts are raw addresses; reservations apply when those ranges become subnets. AMPLS requires at least 11 private IPs and a `/27` or larger subnet; `/26` is recommended when sharing with platform endpoints. ACR consumes at least two IPs and Cosmos DB consumes a global IP plus regional IPs. Budget for replicas, additional regions, and future endpoints. + +### Script Deployment Versus Reference Segmentation + +The current scripts accept **one** App Service integration subnet and **one shared** private endpoint subnet. Supplying an existing VNet does not distribute private endpoints among the five reference subnets. To implement the compact segmented diagram, customize endpoint subnet assignments in IaC or provision them through enterprise network tooling. PaaS services remain outside the VNet; their private endpoint NICs occupy these subnets. Private DNS zones are linked to the VNet, not placed inside subnets. + +| Dependency | Bicep/AZD private endpoint | Terraform / Azure CLI | Compact reference placement | +|---|---|---|---| +| Primary App Service | Yes, inbound | Yes, inbound | App inbound | +| Cosmos DB NoSQL, Blob Storage | Yes | Yes | Data | +| Azure Container Registry, Key Vault | Yes | Yes | Platform | +| OpenAI, AI Search, Document Intelligence | Yes; external OpenAI needs resource metadata | Yes; external OpenAI needs resource metadata | AI | +| Redis | Optional; Managed in Commercial, select `classic` in Government | Not provisioned | Data | +| Content Safety, Speech | Optional; subject to regional availability | Not provisioned | AI | +| Video Indexer | Optional PE in supported Commercial API profile; no Government PE | Same cloud limitation | AI only where PE supported | +| AMPLS | Optional, default off in both clouds | Optional, default off in both clouds | Platform | + +Endpoint entries assume private networking and the corresponding optional resource are enabled. Separate Foundry project/Agent Service runtime networking and optional native/V2 frontend inbound endpoints are not provisioned by this networking module. + ## Prerequisites - Azure subscription with network administrator permissions @@ -89,14 +150,7 @@ If you are unsure which public egress IP is being used, run the lookup from the ### Network Topology Planning -**Decide on VNet structure:** -``` -Production VNet (10.0.0.0/16): -├── App Service Subnet (10.0.1.0/24) -├── Private Endpoint Subnet (10.0.2.0/24) -├── Database Subnet (10.0.3.0/24) -└── Management Subnet (10.0.4.0/24) -``` +Choose between the fresh-deployment defaults and compact enterprise reference above. Avoid ranges overlapping corporate networks, VPNs, or peered VNets. App Service inbound endpoints must not use the delegated outbound subnet. **Consider connectivity requirements:** - On-premises connectivity (ExpressRoute/VPN) @@ -115,6 +169,10 @@ Production VNet (10.0.0.0/16): - Azure Document Intelligence - Azure Content Safety - Azure Speech Services (if used) +- Azure Container Registry and Key Vault +- Redis when enabled through Bicep/AZD +- Video Indexer only where its private endpoint API is supported +- AMPLS when opted in ## Step 2: Create Virtual Network Infrastructure @@ -130,28 +188,24 @@ Production VNet (10.0.0.0/16): 3. **Configure IP addresses:** ``` - IPv4 address space: 10.0.0.0/16 + IPv4 address space: 10.0.0.0/21 ``` 4. **Create subnets:** ``` Subnet 1: - Name: app-service-subnet - Address range: 10.0.1.0/24 + Name: AppServiceIntegration + Address range: 10.0.0.0/24 Subnet 2: - Name: private-endpoint-subnet + Name: PrivateEndpoints Address range: 10.0.2.0/24 - - Subnet 3: - Name: database-subnet - Address range: 10.0.3.0/24 ``` ### Configure Subnet Delegation **For App Service integration:** -1. **Select app-service-subnet** +1. **Select AppServiceIntegration** 2. **Configure delegation:** - Delegate subnet to: `Microsoft.Web/serverFarms` - This allows App Service to integrate with the subnet @@ -193,7 +247,7 @@ Outbound Rules: 4. **Configure:** ``` Virtual Network: simplechat-prod-vnet - Subnet: app-service-subnet + Subnet: AppServiceIntegration ``` ### Configure Route All Traffic @@ -280,12 +334,23 @@ Create Private Endpoints for each Azure service Simple Chat uses. ### Verify DNS Zone Creation -Private DNS zones should be created automatically with Private Endpoints: -- `privatelink.documents.azure.com` -- `privatelink.openai.azure.com` -- `privatelink.search.windows.net` -- `privatelink.blob.core.windows.net` -- `privatelink.cognitiveservices.azure.com` +Use the cloud-specific zones below. Optional zones are only created when their endpoints are deployed. + +| Service | Azure Commercial zone | Azure Government zone | +|---|---|---| +| App Service | `privatelink.azurewebsites.net` | `privatelink.azurewebsites.us` | +| Cosmos DB NoSQL | `privatelink.documents.azure.com` | `privatelink.documents.azure.us` | +| Blob Storage | `privatelink.blob.core.windows.net` | `privatelink.blob.core.usgovcloudapi.net` | +| ACR | `privatelink.azurecr.io` | `privatelink.azurecr.us` | +| Key Vault | `privatelink.vaultcore.azure.net` | `privatelink.vaultcore.usgovcloudapi.net` | +| AI Search | `privatelink.search.windows.net` | `privatelink.search.azure.us` | +| OpenAI | `privatelink.openai.azure.com` | `privatelink.openai.azure.us` | +| Document Intelligence, Content Safety, Speech | `privatelink.cognitiveservices.azure.com` | `privatelink.cognitiveservices.azure.us` | +| Managed Redis | `privatelink.redis.azure.net` | Not available | +| Classic Redis | `privatelink.redis.cache.windows.net` | `privatelink.redis.cache.usgovcloudapi.net` | +| Video Indexer | `privatelink.api.videoindexer.ai` | No PE in the Government API profile used by these scripts | + +Keep normal service hostnames in application configuration. Private DNS resolves them to private IPs; do not replace URLs with private IP addresses or intermediary `privatelink` hostnames. Ensure all ACR registry/data endpoint records exist. App Service app and SCM records share one private zone. Modern Foundry resources may need additional endpoint-specific zones; separate Foundry resource/project endpoints are not deployed here. ### Link DNS Zones to VNet @@ -315,6 +380,37 @@ nslookup your-search-service.search.windows.net ## Step 6: Secure Service Access +### Optional Private Monitoring with AMPLS + +Implemented in version: **0.261.333**; deployer **1.0.34**. Related issue: [#1765](https://github.com/microsoft/simplechat/issues/1765). + +AMPLS provides private Application Insights telemetry ingestion and Log Analytics ingestion/queries. This is a deployment option, not an in-app Admin Settings switch. It is **off by default**, even for private-network deployments. + +| Deployer | Opt-in | +|---|---| +| Bicep/AZD | `azd env set ENABLE_AZURE_MONITOR_PRIVATE_LINK true`, with `ENABLE_PRIVATE_NETWORKING=true` | +| Terraform | `param_enable_azure_monitor_private_link = true`, with `param_enable_private_networking = true` | +| Azure CLI PowerShell | `$param_EnableAzureMonitorPrivateLink = $true`, with `$param_EnablePrivateNetworking = $true` | + +The option creates an AMPLS, associates the deployed Application Insights and Log Analytics resources, and creates one `azuremonitor` endpoint in the shared endpoint subnet. Both access modes are `PrivateOnly`; public ingestion and queries are disabled on the monitoring resources. Portal, identity, and control-plane endpoints still require approved outbound access. AMPLS does not privatize Entra, Microsoft Graph, or external tools. + +| DNS override key | Azure Commercial zone | Azure Government zone | +|---|---|---| +| `monitor` | `privatelink.monitor.azure.com` | `privatelink.monitor.azure.us` | +| `monitorOms` | `privatelink.oms.opinsights.azure.com` | `privatelink.oms.opinsights.azure.us` | +| `monitorOds` | `privatelink.ods.opinsights.azure.com` | `privatelink.ods.opinsights.azure.us` | +| `monitorAgent` | `privatelink.agentsvc.azure-automation.net` | `privatelink.agentsvc.azure-automation.us` | +| `blobStorage` | `privatelink.blob.core.windows.net` | `privatelink.blob.core.usgovcloudapi.net` | +| `monitorAdx` | Not created | `privatelink.adx.monitor.azure.us` | + +Blob DNS is reused rather than created twice. Azure permits five zones in a PE DNS group. The four core monitoring zones plus Blob use that group; Government ADX DNS is linked separately, with any matching DNS/IP metadata from the endpoint NIC written as A-record sets. If the endpoint uses `adx.privatelink.monitor.azure.us`, its record remains in the core Monitor zone. Reprovision after changing Government endpoint IPs or returned DNS metadata. + +Reuse centrally managed zones using `privateDnsZoneConfigs` (Terraform: `param_private_dns_zone_configs`). Use `zoneResourceId` and `createVNetLink` for Bicep/CLI, or `zone_resource_id` and `create_vnet_link` for Terraform. Disable link creation only when the zone already has the necessary VNet link. Custom/on-premises DNS must forward the appropriate public service namespaces to a resolver in a VNet linked to these zones. + +Coordinate with your monitoring team: use **one AMPLS per shared DNS boundary**. A new private-only scope can redirect shared endpoints for monitoring resources outside that scope. Do not enable this automatic new-scope option if enterprise AMPLS already serves the same DNS boundary; have its owner associate these resources instead. All telemetry senders and monitoring users need private routing and DNS. Validate ingestion and queries before rollout. + +Bicep/AZD keeps Redis public during bootstrap so post-configuration can connect and publish settings. `azd up` closes Redis public access in `postup`, after deployment; `azd provision` or `azd deploy` alone does not run that final hook. For separate workflows, run equivalent lockdown only after configuration succeeds. Future provisioning resets Managed Redis bootstrap access, so lockdown must run again. Disabling AMPLS in an incremental Bicep/CLI deployment does not delete its old scope, endpoint, links, or DNS records; coordinate explicit cleanup. + ### Disable Public Access **For each Azure service:** diff --git a/functional_tests/test_azd_private_dns_zone_config_wiring.py b/functional_tests/test_azd_private_dns_zone_config_wiring.py index 4d28c0098..5d172c4c7 100644 --- a/functional_tests/test_azd_private_dns_zone_config_wiring.py +++ b/functional_tests/test_azd_private_dns_zone_config_wiring.py @@ -2,8 +2,9 @@ #!/usr/bin/env python3 """ Functional test for AZD private DNS zone config wiring. -Version: 0.240.006 +Version: 0.261.333 Implemented in: 0.240.006 +Enhanced in: 0.261.333 (Refs #1765) This test ensures that AZD deployments can pass private DNS zone reuse settings through the PRIVATE_DNS_ZONE_CONFIGS environment value instead of silently @@ -11,7 +12,18 @@ """ from pathlib import Path +import importlib.util +import ipaddress +import json +import shutil +import subprocess import sys +from unittest.mock import patch + +import pytest +import yaml + +from test_support.versioning import assert_app_version_at_least REPO_ROOT = Path(__file__).resolve().parents[1] @@ -26,14 +38,13 @@ def require_contains(content: str, expected: str, description: str) -> None: raise AssertionError(f"Missing {description}: {expected}") -def test_azd_private_dns_zone_configs_are_wired() -> bool: +def test_azd_private_dns_zone_configs_are_wired() -> None: print("🧪 Testing AZD private DNS zone config parameter wiring") print("=" * 70) parameters_content = PARAMETERS_PATH.read_text(encoding="utf-8") prerequisites_content = PREREQUISITES_PATH.read_text(encoding="utf-8") readme_content = README_PATH.read_text(encoding="utf-8") - config_content = CONFIG_PATH.read_text(encoding="utf-8") require_contains( parameters_content, @@ -55,22 +66,244 @@ def test_azd_private_dns_zone_configs_are_wired() -> bool: "PRIVATE_DNS_ZONE_CONFIGS", "README guidance for AZD private DNS zone configuration", ) - require_contains( - config_content, - 'VERSION = "0.240.006"', - "config version bump", - ) + assert_app_version_at_least("0.240.006") print("✅ AZD parameters file passes PRIVATE_DNS_ZONE_CONFIGS into Bicep") print("✅ Preprovision validation reads PRIVATE_DNS_ZONE_CONFIGS before deployment") print("✅ README documents the AZD environment variable path") print("✅ Config version was updated for the fix") - return True + + +def load_prerequisites(): + spec = importlib.util.spec_from_file_location('networking_prerequisites_under_test', PREREQUISITES_PATH) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_monitor_private_link_preflight(): + module = load_prerequisites() + with patch.object(module, '_run_command') as command: + disabled = module._validate_monitor_private_link(False, False, '') + invalid = module._validate_monitor_private_link(False, True, '') + new_network = module._validate_monitor_private_link(True, True, '') + assert disabled and new_network and not invalid + command.assert_not_called() + + for prefix, expected in [('10.0.2.0/28', False), ('10.0.2.0/27', True), ('10.0.2.0/26', True)]: + with patch.object(module, '_run_command', return_value=(0, json.dumps({'addressPrefix': prefix}), '')) as command: + valid = module._validate_monitor_private_link(True, True, '/subscriptions/test/subnets/pe') + assert valid is expected + assert command.call_args.args[0][1:5] == ['network', 'vnet', 'subnet', 'show'] + + with patch.object(module, '_run_command', return_value=(1, '', 'access denied')): + failed = module._validate_monitor_private_link(True, True, '/subnet') + assert not failed + + with patch.object(module, '_run_command', return_value=(0, '{"addressPrefix":"not-a-network"}', '')): + malformed = module._validate_monitor_private_link(True, True, '/subnet') + assert not malformed + + +def test_monitor_invalid_combination_stops_before_azure_calls(): + module = load_prerequisites() + with patch.dict(module.os.environ, {'ENABLE_AZURE_MONITOR_PRIVATE_LINK': 'true', 'ENABLE_PRIVATE_NETWORKING': 'false'}, clear=True), patch.object(module, '_run_command') as command: + result = module.main() + assert result == 1 + command.assert_not_called() + + +def test_cloud_dns_and_private_endpoint_contracts(): + modules = REPO_ROOT / 'deployers' / 'bicep' / 'modules' + zones = json.loads((modules / 'privateDNSZones.json').read_text(encoding='utf-8')) + commercial = zones['azurecloud'] + government = zones['azureusgovernment'] + assert government['keyVault'] == 'privatelink.vaultcore.usgovcloudapi.net' + assert commercial['videoIndexer'] == 'privatelink.api.videoindexer.ai' + assert government['videoIndexer'] == '' + assert commercial['redisManaged'] == 'privatelink.redis.azure.net' + assert government['redisClassic'] == 'privatelink.redis.cache.usgovcloudapi.net' + assert government['redisManaged'] == '' + for key, suffix in [('monitor', 'monitor.azure.us'), ('monitorAdx', 'adx.monitor.azure.us'), ('monitorOms', 'oms.opinsights.azure.us'), ('monitorOds', 'ods.opinsights.azure.us'), ('monitorAgent', 'agentsvc.azure-automation.us')]: + assert government[key] == f'privatelink.{suffix}' + assert commercial['monitorAdx'] == '' + + networking = (modules / 'privateNetworking.bicep').read_text(encoding='utf-8') + video = networking[networking.index("module videoIndexerPE "):] + assert 'videoIndexerDNSZone!.outputs.privateDnsZoneId' in video + assert 'docIntelDNSZone.outputs.privateDnsZoneId' not in video + assert "redisCacheKind == 'managed' ? 'redisEnterprise' : 'redisCache'" in networking + + +def test_ampls_template_and_azd_wiring(): + modules = REPO_ROOT / 'deployers' / 'bicep' / 'modules' + template = json.loads((modules.parent / 'main.json').read_text(encoding='utf-8')) + parameters = json.loads(PARAMETERS_PATH.read_text(encoding='utf-8'))['parameters'] + assert parameters['enableAzureMonitorPrivateLink']['value'] == '${ENABLE_AZURE_MONITOR_PRIVATE_LINK=false}' + assert template['parameters']['enableAzureMonitorPrivateLink']['defaultValue'] is False + monitor = (modules / 'azureMonitorPrivateNetworking.bicep').read_text(encoding='utf-8') + assert "ingestionAccessMode: 'PrivateOnly'" in monitor + assert "queryAccessMode: 'PrivateOnly'" in monitor + assert "groupIDs: ['azuremonitor']" in monitor + assert 'take(zoneConfigs, 4)' in monitor + assert '[blobStorageDnsZoneId]' in monitor + assert "module adxDnsRecords 'privateDNSRecords.bicep'" in monitor + assert 'monitorNic.properties.ipConfigurations' in monitor + assert 'endsWith(entry.fqdn' in monitor + for filename in ['applicationInsights.bicep', 'logAnalyticsWorkspace.bicep']: + content = (modules / filename).read_text(encoding='utf-8') + assert "publicNetworkAccessForIngestion: enableAzureMonitorPrivateLink ? 'Disabled' : 'Enabled'" in content + assert "publicNetworkAccessForQuery: enableAzureMonitorPrivateLink ? 'Disabled' : 'Enabled'" in content + assert_app_version_at_least('0.261.333') + + +def test_redis_lockdown_in_both_azd_hooks(): + manifest = yaml.safe_load((REPO_ROOT / 'deployers' / 'azure.yaml').read_text(encoding='utf-8')) + for platform in ['posix', 'windows']: + hook = manifest['hooks']['postup'][platform]['run'] + assert 'var_redisCacheResourceId' in hook + assert '2025-07-01' in hook and '2024-11-01' in hook + assert 'properties.publicNetworkAccess=Disabled' in hook + assert hook.index('var_enablePrivateNetworking') < hook.index('Disabling public network access for Redis') + + +def test_fresh_and_compact_network_tables(): + guide = (REPO_ROOT / 'docs' / 'guides' / 'enterprise-networking.md').read_text(encoding='utf-8') + for cloud in ['Commercial', 'Government']: + section = guide.split(f'### Networking in Azure {cloud}', 1)[1].split('\n### ', 1)[0] + assert '**Fresh deployment defaults:**' in section + assert '**Compact existing-network reference:**' in section + compact_subnets = [] + fresh_subnets = [] + for row in section.splitlines(): + cells = [cell.strip().replace('`', '').replace('*', '') for cell in row.split('|')] + if len(cells) < 7 or cells[1] not in {'AppServiceIntegration', 'PrivateEndpoints'} and not cells[1].startswith('snet-'): + continue + network = ipaddress.IPv4Network(cells[2]) + assert int(cells[3]) == network.num_addresses + assert int(cells[4]) == network.num_addresses - 5 + if cells[1].startswith('snet-'): + compact_subnets.append(network) + else: + fresh_subnets.append(network) + + assert len(compact_subnets) == 5 and len(fresh_subnets) == 2 + for subnet_index, subnet in enumerate(compact_subnets): + assert subnet.subnet_of(ipaddress.IPv4Network('10.42.0.0/24')) + for neighbor in compact_subnets[subnet_index + 1:]: + assert not subnet.overlaps(neighbor) + assert sum(subnet.num_addresses for subnet in compact_subnets) == 208 + assert sum(subnet.num_addresses for subnet in fresh_subnets) == 512 + assert '**48**' in section and '**1536**' in section + assert '10.42.0.64/26' in section + + template = json.loads((REPO_ROOT / 'deployers' / 'bicep' / 'main.json').read_text(encoding='utf-8')) + network_parameters = template['resources']['virtualNetwork']['properties']['parameters'] + assert network_parameters['addressSpaces']['value'] == ['10.0.0.0/21'] + assert [subnet['addressPrefix'] for subnet in network_parameters['subnetConfigs']['value']] == ['10.0.0.0/24', '10.0.2.0/24'] + + for path in ['docs/deploy/special-scenarios.md', 'deployers/bicep/README.md', 'deployers/terraform/ReadMe.md', 'deployers/azurecli/README.md', 'artifacts/PRIVATE_ENDPOINTS_NETWORK_REFERENCE.md']: + reference = (REPO_ROOT / path).read_text(encoding='utf-8') + assert 'enterprise-networking.md#networking-in-azure-commercial' in reference + assert 'enterprise-networking.md#networking-in-azure-government' in reference + + +def test_cli_monitor_failure_and_existing_dns_reconciliation(): + powershell = shutil.which('pwsh') + if not powershell: + pytest.skip('PowerShell is required for CLI mock execution.') + script_path = str(REPO_ROOT / 'deployers' / 'azurecli' / 'deploy-simplechat.ps1').replace("'", "''") + harness = r""" +$tokens = $null +$parseErrors = $null +$ast = [System.Management.Automation.Language.Parser]::ParseFile('__SCRIPT_PATH__', [ref]$tokens, [ref]$parseErrors) +if ($parseErrors.Count) { throw 'CLI script does not parse' } +$names = @('Test-PrivateNetworkingConfiguration', 'Configure-PrivateNetworkingForDeployment', 'Ensure-PrivateEndpoint', 'Show-PrivateNetworkingChecklist') +foreach ($definition in $ast.FindAll({ param($node) $node -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $true)) { + if ($definition.Name -in $names) { + $definitionText = $definition.Extent.Text.Replace('$PSScriptRoot', "(Split-Path '__SCRIPT_PATH__')") + . ([scriptblock]::Create($definitionText)) + } +} +$global:mockNetworkCalls = [System.Collections.Generic.List[string]]::new() +$global:mockMonitorDeploymentFails = $true +function az { + $commandText = $args -join ' ' + $global:mockNetworkCalls.Add($commandText) + $global:LASTEXITCODE = 0 + if ($commandText.StartsWith('network vnet subnet show')) { return '{"addressPrefix":"10.42.0.64/26"}' } + if ($commandText.StartsWith('deployment group create') -and $global:mockMonitorDeploymentFails) { $global:LASTEXITCODE = 7; return } + return '/subscriptions/test/resourceGroups/network/providers/test/resource' +} +function Ensure-PrivateNetworkingInfrastructure { + $script:ResolvedVirtualNetworkId = '/vnet' + $script:ResolvedPrivateEndpointSubnetId = '/subnet' + $script:ResolvedAppServiceSubnetId = '/outbound' +} +function Ensure-PrivateDnsZone { param($ZoneKey) return '/zones/videoIndexer' } +function Get-PrivateDnsZoneConfig { param($ZoneKey) return @{ ZoneName = 'privatelink.api.videoindexer.ai' } } +function Update-ResourcePropertiesById { param($ResourceId, $PropertyAssignments, $DisplayName) } +$paramBaseName = 'test' +$paramEnvironment = 'dev' +$paramLocation = 'eastus' +$resourceGroupName = 'test-rg' +$appServiceName = 'test-app' +$appInsightsName = 'test-ai' +$logAnalyticsWorkspaceId = '/workspaces/test-law' +$tags = @{} +$param_PrivateDnsZoneConfigs = @{} +$param_DeployVideoIndexerService = $false +$globalWhichAzurePlatform = 'AzureCloud' +$param_EnableAzureMonitorPrivateLink = $true +$param_EnablePrivateNetworking = $false +try { Test-PrivateNetworkingConfiguration; throw 'Expected invalid combination failure' } catch { + if ($_.Exception.Message -notlike '*requires param_EnablePrivateNetworking*') { throw } +} +if ($global:mockNetworkCalls.Count) { throw 'Invalid options contacted Azure' } +Configure-PrivateNetworkingForDeployment -OpenAiResourceId '' +if ($global:mockNetworkCalls.Count) { throw 'Disabled networking contacted Azure' } + +Ensure-PrivateEndpoint -NameSuffix videoIndexer -ServiceResourceId '/videoIndexer' -GroupId account -ZoneKey videoIndexer +$dnsWrites = @($global:mockNetworkCalls | Where-Object { $_.StartsWith('network private-endpoint dns-zone-group create') }) +if ($dnsWrites.Count -ne 1 -or $dnsWrites[0] -notlike '*privatelink.api.videoindexer.ai*') { throw 'Existing PE DNS was not reconciled' } +function Ensure-PrivateEndpoint { param($NameSuffix, $ServiceResourceId, $GroupId, $ZoneKey) } +$global:mockNetworkCalls.Clear() +$param_EnablePrivateNetworking = $true +$param_EnableAzureMonitorPrivateLink = $false +$script:SupportedPrivateDnsZoneKeys = @('blobStorage', 'monitor', 'monitorAdx', 'videoIndexer') +$param_AllowedIpAddresses = @() +$checklist = Show-PrivateNetworkingChecklist 6>&1 | Out-String +if ($checklist -like '*monitor =>*' -or $checklist -like '*monitorAdx =>*' -or $checklist -like '*videoIndexer =>*') { throw 'Disabled optional DNS zones appear in checklist' } +$param_EnableAzureMonitorPrivateLink = $true +try { Configure-PrivateNetworkingForDeployment -OpenAiResourceId ''; throw 'Expected AMPLS failure' } catch { + if ($_.Exception.Message -notlike 'Failed to deploy AMPLS*') { throw } +} +$lockdown = @($global:mockNetworkCalls | Where-Object { $_.StartsWith('resource update') -and $_ -like '*publicNetworkAccessForIngestion*' }) +if ($lockdown.Count) { throw 'Monitoring was locked down before AMPLS succeeded' } + +$global:mockNetworkCalls.Clear() +$global:mockMonitorDeploymentFails = $false +Configure-PrivateNetworkingForDeployment -OpenAiResourceId '' +$lockdown = @($global:mockNetworkCalls | Where-Object { $_.StartsWith('resource update') -and $_ -like '*publicNetworkAccessForIngestion=Disabled*' -and $_ -like '*publicNetworkAccessForQuery=Disabled*' }) +if ($lockdown.Count -ne 2) { throw 'Both monitoring resources must be locked down after AMPLS success' } +Write-Output 'CLI mock checks passed' +""".replace('__SCRIPT_PATH__', script_path) + result = subprocess.run([powershell, '-NoProfile', '-Command', harness], capture_output=True, text=True, check=False) + assert result.returncode == 0, result.stdout + result.stderr + assert 'CLI mock checks passed' in result.stdout if __name__ == "__main__": try: - success = test_azd_private_dns_zone_configs_are_wired() + test_azd_private_dns_zone_configs_are_wired() + test_monitor_private_link_preflight() + test_monitor_invalid_combination_stops_before_azure_calls() + test_cloud_dns_and_private_endpoint_contracts() + test_ampls_template_and_azd_wiring() + test_redis_lockdown_in_both_azd_hooks() + test_fresh_and_compact_network_tables() + test_cli_monitor_failure_and_existing_dns_reconciliation() + success = True except Exception as exc: print(f"❌ Test failed: {exc}") raise