Repository navigation
Send safety warnings immediately and show restricted users an Access restricted screen - #1709
Merged
Paul Lizer (paullizer) merged 7 commits intoOct 8, 2026
Conversation
…restricted screen Layer 1 of 3 of the Admin Review Center stack. - Warn user executes when the safety reviewer saves the review, without an approval request, and is audited in the activity log. Saving again does not resend it. Suspend and Block still need a second eligible reviewer. - Executed warnings must be acknowledged: new user-scoped routes GET /api/safety/warnings/pending and POST /api/safety/warnings/<id>/acknowledge (owner-only, idempotent, ETag-conditional), a V2 dialog fed by a bootstrap pending count, and the acknowledgment state in the admin review and the user's Violations tab. - Escalate can no longer be chosen; legacy records keep it, labelled "Escalated (legacy)". escalate_count stays in the stats JSON. - Approved suspensions and blocks store the notice the user was sent in settings.access.notice. - user_required answers restricted users with a structured 403 (error: access_restricted) for API calls and redirects pages to /v2/access-restricted or /access-restricted. New login-only routes serve those pages and GET /api/v2/access-restriction (caller's own data only), exempt from the Terms of Use gate. - Version 0.261.296. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Adds the fix and feature notes, updates the safety review and approval request guides and the Content Safety settings page, records the release notes for 0.261.296, lists the new logging tags, and regenerates the docs app surface inventory for the classic Access restricted page. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…ser test Render the real classic templates and scripts behind a closed API: no Escalate choice, the legacy label, the second-reviewer help text, the warning acknowledgment line, Blocked statistics, and the localized restore time on the classic Access restricted page. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…restricted branch The base now uses 0.261.296, so this branch moves to 0.261.297: config.py, its tests, docs and a new release-notes section above the base's 0.261.296 entry. Both route-inventory coverage lines are kept. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A reviewer can warn about the same violation again. The V2 store hid any warning whose violation it had already acknowledged, so a newer warning never showed in a tab that stayed open. It now keys each warning on the violation and its send time. The acknowledge route also takes the issued_at of the warning the user read. When the violation now holds a newer warning, it answers 409 safety_warning_replaced and records nothing, and the dialog reads the pending warnings again to show the newer one. A body without issued_at keeps the previous behaviour. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Two overlapping saves with Warn user, such as a double-click or two reviewers, both passed the already-sent check, so each sent a notification and only one was recorded. A save now claims the violation with a write conditional on the ETag it read (action_request_status 'sending') before anything is sent; the other save sends nothing and gets 409 safety_warning_in_progress. The outcome is written on the claimed version. While a claim is fresh, other saves and deletes wait, and 'sending' never counts as a warning to acknowledge. A claim older than five minutes reads as a failed send and can be retried once. A send whose claim was lost is audited and reported as safety_warning_not_recorded instead of overwriting another writer. The classic Save Review button is disabled while its request is in flight. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This was referenced Oct 8, 2026
…and drop an unused test variable Addresses two CodeQL review notes: get_pending_safety_warnings returned a 2-tuple for a session without a user id and 3-tuples otherwise, and a classic UI test assigned a modal it never used. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Paul Lizer (paullizer)
merged commit Oct 8, 2026
8a9e0f7
into
microsoft:paullizer-react-v2-ui
11 checks passed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Layer 1 of 3 of the Admin Review Center stack. Layer 2 (Review center at
/admin/review) and Layer 3 (AI assist and batch triage) stack on this branch.action_request_status: "sending"), and the other save sends nothing and gets 409safety_warning_in_progress. A claim left by a save that stopped is released after five minutes. The classic Save Review button is disabled while its request is in flight. Suspend user and Block user keep the second-reviewer approval exactly as before, a requester still cannot approve their own request, and Warn User approvals created before this version still execute when approved.GET /api/safety/warnings/pendingandPOST /api/safety/warnings/<log_id>/acknowledge; bootstrap carries the pending count, so users without warnings make no extra request. Warnings sent before this version never pop up. A violation can be warned about again, so a warning is told apart by its send time: the acknowledgment carries theissued_atthe user read, and one replaced by a newer warning gets 409safety_warning_replacedand records nothing, while the dialog shows the newer warning.Escalate, while records that already carry it stay editable and read Escalated (legacy).escalate_countstays in the stats JSON; the V2 tile is now Blocked.settings.access.notice. Restricted (non-Admin) users can still sign in, but every page sends them to an Access restricted screen (/v2/access-restrictedor classic/access-restricted) with the notice, the restore time in their locale for a suspension, the violation reference and Sign out; API calls get403 {"error": "access_restricted", "message", "restriction", "restricted_url"}with the caller's own restriction only. Admins are never restricted.Linked issue
None. No GitHub issue was requested for this stack.
Release Notes & Latest Features
Is this visible to end users?
Is this admin-facing (Admin Settings, governance, deployment, config)?
Should this become a Latest Feature card?
Screenshot needed for the card?
Version bump
application/single_app/config.pyVERSIONthird segment bumped, or not needed because this is docs-only (0.261.296on the base →0.261.297; the base moved during the work, so the branch merged it and re-bumped)deployers/version.txtbumped, or not needed becausedeployers/was not changedTesting / validation
Run on the merged tree (base
f34094ae5) at head7e192f61unless noted.New and updated tests:
python -m pytest functional_tests/test_safety_warning_send_claim.py functional_tests/test_safety_warning_acknowledgment.py functional_tests/test_access_restricted_gate.py functional_tests/test_safety_escalate_removal.py functional_tests/test_safety_violation_remediation_approvals.py -q -p no:cacheprovider→ 32 passed (two overlapping saves send one warning; saves, deletes and acknowledgments wait while a warning is being sent, andsendingnever counts as pending; a stale claim is retried once; a lost claim is reported and audited, not overwritten; warn immediate, suspend/block still two-person, requester cannot approve, legacy warn approvals, acknowledgment ownership/idempotency/legacy warnings, withdrawn and replaced warnings, Escalate rules, notice persistence, structured 403 and redirects, login-only routes return only the caller's data, expiry restore; real modules in fresh processes, normal and-O)node functional_tests/test_v2_access_restriction_and_safety_warning_logic.mjs→ 11 checks passedpython -m pytest ui_tests/test_v2_access_restricted_and_safety_warning.py ui_tests/test_classic_safety_review_and_access_restricted.py ui_tests/test_v2_approvals_and_terms_pages.py -q -p no:cacheprovider→ 16 passed (V2 restricted page, warning dialog and a warning replaced while on screen; classic review page, including Save disabled while a save is in flight, and classic restricted page; existing approvals/terms fixtures)Route policy:
python functional_tests/route_tests/test_route_blueprint_policy_inventory.py→ 13/13 passedpython functional_tests/route_tests/test_route_unauthenticated_policy_contract.py→ 8/8 passedpython functional_tests/route_tests/test_route_policy_test_coverage.py→ 3/3 passedMutation checks: each of these makes the tests above fail: keying the V2 store on the violation alone; removing the server's replaced-warning check; an unconditional claim; no fresh-claim guard; no retry when a writer kept the claim; claims that never go stale; removing the classic Save button guard.
Existing related tests:
python -m pytest functional_tests/test_v2_admin_review_pages.py functional_tests/test_admin_review_record_lifecycle.py functional_tests/test_content_safety_violation_message.py functional_tests/test_v2_terms_and_approvals.py -q -p no:cacheprovider→ 18 passednode functional_tests/test_v2_api_error_message_logic.mjs→ 8 passednode functional_tests/test_v2_orchestration_workflow_handoff_client.mjs→ 19 pass, 0 failpython -m pytest ui_tests/test_v2_control_center_activity_logs.py ui_tests/test_v2_settings_workspace_open.py ui_tests/test_v2_support_menu.py ui_tests/test_v2_control_center_dashboard.py ui_tests/test_v2_control_center_users.py ui_tests/test_v2_workflow_alerts.py ui_tests/test_v2_appearance_branding_and_nav.py ui_tests/test_v2_sidebar_primary_nav_links.py ui_tests/test_v2_workflow_run_tracker_spa.py ui_tests/test_v2_workspace_draft_navigation.py -q -p no:cacheprovider→ 78 passed, 14 skipped (the skips needSIMPLECHAT_UI_BASE_URL, a live deployment)python -m pytest ui_tests/test_v2_sidebar_conversation_scroll.py -q -p no:cacheprovider→ 8 passed, 1 failed (at2252dae7):test_the_phone_drawer_holds_search_under_new_chatis a pre-existing order-dependent flake. It passes on its own and failed 3 of 4 full-file runs on both the base worktree and this branch.python -m pytest functional_tests/test_v2_bootstrap_branding_and_navigation.py functional_tests/test_v2_control_center_activity_display.py functional_tests/test_v2_control_center_activity_logs_queries.py functional_tests/test_v2_control_center_activity_logs_routes.py functional_tests/test_v2_control_center_cosmos_query_compatibility.py functional_tests/test_v2_control_center_groups.py functional_tests/test_v2_settings_tabs.py functional_tests/test_v2_support_menu.py functional_tests/test_v2_user_settings_tutorials_latest_features.py -q -p no:cacheprovider(atffc943b9) → 118 passed, 32 errors, all 32 intest_v2_control_center_groups.pyfrom this machine's Flask 2.2 / Werkzeug 3 mismatch (werkzeug.__version__). With a shim,python -c "import sys, werkzeug; werkzeug.__version__ = '3.0.0'; import pytest; sys.exit(pytest.main(['functional_tests/test_v2_control_center_groups.py', '-q', '-p', 'no:cacheprovider']))"→ 37 passed.V2 build:
npm run typecheck(inapplication/v2_ui) → passednpm run build(inapplication/v2_ui) → passed (existing chunk-size warning only)Docs:
python scripts/build_docs_inventory.py→ regenerated;docs/_data/app_surface.ymlcommittedpython functional_tests/test_docs_app_surface_coverage.py→ 7/7 passedpython functional_tests/test_docs_site_quality.py→ 6/6 passedCI guardrails, run locally with CI's file filters:
python scripts/check_xss_sinks.py --base-sha f34094ae5 --head-sha HEAD <changed application/** files>→ passed (28 files)python scripts/check_broken_access_control.py --base-sha f34094ae5 --head-sha HEAD <changed application/** .py files>→ passed (9 files)python scripts/check_swagger_routes.py <changed application/single_app/** .py files>→ passed (5 route files)python -m py_compile <changed .py files>→ OK;node --check application/single_app/static/js/admin/admin-safety-violations.js→ OK;git diff --check→ cleanKnown pre-existing failures, unrelated to this change:
python functional_tests/test_logging_tag_standardization.py→ fails on nonstandard tags in other modules (EditorAssist,Workflows). The new tags ([ACCESS_RESTRICTION],[SAFETY_WARNINGS],[V2_BOOTSTRAP]) are documented indocs/reference/logging-tags.md.python functional_tests/test_broken_access_control_guardrails_checker.py→ 3/4; the self-test expectsactions/upload-artifact@v4, but the workflow now pins it by SHA. It runs in CI only when guardrail files change.Known limitation (unchanged writers): archiving, the user's own notes save, and saves that don't send a warning still write without an ETag condition. One that read a violation just before a claim and writes after it can replace the claim, which the send then reports as
safety_warning_not_recorded, or the recorded warning.Not run:
SIMPLECHAT_UI_BASE_URL(a live deployment) were skipped.Documentation
docs/explanation/release_notes.md, new v0.261.297 section)docs/explanation/features/ACCESS_RESTRICTED_SIGN_IN_SCREEN.md;features/v0.241.127/SAFETY_VIOLATION_REMEDIATION_APPROVALS.mdupdated)docs/explanation/fixes/SAFETY_REMEDIATION_ACTIONS_FIX.md, including the separation-of-duties rationale: the two-person rule is relaxed only for non-restrictive warnings, and how overlapping saves send one warning)Also updated:
docs/guides/admin-review-safety-violations.md,docs/guides/review-approval-requests.md,docs/admin/security.md,docs/reference/logging-tags.md.Security checklist
@swagger_route(security=get_auth_security())sanitize_settings_for_user()(the classic restricted page renders sanitized settings;GET /api/v2/access-restrictionreturns only the caller's restriction and the page's branding fields)Access-control notes: the warning routes and
GET /api/v2/access-restrictionread and change only the caller's own records; acknowledging another user's warning, or one that doesn't exist, returns the same 404. The three restricted-screen routes require sign-in but notuser_required(a restricted user must reach them); this is recorded and justified infunctional_tests/route_tests/. They are exempt from the Terms of Use gate so a restricted user is not bounced between gates; the idle-session timeout still applies.