From ff234af70147f9a6617345617707b374aa730b7d Mon Sep 17 00:00:00 2001 From: igor04091968 Date: Thu, 4 Jun 2026 10:36:19 +0300 Subject: [PATCH] feat(agent): switch worktime sessions to rust agent --- adk-rust/Cargo.lock | 10 + adk-rust/crates/awatch-agent-rs/Cargo.toml | 3 + .../awatch-agent-rs/awatch-agent.example.toml | 5 + .../awatch-agent-rs/src/collectors/windows.rs | 239 +++++++++++++++++- adk-rust/crates/awatch-agent-rs/src/config.rs | 15 ++ adk-rust/crates/awatch-agent-rs/src/main.rs | 22 +- .../crates/awatch-agent-rs/src/transport.rs | 156 +++++++++++- windows/ActivityWatch.Windows.Common.psm1 | 6 +- windows/aw-collector-guard.ps1 | 5 +- windows/aw-standalone-service.ps1 | 1 + windows/deploy-ensemble.ps1 | 1 + windows/install-standalone-service.ps1 | 1 + 12 files changed, 455 insertions(+), 9 deletions(-) diff --git a/adk-rust/Cargo.lock b/adk-rust/Cargo.lock index c4ee712..f4e60f9 100644 --- a/adk-rust/Cargo.lock +++ b/adk-rust/Cargo.lock @@ -294,6 +294,7 @@ dependencies = [ "serde", "serde_json", "tempfile", + "windows-sys 0.59.0", ] [[package]] @@ -2628,6 +2629,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.60.2" diff --git a/adk-rust/crates/awatch-agent-rs/Cargo.toml b/adk-rust/crates/awatch-agent-rs/Cargo.toml index b66662b..cd8054e 100644 --- a/adk-rust/crates/awatch-agent-rs/Cargo.toml +++ b/adk-rust/crates/awatch-agent-rs/Cargo.toml @@ -14,6 +14,9 @@ reqwest.workspace = true serde.workspace = true serde_json.workspace = true +[target.'cfg(windows)'.dependencies] +windows-sys = { version = "0.59", features = ["Win32_System_RemoteDesktop"] } + [dev-dependencies] tempfile.workspace = true diff --git a/adk-rust/crates/awatch-agent-rs/awatch-agent.example.toml b/adk-rust/crates/awatch-agent-rs/awatch-agent.example.toml index a8e8767..0bf4dcf 100644 --- a/adk-rust/crates/awatch-agent-rs/awatch-agent.example.toml +++ b/adk-rust/crates/awatch-agent-rs/awatch-agent.example.toml @@ -11,3 +11,8 @@ enable_workforce_activity = true spool_dir = "/var/lib/awatch-agent/spool" timeout_seconds = 10 retry_attempts = 3 + +# Optional ActivityWatch compatibility output. +# Enables Rust replacement for the PowerShell worktime-session collector path. +aw_api_base = "http://awatch.local:5600/api/0" +aw_worktime_enabled = false diff --git a/adk-rust/crates/awatch-agent-rs/src/collectors/windows.rs b/adk-rust/crates/awatch-agent-rs/src/collectors/windows.rs index 56b5558..ced7ac2 100644 --- a/adk-rust/crates/awatch-agent-rs/src/collectors/windows.rs +++ b/adk-rust/crates/awatch-agent-rs/src/collectors/windows.rs @@ -1,5 +1,6 @@ use anyhow::Result; use chrono::Utc; +use std::process::Command; use crate::collectors::common::{ agent_id, command_output, current_session, domain, hostname, role_security_events, username, @@ -37,16 +38,25 @@ impl TelemetryCollector for WindowsCollector { } fn collect_sessions(&self) -> Result { - let mut active = vec![current_session("local")]; - let mut rdp = Vec::new(); + let mut active = windows_query_user_sessions(); + if active.is_empty() { + active.push(current_session("local")); + } + let mut rdp = active + .iter() + .filter(|session| session.session_type == "rdp") + .cloned() + .collect::>(); if std::env::var("SESSIONNAME") .unwrap_or_default() .to_ascii_lowercase() .contains("rdp") { let session = current_session("rdp"); - rdp.push(session.clone()); - active.push(session); + if !rdp.iter().any(|item| item.username == session.username) { + rdp.push(session.clone()); + active.push(session); + } } Ok(SessionSnapshot { active_sessions: active, @@ -105,6 +115,198 @@ fn windows_version() -> String { .unwrap_or_else(|| "Windows".to_string()) } +fn windows_query_user_sessions() -> Vec { + let native = windows_wts_sessions(); + if !native.is_empty() { + return native; + } + let raw = command_output_utf16le("cmd", &["/U", "/C", "query user"]) + .or_else(|| command_output_utf16le("cmd", &["/U", "/C", "quser"])) + .or_else(|| command_output_lossy_combined("cmd", &["/C", "query user"])) + .or_else(|| command_output_lossy_combined("cmd", &["/C", "quser"])) + .unwrap_or_default(); + raw.lines() + .skip(1) + .filter_map(parse_query_user_line) + .collect() +} + +#[cfg(windows)] +fn windows_wts_sessions() -> Vec { + use std::ptr; + use windows_sys::Win32::System::RemoteDesktop::{ + WTS_CURRENT_SERVER_HANDLE, WTS_SESSION_INFOW, WTSActive, WTSEnumerateSessionsW, + WTSFreeMemory, WTSUserName, + }; + + let mut sessions_ptr: *mut WTS_SESSION_INFOW = ptr::null_mut(); + let mut count = 0_u32; + let ok = unsafe { + WTSEnumerateSessionsW( + WTS_CURRENT_SERVER_HANDLE, + 0, + 1, + &mut sessions_ptr, + &mut count, + ) + }; + if ok == 0 || sessions_ptr.is_null() || count == 0 { + return Vec::new(); + } + + let sessions = + unsafe { std::slice::from_raw_parts(sessions_ptr, usize::try_from(count).unwrap_or(0)) }; + let mut items = Vec::new(); + for session in sessions { + let username = wts_session_string(session.SessionId, WTSUserName); + if username.trim().is_empty() { + continue; + } + let station = unsafe { wide_nul_to_string(session.pWinStationName) }; + let state = session.State; + let session_type = if station.to_ascii_lowercase().contains("rdp") { + "rdp" + } else { + "local" + }; + items.push(crate::telemetry::SessionInfo { + session_id: session.SessionId.to_string(), + username, + session_type: session_type.to_string(), + remote_addr: None, + started_at: None, + active: state == WTSActive, + }); + } + unsafe { + WTSFreeMemory(sessions_ptr.cast()); + } + items +} + +#[cfg(not(windows))] +fn windows_wts_sessions() -> Vec { + Vec::new() +} + +#[cfg(windows)] +fn wts_session_string(session_id: u32, class: i32) -> String { + use std::ptr; + use windows_sys::Win32::System::RemoteDesktop::{ + WTS_CURRENT_SERVER_HANDLE, WTSFreeMemory, WTSQuerySessionInformationW, + }; + + let mut buffer = ptr::null_mut(); + let mut bytes = 0_u32; + let ok = unsafe { + WTSQuerySessionInformationW( + WTS_CURRENT_SERVER_HANDLE, + session_id, + class, + &mut buffer, + &mut bytes, + ) + }; + if ok == 0 || buffer.is_null() || bytes == 0 { + return String::new(); + } + let len = usize::try_from(bytes / 2).unwrap_or(0); + let value = unsafe { + let slice = std::slice::from_raw_parts(buffer, len); + String::from_utf16_lossy(slice) + .trim_matches('\0') + .trim() + .to_string() + }; + unsafe { + WTSFreeMemory(buffer.cast()); + } + value +} + +#[cfg(windows)] +unsafe fn wide_nul_to_string(ptr: *const u16) -> String { + if ptr.is_null() { + return String::new(); + } + let mut len = 0; + while unsafe { *ptr.add(len) } != 0 { + len += 1; + } + let slice = unsafe { std::slice::from_raw_parts(ptr, len) }; + String::from_utf16_lossy(slice) +} + +fn command_output_utf16le(program: &str, args: &[&str]) -> Option { + let output = Command::new(program).args(args).output().ok()?; + if !output.status.success() { + return None; + } + let mut bytes = output.stdout; + bytes.extend_from_slice(&output.stderr); + if bytes.is_empty() { + return None; + } + let mut words = Vec::new(); + for chunk in bytes.chunks_exact(2) { + words.push(u16::from_le_bytes([chunk[0], chunk[1]])); + } + String::from_utf16(&words) + .ok() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) +} + +fn command_output_lossy_combined(program: &str, args: &[&str]) -> Option { + let output = Command::new(program).args(args).output().ok()?; + if !output.status.success() { + return None; + } + let mut bytes = output.stdout; + bytes.extend_from_slice(&output.stderr); + Some(String::from_utf8_lossy(&bytes).trim().to_string()).filter(|value| !value.is_empty()) +} + +fn parse_query_user_line(line: &str) -> Option { + let cleaned = line.trim().trim_start_matches('>').trim(); + if cleaned.is_empty() { + return None; + } + let parts = cleaned.split_whitespace().collect::>(); + if parts.len() < 3 { + return None; + } + let username = parts.first()?.to_string(); + let (session_name, session_id, state) = if parts.get(1)?.chars().all(|ch| ch.is_ascii_digit()) { + ("".to_string(), *parts.get(1)?, *parts.get(2)?) + } else { + ( + parts.get(1)?.to_string(), + *parts.get(2)?, + *parts.get(3).unwrap_or(&"Unknown"), + ) + }; + let active = session_state_active(state); + let session_type = if session_name.to_ascii_lowercase().contains("rdp") { + "rdp" + } else { + "local" + }; + Some(crate::telemetry::SessionInfo { + session_id: session_id.to_string(), + username, + session_type: session_type.to_string(), + remote_addr: None, + started_at: None, + active, + }) +} + +fn session_state_active(state: &str) -> bool { + let lower = state.to_lowercase(); + lower.contains("active") || lower.contains("актив") +} + fn windows_memory() -> (u64, u64) { let Some(raw) = command_output( "wmic", @@ -222,3 +424,32 @@ fn split_host_port(value: &str) -> Option<(String, u16)> { port.parse().ok()?, )) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_query_user_line_with_rdp_session() { + let session = + parse_query_user_line(" user1 rdp-tcp#5 3 Active").unwrap(); + assert_eq!(session.username, "user1"); + assert_eq!(session.session_id, "3"); + assert_eq!(session.session_type, "rdp"); + assert!(session.active); + } + + #[test] + fn parses_query_user_line_without_session_name() { + let session = parse_query_user_line(" user2 4 Disc").unwrap(); + assert_eq!(session.username, "user2"); + assert_eq!(session.session_id, "4"); + assert_eq!(session.session_type, "local"); + assert!(!session.active); + } + + #[test] + fn detects_russian_active_state() { + assert!(session_state_active("Активно")); + } +} diff --git a/adk-rust/crates/awatch-agent-rs/src/config.rs b/adk-rust/crates/awatch-agent-rs/src/config.rs index 36a6666..6dd8ed2 100644 --- a/adk-rust/crates/awatch-agent-rs/src/config.rs +++ b/adk-rust/crates/awatch-agent-rs/src/config.rs @@ -16,6 +16,8 @@ pub struct AgentConfig { pub spool_dir: PathBuf, pub timeout_seconds: u64, pub retry_attempts: u32, + pub aw_api_base: Option, + pub aw_worktime_enabled: bool, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -49,6 +51,8 @@ impl Default for AgentConfig { spool_dir: default_spool_dir(), timeout_seconds: 10, retry_attempts: 3, + aw_api_base: None, + aw_worktime_enabled: false, } } } @@ -103,6 +107,10 @@ impl AgentConfig { "spool_dir" => config.spool_dir = PathBuf::from(value), "timeout_seconds" => config.timeout_seconds = value.parse().unwrap_or(10), "retry_attempts" => config.retry_attempts = value.parse().unwrap_or(3), + "aw_api_base" => config.aw_api_base = Some(value.trim_end_matches('/').to_string()), + "aw_worktime_enabled" => { + config.aw_worktime_enabled = parse_bool(value, false); + } _ => {} } } @@ -131,6 +139,8 @@ api_key = "change-me" collect_interval_seconds = 30 role = "firewall" enable_processes = false +aw_api_base = "http://awatch.local:5600/api/0" +aw_worktime_enabled = true spool_dir = "/tmp/awatch-spool" "#, ) @@ -138,6 +148,11 @@ spool_dir = "/tmp/awatch-spool" assert_eq!(config.role, AgentRole::Firewall); assert_eq!(config.collect_interval_seconds, 30); assert!(!config.enable_processes); + assert!(config.aw_worktime_enabled); + assert_eq!( + config.aw_api_base.as_deref(), + Some("http://awatch.local:5600/api/0") + ); assert_eq!(config.spool_dir, PathBuf::from("/tmp/awatch-spool")); } } diff --git a/adk-rust/crates/awatch-agent-rs/src/main.rs b/adk-rust/crates/awatch-agent-rs/src/main.rs index 1a1febd..be389ab 100644 --- a/adk-rust/crates/awatch-agent-rs/src/main.rs +++ b/adk-rust/crates/awatch-agent-rs/src/main.rs @@ -10,7 +10,7 @@ use std::time::Duration; use anyhow::{Context, Result}; use clap::Parser; use config::{AgentConfig, AgentRole, default_config_path}; -use transport::{TelemetryTransport, spool_health}; +use transport::{AwWorktimePublisher, TelemetryTransport, spool_health}; #[derive(Debug, Parser)] #[command(about = "AWatch-rus Rust telemetry agent")] @@ -27,6 +27,12 @@ struct Cli { #[arg(long, env = "AWATCH_AGENT_ROLE")] role: Option, + #[arg(long, env = "AWATCH_AGENT_AW_API_BASE")] + aw_api_base: Option, + + #[arg(long, env = "AWATCH_AGENT_AW_WORKTIME_ENABLED")] + aw_worktime_enabled: Option, + #[arg(long)] once: bool, @@ -63,6 +69,12 @@ fn run() -> Result { if let Some(role) = cli.role { config.role = AgentRole::parse(&role); } + if let Some(aw_api_base) = cli.aw_api_base { + config.aw_api_base = Some(aw_api_base.trim_end_matches('/').to_string()); + } + if let Some(enabled) = cli.aw_worktime_enabled { + config.aw_worktime_enabled = enabled; + } if cli.spool_health { println!( "{}", @@ -72,6 +84,7 @@ fn run() -> Result { } let transport = TelemetryTransport::new(&config); + let aw_worktime = AwWorktimePublisher::new(&config); if cli.flush_spool { let flushed = transport.flush_spool()?; println!("{}", serde_json::json!({"ok": true, "flushed": flushed})); @@ -86,6 +99,13 @@ fn run() -> Result { } else if let Err(err) = transport.send_or_spool(&record) { eprintln!("{err:#}"); } + if !cli.print_json { + if let Some(publisher) = aw_worktime.as_ref() { + if let Err(err) = publisher.publish(&record) { + eprintln!("{err:#}"); + } + } + } if cli.once { break; } diff --git a/adk-rust/crates/awatch-agent-rs/src/transport.rs b/adk-rust/crates/awatch-agent-rs/src/transport.rs index 34b3724..d957cea 100644 --- a/adk-rust/crates/awatch-agent-rs/src/transport.rs +++ b/adk-rust/crates/awatch-agent-rs/src/transport.rs @@ -9,7 +9,7 @@ use reqwest::blocking::Client; use reqwest::header::{HeaderMap, HeaderValue}; use crate::config::AgentConfig; -use crate::telemetry::TelemetryRecord; +use crate::telemetry::{SessionInfo, TelemetryRecord}; #[derive(Debug, Clone)] pub struct TelemetryTransport { @@ -96,6 +96,147 @@ impl TelemetryTransport { } } +#[derive(Debug, Clone)] +pub struct AwWorktimePublisher { + aw_api_base: String, + timeout: Duration, +} + +impl AwWorktimePublisher { + pub fn new(config: &AgentConfig) -> Option { + if !config.aw_worktime_enabled { + return None; + } + let aw_api_base = config + .aw_api_base + .as_ref()? + .trim_end_matches('/') + .to_string(); + if aw_api_base.is_empty() { + return None; + } + Some(Self { + aw_api_base, + timeout: Duration::from_secs(config.timeout_seconds), + }) + } + + pub fn publish(&self, record: &TelemetryRecord) -> Result { + let client = Client::builder() + .timeout(self.timeout) + .build() + .context("build ActivityWatch HTTP client")?; + let bucket_id = format!( + "aw-worktime-sessions_{}", + sanitize_bucket_part(&record.hostname) + ); + ensure_aw_bucket( + &client, + &self.aw_api_base, + &bucket_id, + "aw-worktime-session-collector", + "aw.worktime.session", + &record.hostname, + )?; + let sessions = if record.active_sessions.is_empty() { + vec![SessionInfo { + session_id: "0".to_string(), + username: record.username.clone(), + session_type: "local".to_string(), + remote_addr: None, + started_at: None, + active: true, + }] + } else { + record.active_sessions.clone() + }; + let mut sent = 0; + let sample_seconds = 60_i64; + for session in sessions { + let payload = serde_json::json!({ + "timestamp": record.timestamp, + "duration": sample_seconds, + "data": { + "username": session.username, + "userId": format!("{}\\{}", record.hostname, session.username), + "sessionId": session_id_number(&session), + "sessionName": session.session_type, + "state": if session.active { "Active" } else { "Disconnected" }, + "active": session.active, + "sampleSeconds": sample_seconds, + "pollSeconds": sample_seconds, + "hostname": record.hostname, + "source": "awatch-agent-rs" + } + }); + client + .post(format!( + "{}/buckets/{}/heartbeat?pulsetime=180", + self.aw_api_base, bucket_id + )) + .json(&payload) + .send() + .and_then(|response| response.error_for_status()) + .context("publish ActivityWatch worktime heartbeat")?; + sent += 1; + } + Ok(sent) + } +} + +fn ensure_aw_bucket( + client: &Client, + aw_api_base: &str, + bucket_id: &str, + client_name: &str, + bucket_type: &str, + hostname: &str, +) -> Result<()> { + let bucket_url = format!("{}/buckets/{}", aw_api_base, bucket_id); + if client + .get(&bucket_url) + .send() + .and_then(|response| response.error_for_status()) + .is_ok() + { + return Ok(()); + } + let body = serde_json::json!({ + "client": client_name, + "type": bucket_type, + "hostname": hostname, + }); + client + .post(&bucket_url) + .json(&body) + .send() + .and_then(|response| response.error_for_status()) + .context("create ActivityWatch worktime bucket")?; + Ok(()) +} + +fn session_id_number(session: &SessionInfo) -> i64 { + session + .session_id + .split(|ch: char| !ch.is_ascii_digit()) + .find(|part| !part.is_empty()) + .and_then(|part| part.parse::().ok()) + .unwrap_or(0) +} + +fn sanitize_bucket_part(value: &str) -> String { + value + .chars() + .map(|ch| { + if ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' { + ch + } else { + '_' + } + }) + .collect() +} + pub fn flush_spool_dir(spool_dir: &Path, mut sender: F) -> Result where F: FnMut(&TelemetryRecord) -> Result<()>, @@ -202,4 +343,17 @@ mod tests { assert_eq!(seen, 1); assert!(!path.exists()); } + + #[test] + fn session_id_number_extracts_numeric_id() { + let session = SessionInfo { + session_id: "rdp-12-user".to_string(), + username: "user".to_string(), + session_type: "rdp".to_string(), + remote_addr: None, + started_at: None, + active: true, + }; + assert_eq!(session_id_number(&session), 12); + } } diff --git a/windows/ActivityWatch.Windows.Common.psm1 b/windows/ActivityWatch.Windows.Common.psm1 index 34225ed..856ca12 100755 --- a/windows/ActivityWatch.Windows.Common.psm1 +++ b/windows/ActivityWatch.Windows.Common.psm1 @@ -995,6 +995,7 @@ function New-ActivityWatchDeploymentConfig { windowEnabled = $WindowEnabled fileOpsEnabled = $FileOpsEnabled emailEnabled = $false + worktimeSessionEnabled = $true } logging = [pscustomobject]@{ localAgentLogsEnabled = $LocalAgentLogsEnabled @@ -2033,7 +2034,10 @@ function Invoke-ActivityWatchRecoveryLoop { $sessionRecords = Get-ActivityWatchSessionRecords $stateRoot = [string]$config.paths.stateRoot $sessionCollectorScript = if ($config.paths.PSObject.Properties.Name -contains 'sessionCollectorScript') { [string]$config.paths.sessionCollectorScript } else { Join-Path $stateRoot 'worktime-session-collector.ps1' } - Start-ActivityWatchCollectorScriptGlobalIfNeeded -ScriptPath $sessionCollectorScript -ConfigPath $ConfigPath + $worktimeSessionEnabled = if ($config.PSObject.Properties.Name -contains 'collectors' -and $config.collectors.PSObject.Properties.Name -contains 'worktimeSessionEnabled') { [bool]$config.collectors.worktimeSessionEnabled } else { $true } + if ($worktimeSessionEnabled) { + Start-ActivityWatchCollectorScriptGlobalIfNeeded -ScriptPath $sessionCollectorScript -ConfigPath $ConfigPath + } $configuredLiveTasksStarted = $false foreach ($taskDef in $taskDefs) { diff --git a/windows/aw-collector-guard.ps1 b/windows/aw-collector-guard.ps1 index 9d59022..e54a79d 100644 --- a/windows/aw-collector-guard.ps1 +++ b/windows/aw-collector-guard.ps1 @@ -488,9 +488,10 @@ function Invoke-GuardCycle { $worktimeAge = $bucketChecks["aw-worktime-sessions_$hostname"].ageSeconds $sessionCollectorScript = if ($config.paths.PSObject.Properties.Name -contains 'sessionCollectorScript') { [string]$config.paths.sessionCollectorScript } else { Join-Path $stateRoot 'worktime-session-collector.ps1' } - $sessionCollectorRunning = Test-ActivityWatchCollectorRunningGlobal -ScriptPath $sessionCollectorScript + $worktimeSessionEnabled = if ($config.PSObject.Properties.Name -contains 'collectors' -and $config.collectors.PSObject.Properties.Name -contains 'worktimeSessionEnabled') { [bool]$config.collectors.worktimeSessionEnabled } else { $true } + $sessionCollectorRunning = $worktimeSessionEnabled -and (Test-ActivityWatchCollectorRunningGlobal -ScriptPath $sessionCollectorScript) $headlessKey = 'headless:worktime-session' - $needsHeadlessAction = (-not $sessionCollectorRunning -or $null -eq $worktimeAge -or [int]$worktimeAge -gt $HeadlessMaxAgeSeconds) + $needsHeadlessAction = $worktimeSessionEnabled -and (-not $sessionCollectorRunning -or $null -eq $worktimeAge -or [int]$worktimeAge -gt $HeadlessMaxAgeSeconds) if ($needsHeadlessAction) { $key = $headlessKey $allowed = Test-ActionAllowed -Runtime $Runtime -Key $key -CooldownSeconds $ActionCooldownSeconds -WindowSeconds $RestartWindowSeconds -MaxCount $MaxRestarts diff --git a/windows/aw-standalone-service.ps1 b/windows/aw-standalone-service.ps1 index 0188724..c57ff33 100644 --- a/windows/aw-standalone-service.ps1 +++ b/windows/aw-standalone-service.ps1 @@ -81,6 +81,7 @@ while ($true) { if ($collectors) { if ($collectors.PSObject.Properties.Name -contains 'fileOpsEnabled') { $startFileOps = [bool]$collectors.fileOpsEnabled } if ($collectors.PSObject.Properties.Name -contains 'emailEnabled') { $startEmail = [bool]$collectors.emailEnabled } + if ($collectors.PSObject.Properties.Name -contains 'worktimeSessionEnabled') { $startWorktime = [bool]$collectors.worktimeSessionEnabled } } if ($isSession0) { $startBrowser = $false diff --git a/windows/deploy-ensemble.ps1 b/windows/deploy-ensemble.ps1 index e21413a..0a54042 100644 --- a/windows/deploy-ensemble.ps1 +++ b/windows/deploy-ensemble.ps1 @@ -178,6 +178,7 @@ $report = [ordered]@{ afkEnabled = $AfkEnabled windowEnabled = $WindowEnabled fileOpsEnabled = $FileOpsEnabled + worktimeSessionEnabled = $true } hardeningApplied = (-not $SkipHardening) } diff --git a/windows/install-standalone-service.ps1 b/windows/install-standalone-service.ps1 index e7ef129..fbce510 100644 --- a/windows/install-standalone-service.ps1 +++ b/windows/install-standalone-service.ps1 @@ -94,6 +94,7 @@ $config = [pscustomobject]@{ windowEnabled = $false fileOpsEnabled = $true emailEnabled = $true + worktimeSessionEnabled = $true } logging = [pscustomobject]@{ localAgentLogsEnabled = $true