From 5637e4c1f9f111692f48142712260521484bc1a6 Mon Sep 17 00:00:00 2001 From: azcomputerguru Date: Sun, 31 May 2026 07:54:10 -0700 Subject: [PATCH] spec: add SPEC-013 Windows Session Selection and Backstage Mode --- docs/FEATURE_ROADMAP.md | 1 + ...PEC-013-session-selection-and-backstage.md | 717 ++++++++++++++++++ 2 files changed, 718 insertions(+) create mode 100644 docs/specs/SPEC-013-session-selection-and-backstage.md diff --git a/docs/FEATURE_ROADMAP.md b/docs/FEATURE_ROADMAP.md index a54254e..dba1fde 100644 --- a/docs/FEATURE_ROADMAP.md +++ b/docs/FEATURE_ROADMAP.md @@ -62,6 +62,7 @@ Bringing GC to parity with GuruRMM's release engineering. Full plan: [SPEC-001]( - [x] Protobuf-over-WSS transport, Zstd frame compression - [~] React/TS web viewer (`dashboard/src/components/RemoteViewer.tsx`) — embeddable session viewer - [ ] **Headless Linux mode (direct TTY access)** — P2 — Terminal-based remote access for Linux servers without GUI. PTY spawn (`openpty`), xterm.js web viewer, full ANSI/VT100 support. Enables server management, container debugging, emergency recovery via GuruConnect dashboard with audit logging. SSH replacement with centralized auth. ([SPEC-012](specs/SPEC-012-headless-linux-tty.md)) +- [ ] **Windows session selection and backstage mode** — P2 — Enumerate and switch between Windows user sessions (Terminal Services/RDP/Fast User Switching) and access Session 0 (backstage) for system-level admin tasks. ScreenConnect parity: session selector shows all logged-on users, instant switching without reconnect. Backstage mode provides terminal/command interface for services management without disrupting any user desktop. Critical for multi-user server environments. ([SPEC-013](specs/SPEC-013-session-selection-and-backstage.md)) - [ ] Multi-monitor switching — P2 - [ ] File transfer — P3 (out of scope for native-remote-control v1) - [ ] Session recording — P3 (out of scope for native-remote-control v1) diff --git a/docs/specs/SPEC-013-session-selection-and-backstage.md b/docs/specs/SPEC-013-session-selection-and-backstage.md new file mode 100644 index 0000000..79194c4 --- /dev/null +++ b/docs/specs/SPEC-013-session-selection-and-backstage.md @@ -0,0 +1,717 @@ +# 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"