diff --git a/application/single_app/config.py b/application/single_app/config.py index b64e3d74a..4e90e6aef 100644 --- a/application/single_app/config.py +++ b/application/single_app/config.py @@ -103,7 +103,7 @@ EXECUTOR_MAX_WORKERS = 30 CHAT_STREAM_EXECUTOR_MAX_WORKERS = max(8, int(os.getenv('SIMPLECHAT_CHAT_STREAM_WORKERS', '32') or '32')) SESSION_TYPE = 'filesystem' -VERSION = "0.261.422" +VERSION = "0.261.423" IS_DEVELOPMENT = is_development_env_enabled() # Opt-out for deployments where App Service Easy Auth is active but the platform diff --git a/application/single_app/route_frontend_authentication.py b/application/single_app/route_frontend_authentication.py index 9cab91872..9e373d4de 100644 --- a/application/single_app/route_frontend_authentication.py +++ b/application/single_app/route_frontend_authentication.py @@ -4,6 +4,7 @@ import os import jwt import requests +from flask import Response from config import * from config import DISABLE_APP_SERVICE_EASY_AUTH_LOGOUT @@ -20,6 +21,34 @@ from functions_m365_connections import CHAT_AUTH_STATE_PREFIX, WORKFLOW_AUTH_STATE_PREFIX from swagger_wrapper import swagger_route, get_auth_security + +_OAUTH_LOGIN_ERROR_CODES = frozenset({ + "invalid_request", "invalid_client", "invalid_grant", "unauthorized_client", + "access_denied", "unsupported_response_type", "invalid_scope", "server_error", + "temporarily_unavailable", "interaction_required", "login_required", + "account_selection_required", "consent_required", +}) + + +def _oauth_login_error_response(): + """Return an inert sign-in failure and log only recognized diagnostic codes.""" + error = request.args.get('error') + log_event( + "[AUTH_CALLBACK] OAuth sign-in was not completed.", + extra={ + 'path': request.path, + 'oauth_error': error if error in _OAUTH_LOGIN_ERROR_CODES else 'unrecognized_error', + 'error_description_present': bool(request.args.get('error_description')), + }, + level=logging.WARNING, + ) + return Response( + "Login failed. Please try signing in again.", + status=400, + mimetype="text/plain", + ) + + def build_front_door_urls(front_door_url): """ Build home and login redirect URLs from a Front Door base URL. @@ -245,10 +274,7 @@ def authorized(): return complete_m365_workflow_connection_callback() # Check for errors passed back from Azure AD if request.args.get('error'): - error = request.args.get('error') - error_description = request.args.get('error_description', 'No description provided.') - print(f"Azure AD Login Error: {error} - {error_description}") - return f"Login Error: {error} - {error_description}", 400 # Or render an error page + return _oauth_login_error_response() code = request.args.get('code') if not code: @@ -360,10 +386,7 @@ def authorized(): def authorized_api(): # Check for errors passed back from Azure AD if request.args.get('error'): - error = request.args.get('error') - error_description = request.args.get('error_description', 'No description provided.') - print(f"Azure AD Login Error: {error} - {error_description}") - return f"Login Error: {error} - {error_description}", 400 # Or render an error page + return _oauth_login_error_response() code = request.args.get('code') if not code: diff --git a/docs/explanation/fixes/OAUTH_ERROR_RESPONSE_XSS_FIX.md b/docs/explanation/fixes/OAUTH_ERROR_RESPONSE_XSS_FIX.md new file mode 100644 index 000000000..77e174f5e --- /dev/null +++ b/docs/explanation/fixes/OAUTH_ERROR_RESPONSE_XSS_FIX.md @@ -0,0 +1,152 @@ +# OAuth callback error response fix + +Implemented in version: **0.261.423** + +The application version is tracked in `application/single_app/config.py`. +This change advances only its third segment from `0.261.422` to `0.261.423`. + +## Issue and root cause + +The ordinary provider-error branches of `/getAToken` and `/getATokenApi` +interpolated the callback's `error` and `error_description` query parameters +into a returned Python string. Flask treated that string as HTML, so hostile +provider parameters crossed directly into an HTML response. Both branches +already used HTTP 400; the problem was the response's content and media type. + +This is the bounded correction for the two selected `py/reflective-xss` +instances in `route_frontend_authentication.py`. The Development baseline was +`4dd6d80230fe60e3cbfe8544d1d52308a14a7387`, version `0.261.422`; Python CodeQL +analysis `2117078539` used `.github/workflows/codeql.yml:analyze` and category +`/language:python`. No new issue or changes to scanning configuration are part +of this fix. + +Historical, non-closing reference: [#1903](https://github.com/microsoft/simplechat/pull/1903), +which supplied that Development integration baseline. + +## Implementation + +### Response and diagnostic boundary + +Both ordinary callback error branches now use `_oauth_login_error_response()`. +It returns an explicit Flask `Response` with: + +| Property | Value | +| --- | --- | +| Status | HTTP 400 | +| Content-Type | `text/plain; charset=utf-8` | +| Body | `Login failed. Please try signing in again.` | + +Neither provider parameter is included in the response. The existing +`[AUTH_CALLBACK]` logging tag records a warning with the callback path, a +recognized OAuth/OIDC diagnostic code, and whether a description was present. +Unrecognized codes are recorded as `unrecognized_error`; raw descriptions, +arbitrary error text, authorization codes, tokens, and query strings are not +logged by this helper. This replaces the two raw provider-error `print` calls +without losing safe failure diagnostics. + +### Preserved callback behavior + +The `/getAToken` Microsoft 365 chat and workflow prefix checks still precede +the ordinary login-error branch. Their existing completion helpers remain +responsible for state, session binding, nonce, account checks, popup/page +completion, and independent credentials without replacing the app principal. + +A missing authorization code still redirects `/getAToken` to the public +sign-in entry point. `/getATokenApi` retains its separate missing-code HTTP 400 +response. Missing or empty `error` values do not activate the new helper. +Successful application login, API token exchange, and Teams OBO session +creation retain their existing contracts. + +No shared authentication/cache helper, Microsoft 365 implementation, app +security-header hook, CSP setting, browser asset, or deployer was changed. +`config.py` changes only the application version. The coordinator-assigned +release-note entry and regenerated split pages use the same `0.261.423` version; +this documentation-only addition does not advance the version again. + +### Files changed + +| File | Change | +| --- | --- | +| `application/single_app/route_frontend_authentication.py` | Shared fixed-text error response and safe diagnostic logging for the two selected branches. | +| `application/single_app/config.py` | Application patch version `0.261.423`. | +| `functional_tests/test_oauth_error_responses.py` | Real Flask HTTP regressions plus sign-in, Teams, and real MSAL-backed Microsoft 365 callback compatibility checks. | +| `functional_tests/test_m365_routes.py` | Minimal repair of an existing notification fixture's stale group-helper imports; current version header. | +| `docs/explanation/fixes/OAUTH_ERROR_RESPONSE_XSS_FIX.md` | Root cause, response contract, scope, and validation evidence. | +| `docs/explanation/release_notes.md` | Concise Bug Fix entry under `v0.261.423`, with the historical non-closing reference. | +| `docs/explanation/release-notes/*.md` | Regenerated split release pages derived from the release-note source. | + +## Validation + +### Before the fix + +The new HTTP suite was run against the unchanged production callbacks. All +ten provider-error cases failed because the actual responses declared +`text/html; charset=utf-8`, rather than the required plain-text media type. +The other 23 runtime cases passed, covering missing-code behavior, successful +login/API callbacks, Teams, and Microsoft 365 prefix and guard behavior. + +The original four-selector offline baseline separately reported 116 passed +and one failure. The failure was +`M365RouteTests.test_real_notification_helper_only_targets_subject_and_deduplicates`: +its scoped `functions_group` stub did not export `assert_group_role` and +`get_user_groups`, which the notification module now imports. The approved +test-only repair represents the fixture's no-group scenario with a rejecting +role check and an empty membership list, and asserts neither helper is called. +All existing notification subject, personal-scope, deduplication, status, and +fault-injection assertions are retained. Production notifications and +authorization were not modified. + +### Regression coverage and execution + +The HTTP suite uses the entire registered callback module, not AST-extracted +callback functions. It covers hostile markup and quotes in both provider +parameters, missing descriptions, sensitive-looking text, error precedence +over an authorization code, safe logging, and unchanged session contents. + +Microsoft 365 cases reuse the existing connection fixtures and +`test_support.m365` clock and SDK-default Cosmos doubles. They execute real +MSAL state/nonce and account validation, real AES-GCM workflow storage, and +both page and popup completion through `/getAToken`. The Cosmos network-guard +plugin blocks external socket/DNS access before collection. + +These are offline Flask runtime fixtures, not live Azure, Graph, full-app +bootstrap, or Azure Playwright browser validation. The response's fixed body +and declared plain-text media type are checked at the HTTP boundary. No +resources were provisioned and no authentication settings were changed. + +Run the focused selection with Windows `PYTHONPATH` configured: + +```powershell +$env:PYTHONPATH = "$PWD\application\single_app;$PWD\functional_tests" +python -m pytest -q -p test_support.cosmos_network_guard functional_tests\test_getatoken_missing_code_redirect.py functional_tests\test_m365_routes.py functional_tests\test_m365_connections.py functional_tests\test_m365_chat_connection_persistence.py functional_tests\test_oauth_error_responses.py +``` + +After the callback correction and approved fixture repair, this selection +passed: **150 tests and 119 subtests**, including all 33 new HTTP runtime +cases. The ten previously failing provider-error cases now return the fixed +body in `text/plain; charset=utf-8` with HTTP 400 and safe diagnostics. + +| Additional command | Result | +| --- | --- | +| `python .\scripts\check_swagger_routes.py .\application\single_app\route_frontend_authentication.py .\application\single_app\config.py` | Passed; one changed route file validated. | +| `python .\functional_tests\route_tests\test_route_blueprint_policy_inventory.py` | 15/15 passed. | +| `python .\functional_tests\route_tests\test_route_unauthenticated_policy_contract.py` | 10/10 passed. | +| `python .\functional_tests\route_tests\test_route_policy_test_coverage.py` | 3/3 passed. | +| `python .\functional_tests\test_docs_app_surface_coverage.py` | 7/7 passed; the committed inventory remains current. | +| `python .\functional_tests\test_docs_site_quality.py` | 6/6 source-site checks passed; no built-site validation was performed. | +| `python -m py_compile .\application\single_app\route_frontend_authentication.py .\application\single_app\config.py .\functional_tests\test_oauth_error_responses.py .\functional_tests\test_m365_routes.py` | Passed. | +| `python .\scripts\build_release_notes_pages.py` | Regenerated 15 pages while preserving all 317 source release sections. | +| `python .\scripts\build_release_notes_pages.py --check` | Passed; split release pages are current. | +| `python .\scripts\build_latest_release_docs.py --check` | Passed; the independent app-driven Latest Features catalog is unchanged. | +| `python -m pytest -q -p test_support.cosmos_network_guard functional_tests\test_docs_release_notes_integrity.py functional_tests\test_docs_app_surface_coverage.py functional_tests\test_docs_site_quality.py functional_tests\test_docs_latest_release_catalog_parity.py functional_tests\test_docs_published_references.py::test_release_generation_rejects_source_relative_reader_links` | 21 passed after the assigned release-note/reference update. | + +The initial PR scan also identified one redundant wrapper in the new test-only +settings seam. Using the identical `dict` callable directly removes that +`py/unnecessary-lambda` finding without changing production code or the 33 +runtime cases. The full 150-test/119-subtest selection passed again after this +correction. Final exact-head CodeQL and CI evidence is recorded on the PR. + +Route policy, Swagger, access-control, XSS diff guardrails, and the documentation +inventory/site checks are also required before the PR handoff. Fresh PR-ref +Python CodeQL evidence is distinct from final Development verification, which +can only occur after the user approves and merges the PR. diff --git a/docs/explanation/release-notes/index.md b/docs/explanation/release-notes/index.md index b28aa75f7..ba9e0d2a7 100644 --- a/docs/explanation/release-notes/index.md +++ b/docs/explanation/release-notes/index.md @@ -20,6 +20,7 @@ This page includes the latest release notes inline. Older release sections are s | Version | Page | | --- | --- | +| v0.261.423 | [Release notes index]({{ '/explanation/release_notes/' | relative_url }}) | | v0.261.422 | [Release notes index]({{ '/explanation/release_notes/' | relative_url }}) | | v0.261.421 | [Release notes index]({{ '/explanation/release_notes/' | relative_url }}) | | v0.261.420 | [Release notes index]({{ '/explanation/release_notes/' | relative_url }}) | @@ -31,265 +32,265 @@ This page includes the latest release notes inline. Older release sections are s | v0.261.385 | [Release notes index]({{ '/explanation/release_notes/' | relative_url }}) | | v0.261.381 | [Release notes index]({{ '/explanation/release_notes/' | relative_url }}) | | v0.261.379 | [Release notes index]({{ '/explanation/release_notes/' | relative_url }}) | -| v0.261.378 | [Release notes index]({{ '/explanation/release_notes/' | relative_url }}) | -| v0.261.333 | [Release notes 0.261.333 – 0.261.267]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.315 | [Release notes 0.261.333 – 0.261.267]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.309 | [Release notes 0.261.333 – 0.261.267]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.308 | [Release notes 0.261.333 – 0.261.267]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.306 | [Release notes 0.261.333 – 0.261.267]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.305 | [Release notes 0.261.333 – 0.261.267]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.304 | [Release notes 0.261.333 – 0.261.267]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.303 | [Release notes 0.261.333 – 0.261.267]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.302 | [Release notes 0.261.333 – 0.261.267]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.301 | [Release notes 0.261.333 – 0.261.267]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.300 | [Release notes 0.261.333 – 0.261.267]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.299 | [Release notes 0.261.333 – 0.261.267]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.298 | [Release notes 0.261.333 – 0.261.267]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.297 | [Release notes 0.261.333 – 0.261.267]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.296 | [Release notes 0.261.333 – 0.261.267]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.295 | [Release notes 0.261.333 – 0.261.267]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.293 | [Release notes 0.261.333 – 0.261.267]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.292 | [Release notes 0.261.333 – 0.261.267]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.291 | [Release notes 0.261.333 – 0.261.267]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.290 | [Release notes 0.261.333 – 0.261.267]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.289 | [Release notes 0.261.333 – 0.261.267]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.288 | [Release notes 0.261.333 – 0.261.267]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.287 | [Release notes 0.261.333 – 0.261.267]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.286 | [Release notes 0.261.333 – 0.261.267]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.285 | [Release notes 0.261.333 – 0.261.267]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.284 | [Release notes 0.261.333 – 0.261.267]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.283 | [Release notes 0.261.333 – 0.261.267]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.282 | [Release notes 0.261.333 – 0.261.267]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.280 | [Release notes 0.261.333 – 0.261.267]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.279 | [Release notes 0.261.333 – 0.261.267]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.278 | [Release notes 0.261.333 – 0.261.267]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.277 | [Release notes 0.261.333 – 0.261.267]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.276 | [Release notes 0.261.333 – 0.261.267]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.275 | [Release notes 0.261.333 – 0.261.267]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.274 | [Release notes 0.261.333 – 0.261.267]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.271 | [Release notes 0.261.333 – 0.261.267]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.270 | [Release notes 0.261.333 – 0.261.267]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.269 | [Release notes 0.261.333 – 0.261.267]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.268 | [Release notes 0.261.333 – 0.261.267]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.267 | [Release notes 0.261.333 – 0.261.267]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.265 | [Release notes 0.261.265 – 0.261.223]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.264 | [Release notes 0.261.265 – 0.261.223]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.263 | [Release notes 0.261.265 – 0.261.223]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.262 | [Release notes 0.261.265 – 0.261.223]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.261 | [Release notes 0.261.265 – 0.261.223]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.260 | [Release notes 0.261.265 – 0.261.223]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.259 | [Release notes 0.261.265 – 0.261.223]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.258 | [Release notes 0.261.265 – 0.261.223]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.257 | [Release notes 0.261.265 – 0.261.223]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.256 | [Release notes 0.261.265 – 0.261.223]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.253 | [Release notes 0.261.265 – 0.261.223]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.252 | [Release notes 0.261.265 – 0.261.223]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.251 | [Release notes 0.261.265 – 0.261.223]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.250 | [Release notes 0.261.265 – 0.261.223]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.248 | [Release notes 0.261.265 – 0.261.223]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.247 | [Release notes 0.261.265 – 0.261.223]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.246 | [Release notes 0.261.265 – 0.261.223]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.245 | [Release notes 0.261.265 – 0.261.223]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.244 | [Release notes 0.261.265 – 0.261.223]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.243 | [Release notes 0.261.265 – 0.261.223]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.242 | [Release notes 0.261.265 – 0.261.223]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.241 | [Release notes 0.261.265 – 0.261.223]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.240 | [Release notes 0.261.265 – 0.261.223]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.239 | [Release notes 0.261.265 – 0.261.223]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.238 | [Release notes 0.261.265 – 0.261.223]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.237 | [Release notes 0.261.265 – 0.261.223]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.236 | [Release notes 0.261.265 – 0.261.223]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.235 | [Release notes 0.261.265 – 0.261.223]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.234 | [Release notes 0.261.265 – 0.261.223]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.233 | [Release notes 0.261.265 – 0.261.223]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.232 | [Release notes 0.261.265 – 0.261.223]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.231 | [Release notes 0.261.265 – 0.261.223]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.230 | [Release notes 0.261.265 – 0.261.223]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.229 | [Release notes 0.261.265 – 0.261.223]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.228 | [Release notes 0.261.265 – 0.261.223]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.227 | [Release notes 0.261.265 – 0.261.223]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.226 | [Release notes 0.261.265 – 0.261.223]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.225 | [Release notes 0.261.265 – 0.261.223]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.224 | [Release notes 0.261.265 – 0.261.223]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.223 | [Release notes 0.261.265 – 0.261.223]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.222 | [Release notes 0.261.222 – 0.261.178]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.221 | [Release notes 0.261.222 – 0.261.178]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.220 | [Release notes 0.261.222 – 0.261.178]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.219 | [Release notes 0.261.222 – 0.261.178]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.218 | [Release notes 0.261.222 – 0.261.178]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.217 | [Release notes 0.261.222 – 0.261.178]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.216 | [Release notes 0.261.222 – 0.261.178]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.215 | [Release notes 0.261.222 – 0.261.178]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.214 | [Release notes 0.261.222 – 0.261.178]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.213 | [Release notes 0.261.222 – 0.261.178]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.212 | [Release notes 0.261.222 – 0.261.178]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.210 | [Release notes 0.261.222 – 0.261.178]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.209 | [Release notes 0.261.222 – 0.261.178]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.208 | [Release notes 0.261.222 – 0.261.178]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.207 | [Release notes 0.261.222 – 0.261.178]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.205 | [Release notes 0.261.222 – 0.261.178]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.204 | [Release notes 0.261.222 – 0.261.178]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.203 | [Release notes 0.261.222 – 0.261.178]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.202 | [Release notes 0.261.222 – 0.261.178]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.201 | [Release notes 0.261.222 – 0.261.178]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.200 | [Release notes 0.261.222 – 0.261.178]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.199 | [Release notes 0.261.222 – 0.261.178]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.195 | [Release notes 0.261.222 – 0.261.178]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.194 | [Release notes 0.261.222 – 0.261.178]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.193 | [Release notes 0.261.222 – 0.261.178]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.192 | [Release notes 0.261.222 – 0.261.178]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.191 | [Release notes 0.261.222 – 0.261.178]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.190 | [Release notes 0.261.222 – 0.261.178]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.189 | [Release notes 0.261.222 – 0.261.178]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.188 | [Release notes 0.261.222 – 0.261.178]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.187 | [Release notes 0.261.222 – 0.261.178]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.186 | [Release notes 0.261.222 – 0.261.178]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.185 | [Release notes 0.261.222 – 0.261.178]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.184 | [Release notes 0.261.222 – 0.261.178]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.183 | [Release notes 0.261.222 – 0.261.178]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.182 | [Release notes 0.261.222 – 0.261.178]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.181 | [Release notes 0.261.222 – 0.261.178]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.180 | [Release notes 0.261.222 – 0.261.178]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.179 | [Release notes 0.261.222 – 0.261.178]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.178 | [Release notes 0.261.222 – 0.261.178]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.177 | [Release notes 0.261.177 – 0.261.138]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.176 | [Release notes 0.261.177 – 0.261.138]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.175 | [Release notes 0.261.177 – 0.261.138]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.174 | [Release notes 0.261.177 – 0.261.138]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.173 | [Release notes 0.261.177 – 0.261.138]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.172 | [Release notes 0.261.177 – 0.261.138]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.171 | [Release notes 0.261.177 – 0.261.138]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.170 | [Release notes 0.261.177 – 0.261.138]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.169 | [Release notes 0.261.177 – 0.261.138]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.168 | [Release notes 0.261.177 – 0.261.138]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.167 | [Release notes 0.261.177 – 0.261.138]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.166 | [Release notes 0.261.177 – 0.261.138]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.165 | [Release notes 0.261.177 – 0.261.138]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.164 | [Release notes 0.261.177 – 0.261.138]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.163 | [Release notes 0.261.177 – 0.261.138]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.162 | [Release notes 0.261.177 – 0.261.138]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.161 | [Release notes 0.261.177 – 0.261.138]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.160 | [Release notes 0.261.177 – 0.261.138]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.159 | [Release notes 0.261.177 – 0.261.138]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.158 | [Release notes 0.261.177 – 0.261.138]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.157 | [Release notes 0.261.177 – 0.261.138]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.156 | [Release notes 0.261.177 – 0.261.138]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.155 | [Release notes 0.261.177 – 0.261.138]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.154 | [Release notes 0.261.177 – 0.261.138]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.153 | [Release notes 0.261.177 – 0.261.138]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.152 | [Release notes 0.261.177 – 0.261.138]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.151 | [Release notes 0.261.177 – 0.261.138]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.150 | [Release notes 0.261.177 – 0.261.138]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.149 | [Release notes 0.261.177 – 0.261.138]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.148 | [Release notes 0.261.177 – 0.261.138]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.147 | [Release notes 0.261.177 – 0.261.138]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.146 | [Release notes 0.261.177 – 0.261.138]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.145 | [Release notes 0.261.177 – 0.261.138]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.144 | [Release notes 0.261.177 – 0.261.138]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.143 | [Release notes 0.261.177 – 0.261.138]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.142 | [Release notes 0.261.177 – 0.261.138]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.141 | [Release notes 0.261.177 – 0.261.138]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.140 | [Release notes 0.261.177 – 0.261.138]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.139 | [Release notes 0.261.177 – 0.261.138]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.138 | [Release notes 0.261.177 – 0.261.138]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.137 | [Release notes 0.261.137 – 0.261.060]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.136 | [Release notes 0.261.137 – 0.261.060]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.135 | [Release notes 0.261.137 – 0.261.060]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.134 | [Release notes 0.261.137 – 0.261.060]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.133 | [Release notes 0.261.137 – 0.261.060]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.132 | [Release notes 0.261.137 – 0.261.060]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.131 | [Release notes 0.261.137 – 0.261.060]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.129 | [Release notes 0.261.137 – 0.261.060]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.128 | [Release notes 0.261.137 – 0.261.060]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.127 | [Release notes 0.261.137 – 0.261.060]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.123 | [Release notes 0.261.137 – 0.261.060]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.104 | [Release notes 0.261.137 – 0.261.060]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.102 | [Release notes 0.261.137 – 0.261.060]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.100 | [Release notes 0.261.137 – 0.261.060]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.099 | [Release notes 0.261.137 – 0.261.060]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.098 | [Release notes 0.261.137 – 0.261.060]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.096 | [Release notes 0.261.137 – 0.261.060]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.095 | [Release notes 0.261.137 – 0.261.060]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.093 | [Release notes 0.261.137 – 0.261.060]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.092 | [Release notes 0.261.137 – 0.261.060]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.091 | [Release notes 0.261.137 – 0.261.060]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.090 | [Release notes 0.261.137 – 0.261.060]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.089 | [Release notes 0.261.137 – 0.261.060]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.088 | [Release notes 0.261.137 – 0.261.060]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.087 | [Release notes 0.261.137 – 0.261.060]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.086 | [Release notes 0.261.137 – 0.261.060]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.085 | [Release notes 0.261.137 – 0.261.060]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.084 | [Release notes 0.261.137 – 0.261.060]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.083 | [Release notes 0.261.137 – 0.261.060]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.082 | [Release notes 0.261.137 – 0.261.060]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.081 | [Release notes 0.261.137 – 0.261.060]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.074 | [Release notes 0.261.137 – 0.261.060]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.071 | [Release notes 0.261.137 – 0.261.060]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.070 | [Release notes 0.261.137 – 0.261.060]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.065 | [Release notes 0.261.137 – 0.261.060]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.064 | [Release notes 0.261.137 – 0.261.060]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.063 | [Release notes 0.261.137 – 0.261.060]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.062 | [Release notes 0.261.137 – 0.261.060]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.061 | [Release notes 0.261.137 – 0.261.060]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.060 | [Release notes 0.261.137 – 0.261.060]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.059 | [Release notes 0.261.059 – 0.261.020]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.058 | [Release notes 0.261.059 – 0.261.020]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.057 | [Release notes 0.261.059 – 0.261.020]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.056 | [Release notes 0.261.059 – 0.261.020]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.055 | [Release notes 0.261.059 – 0.261.020]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.054 | [Release notes 0.261.059 – 0.261.020]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.053 | [Release notes 0.261.059 – 0.261.020]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.052 | [Release notes 0.261.059 – 0.261.020]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.051 | [Release notes 0.261.059 – 0.261.020]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.050 | [Release notes 0.261.059 – 0.261.020]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.049 | [Release notes 0.261.059 – 0.261.020]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.048 | [Release notes 0.261.059 – 0.261.020]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.047 | [Release notes 0.261.059 – 0.261.020]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.046 | [Release notes 0.261.059 – 0.261.020]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.045 | [Release notes 0.261.059 – 0.261.020]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.044 | [Release notes 0.261.059 – 0.261.020]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.043 | [Release notes 0.261.059 – 0.261.020]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.042 | [Release notes 0.261.059 – 0.261.020]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.041 | [Release notes 0.261.059 – 0.261.020]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.040 | [Release notes 0.261.059 – 0.261.020]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.039 | [Release notes 0.261.059 – 0.261.020]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.038 | [Release notes 0.261.059 – 0.261.020]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.037 | [Release notes 0.261.059 – 0.261.020]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.036 | [Release notes 0.261.059 – 0.261.020]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.035 | [Release notes 0.261.059 – 0.261.020]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.034 | [Release notes 0.261.059 – 0.261.020]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.033 | [Release notes 0.261.059 – 0.261.020]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.032 | [Release notes 0.261.059 – 0.261.020]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.031 | [Release notes 0.261.059 – 0.261.020]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.030 | [Release notes 0.261.059 – 0.261.020]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.029 | [Release notes 0.261.059 – 0.261.020]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.028 | [Release notes 0.261.059 – 0.261.020]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.027 | [Release notes 0.261.059 – 0.261.020]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.026 | [Release notes 0.261.059 – 0.261.020]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.025 | [Release notes 0.261.059 – 0.261.020]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.024 | [Release notes 0.261.059 – 0.261.020]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.023 | [Release notes 0.261.059 – 0.261.020]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.022 | [Release notes 0.261.059 – 0.261.020]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.021 | [Release notes 0.261.059 – 0.261.020]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.020 | [Release notes 0.261.059 – 0.261.020]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.019 | [Release notes 0.261.019 – 0.261.002]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | -| v0.261.018 | [Release notes 0.261.019 – 0.261.002]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | -| v0.261.017 | [Release notes 0.261.019 – 0.261.002]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | -| v0.261.016 | [Release notes 0.261.019 – 0.261.002]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | -| v0.261.015 | [Release notes 0.261.019 – 0.261.002]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | -| v0.261.014 | [Release notes 0.261.019 – 0.261.002]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | -| v0.261.013 | [Release notes 0.261.019 – 0.261.002]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | -| v0.261.012 | [Release notes 0.261.019 – 0.261.002]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | -| v0.261.011 | [Release notes 0.261.019 – 0.261.002]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | -| v0.261.010 | [Release notes 0.261.019 – 0.261.002]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | -| v0.261.009 | [Release notes 0.261.019 – 0.261.002]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | -| v0.261.008 | [Release notes 0.261.019 – 0.261.002]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | -| v0.261.007 | [Release notes 0.261.019 – 0.261.002]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | -| v0.261.006 | [Release notes 0.261.019 – 0.261.002]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | -| v0.261.005 | [Release notes 0.261.019 – 0.261.002]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | -| v0.261.004 | [Release notes 0.261.019 – 0.261.002]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | -| v0.261.003 | [Release notes 0.261.019 – 0.261.002]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | -| v0.261.002 | [Release notes 0.261.019 – 0.261.002]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | +| v0.261.378 | [Release notes 0.261.378 – 0.261.268]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.333 | [Release notes 0.261.378 – 0.261.268]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.315 | [Release notes 0.261.378 – 0.261.268]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.309 | [Release notes 0.261.378 – 0.261.268]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.308 | [Release notes 0.261.378 – 0.261.268]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.306 | [Release notes 0.261.378 – 0.261.268]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.305 | [Release notes 0.261.378 – 0.261.268]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.304 | [Release notes 0.261.378 – 0.261.268]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.303 | [Release notes 0.261.378 – 0.261.268]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.302 | [Release notes 0.261.378 – 0.261.268]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.301 | [Release notes 0.261.378 – 0.261.268]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.300 | [Release notes 0.261.378 – 0.261.268]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.299 | [Release notes 0.261.378 – 0.261.268]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.298 | [Release notes 0.261.378 – 0.261.268]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.297 | [Release notes 0.261.378 – 0.261.268]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.296 | [Release notes 0.261.378 – 0.261.268]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.295 | [Release notes 0.261.378 – 0.261.268]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.293 | [Release notes 0.261.378 – 0.261.268]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.292 | [Release notes 0.261.378 – 0.261.268]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.291 | [Release notes 0.261.378 – 0.261.268]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.290 | [Release notes 0.261.378 – 0.261.268]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.289 | [Release notes 0.261.378 – 0.261.268]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.288 | [Release notes 0.261.378 – 0.261.268]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.287 | [Release notes 0.261.378 – 0.261.268]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.286 | [Release notes 0.261.378 – 0.261.268]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.285 | [Release notes 0.261.378 – 0.261.268]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.284 | [Release notes 0.261.378 – 0.261.268]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.283 | [Release notes 0.261.378 – 0.261.268]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.282 | [Release notes 0.261.378 – 0.261.268]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.280 | [Release notes 0.261.378 – 0.261.268]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.279 | [Release notes 0.261.378 – 0.261.268]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.278 | [Release notes 0.261.378 – 0.261.268]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.277 | [Release notes 0.261.378 – 0.261.268]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.276 | [Release notes 0.261.378 – 0.261.268]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.275 | [Release notes 0.261.378 – 0.261.268]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.274 | [Release notes 0.261.378 – 0.261.268]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.271 | [Release notes 0.261.378 – 0.261.268]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.270 | [Release notes 0.261.378 – 0.261.268]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.269 | [Release notes 0.261.378 – 0.261.268]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.268 | [Release notes 0.261.378 – 0.261.268]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.267 | [Release notes 0.261.267 – 0.261.224]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.265 | [Release notes 0.261.267 – 0.261.224]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.264 | [Release notes 0.261.267 – 0.261.224]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.263 | [Release notes 0.261.267 – 0.261.224]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.262 | [Release notes 0.261.267 – 0.261.224]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.261 | [Release notes 0.261.267 – 0.261.224]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.260 | [Release notes 0.261.267 – 0.261.224]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.259 | [Release notes 0.261.267 – 0.261.224]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.258 | [Release notes 0.261.267 – 0.261.224]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.257 | [Release notes 0.261.267 – 0.261.224]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.256 | [Release notes 0.261.267 – 0.261.224]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.253 | [Release notes 0.261.267 – 0.261.224]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.252 | [Release notes 0.261.267 – 0.261.224]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.251 | [Release notes 0.261.267 – 0.261.224]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.250 | [Release notes 0.261.267 – 0.261.224]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.248 | [Release notes 0.261.267 – 0.261.224]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.247 | [Release notes 0.261.267 – 0.261.224]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.246 | [Release notes 0.261.267 – 0.261.224]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.245 | [Release notes 0.261.267 – 0.261.224]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.244 | [Release notes 0.261.267 – 0.261.224]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.243 | [Release notes 0.261.267 – 0.261.224]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.242 | [Release notes 0.261.267 – 0.261.224]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.241 | [Release notes 0.261.267 – 0.261.224]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.240 | [Release notes 0.261.267 – 0.261.224]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.239 | [Release notes 0.261.267 – 0.261.224]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.238 | [Release notes 0.261.267 – 0.261.224]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.237 | [Release notes 0.261.267 – 0.261.224]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.236 | [Release notes 0.261.267 – 0.261.224]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.235 | [Release notes 0.261.267 – 0.261.224]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.234 | [Release notes 0.261.267 – 0.261.224]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.233 | [Release notes 0.261.267 – 0.261.224]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.232 | [Release notes 0.261.267 – 0.261.224]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.231 | [Release notes 0.261.267 – 0.261.224]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.230 | [Release notes 0.261.267 – 0.261.224]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.229 | [Release notes 0.261.267 – 0.261.224]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.228 | [Release notes 0.261.267 – 0.261.224]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.227 | [Release notes 0.261.267 – 0.261.224]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.226 | [Release notes 0.261.267 – 0.261.224]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.225 | [Release notes 0.261.267 – 0.261.224]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.224 | [Release notes 0.261.267 – 0.261.224]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.223 | [Release notes 0.261.223 – 0.261.179]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.222 | [Release notes 0.261.223 – 0.261.179]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.221 | [Release notes 0.261.223 – 0.261.179]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.220 | [Release notes 0.261.223 – 0.261.179]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.219 | [Release notes 0.261.223 – 0.261.179]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.218 | [Release notes 0.261.223 – 0.261.179]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.217 | [Release notes 0.261.223 – 0.261.179]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.216 | [Release notes 0.261.223 – 0.261.179]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.215 | [Release notes 0.261.223 – 0.261.179]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.214 | [Release notes 0.261.223 – 0.261.179]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.213 | [Release notes 0.261.223 – 0.261.179]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.212 | [Release notes 0.261.223 – 0.261.179]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.210 | [Release notes 0.261.223 – 0.261.179]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.209 | [Release notes 0.261.223 – 0.261.179]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.208 | [Release notes 0.261.223 – 0.261.179]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.207 | [Release notes 0.261.223 – 0.261.179]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.205 | [Release notes 0.261.223 – 0.261.179]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.204 | [Release notes 0.261.223 – 0.261.179]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.203 | [Release notes 0.261.223 – 0.261.179]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.202 | [Release notes 0.261.223 – 0.261.179]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.201 | [Release notes 0.261.223 – 0.261.179]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.200 | [Release notes 0.261.223 – 0.261.179]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.199 | [Release notes 0.261.223 – 0.261.179]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.195 | [Release notes 0.261.223 – 0.261.179]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.194 | [Release notes 0.261.223 – 0.261.179]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.193 | [Release notes 0.261.223 – 0.261.179]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.192 | [Release notes 0.261.223 – 0.261.179]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.191 | [Release notes 0.261.223 – 0.261.179]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.190 | [Release notes 0.261.223 – 0.261.179]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.189 | [Release notes 0.261.223 – 0.261.179]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.188 | [Release notes 0.261.223 – 0.261.179]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.187 | [Release notes 0.261.223 – 0.261.179]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.186 | [Release notes 0.261.223 – 0.261.179]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.185 | [Release notes 0.261.223 – 0.261.179]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.184 | [Release notes 0.261.223 – 0.261.179]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.183 | [Release notes 0.261.223 – 0.261.179]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.182 | [Release notes 0.261.223 – 0.261.179]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.181 | [Release notes 0.261.223 – 0.261.179]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.180 | [Release notes 0.261.223 – 0.261.179]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.179 | [Release notes 0.261.223 – 0.261.179]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.178 | [Release notes 0.261.178 – 0.261.139]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.177 | [Release notes 0.261.178 – 0.261.139]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.176 | [Release notes 0.261.178 – 0.261.139]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.175 | [Release notes 0.261.178 – 0.261.139]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.174 | [Release notes 0.261.178 – 0.261.139]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.173 | [Release notes 0.261.178 – 0.261.139]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.172 | [Release notes 0.261.178 – 0.261.139]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.171 | [Release notes 0.261.178 – 0.261.139]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.170 | [Release notes 0.261.178 – 0.261.139]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.169 | [Release notes 0.261.178 – 0.261.139]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.168 | [Release notes 0.261.178 – 0.261.139]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.167 | [Release notes 0.261.178 – 0.261.139]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.166 | [Release notes 0.261.178 – 0.261.139]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.165 | [Release notes 0.261.178 – 0.261.139]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.164 | [Release notes 0.261.178 – 0.261.139]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.163 | [Release notes 0.261.178 – 0.261.139]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.162 | [Release notes 0.261.178 – 0.261.139]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.161 | [Release notes 0.261.178 – 0.261.139]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.160 | [Release notes 0.261.178 – 0.261.139]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.159 | [Release notes 0.261.178 – 0.261.139]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.158 | [Release notes 0.261.178 – 0.261.139]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.157 | [Release notes 0.261.178 – 0.261.139]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.156 | [Release notes 0.261.178 – 0.261.139]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.155 | [Release notes 0.261.178 – 0.261.139]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.154 | [Release notes 0.261.178 – 0.261.139]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.153 | [Release notes 0.261.178 – 0.261.139]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.152 | [Release notes 0.261.178 – 0.261.139]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.151 | [Release notes 0.261.178 – 0.261.139]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.150 | [Release notes 0.261.178 – 0.261.139]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.149 | [Release notes 0.261.178 – 0.261.139]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.148 | [Release notes 0.261.178 – 0.261.139]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.147 | [Release notes 0.261.178 – 0.261.139]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.146 | [Release notes 0.261.178 – 0.261.139]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.145 | [Release notes 0.261.178 – 0.261.139]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.144 | [Release notes 0.261.178 – 0.261.139]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.143 | [Release notes 0.261.178 – 0.261.139]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.142 | [Release notes 0.261.178 – 0.261.139]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.141 | [Release notes 0.261.178 – 0.261.139]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.140 | [Release notes 0.261.178 – 0.261.139]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.139 | [Release notes 0.261.178 – 0.261.139]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.138 | [Release notes 0.261.138 – 0.261.061]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.137 | [Release notes 0.261.138 – 0.261.061]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.136 | [Release notes 0.261.138 – 0.261.061]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.135 | [Release notes 0.261.138 – 0.261.061]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.134 | [Release notes 0.261.138 – 0.261.061]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.133 | [Release notes 0.261.138 – 0.261.061]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.132 | [Release notes 0.261.138 – 0.261.061]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.131 | [Release notes 0.261.138 – 0.261.061]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.129 | [Release notes 0.261.138 – 0.261.061]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.128 | [Release notes 0.261.138 – 0.261.061]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.127 | [Release notes 0.261.138 – 0.261.061]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.123 | [Release notes 0.261.138 – 0.261.061]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.104 | [Release notes 0.261.138 – 0.261.061]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.102 | [Release notes 0.261.138 – 0.261.061]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.100 | [Release notes 0.261.138 – 0.261.061]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.099 | [Release notes 0.261.138 – 0.261.061]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.098 | [Release notes 0.261.138 – 0.261.061]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.096 | [Release notes 0.261.138 – 0.261.061]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.095 | [Release notes 0.261.138 – 0.261.061]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.093 | [Release notes 0.261.138 – 0.261.061]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.092 | [Release notes 0.261.138 – 0.261.061]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.091 | [Release notes 0.261.138 – 0.261.061]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.090 | [Release notes 0.261.138 – 0.261.061]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.089 | [Release notes 0.261.138 – 0.261.061]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.088 | [Release notes 0.261.138 – 0.261.061]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.087 | [Release notes 0.261.138 – 0.261.061]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.086 | [Release notes 0.261.138 – 0.261.061]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.085 | [Release notes 0.261.138 – 0.261.061]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.084 | [Release notes 0.261.138 – 0.261.061]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.083 | [Release notes 0.261.138 – 0.261.061]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.082 | [Release notes 0.261.138 – 0.261.061]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.081 | [Release notes 0.261.138 – 0.261.061]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.074 | [Release notes 0.261.138 – 0.261.061]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.071 | [Release notes 0.261.138 – 0.261.061]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.070 | [Release notes 0.261.138 – 0.261.061]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.065 | [Release notes 0.261.138 – 0.261.061]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.064 | [Release notes 0.261.138 – 0.261.061]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.063 | [Release notes 0.261.138 – 0.261.061]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.062 | [Release notes 0.261.138 – 0.261.061]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.061 | [Release notes 0.261.138 – 0.261.061]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.060 | [Release notes 0.261.060 – 0.261.021]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.059 | [Release notes 0.261.060 – 0.261.021]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.058 | [Release notes 0.261.060 – 0.261.021]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.057 | [Release notes 0.261.060 – 0.261.021]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.056 | [Release notes 0.261.060 – 0.261.021]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.055 | [Release notes 0.261.060 – 0.261.021]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.054 | [Release notes 0.261.060 – 0.261.021]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.053 | [Release notes 0.261.060 – 0.261.021]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.052 | [Release notes 0.261.060 – 0.261.021]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.051 | [Release notes 0.261.060 – 0.261.021]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.050 | [Release notes 0.261.060 – 0.261.021]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.049 | [Release notes 0.261.060 – 0.261.021]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.048 | [Release notes 0.261.060 – 0.261.021]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.047 | [Release notes 0.261.060 – 0.261.021]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.046 | [Release notes 0.261.060 – 0.261.021]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.045 | [Release notes 0.261.060 – 0.261.021]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.044 | [Release notes 0.261.060 – 0.261.021]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.043 | [Release notes 0.261.060 – 0.261.021]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.042 | [Release notes 0.261.060 – 0.261.021]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.041 | [Release notes 0.261.060 – 0.261.021]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.040 | [Release notes 0.261.060 – 0.261.021]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.039 | [Release notes 0.261.060 – 0.261.021]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.038 | [Release notes 0.261.060 – 0.261.021]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.037 | [Release notes 0.261.060 – 0.261.021]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.036 | [Release notes 0.261.060 – 0.261.021]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.035 | [Release notes 0.261.060 – 0.261.021]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.034 | [Release notes 0.261.060 – 0.261.021]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.033 | [Release notes 0.261.060 – 0.261.021]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.032 | [Release notes 0.261.060 – 0.261.021]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.031 | [Release notes 0.261.060 – 0.261.021]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.030 | [Release notes 0.261.060 – 0.261.021]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.029 | [Release notes 0.261.060 – 0.261.021]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.028 | [Release notes 0.261.060 – 0.261.021]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.027 | [Release notes 0.261.060 – 0.261.021]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.026 | [Release notes 0.261.060 – 0.261.021]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.025 | [Release notes 0.261.060 – 0.261.021]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.024 | [Release notes 0.261.060 – 0.261.021]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.023 | [Release notes 0.261.060 – 0.261.021]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.022 | [Release notes 0.261.060 – 0.261.021]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.021 | [Release notes 0.261.060 – 0.261.021]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.020 | [Release notes 0.261.020 – 0.261.002]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | +| v0.261.019 | [Release notes 0.261.020 – 0.261.002]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | +| v0.261.018 | [Release notes 0.261.020 – 0.261.002]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | +| v0.261.017 | [Release notes 0.261.020 – 0.261.002]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | +| v0.261.016 | [Release notes 0.261.020 – 0.261.002]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | +| v0.261.015 | [Release notes 0.261.020 – 0.261.002]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | +| v0.261.014 | [Release notes 0.261.020 – 0.261.002]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | +| v0.261.013 | [Release notes 0.261.020 – 0.261.002]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | +| v0.261.012 | [Release notes 0.261.020 – 0.261.002]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | +| v0.261.011 | [Release notes 0.261.020 – 0.261.002]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | +| v0.261.010 | [Release notes 0.261.020 – 0.261.002]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | +| v0.261.009 | [Release notes 0.261.020 – 0.261.002]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | +| v0.261.008 | [Release notes 0.261.020 – 0.261.002]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | +| v0.261.007 | [Release notes 0.261.020 – 0.261.002]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | +| v0.261.006 | [Release notes 0.261.020 – 0.261.002]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | +| v0.261.005 | [Release notes 0.261.020 – 0.261.002]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | +| v0.261.004 | [Release notes 0.261.020 – 0.261.002]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | +| v0.261.003 | [Release notes 0.261.020 – 0.261.002]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | +| v0.261.002 | [Release notes 0.261.020 – 0.261.002]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | | v0.261.001 | [Release notes 0.261.001]({{ '/explanation/release-notes/v0.261-part-8/' | 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 }}) | @@ -339,6 +340,15 @@ This page includes the latest release notes inline. Older release sections are s ## Latest release notes +### **(v0.261.423)** + +#### Bug Fixes + +* **Safe Plain-Text OAuth Sign-In Errors** + * `/getAToken` and `/getATokenApi` now return a consistent plain-text HTTP 400 login-failure message without exposing provider error contents. + * Keeps safe diagnostic fields server-side and preserves missing-code handling, Teams sign-in, and separate Microsoft 365 chat/workflow connection callbacks. + * (Ref: `route_frontend_authentication.py`, `test_oauth_error_responses.py`, `OAUTH_ERROR_RESPONSE_XSS_FIX.md`, [#1903](https://github.com/microsoft/simplechat/pull/1903) as a historical non-closing reference) + ### **(v0.261.422)** #### Bug Fixes @@ -486,12 +496,3 @@ This page includes the latest release notes inline. Older release sections are s * Fixed `sort_messages_by_thread` so unthreaded orchestration turns (`user_orchestration_*` and `assistant_orchestration_*`) are interleaved chronologically with threaded manual chat turns based on each thread's earliest timestamp rather than placing all orchestration turns before all manual turns. * Clarified saved `Instruction Memory`, `Fact Memory`, and orchestration composition/planning prompts so updated profile memories supersede earlier assistant prefixes, names, call signs, and roles without quoting, contrasting, or flagging the earlier values as note inconsistencies during consistency checks. * (Ref: `functions_chat.py`, `functions_fact_memory_context.py`, `functions_orchestration_composition.py`, `functions_orchestration_planner.py`, [Mixed-Mode History Order and Superseded Memory Fix](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/fixes/MIXED_MODE_HISTORY_ORDER_AND_SUPERSEDED_MEMORY_FIX.md)) - -### **(v0.261.378)** - -#### Bug Fixes - -* **V2 Chat Orchestration Unseeded Agent Availability And Catalog Ranking Fix** - * Fixed an issue where V2 chat orchestration turns without a manually pinned composer agent (`seeds.agent: null`) dropped `agent_invoke` whenever `user_settings.settings.enable_agents` had been persisted as `false` (for example after saving any user setting when `enable_agents` was not yet stored). V2 orchestration now evaluates `agent_invoke` using admin capability settings, governance, scope/group membership, and optional `seeds.agent` narrowing rather than the Classic `/chats` composer toggle. - * Updated `update_user_settings()` to default missing `enable_agents` to `True`, ranked agents and external sources named in `request_text` ahead of unnamed entries before applying the 25-item workflow planning catalog cap, and preserved `group` scope metadata when background chat upload ingestion completes. - * (Ref: `functions_settings.py`, `route_backend_orchestration.py`, `functions_orchestration_bootstrap.py`, `functions_orchestration_external_sources.py`, `functions_orchestration_workflow_context.py`, `functions_documents.py`, [Orchestration Unseeded Agent Availability Fix](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/fixes/ORCHESTRATION_UNSEEDED_AGENT_AVAILABILITY_FIX.md)) diff --git a/docs/explanation/release-notes/v0.261-part-1.md b/docs/explanation/release-notes/v0.261-part-1.md index 2dfc256be..abcecc88d 100644 --- a/docs/explanation/release-notes/v0.261-part-1.md +++ b/docs/explanation/release-notes/v0.261-part-1.md @@ -1,16 +1,25 @@ --- -title: "Release notes 0.261.333 – 0.261.267" -description: "SimpleChat release notes for 0.261.333 – 0.261.267." +title: "Release notes 0.261.378 – 0.261.268" +description: "SimpleChat release notes for 0.261.378 – 0.261.268." section: "Reference" layout: page --- -# Release notes 0.261.333 – 0.261.267 +# Release notes 0.261.378 – 0.261.268 [Back to release notes index]({{ '/explanation/release_notes/' | relative_url }}) +### **(v0.261.378)** + +#### Bug Fixes + +* **V2 Chat Orchestration Unseeded Agent Availability And Catalog Ranking Fix** + * Fixed an issue where V2 chat orchestration turns without a manually pinned composer agent (`seeds.agent: null`) dropped `agent_invoke` whenever `user_settings.settings.enable_agents` had been persisted as `false` (for example after saving any user setting when `enable_agents` was not yet stored). V2 orchestration now evaluates `agent_invoke` using admin capability settings, governance, scope/group membership, and optional `seeds.agent` narrowing rather than the Classic `/chats` composer toggle. + * Updated `update_user_settings()` to default missing `enable_agents` to `True`, ranked agents and external sources named in `request_text` ahead of unnamed entries before applying the 25-item workflow planning catalog cap, and preserved `group` scope metadata when background chat upload ingestion completes. + * (Ref: `functions_settings.py`, `route_backend_orchestration.py`, `functions_orchestration_bootstrap.py`, `functions_orchestration_external_sources.py`, `functions_orchestration_workflow_context.py`, `functions_documents.py`, [Orchestration Unseeded Agent Availability Fix](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/fixes/ORCHESTRATION_UNSEEDED_AGENT_AVAILABILITY_FIX.md)) + ### **(v0.261.333)** #### New Features @@ -746,12 +755,3 @@ layout: page * 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](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/features/V2_ADMIN_SETTINGS_RAIL_COLLAPSE.md)) diff --git a/docs/explanation/release-notes/v0.261-part-2.md b/docs/explanation/release-notes/v0.261-part-2.md index 7a995c092..ec0bdd0da 100644 --- a/docs/explanation/release-notes/v0.261-part-2.md +++ b/docs/explanation/release-notes/v0.261-part-2.md @@ -1,16 +1,25 @@ --- -title: "Release notes 0.261.265 – 0.261.223" -description: "SimpleChat release notes for 0.261.265 – 0.261.223." +title: "Release notes 0.261.267 – 0.261.224" +description: "SimpleChat release notes for 0.261.267 – 0.261.224." section: "Reference" layout: page --- -# Release notes 0.261.265 – 0.261.223 +# Release notes 0.261.267 – 0.261.224 [Back to release notes index]({{ '/explanation/release_notes/' | relative_url }}) +### **(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](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/features/V2_ADMIN_SETTINGS_RAIL_COLLAPSE.md)) + ### **(v0.261.265)** #### User Interface Enhancements @@ -603,28 +612,3 @@ layout: page * 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](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/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](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/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 %} diff --git a/docs/explanation/release-notes/v0.261-part-3.md b/docs/explanation/release-notes/v0.261-part-3.md index 45843a35d..18a485efd 100644 --- a/docs/explanation/release-notes/v0.261-part-3.md +++ b/docs/explanation/release-notes/v0.261-part-3.md @@ -1,16 +1,41 @@ --- -title: "Release notes 0.261.222 – 0.261.178" -description: "SimpleChat release notes for 0.261.222 – 0.261.178." +title: "Release notes 0.261.223 – 0.261.179" +description: "SimpleChat release notes for 0.261.223 – 0.261.179." section: "Reference" layout: page --- -# Release notes 0.261.222 – 0.261.178 +# Release notes 0.261.223 – 0.261.179 [Back to release notes index]({{ '/explanation/release_notes/' | relative_url }}) +{% 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](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/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 @@ -841,12 +866,3 @@ Merged from the React V2 branch at `d243b0ca`. These changes shipped there as 0. * **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](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/features/V2_GROUP_WORKFLOWS.md)) diff --git a/docs/explanation/release-notes/v0.261-part-4.md b/docs/explanation/release-notes/v0.261-part-4.md index 48121fb06..246322e9a 100644 --- a/docs/explanation/release-notes/v0.261-part-4.md +++ b/docs/explanation/release-notes/v0.261-part-4.md @@ -1,16 +1,25 @@ --- -title: "Release notes 0.261.177 – 0.261.138" -description: "SimpleChat release notes for 0.261.177 – 0.261.138." +title: "Release notes 0.261.178 – 0.261.139" +description: "SimpleChat release notes for 0.261.178 – 0.261.139." section: "Reference" layout: page --- -# Release notes 0.261.177 – 0.261.138 +# Release notes 0.261.178 – 0.261.139 [Back to release notes index]({{ '/explanation/release_notes/' | relative_url }}) +### **(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](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/features/V2_GROUP_WORKFLOWS.md)) + ### **(v0.261.177)** #### New Features @@ -578,35 +587,3 @@ layout: page * 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](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/features/V2_GROUP_IDENTITIES.md), [Group Identity APIs](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/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](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/features/V2_GROUP_AGENTS.md), [Group Agent APIs](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/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](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/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](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/fixes/FILE_SYNC_CONCURRENT_WRITE_FIX.md)) diff --git a/docs/explanation/release-notes/v0.261-part-5.md b/docs/explanation/release-notes/v0.261-part-5.md index 7fbe0b798..dd783ddbe 100644 --- a/docs/explanation/release-notes/v0.261-part-5.md +++ b/docs/explanation/release-notes/v0.261-part-5.md @@ -1,16 +1,48 @@ --- -title: "Release notes 0.261.137 – 0.261.060" -description: "SimpleChat release notes for 0.261.137 – 0.261.060." +title: "Release notes 0.261.138 – 0.261.061" +description: "SimpleChat release notes for 0.261.138 – 0.261.061." section: "Reference" layout: page --- -# Release notes 0.261.137 – 0.261.060 +# Release notes 0.261.138 – 0.261.061 [Back to release notes index]({{ '/explanation/release_notes/' | relative_url }}) +### **(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](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/features/V2_GROUP_AGENTS.md), [Group Agent APIs](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/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](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/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](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/fixes/FILE_SYNC_CONCURRENT_WRITE_FIX.md)) + ### **(v0.261.137)** #### New Features @@ -1287,89 +1319,3 @@ part of the Chat group feature work. * Added opt-in environment-variable diagnostics (`SIMPLECHAT_TABULAR_HTTP_DIAGNOSTICS`, `SIMPLECHAT_TABULAR_HTTP_BUDGET_SECONDS`, `SIMPLECHAT_TABULAR_HTTP_MAX_RETRIES`, and `SIMPLECHAT_TABULAR_HTTP_TIMEOUT_SECONDS`) for tabular Azure OpenAI calls. * Records structured `[TABULAR_MODEL_HTTP]` telemetry covering request attempts, retries, elapsed durations, correlation headers, and pre-header transport failures without logging prompts, completions, or credentials. * (Ref: `functions_tabular_model_diagnostics.py`, `functions_model_endpoint_runtime.py`, `route_backend_chats.py`, `docs/admin/operations.md`) - -### **(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](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/features/CHAT_ORCHESTRATION.md), [Orchestration settings](https://microsoft.github.io/simplechat/admin/orchestration/)) - -* **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](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/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`) - -* **Safety Violation Date and User Metadata** - * Safety violation list and card views now show created dates and target user names, reducing the need to open each violation to identify when it occurred and who it affected. - * CSV exports now include a user name column and populate created, last updated, and archived timestamps using existing safety log metadata when older records are missing explicit date fields. - * (Ref: `route_backend_safety.py`, `admin-safety-violations.js`, [Safety Violation Metadata Display Fix](https://github.com/microsoft/simplechat/blob/f4e186401b58e284ee9b626aa84c903f69ea100b/docs/explanation/fixes/SAFETY_VIOLATION_METADATA_DISPLAY_FIX.md)) diff --git a/docs/explanation/release-notes/v0.261-part-6.md b/docs/explanation/release-notes/v0.261-part-6.md index 7ed141818..a4b00006f 100644 --- a/docs/explanation/release-notes/v0.261-part-6.md +++ b/docs/explanation/release-notes/v0.261-part-6.md @@ -1,16 +1,102 @@ --- -title: "Release notes 0.261.059 – 0.261.020" -description: "SimpleChat release notes for 0.261.059 – 0.261.020." +title: "Release notes 0.261.060 – 0.261.021" +description: "SimpleChat release notes for 0.261.060 – 0.261.021." section: "Reference" layout: page --- -# Release notes 0.261.059 – 0.261.020 +# Release notes 0.261.060 – 0.261.021 [Back to release notes index]({{ '/explanation/release_notes/' | relative_url }}) +### **(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](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/features/CHAT_ORCHESTRATION.md), [Orchestration settings](https://microsoft.github.io/simplechat/admin/orchestration/)) + +* **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](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/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`) + +* **Safety Violation Date and User Metadata** + * Safety violation list and card views now show created dates and target user names, reducing the need to open each violation to identify when it occurred and who it affected. + * CSV exports now include a user name column and populate created, last updated, and archived timestamps using existing safety log metadata when older records are missing explicit date fields. + * (Ref: `route_backend_safety.py`, `admin-safety-violations.js`, [Safety Violation Metadata Display Fix](https://github.com/microsoft/simplechat/blob/f4e186401b58e284ee9b626aa84c903f69ea100b/docs/explanation/fixes/SAFETY_VIOLATION_METADATA_DISPLAY_FIX.md)) + ### **(v0.261.059)** #### New Features @@ -1283,27 +1369,3 @@ Tracking: [#1489](https://github.com/microsoft/simplechat/issues/1489); implemen * 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)) diff --git a/docs/explanation/release-notes/v0.261-part-7.md b/docs/explanation/release-notes/v0.261-part-7.md index 9c676bc95..86bf26504 100644 --- a/docs/explanation/release-notes/v0.261-part-7.md +++ b/docs/explanation/release-notes/v0.261-part-7.md @@ -1,16 +1,40 @@ --- -title: "Release notes 0.261.019 – 0.261.002" -description: "SimpleChat release notes for 0.261.019 – 0.261.002." +title: "Release notes 0.261.020 – 0.261.002" +description: "SimpleChat release notes for 0.261.020 – 0.261.002." section: "Reference" layout: page --- -# Release notes 0.261.019 – 0.261.002 +# Release notes 0.261.020 – 0.261.002 [Back to release notes index]({{ '/explanation/release_notes/' | relative_url }}) +### **(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 diff --git a/docs/explanation/release_notes.md b/docs/explanation/release_notes.md index 4e1598235..aed58e0a3 100644 --- a/docs/explanation/release_notes.md +++ b/docs/explanation/release_notes.md @@ -2,6 +2,15 @@ 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.423)** + +#### Bug Fixes + +* **Safe Plain-Text OAuth Sign-In Errors** + * `/getAToken` and `/getATokenApi` now return a consistent plain-text HTTP 400 login-failure message without exposing provider error contents. + * Keeps safe diagnostic fields server-side and preserves missing-code handling, Teams sign-in, and separate Microsoft 365 chat/workflow connection callbacks. + * (Ref: `route_frontend_authentication.py`, `test_oauth_error_responses.py`, `OAUTH_ERROR_RESPONSE_XSS_FIX.md`, [#1903](https://github.com/microsoft/simplechat/pull/1903) as a historical non-closing reference) + ### **(v0.261.422)** #### Bug Fixes diff --git a/functional_tests/test_m365_routes.py b/functional_tests/test_m365_routes.py index 5f2b993c1..6cbc946d6 100644 --- a/functional_tests/test_m365_routes.py +++ b/functional_tests/test_m365_routes.py @@ -1,9 +1,10 @@ # test_m365_routes.py """ Functional tests for Microsoft 365 Profile, approval, and audit routes. -Version: 0.261.374 +Version: 0.261.423 Implemented in: 0.261.029 Workflow authorization names and visibility implemented in: 0.261.374 +Notification fixture import parity repaired in: 0.261.423 Imports the real route module with scoped authentication/logging I/O seams. Exercises exact subject/object scope, CSRF, safe errors, callback registration, @@ -992,6 +993,8 @@ def test_chat_disconnect_requires_csrf_and_uses_only_signed_in_subject(self): def test_real_notification_helper_only_targets_subject_and_deduplicates(self): notification_container = CosmosContainer("user_id") logger = Mock() + group_role = Mock(side_effect=LookupError("Group not found")) + user_groups = Mock(return_value=[]) spec = importlib.util.spec_from_file_location( "m365_test_notifications", APP_DIR / "functions_notifications.py", ) @@ -999,7 +1002,9 @@ def test_real_notification_helper_only_targets_subject_and_deduplicates(self): with patch.dict(sys.modules, { "config": module_stub("config", cosmos_notifications_container=notification_container), "functions_appinsights": module_stub("functions_appinsights", log_event=logger), - "functions_group": module_stub("functions_group", find_group_by_id=lambda value: None), + "functions_group": module_stub( + "functions_group", assert_group_role=group_role, get_user_groups=user_groups, + ), "functions_debug": module_stub("functions_debug", debug_print=lambda *args, **kwargs: None), "functions_public_workspaces": module_stub( "functions_public_workspaces", @@ -1045,6 +1050,8 @@ def test_real_notification_helper_only_targets_subject_and_deduplicates(self): if item["metadata"]["approval_id"] == cancel_id ] self.assertEqual(statuses, ["cancelled"]) + group_role.assert_not_called() + user_groups.assert_not_called() def test_workflow_authorization_sdk_contract(): diff --git a/functional_tests/test_oauth_error_responses.py b/functional_tests/test_oauth_error_responses.py new file mode 100644 index 000000000..df4978033 --- /dev/null +++ b/functional_tests/test_oauth_error_responses.py @@ -0,0 +1,406 @@ +# test_oauth_error_responses.py +""" +Functional HTTP regressions for the registered OAuth callback error responses. +Version: 0.261.423 +Implemented in: 0.261.423 + +Real Flask callback registration exercises hostile provider parameters, unchanged +missing-code behavior, sign-in, and Microsoft 365 prefix dispatch. Existing M365 +fixtures supply real MSAL state/nonce checks, AES-GCM, and SDK-default Cosmos +responses without Azure or Graph calls. Startup I/O and login token exchange are +scoped seams; this is not live browser or full application-bootstrap validation. +""" + +import copy +import importlib.util +import logging +import sys +import time +from types import SimpleNamespace +from urllib.parse import parse_qs, quote, urlsplit +from unittest.mock import Mock, patch + +import msal +import pytest +from flask import Blueprint, Flask, jsonify, redirect, render_template, request, session, url_for + +import test_m365_connections as connection_fixtures +import test_m365_routes as route_fixtures + + +APP_DIR = route_fixtures.APP_DIR +LOGIN_FAILURE_MESSAGE = "Login failed. Please try signing in again." +USER = {"oid": "user-a", "tid": "tenant-a", "roles": ["User"], "name": "Test User"} +HOSTILE_DESCRIPTION = '' +SESSION_VALUES = { + "user": USER, + "token_cache": "existing-login-cache", + "requested_oauth_scopes": ["User.Read"], + "m365_chat_token_cache": "existing-chat-cache", + "m365_workflow_oauth_binding": "existing-workflow-binding", +} + + +def _load_routes(name, filename): + spec = importlib.util.spec_from_file_location(name, APP_DIR / filename) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _set_session(client, values): + with client.session_transaction() as saved: + saved.clear() + saved.update(copy.deepcopy(values)) + + +def _read_session(client): + with client.session_transaction() as saved: + return copy.deepcopy(dict(saved)) + + +@pytest.fixture +def callbacks(): + """Register both real route modules with the existing scoped M365 seams.""" + app = Flask( + __name__, template_folder=str(APP_DIR / "templates"), + static_folder=str(APP_DIR / "static"), + ) + app.config.update(TESTING=True, SECRET_KEY="oauth-test-only") + logger = Mock() + cache = msal.SerializableTokenCache() + token_client = Mock(token_cache=cache) + builder = Mock(return_value=token_client) + load_cache = Mock(return_value=cache) + save_cache = Mock() + scopes = Mock(return_value=["User.Read"]) + settings = Mock(return_value={}) + login_activity = Mock() + dependencies = { + "config": route_fixtures.module_stub( + "config", request=request, session=session, jsonify=jsonify, + redirect=redirect, render_template=render_template, url_for=url_for, + time=time, quote=quote, LOGIN_REDIRECT_URL=None, HOME_REDIRECT_URL=None, + DISABLE_APP_SERVICE_EASY_AUTH_LOGOUT=False, ENABLE_TEAMS_SSO=False, + CLIENT_ID="client-a", TENANT_ID="tenant-a", SCOPE=["User.Read"], + ), + "functions_appinsights": route_fixtures.module_stub("functions_appinsights", log_event=logger), + "functions_authentication": route_fixtures.module_stub( + "functions_authentication", _build_msal_app=builder, + _load_cache=load_cache, _save_cache=save_cache, + clear_requested_oauth_scopes=Mock(), get_requested_oauth_scopes=scopes, + create_ci_bearer_session=Mock(), + get_graph_authority=lambda: "https://login.microsoftonline.com/tenant-a", + get_graph_endpoint=lambda path: f"https://graph.microsoft.com{path}", + login_required=route_fixtures.login_guard, user_required=route_fixtures.user_guard, + user_required_blueprint=route_fixtures.blueprint_guard, + ), + "functions_activity_logging": route_fixtures.module_stub( + "functions_activity_logging", log_user_login=login_activity, + record_user_login_session_activity=Mock(), + ), + "functions_terms_of_use": route_fixtures.module_stub( + "functions_terms_of_use", apply_pending_pre_auth_terms_of_use=Mock(), + get_terms_of_use_config=lambda value: {"enabled": False}, + has_terms_of_use_acceptance=lambda value: True, + ), + "functions_debug": route_fixtures.module_stub("functions_debug", debug_print=Mock()), + "functions_settings": route_fixtures.module_stub( + "functions_settings", get_settings=settings, + sanitize_settings_for_user=dict, + ), + "swagger_wrapper": route_fixtures.module_stub( + "swagger_wrapper", swagger_route=lambda **kwargs: lambda function: function, + get_auth_security=lambda: [], + ), + "functions_m365_request_resume": route_fixtures.module_stub( + "functions_m365_request_resume", + get_m365_chat_request=Mock(side_effect=AssertionError("Profile sign-in must not resume a request.")), + ), + "functions_m365_runtime": route_fixtures.module_stub( + "functions_m365_runtime", + _conversation_access=Mock(side_effect=AssertionError("Profile sign-in must not resume a conversation.")), + ), + } + with patch.dict(sys.modules, dependencies): + frontend = _load_routes("oauth_test_frontend", "route_frontend_authentication.py") + backend = _load_routes("oauth_test_m365_backend", "route_backend_m365.py") + sys.modules["route_backend_m365"] = backend + blueprint = Blueprint("frontend_authentication", __name__) + frontend.register_route_frontend_authentication(blueprint) + app.register_blueprint(blueprint) + public = Blueprint("public_app", __name__) + public.add_url_rule("/", endpoint="index", view_func=lambda: "Sign in") + app.register_blueprint(public) + client = app.test_client() + _set_session(client, SESSION_VALUES) + yield SimpleNamespace( + app=app, client=client, frontend=frontend, backend=backend, + logger=logger, builder=builder, load_cache=load_cache, save_cache=save_cache, + scopes=scopes, settings=settings, token_client=token_client, + cache=cache, login_activity=login_activity, + ) + + +@pytest.mark.parametrize("callback", ["/getAToken", "/getATokenApi"]) +@pytest.mark.parametrize( + "query, diagnostic", + [ + ({"error": "access_denied", "error_description": HOSTILE_DESCRIPTION}, "access_denied"), + ({"error": HOSTILE_DESCRIPTION, "error_description": '">'}, "unrecognized_error"), + ({"error": "access_denied"}, "access_denied"), + ({"error": "server_error", "error_description": 'secret=do-not-return\r\n"provider detail"'}, "server_error"), + ({ + "error": 'secret=do-not-log-"', + "error_description": HOSTILE_DESCRIPTION, "code": "must-not-exchange", "state": "ordinary-login", + }, "unrecognized_error"), + ], +) +def test_provider_error_is_a_fixed_plain_text_400(callbacks, capsys, callback, query, diagnostic): + """Provider markup, quotes, descriptions, and secrets never reach HTML or logs.""" + before = _read_session(callbacks.client) + response = callbacks.client.get(callback, query_string=query, headers={"Accept": "text/html"}) + body = response.get_data(as_text=True) + after = _read_session(callbacks.client) + output = capsys.readouterr() + + assert response.status_code == 400 + assert response.headers["Content-Type"] == "text/plain; charset=utf-8" + assert body == LOGIN_FAILURE_MESSAGE + assert after == before + callbacks.builder.assert_not_called() + callbacks.load_cache.assert_not_called() + callbacks.save_cache.assert_not_called() + callbacks.scopes.assert_not_called() + callbacks.settings.assert_not_called() + callbacks.logger.assert_called_once_with( + "[AUTH_CALLBACK] OAuth sign-in was not completed.", + extra={ + "path": callback, "oauth_error": diagnostic, + "error_description_present": bool(query.get("error_description")), + }, + level=logging.WARNING, + ) + assert not output.out + assert not output.err + + +@pytest.mark.parametrize("callback", ["/getAToken", "/getATokenApi"]) +@pytest.mark.parametrize("query", [{}, {"error": ""}, {"error_description": HOSTILE_DESCRIPTION}]) +def test_missing_code_behavior_is_unchanged_at_http_boundary(callbacks, callback, query): + """A missing/empty error is not a provider failure or a token exchange.""" + before = _read_session(callbacks.client) + response = callbacks.client.get(callback, query_string=query) + after = _read_session(callbacks.client) + body = response.get_data(as_text=True) + + if callback == "/getAToken": + assert response.status_code == 302 + assert response.headers["Location"] == "/" + else: + assert response.status_code == 400 + assert body == "Authorization code not found" + assert HOSTILE_DESCRIPTION not in body + assert after == before + callbacks.builder.assert_not_called() + callbacks.load_cache.assert_not_called() + callbacks.save_cache.assert_not_called() + + +@pytest.mark.parametrize("callback", ["/getAToken", "/getATokenApi"]) +def test_successful_login_and_api_callback_contracts_are_unchanged(callbacks, callback): + """The app login still saves identity/cache; the API flow remains separate.""" + claims = {**USER, "name": "Signed-in User"} + result = {"id_token_claims": claims, "access_token": "test-only-api-token"} + callbacks.token_client.acquire_token_by_authorization_code.return_value = result + response = callbacks.client.get(callback, query_string={"code": "test-only-code"}) + saved = _read_session(callbacks.client) + + callbacks.builder.assert_called_once_with( + cache=callbacks.cache, authority_override="https://login.microsoftonline.com/tenant-a", + ) + callbacks.scopes.assert_called_once_with(clear_after_read=True) + callbacks.token_client.acquire_token_by_authorization_code.assert_called_once_with( + code="test-only-code", scopes=["User.Read"], redirect_uri="https://localhost/getAToken", + ) + if callback == "/getAToken": + assert response.status_code == 302 + assert response.headers["Location"] == "/" + assert saved["user"] == claims + assert isinstance(saved["last_activity_epoch"], int) + callbacks.save_cache.assert_called_once_with(callbacks.cache) + callbacks.login_activity.assert_called_once_with("user-a", "azure_ad") + else: + assert response.status_code == 200 + assert response.get_json() == [result, 200] + assert saved["user"] == USER + callbacks.save_cache.assert_not_called() + callbacks.login_activity.assert_not_called() + assert saved["m365_chat_token_cache"] == SESSION_VALUES["m365_chat_token_cache"] + assert saved["m365_workflow_oauth_binding"] == SESSION_VALUES["m365_workflow_oauth_binding"] + + +@pytest.fixture +def m365_case(callbacks): + """Reuse test_support.m365-backed real MSAL/encryption/account fixtures.""" + case = connection_fixtures.M365ConnectionTests() + case.setUp() + try: + yield case + finally: + case.doCleanups() + + +def _begin_m365(callbacks, case, kind, completion="page"): + connections = connection_fixtures.connections + with callbacks.app.test_request_context(): + session.update(copy.deepcopy(SESSION_VALUES)) + if kind == "chat": + started = case.service.start_profile_chat_connection( + "user-a", "tenant-a", ["email"], "https://localhost/getAToken", completion=completion, + ) + else: + started = case.service.start_connection( + "user-a", "tenant-a", ["email"], "https://localhost/getAToken", + SESSION_VALUES["m365_workflow_oauth_binding"], completion=completion, + ) + values = copy.deepcopy(dict(session)) + query = parse_qs(urlsplit(started["authorization_url"]).query) + case.http.nonce = query["nonce"][0] + state = query["state"][0] + prefix = connections.CHAT_AUTH_STATE_PREFIX if kind == "chat" else connections.WORKFLOW_AUTH_STATE_PREFIX + assert state.startswith(prefix) + _set_session(callbacks.client, values) + return state + + +@pytest.mark.parametrize("kind", ["chat", "workflow"]) +def test_m365_provider_errors_keep_their_separate_completion_flow(callbacks, m365_case, kind): + """Prefix dispatch precedes the generic error response and preserves identity.""" + state = _begin_m365(callbacks, m365_case, kind) + response = callbacks.client.get("/getAToken", query_string={ + "state": state, "error": "access_denied", "error_description": HOSTILE_DESCRIPTION, + }) + saved = _read_session(callbacks.client) + body = response.get_data(as_text=True) + + assert response.status_code == 400 + assert response.mimetype == "text/html" + assert '"code": "m365_consent_required"' in body + assert HOSTILE_DESCRIPTION not in body + assert LOGIN_FAILURE_MESSAGE not in body + assert response.headers["Cache-Control"] == "private, no-store" + assert response.headers["Pragma"] == "no-cache" + assert saved["user"] == USER + assert saved["token_cache"] == SESSION_VALUES["token_cache"] + assert saved["m365_chat_token_cache"] == SESSION_VALUES["m365_chat_token_cache"] + assert m365_case.http.posts == [] + callbacks.builder.assert_not_called() + callbacks.save_cache.assert_not_called() + callbacks.logger.assert_not_called() + + +@pytest.mark.parametrize("kind", ["chat", "workflow"]) +@pytest.mark.parametrize( + "guard, status, code", + [ + ("state", 400, "m365_auth_state_invalid"), + ("session", 400, "m365_auth_state_invalid"), + ("account", 403, "m365_account_mismatch"), + ("nonce", 400, "m365_auth_validation_failed"), + ], +) +def test_m365_callback_guards_still_run_through_getatoken(callbacks, m365_case, kind, guard, status, code): + """Invalid state/session/account/nonce cannot publish another credential.""" + state = _begin_m365(callbacks, m365_case, kind) + if guard == "state": + state += "different" + elif guard == "session": + with callbacks.client.session_transaction() as saved: + if kind == "chat": + saved.pop(connection_fixtures.connections.CHAT_AUTH_SESSION_KEY) + else: + saved["m365_workflow_oauth_binding"] = "another-session-binding" + elif guard == "account": + m365_case.http.claim_overrides = {"oid": "another-user"} + else: + m365_case.http.claim_overrides = {"nonce": "wrong-nonce"} + response = callbacks.client.get("/getAToken", query_string={"state": state, "code": "test-only-code"}) + saved = _read_session(callbacks.client) + body = response.get_data(as_text=True) + + assert response.status_code == status + assert f'"code": "{code}"' in body + assert saved["user"] == USER + assert saved["token_cache"] == SESSION_VALUES["token_cache"] + assert saved["m365_chat_token_cache"] == SESSION_VALUES["m365_chat_token_cache"] + assert all(not row.get("encrypted_cache") for row in m365_case.container.items.values()) + if guard in {"state", "session"}: + assert m365_case.http.posts == [] + callbacks.builder.assert_not_called() + callbacks.save_cache.assert_not_called() + + +@pytest.mark.parametrize("kind", ["chat", "workflow"]) +@pytest.mark.parametrize("completion", ["page", "popup"]) +def test_successful_m365_callbacks_preserve_principal_and_distinct_caches(callbacks, m365_case, kind, completion): + """Actual prefixed callbacks finish real MSAL flows, not the app login flow.""" + state = _begin_m365(callbacks, m365_case, kind, completion) + response = callbacks.client.get("/getAToken", query_string={"state": state, "code": "test-only-code"}) + saved = _read_session(callbacks.client) + body = response.get_data(as_text=True) + + assert response.status_code == (302 if completion == "page" else 200) + if completion == "page": + expected = ( + callbacks.backend.CLASSIC_CHAT_CONNECTED_URL if kind == "chat" + else callbacks.backend.CLASSIC_WORKFLOW_CONNECTED_URL + ) + assert response.headers["Location"] == expected + else: + message_type = "m365-profile-reconnected" if kind == "chat" else "m365-workflow-connected" + assert f'"type": "{message_type}"' in body + assert response.headers["Cache-Control"] == "private, no-store" + assert saved["user"] == USER + assert saved["token_cache"] == SESSION_VALUES["token_cache"] + assert "access-token-must-stay-server-side" not in body + assert "refresh-token-must-stay-server-side" not in body + if kind == "chat": + assert "refresh-token-must-stay-server-side" in saved["m365_chat_token_cache"] + assert not m365_case.container.items + else: + assert saved["m365_chat_token_cache"] == SESSION_VALUES["m365_chat_token_cache"] + assert any(row.get("encrypted_cache") for row in m365_case.container.items.values()) + callbacks.builder.assert_not_called() + callbacks.save_cache.assert_not_called() + callbacks.login_activity.assert_not_called() + + +def test_teams_token_exchange_remains_a_separate_session_flow(callbacks, monkeypatch): + """The Teams endpoint still uses OBO and its own session identity contract.""" + monkeypatch.setattr(callbacks.frontend, "ENABLE_TEAMS_SSO", True) + graph = Mock(return_value={}) + monkeypatch.setattr(callbacks.frontend, "_fetch_graph_me", graph) + claims = {**USER, "preferred_username": "user-a@example.test"} + callbacks.token_client.acquire_token_on_behalf_of.return_value = { + "id_token_claims": claims, "access_token": "test-only-teams-token", + } + response = callbacks.client.post("/auth/teams/token-exchange", json={"token": "test-only-teams-assertion"}) + saved = _read_session(callbacks.client) + + assert response.status_code == 200 + assert response.get_json() == { + "success": True, "user": {"name": "Test User", "email": "user-a@example.test", "id": "user-a"}, + } + assert saved["user"]["oid"] == "user-a" + assert saved["user"]["tid"] == "tenant-a" + callbacks.token_client.acquire_token_on_behalf_of.assert_called_once_with( + user_assertion="test-only-teams-assertion", scopes=["User.Read"], + ) + graph.assert_called_once_with("test-only-teams-token") + callbacks.save_cache.assert_called_once_with(callbacks.cache) + callbacks.login_activity.assert_called_once_with("user-a", "teams_sso") + + +if __name__ == "__main__": + sys.exit(pytest.main([__file__, "-q", "-p", "test_support.cosmos_network_guard"]))