# 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<Vec<SessionInfo>> {
      // 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<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):**
  ```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<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:
  ```rust
  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
  ```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<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:**

```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<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:**

```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<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:**
```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"
