# SPEC-013: Windows Session Selection and Backstage Mode **Status:** Proposed **Priority:** P2 **Requested By:** Mike Swanson (2026-05-30) **Estimated Effort:** Large ## Overview Enable GuruConnect to enumerate and switch between multiple Windows user sessions (Terminal Services/RDP/Fast User Switching) and access Session 0 (backstage mode) for system-level administrative tasks. This addresses a critical gap for MSPs managing multi-user Windows servers and workstations — ScreenConnect's session selector allows technicians to see all logged-on users and switch between them instantly, and the backstage mode provides a command/task-manager interface for Session 0 (services) without disrupting any user's desktop. Success criteria: technicians can view and select from all active sessions in the dashboard/viewer, switch sessions mid-stream without reconnecting, and enter backstage mode to run commands and view services when no user desktop is needed. ## Scope ### Included in v1 **Session Enumeration and Switching:** - Agent enumerates all Windows sessions via `WTSEnumerateSessions` API at startup and on-demand - Reports session list to server: session ID, user name, session state (active/disconnected), session type (console/RDP) - Viewer displays session selector UI (dropdown or list) showing all available sessions - User selects a session → server sends `SwitchSession` message → agent switches capture/input to that session - Session switching without WebSocket disconnect (same session token, just changes the desktop being captured) - Dashboard shows session count and logged-on users per machine **Backstage Mode (Session 0 Access):** - Agent can enter "backstage" mode targeting Session 0 (services session) - Backstage mode provides a terminal/command interface (not DXGI screen capture, since Session 0 has no interactive desktop in modern Windows) - Dashboard/viewer shows backstage UI with: - Command prompt / PowerShell terminal (PTY-like, text-based) - Running services list (via `EnumServicesStatusEx`) - Running processes list (via `EnumProcesses` / `CreateToolhelp32Snapshot`) - System info panel (OS version, uptime, logged-on sessions) - Backstage mode uses `SessionType::BACKSTAGE` (already in protobuf line 42) - Agent runs commands in Session 0 context via `CreateProcessAsUser` with Session 0 token **Protobuf Extensions:** - New messages: `SessionInfo`, `SessionList`, `SwitchSession`, `BackstageCommand`, `BackstageOutput` - Extend `AgentStatus` to include session list - Extend `StartStream` to specify target session ID **Security:** - Session switching requires elevated (admin) agent process - Backstage mode is admin-only (gated server-side by user role) - Audit log records all session switches and backstage commands ### Explicitly out of scope - **Session shadowing** (viewing a session while another user is active in it) — defer to Phase 2; complex Terminal Services permission model - **Session connect/disconnect control** (logging users off, disconnecting RDP sessions) — defer; high-impact action - **Cross-session clipboard** — clipboard currently session-scoped; multi-session clipboard is a future enhancement - **Linux/macOS session switching** — this spec is Windows-only; cross-platform session concepts differ significantly - **Backstage GUI tools** (registry editor, event log viewer) — v1 is terminal/command-only; GUI tools are future enhancements - **Session 0 screen capture** — Session 0 has no interactive desktop on modern Windows (Session 0 isolation, Vista+); backstage is terminal/data-only ## Architecture ### Agent Changes **Session Management Module** (`agent/src/session/mod.rs` or new `agent/src/sessions/`) - **Enumerate sessions:** ```rust use windows::Win32::System::RemoteDesktop::{ WTSEnumerateSessionsW, WTSFreeMemory, WTSQuerySessionInformationW, WTS_SESSION_INFOW, WTSUserName, WTSSessionInfo, WTS_CURRENT_SERVER_HANDLE }; pub struct SessionInfo { pub session_id: u32, pub user_name: String, pub state: SessionState, // Active, Disconnected, Idle pub session_type: SessionType, // Console, RDP } pub fn enumerate_sessions() -> Result> { // Call WTSEnumerateSessionsW // For each session, call WTSQuerySessionInformationW(WTSUserName, WTSSessionInfo) // Filter out Session 0 (services) from user session list } ``` - **Switch session desktop:** ```rust use windows::Win32::System::RemoteDesktop::WTSGetActiveConsoleSessionId; use windows::Win32::UI::WindowsAndMessaging::{OpenDesktop, CloseDesktop, SwitchDesktop}; pub struct SessionContext { pub session_id: u32, pub desktop_handle: HDESK, // Handle to the session's desktop } pub fn switch_to_session(session_id: u32) -> Result { // OpenDesktop for the target session (desktop name: "WinSta0\\Default" or session-specific) // SwitchDesktop to make it the active desktop for capture // Reinitialize DXGI capturer on the new desktop } ``` - **Backstage mode (Session 0 command execution):** ```rust use windows::Win32::System::Threading::{CreateProcessAsUserW, PROCESS_INFORMATION}; use windows::Win32::Security::{LogonUserW, DuplicateTokenEx}; pub struct BackstageSession { pub session_id: u32, // Always 0 for backstage pub process_handle: HANDLE, pub stdout_pipe: HANDLE, } pub fn execute_backstage_command(command: &str) -> Result { // CreateProcessAsUserW with Session 0 token (obtained via WTSQueryUserToken for Session 0) // Capture stdout/stderr via anonymous pipes // Return output as text (no screen capture) } pub fn list_services() -> Result> { // EnumServicesStatusEx(SC_MANAGER_ENUMERATE_SERVICE) } pub fn list_processes() -> Result> { // CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS) } ``` **Capture Module Changes** (`agent/src/capture/mod.rs`, `agent/src/capture/dxgi.rs`) - Refactor `DxgiCapturer::new()` to accept a `session_id` parameter - `OpenDesktop` for the target session before initializing DXGI duplication - Store current session ID in capturer state; reinitialize on session switch - Backstage mode does NOT use DXGI — it's terminal/data-only **Input Injection Changes** (`agent/src/input/mod.rs`) - Ensure `SendInput` targets the correct session's desktop (already handles this via desktop handle) - Backstage mode does NOT inject input (command execution only) **New Files:** - `agent/src/sessions/mod.rs` — session enumeration, switching, backstage execution - `agent/src/sessions/wts.rs` — WTS API wrappers (enumerate, query, get token) - `agent/src/sessions/backstage.rs` — Session 0 command execution, service/process enumeration ### Relay Server Changes **Session State Management** (`server/src/relay/mod.rs` or `server/src/session/`) - Extend `SessionState` struct to track current session ID: ```rust pub struct SessionState { pub session_id: String, pub session_type: SessionType, // SCREEN_CONTROL, VIEW_ONLY, BACKSTAGE pub target_session_id: Option, // Windows session ID (1, 2, 3, etc.) or 0 for backstage // ... existing fields } ``` - Handle `SessionList` message from agent (periodic update of available sessions) - Store session list in `SessionState` for dashboard queries - Handle `SwitchSession` message from viewer → forward to agent - Handle `BackstageCommand` message from viewer → forward to agent - Handle `BackstageOutput` message from agent → forward to viewer **API Endpoints** (`server/src/api/sessions.rs`) - `GET /api/sessions/:session_id/windows-sessions` — return list of Windows sessions for this agent ```json { "sessions": [ {"id": 1, "user": "jdoe", "state": "Active", "type": "Console"}, {"id": 2, "user": "admin", "state": "Disconnected", "type": "RDP"} ] } ``` - Backstage commands are sent via WebSocket, not HTTP (same as mouse/keyboard input) **Database Schema** (`server/migrations/`) - Extend `connect_sessions` table: ```sql ALTER TABLE connect_sessions ADD COLUMN windows_session_id INT NULL; ALTER TABLE connect_sessions ADD COLUMN session_type VARCHAR(20) DEFAULT 'screen_control'; ``` - Extend `events` table to log session switches: ```sql INSERT INTO events (session_id, event_type, details) VALUES ('...', 'session_switched', '{"from_session": 1, "to_session": 2, "user": "admin"}'); ``` ### Protobuf Changes (`proto/guruconnect.proto`) ```protobuf // Session information (Windows Terminal Services session) message SessionInfo { uint32 session_id = 1; // Windows session ID (1, 2, 3, etc.) string user_name = 2; // Logged-on user name SessionState state = 3; // Active, Disconnected, Idle WindowsSessionType type = 4; // Console, RDP } enum SessionState { SESSION_ACTIVE = 0; SESSION_DISCONNECTED = 1; SESSION_IDLE = 2; } enum WindowsSessionType { SESSION_CONSOLE = 0; // Local console session SESSION_RDP = 1; // Remote Desktop session } // List of available Windows sessions (agent -> server) message SessionList { repeated SessionInfo sessions = 1; uint32 current_session_id = 2; // Which session the agent is currently capturing } // Switch to a different Windows session (viewer -> agent) message SwitchSession { uint32 target_session_id = 1; // Session ID to switch to (0 for backstage) } // Backstage command execution (viewer -> agent) message BackstageCommand { BackstageCommandType command_type = 1; string command_text = 2; // For EXEC_COMMAND: PowerShell or CMD command } enum BackstageCommandType { BACKSTAGE_EXEC_COMMAND = 0; // Execute a command in Session 0 BACKSTAGE_LIST_SERVICES = 1; // Enumerate running services BACKSTAGE_LIST_PROCESSES = 2; // Enumerate running processes BACKSTAGE_SYSTEM_INFO = 3; // Get system information } // Backstage output (agent -> viewer) message BackstageOutput { BackstageCommandType command_type = 1; oneof output { CommandOutput command_output = 10; ServiceList service_list = 11; ProcessList process_list = 12; SystemInfo system_info = 13; } } message CommandOutput { string stdout = 1; string stderr = 2; int32 exit_code = 3; } message ServiceList { repeated ServiceInfo services = 1; } message ServiceInfo { string name = 1; string display_name = 2; ServiceStatus status = 3; // Running, Stopped, Paused string startup_type = 4; // Automatic, Manual, Disabled } enum ServiceStatus { SERVICE_RUNNING = 0; SERVICE_STOPPED = 1; SERVICE_PAUSED = 2; } message ProcessList { repeated ProcessInfo processes = 1; } message ProcessInfo { uint32 pid = 1; string name = 2; string user = 3; uint64 memory_kb = 4; } message SystemInfo { string os_version = 1; string hostname = 2; uint64 uptime_secs = 3; repeated SessionInfo logged_on_sessions = 4; } // Extend Message wrapper to include new message types message Message { oneof payload { // ... existing messages ... // Session switching (field numbers 90-99) SessionList session_list = 90; SwitchSession switch_session = 91; BackstageCommand backstage_command = 92; BackstageOutput backstage_output = 93; } } ``` ### Dashboard/Viewer Changes **Dashboard** (`server/static/dashboard.html` or React component) - Machine detail view shows "Sessions (3)" next to online status - Click sessions count → modal showing session list with Switch button - "Enter Backstage" button for admin users (gated by `user.role === 'admin'`) **Native Viewer** (`agent/src/viewer/mod.rs`) - Session selector dropdown in viewer toolbar - Populate dropdown from `SessionList` message - On selection change → send `SwitchSession` message - "Backstage Mode" button (toggle) — switches to terminal UI instead of video frames **Web Viewer** (`dashboard/src/components/RemoteViewer.tsx`) - Same session selector dropdown as native viewer - Backstage mode shows a terminal-like UI (xterm.js or custom) displaying command output - Text input for commands, buttons for List Services / List Processes / System Info ## Implementation Details ### Files to Create **Agent:** - `agent/src/sessions/mod.rs` (300 lines) — session enumeration, switching, state - `agent/src/sessions/wts.rs` (200 lines) — WTS API wrappers - `agent/src/sessions/backstage.rs` (400 lines) — Session 0 command execution, service/process enum **Server:** - `server/src/relay/sessions.rs` (150 lines) — session list state, switch logic - `server/migrations/00N_session_selection.sql` (50 lines) — schema changes **Dashboard:** - `server/static/js/session-selector.js` (200 lines) — session dropdown UI - `server/static/css/backstage.css` (100 lines) — backstage terminal styling ### Files to Modify **Agent:** - `agent/src/capture/dxgi.rs:42` — Add session ID parameter to `DxgiCapturer::new()` - `agent/src/capture/mod.rs:76` — Call `OpenDesktop` for target session before DXGI init - `agent/src/session/mod.rs:120` — Extend session loop to handle `SwitchSession` and `BackstageCommand` - `agent/src/main.rs:15` — Add `mod sessions;` **Server:** - `server/src/relay/mod.rs:85` — Handle `SessionList`, `SwitchSession`, `BackstageCommand`, `BackstageOutput` messages - `server/src/api/sessions.rs:40` — Add endpoint for session list query - `server/src/db/events.rs:25` — Log session switch events **Proto:** - `proto/guruconnect.proto:454` — Add new message types (as shown above) ### Key Logic **Agent Session Enumeration:** ```rust // agent/src/sessions/wts.rs use windows::Win32::System::RemoteDesktop::{ WTSEnumerateSessionsW, WTSQuerySessionInformationW, WTSFreeMemory, WTS_SESSION_INFOW, WTSUserName, WTSConnectState, WTS_CURRENT_SERVER_HANDLE }; pub fn enumerate_sessions() -> Result> { let mut sessions = Vec::new(); let mut session_info_ptr: *mut WTS_SESSION_INFOW = std::ptr::null_mut(); let mut count: u32 = 0; unsafe { if WTSEnumerateSessionsW( WTS_CURRENT_SERVER_HANDLE, 0, // Reserved 1, // Version &mut session_info_ptr, &mut count ).as_bool() { let session_array = std::slice::from_raw_parts(session_info_ptr, count as usize); for wts_session in session_array { // Skip Session 0 (services) for user session list if wts_session.SessionId == 0 { continue; } // Query user name let mut user_name_ptr: *mut u16 = std::ptr::null_mut(); let mut bytes_returned: u32 = 0; if WTSQuerySessionInformationW( WTS_CURRENT_SERVER_HANDLE, wts_session.SessionId, WTSUserName, &mut user_name_ptr as *mut _ as *mut _, &mut bytes_returned ).as_bool() { let user_name = String::from_utf16_lossy( std::slice::from_raw_parts(user_name_ptr, (bytes_returned / 2) as usize) ); sessions.push(SessionInfo { session_id: wts_session.SessionId, user_name, state: map_wts_state(wts_session.State), session_type: if wts_session.pWinStationName.to_string()?.contains("RDP") { WindowsSessionType::RDP } else { WindowsSessionType::Console } }); WTSFreeMemory(user_name_ptr as *mut _); } } WTSFreeMemory(session_info_ptr as *mut _); } } Ok(sessions) } ``` **Agent Session Switching:** ```rust // agent/src/sessions/mod.rs use windows::Win32::UI::WindowsAndMessaging::{ OpenDesktopW, CloseDesktop, SwitchDesktop, HDESK }; use windows::core::PCWSTR; pub struct SessionManager { current_session_id: u32, desktop_handle: Option, } impl SessionManager { pub fn switch_session(&mut self, target_session_id: u32) -> Result<()> { tracing::info!("Switching from session {} to {}", self.current_session_id, target_session_id); // Close current desktop handle if let Some(handle) = self.desktop_handle.take() { unsafe { CloseDesktop(handle) }; } // Open the target session's desktop // Desktop name format: "WinSta0\\Default" for console session let desktop_name = format!("WinSta0\\Default"); let desktop_name_wide: Vec = desktop_name.encode_utf16().chain(Some(0)).collect(); let desktop_handle = unsafe { OpenDesktopW( PCWSTR::from_raw(desktop_name_wide.as_ptr()), 0, // Flags false.into(), // Inherit handles 0x0001 | 0x0002 | 0x0004 | 0x0008, // DESKTOP_READOBJECTS | DESKTOP_CREATEWINDOW | etc. )? }; // Switch to the new desktop unsafe { SwitchDesktop(desktop_handle)?; } self.current_session_id = target_session_id; self.desktop_handle = Some(desktop_handle); // Reinitialize screen capturer for the new desktop // (caller must recreate DxgiCapturer) Ok(()) } } ``` **Backstage Command Execution:** ```rust // agent/src/sessions/backstage.rs use windows::Win32::System::Threading::{CreateProcessW, PROCESS_INFORMATION, STARTUPINFOW}; use windows::Win32::System::Pipes::{CreatePipe, ReadFile}; pub fn execute_command(command: &str) -> Result { // Create anonymous pipes for stdout/stderr let mut stdout_read: HANDLE = HANDLE::default(); let mut stdout_write: HANDLE = HANDLE::default(); unsafe { CreatePipe(&mut stdout_read, &mut stdout_write, None, 0)?; } // Launch cmd.exe /c with redirected output let command_line = format!("cmd.exe /c {}", command); let mut command_line_wide: Vec = command_line.encode_utf16().chain(Some(0)).collect(); let mut startup_info = STARTUPINFOW { cb: std::mem::size_of::() as u32, hStdOutput: stdout_write, hStdError: stdout_write, dwFlags: STARTF_USESTDHANDLES, ..Default::default() }; let mut process_info = PROCESS_INFORMATION::default(); unsafe { CreateProcessW( None, PWSTR::from_raw(command_line_wide.as_mut_ptr()), None, None, true.into(), // Inherit handles 0, None, None, &startup_info, &mut process_info )?; CloseHandle(stdout_write); // Read output let mut output = Vec::new(); let mut buffer = [0u8; 4096]; let mut bytes_read: u32 = 0; loop { if ReadFile(stdout_read, Some(&mut buffer), Some(&mut bytes_read), None).is_ok() { if bytes_read == 0 { break; } output.extend_from_slice(&buffer[..bytes_read as usize]); } else { break; } } // Wait for process to exit WaitForSingleObject(process_info.hProcess, INFINITE); let mut exit_code: u32 = 0; GetExitCodeProcess(process_info.hProcess, &mut exit_code)?; CloseHandle(process_info.hProcess); CloseHandle(process_info.hThread); CloseHandle(stdout_read); Ok(CommandOutput { stdout: String::from_utf8_lossy(&output).to_string(), stderr: String::new(), exit_code: exit_code as i32, }) } } ``` ## Security Considerations ### Authentication & Authorization - **Session switching requires elevated agent:** Agent must run as SYSTEM or Administrator to call WTS APIs and `OpenDesktop` across sessions - **Backstage mode is admin-only:** Server checks `user.role === 'admin'` before allowing `BackstageCommand` messages - **Audit logging:** All session switches and backstage commands are logged to `events` table with: - Timestamp - Technician user ID - Session ID (from/to) - Command text (for backstage) - Output (sanitized, no secrets) ### Input Validation - **Session ID bounds checking:** Agent validates `target_session_id` exists in `WTSEnumerateSessions` results before switching - **Command injection prevention:** Backstage commands are executed via `cmd.exe /c` with proper escaping; no shell metacharacter expansion - **Output sanitization:** Command output is truncated to 100KB max to prevent memory exhaustion ### Threat Model - **Malicious session switch:** Attacker with viewer access tries to switch to a privileged session (e.g., admin RDP session) → mitigated by server-side session ownership validation (can only switch sessions on machines you have access to) - **Backstage command abuse:** Attacker executes destructive commands (e.g., `rd /s /q C:\`) → mitigated by admin-only role gate + audit logging - **Session 0 privilege escalation:** Attacker uses backstage to run code as SYSTEM → inherent risk, same as any admin tool; audit log is the control ### Audit Events **New event types:** - `session_switched` — Log when technician switches Windows sessions - `backstage_command` — Log all backstage command executions - `backstage_service_query` — Log service list queries - `backstage_process_query` — Log process list queries **Event schema:** ```sql CREATE TABLE events ( id UUID PRIMARY KEY, session_id UUID REFERENCES connect_sessions(id), user_id UUID REFERENCES users(id), event_type VARCHAR(50) NOT NULL, details JSONB NOT NULL, -- {"from_session": 1, "to_session": 2, "command": "ipconfig", "exit_code": 0} created_at TIMESTAMPTZ DEFAULT NOW() ); ``` ## Testing Strategy ### Unit Tests **Agent (Rust):** - `sessions::wts::test_enumerate_sessions()` — mock WTS API, verify session parsing - `sessions::backstage::test_execute_command()` — mock CreateProcess, verify output capture - `sessions::test_switch_session()` — mock OpenDesktop/SwitchDesktop, verify state change **Server (Rust):** - `relay::sessions::test_session_list_update()` — verify session list parsing and storage - `relay::sessions::test_switch_session_message()` — verify message forwarding to agent - `api::sessions::test_get_sessions_endpoint()` — verify API response format ### Integration Tests **End-to-end session switching:** 1. Start agent on Windows machine with 2 logged-on users (console + RDP) 2. Connect viewer to session 3. Agent sends `SessionList` with 2 sessions 4. Viewer sends `SwitchSession(2)` 5. Agent switches to RDP session, restarts DXGI capture 6. Viewer receives frames from RDP session desktop 7. Verify session switch logged in `events` table **Backstage command execution:** 1. Admin user connects to agent 2. Enter backstage mode 3. Execute command: `ipconfig /all` 4. Verify output displayed in viewer 5. Execute command: `sc query wuauserv` (query Windows Update service) 6. Verify service status returned 7. Verify commands logged with full details ### Manual Testing Scenarios 1. **Multi-user server:** - Windows Server 2019 with 3 RDP users logged in - Enumerate sessions → verify all 3 shown in dropdown - Switch between sessions → verify screen updates correctly - Verify no session 0 in dropdown (backstage mode is separate) 2. **Fast User Switching workstation:** - Windows 10 workstation with 2 users (one active, one locked) - Enumerate sessions → verify both shown with correct state - Switch to locked session → verify lock screen is captured 3. **Backstage mode:** - Connect to agent - Enter backstage → verify no video frames, just terminal UI - Run `hostname` → verify output - List services → verify service table displayed - List processes → verify process list - Exit backstage → return to normal screen control 4. **Permission enforcement:** - Non-admin user tries to enter backstage → verify 403 Forbidden - Non-admin user tries to send `BackstageCommand` → server rejects 5. **Audit logging:** - Perform session switch + backstage commands - Query `events` table → verify all actions logged with timestamps, user IDs, details ### CI/CD Additions - **Windows test VM:** Gitea Actions Windows runner with 2 test users configured (via Local Users and Groups) - **Automated test:** PowerShell script to create RDP session, run integration test, verify session switch - **Audit log verification:** Integration test queries events table after actions, asserts correct event_type and details ## Effort Estimate & Dependencies **Size:** Large (8-10 weeks, 1 developer) **Breakdown:** - Agent session enumeration + WTS API wrappers: 1.5 weeks - Agent session switching (OpenDesktop, SwitchDesktop, DXGI reinit): 2 weeks - Agent backstage mode (command execution, service/process enum): 2 weeks - Server session state management + protobuf: 1 week - Dashboard session selector UI: 1 week - Backstage terminal UI (web viewer): 1.5 weeks - Testing (integration tests, manual scenarios, audit verification): 1.5 weeks - Buffer for Windows session edge cases (disconnected sessions, fast user switching quirks): 1 week **Dependencies:** - **SPEC-002 v2 Phase 1 completion** — per-agent keys and secure session core must be stable (already shipped) - **Admin role enforcement** — server must have role-based access control for backstage gating (already exists: `users.role`) - **Event logging infrastructure** — audit events table and logging (already exists: `events` table) **Unblocks:** - Multi-user server management (critical for MSPs with Windows Server environments) - Session 0 administrative tasks without GUI disruption (backstage mode) - SPEC-005 machines list view (can show session count and logged-on users per machine) - Future: session recording (can record specific session, not just active console) ## Open Questions 1. **Session 0 GUI in older Windows?** — Windows XP/Server 2003 allowed interactive Session 0. GuruConnect targets Windows 7+ where Session 0 is non-interactive. Confirm backstage terminal-only approach is acceptable, or add a "force capture Session 0 desktop" option for legacy systems? 2. **Session switching during active control?** — If technician is moving the mouse and switches sessions mid-action, should the agent queue the switch until input is idle, or switch immediately? (Recommend: immediate switch, input events are session-scoped.) 3. **Disconnected session handling?** — If technician switches to a disconnected RDP session, should the agent attempt to reconnect it (via `WTSConnectSession`), or just capture the "disconnected" screen? (Recommend: just capture; reconnection is a high-impact action.) 4. **Backstage command timeout?** — Long-running commands (e.g., `chkdsk`) could hang. Implement a timeout (e.g., 60 seconds) and kill the process? (Recommend: yes, 60s default, configurable.) 5. **Clipboard across sessions?** — Current clipboard implementation is session-scoped. Should session switching clear the clipboard state, or try to preserve it? (Recommend: clear on switch; cross-session clipboard is a future enhancement.) --- **Cross-references:** - SPEC-002: v2 modernization (protobuf already has `BACKSTAGE = 2`) - SPEC-005: Machines list view (can show session count + logged-on users) - SPEC-008: Structured errors (session switch failures need clear error codes) - REQUIREMENTS.md:498 — "Backstage Tools (No Screen Required)" - REQUIREMENTS.md:727 — "Backstage / Silent Support Mode"