Files
guru-connect/docs/specs/SPEC-013-session-selection-and-backstage.md
azcomputerguru 5637e4c1f9
All checks were successful
Build and Test / Build Agent (Windows) (push) Successful in 8m5s
Build and Test / Build Server (Linux) (push) Successful in 11m24s
Build and Test / Security Audit (push) Successful in 4m30s
Build and Test / Build Summary (push) Successful in 12s
spec: add SPEC-013 Windows Session Selection and Backstage Mode
2026-05-31 07:54:25 -07:00

28 KiB

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:

    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<Vec<SessionInfo>> {
        // Call WTSEnumerateSessionsW
        // For each session, call WTSQuerySessionInformationW(WTSUserName, WTSSessionInfo)
        // Filter out Session 0 (services) from user session list
    }
    
  • Switch session desktop:

    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<SessionContext> {
        // 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):

    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<BackstageOutput> {
        // 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<Vec<ServiceInfo>> {
        // EnumServicesStatusEx(SC_MANAGER_ENUMERATE_SERVICE)
    }
    
    pub fn list_processes() -> Result<Vec<ProcessInfo>> {
        // 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:

    pub struct SessionState {
        pub session_id: String,
        pub session_type: SessionType,  // SCREEN_CONTROL, VIEW_ONLY, BACKSTAGE
        pub target_session_id: Option<u32>,  // 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

    {
      "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:

    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:

    INSERT INTO events (session_id, event_type, details) VALUES
      ('...', 'session_switched', '{"from_session": 1, "to_session": 2, "user": "admin"}');
    

Protobuf Changes (proto/guruconnect.proto)

// 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:

// 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<Vec<SessionInfo>> {
    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:

// 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<HDESK>,
}

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<u16> = 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:

// 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<CommandOutput> {
    // 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 <command> with redirected output
    let command_line = format!("cmd.exe /c {}", command);
    let mut command_line_wide: Vec<u16> = command_line.encode_utf16().chain(Some(0)).collect();

    let mut startup_info = STARTUPINFOW {
        cb: std::mem::size_of::<STARTUPINFOW>() 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:

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"