Refactored & Modularized Agent Roles

This commit is contained in:
2025-09-18 14:29:29 -06:00
parent 30875b780b
commit 8a3f2ecd77
22 changed files with 1347 additions and 2190 deletions

2
.gitignore vendored
View File

@@ -9,7 +9,6 @@ Borealis-Server.exe
/Server/
/ElectronApp/
/Logs/
/Temp/
# On-the-Fly Downloaded Dependencies
/Dependencies/NodeJS/
@@ -20,6 +19,7 @@ Borealis-Server.exe
# Misc Files/Folders
.vs/s
__pycache__
/Agent/Python_API_Endpoints/__pycache__/
/Update_Staging/
agent_settings.json
users.json

View File

@@ -9,7 +9,7 @@ Today the stable core focuses on workflow-driven API and automation scenarios. R
- `Borealis.ps1` is the entry point for every component. It bootstraps dependencies, clones bundled virtual environments, and spins up server, agent, Vite, or Flask modes on demand.
- Bundled assets live under `Data/Agent`, `Data/Server`, and `Dependencies`. Launching installs copies into sibling `Agent/` and `Server/` directories so the development tree stays clean and the runtime stays portable.
- The server stack spans NodeJS + Vite for live development and Flask (`Data/Server/server.py`) for production APIs, backed by Python helpers (`Data/Server/Python_API_Endpoints`) for OCR, scripting, and other services.
- Agents run inside the packaged Python venv (`Data/Agent` mirrored to `Agent/`). `borealis-agent.py` handles the primary connection, with `agent_supervisor.py` and the PowerShell watchdog managing SYSTEM-level operations and resilience.
- Agents run inside the packaged Python venv (`Data/Agent` mirrored to `Agent/`). `agent.py` handles the primary connection and hot-loads roles from `Data/Agent/Roles` at startup.
## Dependencies & Packaging
`Dependencies/` holds the installers/download payloads Borealis bootstraps on first launch: Python, 7-Zip, AutoHotkey, and NodeJS. Versions are hard-pinned in `Borealis.ps1`; upgrading any runtime requires updating those version constants before repackaging. Nothing self-updates, so Codex should coordinate dependency bumps carefully and test both server and agent bootstrap paths.
@@ -20,20 +20,44 @@ Today the stable core focuses on workflow-driven API and automation scenarios. R
Agents establish REST calls to the Flask backend on port 5000 and keep a WebSocket session for interactive features such as screenshot capture. Future plans include WebRTC for higher-performance remote desktop. No authentication or enrollment handshake exists yet, so agents are implicitly trusted once launched.
### Execution Contexts
`agent_supervisor.py` runs as `NT AUTHORITY\SYSTEM` via a scheduled task created during installation. `Scripts/watchdog.ps1` checks every five minutes that the supervisor stays alive and restarts it when needed. The primary `borealis-agent.py` process runs as the interactive user to cover foreground automation while delegating privileged work to the supervisor.
The agent runs in the interactive user session. SYSTEM-level script execution is provided by the ScriptExec SYSTEM role using ephemeral scheduled tasks; no separate supervisor or watchdog is required.
### Logging & State
All runtime logs live under `Logs/<ServiceName>` relative to the project root (`Logs/Agent` for the agent family). The project avoids writing to `%ProgramData%`, `%AppData%`, or other system directories so the entire footprint stays under the Borealis folder. Log rotation is not yet implemented; contributions should consider a built-in retention strategy. Configuration and state currently live alongside the agent code.
## Roles & Extensibility
- Role modules follow a `role_<purpose>.py` naming convention and should implement a configurable expiration window so the agent can abandon long-running work when the server signals a timeout.
- At present, roles aggregate inside `Data/Agent/agent_roles.py`. The desired end state is a `Agent/Borealis/Roles/` directory where each role lives in its own file and is auto-discovered or explicitly registered on startup.
- New roles should expose clear hooks for initialization, execution, cancellation, and cleanup. They must tolerate the agent being restarted, handle both Windows and (eventual) Linux paths, and avoid blocking the main event loop.
- Planned script execution split examples: `role_ScriptExec_SYSTEM.py` for privileged script or task execution and `role_ScriptExec_CURRENTUSER.py` for interactive scripts and tasks, keeping core orchestration in `borealis-agent.py`.
- Roles live under `Data/Agent/Roles/` and are autodiscovered at startup; no changes are needed in `agent.py` when adding new roles.
- Naming convention: `role_<Purpose>.py` per role.
- Role interface (per module):
- `ROLE_NAME`: canonical role name used by config (e.g., `screenshot`, `script_exec_system`).
- `ROLE_CONTEXTS`: list of contexts this role runs in (`interactive`, `system`).
- `class Role(ctx)`: optional hooks the agent loader will call:
- `register_events()`: bind any Socket.IO listeners.
- `on_config(roles: List[dict])`: start/stop perrole tasks based on server config.
- `stop_all()`: cancel tasks and cleanup.
- Standard roles currently shipped:
- `role_DeviceInventory.py` — collects and periodically posts device inventory/summary.
- `role_Screenshot.py` — region overlay + periodic capture with WebSocket updates.
- `role_ScriptExec_CURRENTUSER.py` — runs PowerShell in the loggedin session and provides the tray icon (restart/quit).
- `role_ScriptExec_SYSTEM.py` — runs PowerShell as SYSTEM via ephemeral Scheduled Tasks.
- `role_Macro.py` — macro and key/text send helpers.
- Considerations:
- SYSTEM role requires administrative rights to create/run scheduled tasks as SYSTEM. If elevation is unavailable or policies restrict task creation, SYSTEM jobs will fail gracefully and report errors to the server.
- Roles are “hotloaded” on startup only (no dynamic import while running).
- Roles must avoid blocking the main event loop and be resilient to restarts.
## Packaging Notes
- `Borealis.ps1` deploys `agent.py`, `role_manager.py`, `Roles/`, and `Python_API_Endpoints/` into `Agent/Borealis/`.
- If packaging a singlefile EXE (PyInstaller), ensure `Roles/` and `Python_API_Endpoints/` are included as data files so role autodiscovery works at runtime.
## Migration Summary
- Replaced monolithic role code with modular roles under `Data/Agent/Roles/`.
- Removed legacy helpers: `agent_supervisor.py`, `agent_roles.py`, `tray_launcher.py`, `agent_info.py`, and `script_agent.py` (functionality is now inside roles).
- `agent.py` contains only core transport/config logic and role loading.
## Operational Guidance
- Launch or test a single agent locally with `.\Borealis.ps1 -Agent` (or combine with `-AgentAction install|repair|launch|remove` as needed). The same entry point manages the server (`-Server`) with either Vite or Flask flags.
- When debugging, tail files under `Logs/Agent` and inspect the watchdog output to confirm the supervisor is running. Use the PowerShell packaging scripts in `Data/Agent/Scripts` to reinstall scheduled tasks if they drift.
- When debugging, tail files under `Logs/Agent`. Use the PowerShell packaging scripts in `Data/Agent/Scripts` to reinstall the user logon scheduled task if it drifts.
- Updates today require manually stopping related processes (`taskkill /IM "node.exe" /IM "pythonw.exe" /IM "python.exe" /F`) followed by a fresh run of `Borealis.ps1 -Agent`. This is a known limitation; future work should automate graceful agent restarts and remote updates without collateral downtime.
- Known stability gaps include suspected Python memory leaks in both the server and agents under multi-day workloads, occasional heartbeat mismatches, and the flashing watchdog console window. A more robust keepalive should eventually remove the watchdog dependency.
- Expect the agent to remain running for days or weeks; contributions should focus on reconnect logic, light resource usage, and graceful shutdown/restart semantics.
@@ -53,3 +77,11 @@ Windows is the reference environment today. `Borealis.ps1` owns the full deploym
## Security Outlook
Security and authentication are intentionally deferred. There is currently no agent/server handshake, credential model, or ACL on powerful endpoints, so deployments must remain in controlled environments. A future milestone will introduce mutual registration, scoped API tokens, and hardened remote execution surfaces; until then, prioritize resilience and modularity while acknowledging the risk.

View File

@@ -62,6 +62,44 @@ $symbols = @{
Info = [char]0x2139
}
# Admin/Elevation helpers for Agent deployment
function Test-IsAdmin {
try {
$id = [Security.Principal.WindowsIdentity]::GetCurrent()
$p = New-Object Security.Principal.WindowsPrincipal($id)
return $p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
} catch { return $false }
}
function Request-AgentElevation {
param(
[string]$ScriptPath,
[string]$AgentActionParam,
[switch]$Auto
)
if (Test-IsAdmin) { return $true }
if (-not $Auto) {
Write-Host "" # spacer
Write-Host "Agent requires Administrator permissions to register scheduled tasks and run reliably." -ForegroundColor Yellow -BackgroundColor Black
Write-Host "Grant elevated permissions now? (Y/N)" -ForegroundColor Yellow -BackgroundColor Black
$resp = Read-Host
if ($resp -notin @('y','Y','yes','YES')) { return $false }
}
$args = @('-NoProfile','-ExecutionPolicy','Bypass','-File', '"' + $ScriptPath + '"', '-Agent')
if ($AgentActionParam -and $AgentActionParam.Trim()) {
$args += @('-AgentAction', $AgentActionParam)
}
try {
Start-Process -FilePath 'powershell.exe' -Verb RunAs -ArgumentList $args -WindowStyle Normal | Out-Null
return $false # stop current non-elevated instance
} catch {
Write-Host "Elevation was denied or failed." -ForegroundColor Red
return $false
}
}
# Ensure log directories
function Ensure-AgentLogDir {
$logRoot = Join-Path $scriptDir 'Logs'
@@ -324,91 +362,21 @@ function Install_Agent_Dependencies {
}
function Ensure-AgentTasks {
# Registers SYSTEM Supervisor + 5-min Watchdog via UAC-elevated stub
param(
[string]$ScriptRoot
)
$supName = 'Borealis Agent - Supervisor'
$py = Join-Path $ScriptRoot 'Agent\Scripts\python.exe'
$supScript = Join-Path $ScriptRoot 'Data\Agent\agent_supervisor.py'
$wdName = 'Borealis Agent - Watchdog'
# Per-user tray helper task (ensure within same elevation to avoid second UAC)
param([string]$ScriptRoot)
$userTaskName = 'Borealis Agent'
$userExe = Join-Path $ScriptRoot 'Agent\Scripts\pythonw.exe'
$userScript = Join-Path $ScriptRoot 'Agent\Borealis\tray_launcher.py'
# Elevate and run the external registrar script with parameters
$regScript = Join-Path $ScriptRoot 'Data\Agent\Scripts\register_agent_tasks.ps1'
$wdSource = Join-Path $ScriptRoot 'Data\Agent\Scripts\watchdog.ps1'
if (-not (Test-Path $regScript)) { Write-Host "Register helper script not found: $regScript" -ForegroundColor Red; return }
if (-not (Test-Path $wdSource)) { Write-Host "Watchdog script not found: $wdSource" -ForegroundColor Red; return }
# Launch registrar elevated using -EncodedCommand to avoid quoting/binding issues
$qSupName = $supName -replace "'","''"
$qPy = $py -replace "'","''"
$qSupScript = $supScript -replace "'","''"
$qWdName = $wdName -replace "'","''"
$qWdSource = $wdSource -replace "'","''"
$qRegScript = $regScript -replace "'","''"
$qUserTaskName = $userTaskName -replace "'","''"
$qUserExe = $userExe -replace "'","''"
$qUserScript = $userScript -replace "'","''"
$agentScript = Join-Path $ScriptRoot 'Agent\Borealis\agent.py'
if (-not (Test-Path $userExe)) { Write-Host "pythonw.exe not found under Agent\Scripts" -ForegroundColor Yellow; return }
if (-not (Test-Path $agentScript)) { Write-Host "Agent script not found under Agent\Borealis" -ForegroundColor Yellow; return }
try { Unregister-ScheduledTask -TaskName $userTaskName -Confirm:$false -ErrorAction SilentlyContinue } catch {}
$usrArg = ('-W ignore::SyntaxWarning "{0}"' -f $agentScript)
$usrAction = New-ScheduledTaskAction -Execute $userExe -Argument $usrArg
$usrTrig = New-ScheduledTaskTrigger -AtLogOn
$usrSet = New-ScheduledTaskSettingsSet -Hidden -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) -ExecutionTimeLimit ([TimeSpan]::Zero)
$currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
$qUserPrincipal= $currentUser -replace "'","''"
$inline = @"
`$p = @{
SupName = '$qSupName'
PythonExe = '$qPy'
SupScript = '$qSupScript'
WdName = '$qWdName'
WdSource = '$qWdSource'
UserTaskName = '$qUserTaskName'
UserExe = '$qUserExe'
UserScript = '$qUserScript'
UserPrincipal = '$qUserPrincipal'
}
& '$qRegScript' @p
"@
$bytes = [System.Text.Encoding]::Unicode.GetBytes($inline)
$encoded = [Convert]::ToBase64String($bytes)
# Use a temporary VBS shim to elevate and run PowerShell fully hidden (no flashing console)
$tempDir = Join-Path $ScriptRoot 'Temp'
if (-not (Test-Path $tempDir)) { New-Item -ItemType Directory -Path $tempDir -Force | Out-Null }
$vbsPath = Join-Path $tempDir 'RunAgentRegistrarElevatedHidden.vbs'
$vbs = @'
Set sh = CreateObject("Shell.Application")
cmd = "powershell.exe"
args = " -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -EncodedCommand " & "<ENC>"
sh.ShellExecute cmd, args, "", "runas", 0
'@
$vbs = $vbs -replace '<ENC>', $encoded
Write-AgentLog -FileName 'AgentTaskRegistration.log' -Message 'Preparing elevated hidden registrar VBS shim.'
Set-Content -Path $vbsPath -Value $vbs -Encoding ASCII -Force
try {
Write-AgentLog -FileName 'AgentTaskRegistration.log' -Message 'Invoking wscript.exe to run registrar hidden with elevation.'
Start-Process -FilePath 'wscript.exe' -ArgumentList ('"{0}"' -f $vbsPath) -WindowStyle Hidden -Wait | Out-Null
} catch {
Write-Host "Failed to elevate for task registration." -ForegroundColor Red
Write-AgentLog -FileName 'AgentTaskRegistration.log' -Message ("Registrar elevation failed: " + $_)
} finally {
Remove-Item $vbsPath -Force -ErrorAction SilentlyContinue
}
# Best-effort: wait briefly for tasks to be registered so subsequent steps see them
try {
$maxWaitSec = 30
$sw = [System.Diagnostics.Stopwatch]::StartNew()
do {
Start-Sleep -Milliseconds 500
try { $ts = Get-ScheduledTask -TaskName $supName -ErrorAction SilentlyContinue } catch { $ts = $null }
} while (-not $ts -and $sw.Elapsed.TotalSeconds -lt $maxWaitSec)
$sw.Stop()
if ($ts) {
Write-AgentLog -FileName 'AgentTaskRegistration.log' -Message "Supervisor task detected after $([int]$sw.Elapsed.TotalSeconds)s."
} else {
Write-AgentLog -FileName 'AgentTaskRegistration.log' -Message "Supervisor task not detected within timeout; continuing."
}
} catch {}
$usrPrin = New-ScheduledTaskPrincipal -UserId $currentUser -LogonType Interactive -RunLevel Limited
Register-ScheduledTask -TaskName $userTaskName -Action $usrAction -Trigger $usrTrig -Settings $usrSet -Principal $usrPrin -Force | Out-Null
try { Start-ScheduledTask -TaskName $userTaskName | Out-Null } catch {}
}
function InstallOrUpdate-BorealisAgent {
Write-Host "Ensuring Agent Dependencies Exist..." -ForegroundColor DarkCyan
@@ -422,10 +390,10 @@ function InstallOrUpdate-BorealisAgent {
Write-Host "Deploying Borealis Agent..." -ForegroundColor Blue
$venvFolder = "Agent"
$agentSourcePath = "Data\Agent\borealis-agent.py"
$agentSourcePath = "Data\Agent\agent.py"
$agentRequirements = "Data\Agent\agent-requirements.txt"
$agentDestinationFolder = "$venvFolder\Borealis"
$agentDestinationFile = "$venvFolder\Borealis\borealis-agent.py"
$agentDestinationFile = "$venvFolder\Borealis\agent.py"
$venvPython = Join-Path $scriptDir $venvFolder | Join-Path -ChildPath 'Scripts\python.exe'
Run-Step "Create Virtual Python Environment" {
@@ -446,12 +414,14 @@ function InstallOrUpdate-BorealisAgent {
if (Test-Path $agentSourcePath) {
Remove-Item $agentDestinationFolder -Recurse -Force -ErrorAction SilentlyContinue
New-Item -Path $agentDestinationFolder -ItemType Directory -Force | Out-Null
Copy-Item "Data\Agent\borealis-agent.py" $agentDestinationFolder -Recurse
Copy-Item "Data\Agent\agent_info.py" $agentDestinationFolder -Recurse
Copy-Item "Data\Agent\agent_roles.py" $agentDestinationFolder -Recurse
Copy-Item "Data\Agent\agent.py" $agentDestinationFolder -Recurse
# agent_info has been migrated into roles; no longer copied
# Legacy agent_roles kept for compatibility only if needed
Copy-Item "Data\Agent\Python_API_Endpoints" $agentDestinationFolder -Recurse
Copy-Item "Data\Agent\role_manager.py" $agentDestinationFolder -Force
if (Test-Path "Data\Agent\Roles") { Copy-Item "Data\Agent\Roles" $agentDestinationFolder -Recurse }
Copy-Item "Data\Agent\agent_deployment.py" $agentDestinationFolder -Force
Copy-Item "Data\Agent\tray_launcher.py" $agentDestinationFolder -Force
# tray is now embedded in CURRENTUSER role; no launcher to copy
if (Test-Path "Data\Agent\Borealis.ico") { Copy-Item "Data\Agent\Borealis.ico" $agentDestinationFolder -Force }
}
. "$venvFolder\Scripts\Activate"
@@ -496,8 +466,7 @@ if (-not $choice) {
Write-Host "[Experimental]" -ForegroundColor Red
Write-Host " 5) Update Borealis " -NoNewLine -ForegroundColor DarkGray
Write-Host "[Requires Re-Build]" -ForegroundColor Red
Write-Host " 6) Perform AutoHotKey Automation Testing " -NoNewline -ForegroundColor DarkGray
Write-Host "[Experimental - Dev Testing]" -ForegroundColor Red
# (Removed) AutoHotKey experimental testing
Write-Host "Type a number and press " -NoNewLine
Write-Host "<ENTER>" -ForegroundColor DarkCyan
$choice = Read-Host
@@ -613,6 +582,15 @@ switch ($choice) {
"2" {
$host.UI.RawUI.WindowTitle = "Borealis Agent"
Write-Host " "
# Ensure elevation before showing Agent menu
$scriptPath = $PSCommandPath
if (-not $scriptPath -or $scriptPath -eq '') { $scriptPath = $MyInvocation.MyCommand.Definition }
# If already elevated, skip prompt; otherwise prompt, then relaunch directly to the Agent menu via -Agent
$cont = Request-AgentElevation -ScriptPath $scriptPath -AgentActionParam $AgentAction
if (-not $cont -and -not (Test-IsAdmin)) { return }
if (Test-IsAdmin) {
Write-Host "Escalated Permissions Granted > Agent is Eligible for Deployment." -ForegroundColor Green
}
Write-Host "Agent Menu:" -ForegroundColor Cyan
Write-Host " 1) Install/Update Agent"
Write-Host " 2) Repair Borealis Agent"
@@ -627,7 +605,7 @@ switch ($choice) {
'3' { Remove-BorealisAgent; break }
'4' {
$venvPythonw = Join-Path $scriptDir 'Agent\Scripts\pythonw.exe'
$helper = Join-Path $scriptDir 'Agent\Borealis\borealis-agent.py'
$helper = Join-Path $scriptDir 'Agent\Borealis\agent.py'
if (-not (Test-Path $venvPythonw)) { Write-Host "pythonw.exe not found under Agent\Scripts" -ForegroundColor Yellow }
if (-not (Test-Path $helper)) { Write-Host "Helper not found under Agent\Borealis" -ForegroundColor Yellow }
if ((Test-Path $venvPythonw) -and (Test-Path $helper)) {
@@ -758,43 +736,7 @@ switch ($choice) {
Exit 0
}
"6" {
$host.UI.RawUI.WindowTitle = "AutoHotKey Automation Testing"
Write-Host " "
Write-Host "Lauching AutoHotKey Testing Script..." -ForegroundColor Blue
$venvFolder = "Macro_Testing"
$scriptSourcePath = "Data\Experimental\Macros\Macro_Script.py"
$scriptRequirements = "Data\Experimental\Macros\macro-requirements.txt"
$scriptDestinationFolder= "$venvFolder\Borealis"
$scriptDestinationFile = "$venvFolder\Borealis\Macro_Script.py"
$venvPython = Join-Path $scriptDir $venvFolder | Join-Path -ChildPath 'Scripts\python.exe'
Run-Step "Create Virtual Python Environment" {
if (-not (Test-Path "$venvFolder\Scripts\Activate")) {
$pythonForVenv = $pythonExe
if (-not (Test-Path $pythonForVenv)) {
$pyCmd = Get-Command py -ErrorAction SilentlyContinue
$pythonCmd = Get-Command python -ErrorAction SilentlyContinue
if ($pyCmd) { $pythonForVenv = $pyCmd.Source }
elseif ($pythonCmd) { $pythonForVenv = $pythonCmd.Source }
else { Write-Host "Python not found. Install Python or run Server setup (option 1)." -ForegroundColor Red; exit 1 }
}
& $pythonForVenv -m venv $venvFolder
}
if (Test-Path $scriptSourcePath) {
Remove-Item $scriptDestinationFolder -Recurse -Force -ErrorAction SilentlyContinue
New-Item -Path $scriptDestinationFolder -ItemType Directory -Force | Out-Null
Copy-Item $scriptSourcePath $scriptDestinationFile -Force
Copy-Item "Dependencies\AutoHotKey" $scriptDestinationFolder -Recurse
}
. "$venvFolder\Scripts\Activate"
}
Run-Step "Install Python Dependencies" { if (Test-Path $scriptRequirements) { & $venvPython -m pip install --disable-pip-version-check -q -r $scriptRequirements | Out-Null } }
Write-Host "`nLaunching Macro Testing Script..." -ForegroundColor Blue
Write-Host "===================================================================================="
& $venvPython -W ignore::SyntaxWarning $scriptDestinationFile
}
# (Removed) case "6" experimental AHK test
default { Write-Host "Invalid selection. Exiting..." -ForegroundColor Red; exit 1 }
}

View File

@@ -151,7 +151,7 @@ launch_agent() {
run_step "Install System Dependencies" install_core_dependencies
venvFolder="Agent"
agentSourcePath="Data/Agent/borealis-agent.py"
agentSourcePath="Data/Agent/agent.py"
agentRequirements="Data/Agent/agent-requirements.txt"
agentDestinationFolder="${venvFolder}/Borealis"
@@ -169,13 +169,16 @@ launch_agent() {
run_step "Copy Agent Script" bash -c "
mkdir -p '${agentDestinationFolder}'
cp '${agentSourcePath}' '${agentDestinationFolder}/'
cp 'Data/Agent/agent.py' '${agentDestinationFolder}/'
cp 'Data/Agent/role_manager.py' '${agentDestinationFolder}/'
if [ -d 'Data/Agent/Roles' ]; then cp -r 'Data/Agent/Roles' '${agentDestinationFolder}/'; fi
if [ -d 'Data/Agent/Python_API_Endpoints' ]; then cp -r 'Data/Agent/Python_API_Endpoints' '${agentDestinationFolder}/'; fi
"
echo -e "\n${GREEN}Launching Borealis Agent...${RESET}"
echo "===================================================================================="
source '${venvFolder}/bin/activate'
python3 "${agentDestinationFolder}/borealis-agent.py"
python3 "${agentDestinationFolder}/agent.py"
}
# Main menu

View File

@@ -6,7 +6,7 @@ $venvDir = "$packagingDir\Pyinstaller_Virtual_Environment"
$distDir = "$packagingDir\dist"
$buildDir = "$packagingDir\build"
$specPath = "$packagingDir"
$agentScript = "borealis-agent.py"
$agentScript = "agent.py"
$outputName = "Borealis-Agent"
$finalExeName = "$outputName.exe"
$requirementsPath = "agent-requirements.txt"

View File

@@ -0,0 +1,2 @@
# Roles package for Borealis Agent

View File

@@ -0,0 +1,280 @@
import os
import json
import time
import socket
import platform
import subprocess
import shutil
import string
import asyncio
try:
import psutil # type: ignore
except Exception:
psutil = None
try:
import aiohttp
except Exception:
aiohttp = None
ROLE_NAME = 'device_inventory'
ROLE_CONTEXTS = ['interactive']
IS_WINDOWS = os.name == 'nt'
def detect_agent_os():
try:
plat = platform.system().lower()
if plat.startswith('win'):
try:
import winreg # type: ignore
reg_path = r"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion"
access = getattr(winreg, 'KEY_READ', 0x20019)
try:
access |= winreg.KEY_WOW64_64KEY
except Exception:
pass
try:
key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, reg_path, 0, access)
except OSError:
key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, reg_path, 0, winreg.KEY_READ)
def _get(name, default=None):
try:
return winreg.QueryValueEx(key, name)[0]
except Exception:
return default
product_name = _get("ProductName", "")
display_version = _get("DisplayVersion", "")
release_id = _get("ReleaseId", "")
build_number = _get("CurrentBuildNumber", "") or _get("CurrentBuild", "")
ubr = _get("UBR", None)
try:
build_int = int(str(build_number).split(".")[0]) if build_number else 0
except Exception:
build_int = 0
if build_int >= 22000:
major_label = "11"
elif build_int >= 10240:
major_label = "10"
else:
major_label = platform.release()
os_name = f"Windows {major_label}"
version_label = display_version or release_id or ""
if isinstance(ubr, int):
build_str = f"{build_number}.{ubr}" if build_number else str(ubr)
else:
try:
build_str = f"{build_number}.{int(ubr)}" if build_number and ubr else (build_number or "")
except Exception:
build_str = build_number or ""
parts = [os_name]
if product_name and product_name.lower().startswith('windows '):
try:
tail = product_name.split(' ', 2)[2]
if tail:
parts.append(tail)
except Exception:
pass
if version_label:
parts.append(version_label)
if build_str:
parts.append(f"Build {build_str}")
return " ".join([p for p in parts if p]).strip() or platform.platform()
except Exception:
return platform.platform()
elif plat == 'darwin':
try:
out = subprocess.run(["sw_vers", "-productVersion"], capture_output=True, text=True, timeout=3)
ver = (out.stdout or '').strip()
return f"macOS {ver}" if ver else "macOS"
except Exception:
return "macOS"
else:
try:
import distro # type: ignore
name = distro.name(pretty=True) or distro.id()
ver = distro.version()
return f"{name} {ver}".strip()
except Exception:
return platform.platform()
except Exception:
return "Unknown"
def collect_summary(CONFIG):
try:
hostname = socket.gethostname()
return {
'hostname': hostname,
'os': CONFIG.data.get('agent_operating_system', detect_agent_os()),
'username': os.environ.get('USERNAME') or os.environ.get('USER') or '',
'domain': os.environ.get('USERDOMAIN') or '',
'uptime_sec': int(time.time() - psutil.boot_time()) if psutil else None,
}
except Exception:
return {'hostname': socket.gethostname()}
def collect_software():
# Placeholder: fuller inventory can be added later
return []
def collect_memory():
entries = []
try:
plat = platform.system().lower()
if plat == 'windows':
try:
ps_cmd = (
"Get-CimInstance Win32_PhysicalMemory | "
"Select-Object BankLabel,Speed,SerialNumber,Capacity | ConvertTo-Json"
)
out = subprocess.run(["powershell", "-NoProfile", "-Command", ps_cmd], capture_output=True, text=True, timeout=60)
data = json.loads(out.stdout or "[]")
if isinstance(data, dict):
data = [data]
for stick in data:
entries.append({
'slot': stick.get('BankLabel', 'unknown'),
'speed': str(stick.get('Speed', 'unknown')),
'serial': stick.get('SerialNumber', 'unknown'),
'capacity': stick.get('Capacity', 'unknown'),
})
except Exception:
pass
except Exception:
pass
if not entries and psutil:
try:
vm = psutil.virtual_memory()
entries.append({'slot': 'physical', 'speed': 'unknown', 'serial': 'unknown', 'capacity': vm.total})
except Exception:
pass
return entries
def collect_storage():
disks = []
try:
if psutil:
for part in psutil.disk_partitions():
try:
usage = psutil.disk_usage(part.mountpoint)
except Exception:
continue
disks.append({
'drive': part.device,
'disk_type': 'Removable' if isinstance(part.opts, str) and 'removable' in part.opts.lower() else 'Fixed Disk',
'usage': usage.percent,
'total': usage.total,
'free': usage.free,
'used': usage.used,
})
else:
# Fallback basic detection on Windows via drive letters
if IS_WINDOWS:
for letter in string.ascii_uppercase:
drive = f"{letter}:\\"
if os.path.exists(drive):
try:
usage = shutil.disk_usage(drive)
except Exception:
continue
disks.append({
'drive': drive,
'disk_type': 'Fixed Disk',
'usage': (usage.used / usage.total * 100) if usage.total else 0,
'total': usage.total,
'free': usage.free,
'used': usage.used,
})
except Exception:
pass
return disks
def collect_network():
adapters = []
try:
if IS_WINDOWS:
try:
ps_cmd = (
"Get-NetAdapter | Where-Object { $_.Status -eq 'Up' } | "
"ForEach-Object { $_ | Select-Object -Property InterfaceAlias, MacAddress } | ConvertTo-Json"
)
out = subprocess.run(["powershell", "-NoProfile", "-Command", ps_cmd], capture_output=True, text=True, timeout=60)
data = json.loads(out.stdout or "[]")
if isinstance(data, dict):
data = [data]
for a in data:
adapters.append({'adapter': a.get('InterfaceAlias', 'unknown'), 'ips': [], 'mac': a.get('MacAddress', 'unknown')})
except Exception:
pass
else:
out = subprocess.run(["ip", "-o", "-4", "addr", "show"], capture_output=True, text=True, timeout=60)
for line in out.stdout.splitlines():
parts = line.split()
if len(parts) >= 4:
name = parts[1]
ip = parts[3].split("/")[0]
adapters.append({'adapter': name, 'ips': [ip], 'mac': 'unknown'})
except Exception:
pass
return adapters
class Role:
def __init__(self, ctx):
self.ctx = ctx
try:
# Set OS string once
self.ctx.config.data['agent_operating_system'] = detect_agent_os()
self.ctx.config._write()
except Exception:
pass
# Start periodic reporter
try:
self.task = self.ctx.loop.create_task(self._report_loop())
except Exception:
self.task = None
def stop_all(self):
try:
if self.task:
self.task.cancel()
except Exception:
pass
async def _report_loop(self):
while True:
try:
details = {
'summary': collect_summary(self.ctx.config),
'software': collect_software(),
'memory': collect_memory(),
'storage': collect_storage(),
'network': collect_network(),
}
url = (self.ctx.config.data.get('borealis_server_url', 'http://localhost:5000') or '').rstrip('/') + '/api/agent/details'
payload = {
'agent_id': self.ctx.agent_id,
'hostname': details.get('summary', {}).get('hostname', socket.gethostname()),
'details': details,
}
if aiohttp is not None:
async with aiohttp.ClientSession() as session:
await session.post(url, json=payload, timeout=10)
except Exception:
pass
await asyncio.sleep(300)

View File

@@ -0,0 +1,122 @@
import os
import asyncio
import importlib.util
ROLE_NAME = 'macro'
ROLE_CONTEXTS = ['interactive']
def _load_macro_engines():
try:
base = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))
path = os.path.join(base, 'Python_API_Endpoints', 'macro_engines.py')
spec = importlib.util.spec_from_file_location('macro_engines', path)
mod = importlib.util.module_from_spec(spec)
assert spec and spec.loader
spec.loader.exec_module(mod)
return mod
except Exception:
class _Dummy:
def list_windows(self):
return []
def send_keypress_to_window(self, handle, key):
return False, 'unavailable'
def type_text_to_window(self, handle, text):
return False, 'unavailable'
return _Dummy()
macro_engines = _load_macro_engines()
class Role:
def __init__(self, ctx):
self.ctx = ctx
self.tasks = {}
def stop_all(self):
for t in list(self.tasks.values()):
try:
t.cancel()
except Exception:
pass
self.tasks.clear()
def on_config(self, roles_cfg):
macro_roles = [r for r in roles_cfg if (r.get('role') == 'macro')]
new_ids = {r.get('node_id') for r in macro_roles if r.get('node_id')}
old_ids = set(self.tasks.keys())
removed = old_ids - new_ids
for rid in removed:
t = self.tasks.pop(rid, None)
if t:
try:
t.cancel()
except Exception:
pass
for rcfg in macro_roles:
nid = rcfg.get('node_id')
if nid and nid not in self.tasks:
self.tasks[nid] = asyncio.create_task(self._macro_task(rcfg))
async def _macro_task(self, cfg):
nid = cfg.get('node_id')
last_trigger_value = 0
has_run_once = False
while True:
window_handle = cfg.get('window_handle')
macro_type = cfg.get('macro_type', 'keypress')
operation_mode = cfg.get('operation_mode', 'Continuous')
key = cfg.get('key')
text = cfg.get('text')
interval_ms = int(cfg.get('interval_ms', 1000))
randomize = cfg.get('randomize_interval', False)
random_min = int(cfg.get('random_min', 750))
random_max = int(cfg.get('random_max', 950))
active = cfg.get('active', True)
trigger = int(cfg.get('trigger', 0))
async def emit_macro_status(success, message=""):
try:
await self.ctx.sio.emit('macro_status', {
'agent_id': self.ctx.agent_id,
'node_id': nid,
'success': success,
'message': message,
})
except Exception:
pass
if not (active is True or str(active).lower() == 'true'):
await asyncio.sleep(0.2)
continue
try:
send_macro = False
if operation_mode == 'Run Once':
if not has_run_once:
send_macro = True
has_run_once = True
elif operation_mode == 'Continuous':
send_macro = True
elif operation_mode == 'Trigger-Continuous':
send_macro = (trigger == 1)
elif operation_mode == 'Trigger-Once':
if trigger == 1 and last_trigger_value != 1:
send_macro = True
else:
send_macro = False
if send_macro:
ok = False
if macro_type == 'keypress' and key:
ok = bool(macro_engines.send_keypress_to_window(window_handle, key))
elif macro_type == 'text' and text:
ok = bool(macro_engines.type_text_to_window(window_handle, text))
await emit_macro_status(ok, 'sent' if ok else 'failed')
last_trigger_value = trigger
except Exception as e:
await emit_macro_status(False, str(e))
# interval wait
await asyncio.sleep(max(0.05, (interval_ms or 1000) / 1000.0))

View File

@@ -0,0 +1,322 @@
import os
import asyncio
import concurrent.futures
from functools import partial
from io import BytesIO
import base64
import traceback
from PyQt5 import QtCore, QtGui, QtWidgets
from PIL import ImageGrab
import importlib.util
ROLE_NAME = 'screenshot'
ROLE_CONTEXTS = ['interactive']
# Load macro engines from the local Python_API_Endpoints directory for window listings
def _load_macro_engines():
try:
base = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))
path = os.path.join(base, 'Python_API_Endpoints', 'macro_engines.py')
spec = importlib.util.spec_from_file_location('macro_engines', path)
mod = importlib.util.module_from_spec(spec)
assert spec and spec.loader
spec.loader.exec_module(mod)
return mod
except Exception:
class _Dummy:
def list_windows(self):
return []
return _Dummy()
macro_engines = _load_macro_engines()
overlay_green_thickness = 4
overlay_gray_thickness = 2
handle_size = overlay_green_thickness * 2
extra_top_padding = overlay_green_thickness * 2 + 4
overlay_widgets = {}
class ScreenshotRegion(QtWidgets.QWidget):
def __init__(self, ctx, node_id, x=100, y=100, w=300, h=200, alias=None):
super().__init__()
self.ctx = ctx
self.node_id = node_id
self.alias = alias
self.setGeometry(
x - handle_size,
y - handle_size - extra_top_padding,
w + handle_size * 2,
h + handle_size * 2 + extra_top_padding,
)
self.setWindowFlags(QtCore.Qt.FramelessWindowHint | QtCore.Qt.WindowStaysOnTopHint)
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
self.resize_dir = None
self.drag_offset = None
self._start_geom = None
self._start_pos = None
self.setMouseTracking(True)
def paintEvent(self, event):
p = QtGui.QPainter(self)
p.setRenderHint(QtGui.QPainter.Antialiasing)
w = self.width()
h = self.height()
p.setPen(QtGui.QPen(QtGui.QColor(130, 130, 130), overlay_gray_thickness))
p.drawRect(handle_size, handle_size + extra_top_padding, w - handle_size * 2, h - handle_size * 2 - extra_top_padding)
p.setPen(QtCore.Qt.NoPen)
p.setBrush(QtGui.QBrush(QtGui.QColor(0, 191, 255)))
edge = overlay_green_thickness * 3
p.drawRect(0, extra_top_padding, edge, overlay_green_thickness)
p.drawRect(0, extra_top_padding, overlay_green_thickness, edge)
p.drawRect(w - edge, extra_top_padding, edge, overlay_green_thickness)
p.drawRect(w - overlay_green_thickness, extra_top_padding, overlay_green_thickness, edge)
p.drawRect(0, h - overlay_green_thickness, edge, overlay_green_thickness)
p.drawRect(0, h - edge, overlay_green_thickness, edge)
p.drawRect(w - edge, h - overlay_green_thickness, edge, overlay_green_thickness)
p.drawRect(w - overlay_green_thickness, h - edge, overlay_green_thickness, edge)
long = overlay_green_thickness * 6
p.drawRect((w - long) // 2, extra_top_padding, long, overlay_green_thickness)
p.drawRect((w - long) // 2, h - overlay_green_thickness, long, overlay_green_thickness)
p.drawRect(0, (h + extra_top_padding - long) // 2, overlay_green_thickness, long)
p.drawRect(w - overlay_green_thickness, (h + extra_top_padding - long) // 2, overlay_green_thickness, long)
bar_width = overlay_green_thickness * 6
bar_height = overlay_green_thickness
bar_x = (w - bar_width) // 2
bar_y = 6
p.setBrush(QtGui.QColor(0, 191, 255))
p.drawRect(bar_x, bar_y - bar_height - 10, bar_width, bar_height * 4)
def get_geometry(self):
g = self.geometry()
return (
g.x() + handle_size,
g.y() + handle_size + extra_top_padding,
g.width() - handle_size * 2,
g.height() - handle_size * 2 - extra_top_padding,
)
def mousePressEvent(self, e):
if e.button() == QtCore.Qt.LeftButton:
pos = e.pos()
bar_width = overlay_green_thickness * 6
bar_height = overlay_green_thickness
bar_x = (self.width() - bar_width) // 2
bar_y = 2
bar_rect = QtCore.QRect(bar_x, bar_y, bar_width, bar_height)
if bar_rect.contains(pos):
self.drag_offset = e.globalPos() - self.frameGeometry().topLeft()
return
m = handle_size
dirs = []
if pos.x() <= m:
dirs.append('left')
if pos.x() >= self.width() - m:
dirs.append('right')
if pos.y() <= m + extra_top_padding:
dirs.append('top')
if pos.y() >= self.height() - m:
dirs.append('bottom')
if dirs:
self.resize_dir = '_'.join(dirs)
self._start_geom = self.geometry()
self._start_pos = e.globalPos()
else:
self.drag_offset = e.globalPos() - self.frameGeometry().topLeft()
def mouseMoveEvent(self, e):
if self.resize_dir and self._start_geom and self._start_pos:
dx = e.globalX() - self._start_pos.x()
dy = e.globalY() - self._start_pos.y()
geom = QtCore.QRect(self._start_geom)
if 'left' in self.resize_dir:
new_x = geom.x() + dx
new_w = geom.width() - dx
geom.setX(new_x)
geom.setWidth(new_w)
if 'right' in self.resize_dir:
geom.setWidth(self._start_geom.width() + dx)
if 'top' in self.resize_dir:
new_y = geom.y() + dy
new_h = geom.height() - dy
geom.setY(new_y)
geom.setHeight(new_h)
if 'bottom' in self.resize_dir:
geom.setHeight(self._start_geom.height() + dy)
self.setGeometry(geom)
elif self.drag_offset and e.buttons() & QtCore.Qt.LeftButton:
self.move(e.globalPos() - self.drag_offset)
def mouseReleaseEvent(self, e):
self.drag_offset = None
self.resize_dir = None
self._start_geom = None
self._start_pos = None
x, y, w, h = self.get_geometry()
self.ctx.config.data['regions'][self.node_id] = {'x': x, 'y': y, 'w': w, 'h': h}
try:
self.ctx.config._write()
except Exception:
pass
asyncio.create_task(self.ctx.sio.emit('agent_screenshot_task', {
'agent_id': self.ctx.agent_id,
'node_id': self.node_id,
'image_base64': '',
'x': x, 'y': y, 'w': w, 'h': h
}))
class Role:
def __init__(self, ctx):
self.ctx = ctx
self.tasks = {}
def register_events(self):
sio = self.ctx.sio
@sio.on('list_agent_windows')
async def _handle_list_windows(payload):
try:
windows = macro_engines.list_windows()
except Exception:
windows = []
await sio.emit('agent_window_list', {
'agent_id': self.ctx.agent_id,
'windows': windows,
})
def _close_overlay(self, node_id: str):
w = overlay_widgets.pop(node_id, None)
if w:
try:
w.close()
except Exception:
pass
def stop_all(self):
for t in list(self.tasks.values()):
try:
t.cancel()
except Exception:
pass
self.tasks.clear()
# Close all widgets
for nid in list(overlay_widgets.keys()):
self._close_overlay(nid)
def on_config(self, roles_cfg):
# Filter only screenshot roles
screenshot_roles = [r for r in roles_cfg if (r.get('role') == 'screenshot')]
# Optional: forward interval to SYSTEM helper via hook
try:
if screenshot_roles and 'send_service_control' in self.ctx.hooks:
interval_ms = int(screenshot_roles[0].get('interval', 1000))
try:
self.ctx.hooks['send_service_control']({'type': 'screenshot_config', 'interval_ms': interval_ms})
except Exception:
pass
except Exception:
pass
# Cancel tasks that are no longer present
new_ids = {r.get('node_id') for r in screenshot_roles if r.get('node_id')}
old_ids = set(self.tasks.keys())
removed = old_ids - new_ids
for rid in removed:
t = self.tasks.pop(rid, None)
if t:
try:
t.cancel()
except Exception:
pass
# Remove stored region and overlay
self.ctx.config.data.get('regions', {}).pop(rid, None)
try:
self._close_overlay(rid)
except Exception:
pass
if removed:
try:
self.ctx.config._write()
except Exception:
pass
# Start tasks for all screenshot roles in config
for rcfg in screenshot_roles:
nid = rcfg.get('node_id')
if not nid:
continue
if nid in self.tasks:
continue
task = asyncio.create_task(self._screenshot_task(rcfg))
self.tasks[nid] = task
async def _screenshot_task(self, cfg):
nid = cfg.get('node_id')
alias = cfg.get('alias', '')
reg = self.ctx.config.data.setdefault('regions', {})
r = reg.get(nid)
if r:
region = (r['x'], r['y'], r['w'], r['h'])
else:
region = (
cfg.get('x', 100),
cfg.get('y', 100),
cfg.get('w', 300),
cfg.get('h', 200),
)
reg[nid] = {'x': region[0], 'y': region[1], 'w': region[2], 'h': region[3]}
try:
self.ctx.config._write()
except Exception:
pass
if nid not in overlay_widgets:
widget = ScreenshotRegion(self.ctx, nid, *region, alias=alias)
overlay_widgets[nid] = widget
widget.show()
await self.ctx.sio.emit('agent_screenshot_task', {
'agent_id': self.ctx.agent_id,
'node_id': nid,
'image_base64': '',
'x': region[0], 'y': region[1], 'w': region[2], 'h': region[3]
})
interval = cfg.get('interval', 1000) / 1000.0
loop = asyncio.get_event_loop()
executor = concurrent.futures.ThreadPoolExecutor(max_workers=self.ctx.config.data.get('max_task_workers', 8))
try:
while True:
x, y, w, h = overlay_widgets[nid].get_geometry()
grab = partial(ImageGrab.grab, bbox=(x, y, x + w, y + h))
img = await loop.run_in_executor(executor, grab)
buf = BytesIO(); img.save(buf, format='PNG')
encoded = base64.b64encode(buf.getvalue()).decode('utf-8')
await self.ctx.sio.emit('agent_screenshot_task', {
'agent_id': self.ctx.agent_id,
'node_id': nid,
'image_base64': encoded,
'x': x, 'y': y, 'w': w, 'h': h
})
await asyncio.sleep(interval)
except asyncio.CancelledError:
pass
except Exception:
traceback.print_exc()

View File

@@ -0,0 +1,217 @@
import os
import sys
import asyncio
import tempfile
import uuid
from PyQt5 import QtWidgets, QtGui
ROLE_NAME = 'script_exec_currentuser'
ROLE_CONTEXTS = ['interactive']
IS_WINDOWS = os.name == 'nt'
def _write_temp_script(content: str, suffix: str):
temp_dir = os.path.join(tempfile.gettempdir(), "Borealis", "quick_jobs")
os.makedirs(temp_dir, exist_ok=True)
fd, path = tempfile.mkstemp(prefix="bj_", suffix=suffix, dir=temp_dir, text=True)
with os.fdopen(fd, 'w', encoding='utf-8', newline='\n') as fh:
fh.write(content or "")
return path
async def _run_powershell_local(path: str):
if IS_WINDOWS:
ps = os.path.expandvars(r"%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe")
if not os.path.isfile(ps):
ps = "powershell.exe"
else:
ps = "pwsh"
try:
proc = await asyncio.create_subprocess_exec(
ps,
"-ExecutionPolicy", "Bypass" if IS_WINDOWS else "Bypass",
"-NoProfile",
"-File", path,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
creationflags=(0x08000000 if IS_WINDOWS else 0)
)
out_b, err_b = await proc.communicate()
return proc.returncode, (out_b or b"").decode(errors='replace'), (err_b or b"").decode(errors='replace')
except Exception as e:
return -1, "", str(e)
async def _run_powershell_via_user_task(content: str):
if not IS_WINDOWS:
return -999, '', 'Windows only'
ps = os.path.expandvars(r"%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe")
if not os.path.isfile(ps):
ps = 'powershell.exe'
path = None
out_path = None
import tempfile as _tf
try:
temp_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'Temp'))
os.makedirs(temp_dir, exist_ok=True)
fd, path = _tf.mkstemp(prefix='usr_task_', suffix='.ps1', dir=temp_dir, text=True)
with os.fdopen(fd, 'w', encoding='utf-8', newline='\n') as f:
f.write(content or '')
out_path = os.path.join(temp_dir, f'out_{uuid.uuid4().hex}.txt')
name = f"Borealis Agent - Task - {uuid.uuid4().hex} @ CurrentUser"
task_ps = f"""
$ErrorActionPreference='Continue'
$task = "{name}"
$ps = "{ps}"
$scr = "{path}"
$out = "{out_path}"
try {{ Unregister-ScheduledTask -TaskName $task -Confirm:$false -ErrorAction SilentlyContinue }} catch {{}}
$action = New-ScheduledTaskAction -Execute $ps -Argument ('-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File "' + $scr + '" *> "' + $out + '"')
$settings = New-ScheduledTaskSettingsSet -DeleteExpiredTaskAfter (New-TimeSpan -Minutes 5) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
$principal= New-ScheduledTaskPrincipal -UserId ([System.Security.Principal.WindowsIdentity]::GetCurrent().Name) -LogonType Interactive -RunLevel Limited
Register-ScheduledTask -TaskName $task -Action $action -Settings $settings -Principal $principal -Force | Out-Null
Start-ScheduledTask -TaskName $task | Out-Null
Start-Sleep -Seconds 2
Get-ScheduledTask -TaskName $task | Out-Null
"""
proc = await asyncio.create_subprocess_exec(ps, '-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', task_ps,
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
out_b, err_b = await proc.communicate()
if proc.returncode != 0:
return -999, '', (err_b or out_b or b'').decode(errors='replace')
# Wait a short time for output file; best-effort
import time as _t
deadline = _t.time() + 30
out_data = ''
while _t.time() < deadline:
try:
if os.path.isfile(out_path) and os.path.getsize(out_path) > 0:
with open(out_path, 'r', encoding='utf-8', errors='replace') as f:
out_data = f.read()
break
except Exception:
pass
await asyncio.sleep(1)
# Cleanup best-effort
try:
await asyncio.create_subprocess_exec('powershell.exe', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', f"try {{ Unregister-ScheduledTask -TaskName '{name}' -Confirm:$false }} catch {{}}")
except Exception:
pass
try:
if path and os.path.isfile(path):
os.remove(path)
except Exception:
pass
try:
if out_path and os.path.isfile(out_path):
os.remove(out_path)
except Exception:
pass
return 0, out_data or '', ''
except Exception as e:
return -999, '', str(e)
class Role:
def __init__(self, ctx):
self.ctx = ctx
# Setup tray icon in interactive session
try:
self._setup_tray()
except Exception:
pass
def register_events(self):
sio = self.ctx.sio
@sio.on('quick_job_run')
async def _on_quick_job_run(payload):
try:
import socket
hostname = socket.gethostname()
target = (payload.get('target_hostname') or '').strip().lower()
if not target or target != hostname.lower():
return
job_id = payload.get('job_id')
script_type = (payload.get('script_type') or '').lower()
run_mode = (payload.get('run_mode') or 'current_user').lower()
content = payload.get('script_content') or ''
if run_mode == 'system':
return
if script_type != 'powershell':
await sio.emit('quick_job_result', { 'job_id': job_id, 'status': 'Failed', 'stdout': '', 'stderr': f"Unsupported type: {script_type}" })
return
if run_mode == 'admin':
rc, out, err = -1, '', 'Admin credentialed runs are disabled; use SYSTEM or Current User.'
else:
rc, out, err = await _run_powershell_via_user_task(content)
if rc == -999:
path = _write_temp_script(content, '.ps1')
rc, out, err = await _run_powershell_local(path)
status = 'Success' if rc == 0 else 'Failed'
await sio.emit('quick_job_result', {
'job_id': job_id,
'status': status,
'stdout': out,
'stderr': err,
})
except Exception as e:
try:
await sio.emit('quick_job_result', {
'job_id': payload.get('job_id') if isinstance(payload, dict) else None,
'status': 'Failed',
'stdout': '',
'stderr': str(e),
})
except Exception:
pass
def _setup_tray(self):
app = QtWidgets.QApplication.instance()
if app is None:
return
icon = None
try:
icon_path = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, 'Borealis.ico'))
if os.path.isfile(icon_path):
icon = QtGui.QIcon(icon_path)
except Exception:
pass
if icon is None:
icon = app.style().standardIcon(QtWidgets.QStyle.SP_ComputerIcon)
self.tray = QtWidgets.QSystemTrayIcon(icon)
self.tray.setToolTip('Borealis Agent')
menu = QtWidgets.QMenu()
act_restart = menu.addAction('Restart Agent')
act_quit = menu.addAction('Quit Agent')
act_restart.triggered.connect(self._restart_agent)
act_quit.triggered.connect(self._quit_agent)
self.tray.setContextMenu(menu)
self.tray.show()
def _restart_agent(self):
try:
# __file__ => Agent/Borealis/Roles/...
borealis_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))
venv_root = os.path.abspath(os.path.join(borealis_dir, os.pardir))
venv_scripts = os.path.join(venv_root, 'Scripts')
pyw = os.path.join(venv_scripts, 'pythonw.exe')
exe = pyw if os.path.isfile(pyw) else sys.executable
agent_script = os.path.join(borealis_dir, 'agent.py')
import subprocess
subprocess.Popen([exe, '-W', 'ignore::SyntaxWarning', agent_script], cwd=borealis_dir)
except Exception:
pass
try:
QtWidgets.QApplication.instance().quit()
except Exception:
os._exit(0)
def _quit_agent(self):
try:
QtWidgets.QApplication.instance().quit()
except Exception:
os._exit(0)

View File

@@ -0,0 +1,153 @@
import os
import asyncio
import tempfile
import uuid
import time
import subprocess
ROLE_NAME = 'script_exec_system'
ROLE_CONTEXTS = ['system']
def _project_root():
return os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
def _run_powershell_script_content(content: str):
temp_dir = os.path.join(_project_root(), "Temp")
os.makedirs(temp_dir, exist_ok=True)
fd, path = tempfile.mkstemp(prefix="sj_", suffix=".ps1", dir=temp_dir, text=True)
with os.fdopen(fd, 'w', encoding='utf-8', newline='\n') as fh:
fh.write(content or "")
ps = os.path.expandvars(r"%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe")
if not os.path.isfile(ps):
ps = "powershell.exe"
try:
flags = 0x08000000 if os.name == 'nt' else 0
proc = subprocess.run(
[ps, "-ExecutionPolicy", "Bypass", "-NoProfile", "-File", path],
capture_output=True,
text=True,
timeout=60*60,
creationflags=flags,
)
return proc.returncode, proc.stdout or "", proc.stderr or ""
except Exception as e:
return -1, "", str(e)
finally:
try:
if os.path.isfile(path):
os.remove(path)
except Exception:
pass
def _run_powershell_via_system_task(content: str):
ps_exe = os.path.expandvars(r"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe")
if not os.path.isfile(ps_exe):
ps_exe = 'powershell.exe'
try:
os.makedirs(os.path.join(_project_root(), 'Temp'), exist_ok=True)
script_fd, script_path = tempfile.mkstemp(prefix='sys_task_', suffix='.ps1', dir=os.path.join(_project_root(), 'Temp'), text=True)
with os.fdopen(script_fd, 'w', encoding='utf-8', newline='\n') as f:
f.write(content or '')
out_path = os.path.join(_project_root(), 'Temp', f'out_{uuid.uuid4().hex}.txt')
task_name = f"Borealis Agent - Task - {uuid.uuid4().hex} @ SYSTEM"
task_ps = f"""
$ErrorActionPreference='Continue'
$task = "{task_name}"
$ps = "{ps_exe}"
$scr = "{script_path}"
$out = "{out_path}"
try {{ Unregister-ScheduledTask -TaskName $task -Confirm:$false -ErrorAction SilentlyContinue }} catch {{}}
$action = New-ScheduledTaskAction -Execute $ps -Argument ('-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File "' + $scr + '" *> "' + $out + '"')
$settings = New-ScheduledTaskSettingsSet -DeleteExpiredTaskAfter (New-TimeSpan -Minutes 5) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
$principal= New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest
Register-ScheduledTask -TaskName $task -Action $action -Settings $settings -Principal $principal -Force | Out-Null
Start-ScheduledTask -TaskName $task | Out-Null
Start-Sleep -Seconds 2
Get-ScheduledTask -TaskName $task | Out-Null
"""
proc = subprocess.run([ps_exe, '-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', task_ps], capture_output=True, text=True)
if proc.returncode != 0:
return -999, '', (proc.stderr or proc.stdout or 'scheduled task creation failed')
deadline = time.time() + 60
out_data = ''
while time.time() < deadline:
try:
if os.path.isfile(out_path) and os.path.getsize(out_path) > 0:
with open(out_path, 'r', encoding='utf-8', errors='replace') as f:
out_data = f.read()
break
except Exception:
pass
time.sleep(1)
cleanup_ps = f"try {{ Unregister-ScheduledTask -TaskName '{task_name}' -Confirm:$false }} catch {{}}"
subprocess.run([ps_exe, '-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', cleanup_ps], capture_output=True, text=True)
try:
if os.path.isfile(script_path):
os.remove(script_path)
except Exception:
pass
try:
if os.path.isfile(out_path):
os.remove(out_path)
except Exception:
pass
return 0, out_data or '', ''
except Exception as e:
return -999, '', str(e)
class Role:
def __init__(self, ctx):
self.ctx = ctx
def register_events(self):
sio = self.ctx.sio
@sio.on('quick_job_run')
async def _on_quick_job_run(payload):
try:
import socket
hostname = socket.gethostname()
target = (payload.get('target_hostname') or '').strip().lower()
if target and target != hostname.lower():
return
run_mode = (payload.get('run_mode') or 'current_user').lower()
if run_mode != 'system':
return
job_id = payload.get('job_id')
script_type = (payload.get('script_type') or '').lower()
content = payload.get('script_content') or ''
if script_type != 'powershell':
await sio.emit('quick_job_result', {
'job_id': job_id,
'status': 'Failed',
'stdout': '',
'stderr': f"Unsupported type: {script_type}"
})
return
rc, out, err = _run_powershell_via_system_task(content)
if rc == -999:
rc, out, err = _run_powershell_script_content(content)
status = 'Success' if rc == 0 else 'Failed'
await sio.emit('quick_job_result', {
'job_id': job_id,
'status': status,
'stdout': out,
'stderr': err,
})
except Exception as e:
try:
await sio.emit('quick_job_result', {
'job_id': payload.get('job_id') if isinstance(payload, dict) else None,
'status': 'Failed',
'stdout': '',
'stderr': str(e),
})
except Exception:
pass

View File

@@ -11,8 +11,8 @@ PyQt5
qasync
# Computer Vision & OCR Dependencies
opencv-python # Computer vision processing
Pillow # Image processing (Windows)
opencv-python # Computer vision processing
Pillow # Image processing (Windows)
###mss # Fast cross-platform screen capture
# WebRTC Video Libraries

View File

@@ -42,8 +42,7 @@ except Exception:
from PIL import ImageGrab
# New modularized components
import agent_info
import agent_roles
from role_manager import RoleManager
# //////////////////////////////////////////////////////////////////////////
# CORE SECTION: CONFIG MANAGER
@@ -351,7 +350,7 @@ def detect_agent_os():
print(f"[WARN] OS detection failed: {e}")
return "Unknown"
CONFIG.data['agent_operating_system'] = agent_info.detect_agent_os()
CONFIG.data['agent_operating_system'] = detect_agent_os()
CONFIG._write()
# //////////////////////////////////////////////////////////////////////////
@@ -361,8 +360,8 @@ CONFIG._write()
sio = socketio.AsyncClient(reconnection=True, reconnection_attempts=0, reconnection_delay=5)
role_tasks = {}
background_tasks = []
roles_ctx = None
AGENT_LOOP = None
ROLE_MANAGER = None
# ---------------- Local IPC Bridge (Service -> Agent) ----------------
def start_agent_bridge_pipe(loop_ref):
@@ -563,13 +562,9 @@ async def _run_powershell_with_credentials(path: str, username: str, password: s
async def stop_all_roles():
print("[DEBUG] Stopping all roles.")
for task in list(role_tasks.values()):
print(f"[DEBUG] Cancelling task for node: {task}")
task.cancel()
role_tasks.clear()
# Close overlays managed in agent_roles module
try:
agent_roles.close_all_overlays()
if ROLE_MANAGER is not None:
ROLE_MANAGER.stop_all()
except Exception:
pass
@@ -587,7 +582,7 @@ async def send_heartbeat():
payload = {
"agent_id": AGENT_ID,
"hostname": socket.gethostname(),
"agent_operating_system": CONFIG.data.get("agent_operating_system", agent_info.detect_agent_os()),
"agent_operating_system": CONFIG.data.get("agent_operating_system", detect_agent_os()),
"last_seen": int(time.time())
}
await sio.emit("agent_heartbeat", payload)
@@ -608,16 +603,15 @@ async def send_heartbeat():
## Moved to agent_info module
def collect_summary():
# Moved to agent_info.collect_summary
return agent_info.collect_summary(CONFIG)
# migrated to role_DeviceInventory
return {}
def collect_software():
# Moved to agent_info.collect_software
return agent_info.collect_software()
# migrated to role_DeviceInventory
return []
def collect_memory():
# Delegated to agent_info module
return agent_info.collect_memory()
# migrated to role_DeviceInventory
entries = []
plat = platform.system().lower()
try:
@@ -706,8 +700,7 @@ def collect_memory():
return entries
def collect_storage():
# Delegated to agent_info module
return agent_info.collect_storage()
# migrated to role_DeviceInventory
disks = []
plat = platform.system().lower()
try:
@@ -824,8 +817,7 @@ def collect_storage():
return disks
def collect_network():
# Delegated to agent_info module
return agent_info.collect_network()
# migrated to role_DeviceInventory
adapters = []
plat = platform.system().lower()
try:
@@ -906,7 +898,7 @@ async def connect():
await sio.emit("agent_heartbeat", {
"agent_id": AGENT_ID,
"hostname": socket.gethostname(),
"agent_operating_system": CONFIG.data.get("agent_operating_system", agent_info.detect_agent_os()),
"agent_operating_system": CONFIG.data.get("agent_operating_system", detect_agent_os()),
"last_seen": int(time.time())
})
except Exception as e:
@@ -947,98 +939,23 @@ async def on_agent_config(cfg):
print(f"[CONFIG] Received New Agent Config with {len(roles)} Role(s).")
new_ids = {r.get('node_id') for r in roles if r.get('node_id')}
old_ids = set(role_tasks.keys())
removed = old_ids - new_ids
for rid in removed:
print(f"[DEBUG] Removing node {rid} from regions/overlays.")
CONFIG.data['regions'].pop(rid, None)
try:
agent_roles.close_overlay(rid)
except Exception:
pass
if removed:
CONFIG._write()
for task in list(role_tasks.values()):
task.cancel()
role_tasks.clear()
# Forward screenshot config to service helper (interval only)
try:
for role_cfg in roles:
if role_cfg.get('role') == 'screenshot':
interval_ms = int(role_cfg.get('interval', 1000))
send_service_control({ 'type': 'screenshot_config', 'interval_ms': interval_ms })
send_service_control({'type': 'screenshot_config', 'interval_ms': interval_ms})
break
except Exception:
pass
for role_cfg in roles:
nid = role_cfg.get('node_id')
role = role_cfg.get('role')
if role == 'screenshot':
print(f"[DEBUG] Starting screenshot task for {nid}")
task = asyncio.create_task(agent_roles.screenshot_task(roles_ctx, role_cfg))
role_tasks[nid] = task
elif role == 'macro':
print(f"[DEBUG] Starting macro task for {nid}")
task = asyncio.create_task(agent_roles.macro_task(roles_ctx, role_cfg))
role_tasks[nid] = task
@sio.on('quick_job_run')
async def on_quick_job_run(payload):
try:
target = (payload.get('target_hostname') or '').strip().lower()
if not target or target != socket.gethostname().lower():
return
job_id = payload.get('job_id')
script_type = (payload.get('script_type') or '').lower()
run_mode = (payload.get('run_mode') or 'current_user').lower()
content = payload.get('script_content') or ''
# Only handle non-SYSTEM runs here; SYSTEM runs are handled by the LocalSystem service agent
if run_mode == 'system':
# Ignore: handled by SYSTEM supervisor/agent
return
if script_type != 'powershell':
await sio.emit('quick_job_result', { 'job_id': job_id, 'status': 'Failed', 'stdout': '', 'stderr': f"Unsupported type: {script_type}" })
return
rc = 0; out = ''; err = ''
if run_mode == 'admin':
# Admin credentialed runs are disabled in current design
rc, out, err = -1, '', 'Admin credentialed runs are disabled; use SYSTEM or Current User.'
else:
# Prefer ephemeral scheduled task in current user context
rc, out, err = await _run_powershell_via_user_task(content)
if rc == -999:
# Fallback to direct execution
path = _write_temp_script(content, '.ps1')
rc, out, err = await _run_powershell_local(path)
status = 'Success' if rc == 0 else 'Failed'
await sio.emit('quick_job_result', {
'job_id': job_id,
'status': status,
'stdout': out,
'stderr': err,
})
if ROLE_MANAGER is not None:
ROLE_MANAGER.on_config(roles)
except Exception as e:
try:
await sio.emit('quick_job_result', {
'job_id': payload.get('job_id') if isinstance(payload, dict) else None,
'status': 'Failed',
'stdout': '',
'stderr': str(e),
})
except Exception:
pass
print(f"[WARN] role manager apply config failed: {e}")
@sio.on('list_agent_windows')
async def handle_list_agent_windows(data):
windows = agent_roles.get_window_list()
await sio.emit('agent_window_list', {
'agent_id': AGENT_ID,
'windows': windows
})
## Script execution and list windows handlers are registered by roles
# ---------------- Config Watcher ----------------
async def config_watcher():
@@ -1159,17 +1076,31 @@ if __name__=='__main__':
pass
dummy_window=PersistentWindow(); dummy_window.show()
# Initialize roles context for role tasks
roles_ctx = agent_roles.RolesContext(sio=sio, agent_id=AGENT_ID, config=CONFIG)
# Initialize role manager and hot-load roles from Roles/
try:
hooks = {'send_service_control': send_service_control}
ROLE_MANAGER = RoleManager(
base_dir=os.path.dirname(__file__),
context='interactive',
sio=sio,
agent_id=AGENT_ID,
config=CONFIG,
loop=loop,
hooks=hooks,
)
ROLE_MANAGER.load()
except Exception:
pass
try:
background_tasks.append(loop.create_task(config_watcher()))
background_tasks.append(loop.create_task(connect_loop()))
background_tasks.append(loop.create_task(idle_task()))
# Start periodic heartbeats
background_tasks.append(loop.create_task(send_heartbeat()))
background_tasks.append(loop.create_task(agent_info.send_agent_details(AGENT_ID, CONFIG)))
loop.run_forever()
except Exception as e:
print(f"[FATAL] Event loop crashed: {e}")
traceback.print_exc()
finally:
print("[FATAL] Agent exited unexpectedly.")

View File

@@ -24,7 +24,7 @@ def project_paths():
"borealis_dir": borealis_dir,
"logs_dir": logs_dir,
"temp_dir": temp_dir,
"agent_script": os.path.join(borealis_dir, "tray_launcher.py"),
"agent_script": os.path.join(borealis_dir, "agent.py"),
}

View File

@@ -1,845 +0,0 @@
import os
import sys
import json
import time
import socket
import platform
import subprocess
import getpass
import datetime
import shutil
import string
import requests
try:
import psutil # type: ignore
except Exception:
psutil = None # graceful degradation if unavailable
import aiohttp
import asyncio
# ---------------- Helpers for hidden subprocess on Windows ----------------
IS_WINDOWS = os.name == 'nt'
CREATE_NO_WINDOW = 0x08000000 if IS_WINDOWS else 0
def _run_hidden(cmd_list, timeout=None):
"""Run a subprocess hidden on Windows (no visible console window)."""
kwargs = {"capture_output": True, "text": True}
if timeout is not None:
kwargs["timeout"] = timeout
if IS_WINDOWS:
kwargs["creationflags"] = CREATE_NO_WINDOW
return subprocess.run(cmd_list, **kwargs)
def _run_powershell_hidden(ps_cmd: str, timeout: int = 60):
"""Run a powershell -NoProfile -Command string fully hidden on Windows."""
ps = os.path.expandvars(r"%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe")
if not os.path.isfile(ps):
ps = "powershell.exe"
return _run_hidden([ps, "-NoProfile", "-Command", ps_cmd], timeout=timeout)
def detect_agent_os():
"""
Detects the full, user-friendly operating system name and version.
Examples:
- "Windows 11"
- "Windows 10"
- "Fedora Workstation 42"
- "Ubuntu 22.04 LTS"
- "macOS Sonoma"
Falls back to a generic name if detection fails.
"""
try:
plat = platform.system().lower()
if plat.startswith('win'):
try:
import winreg # Only available on Windows
reg_path = r"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion"
access = winreg.KEY_READ
try:
access |= winreg.KEY_WOW64_64KEY
except Exception:
pass
try:
key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, reg_path, 0, access)
except OSError:
key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, reg_path, 0, winreg.KEY_READ)
def _get(name, default=None):
try:
return winreg.QueryValueEx(key, name)[0]
except Exception:
return default
product_name = _get("ProductName", "") # e.g., "Windows 11 Pro"
edition_id = _get("EditionID", "") # e.g., "Professional"
display_version = _get("DisplayVersion", "") # e.g., "24H2" / "22H2"
release_id = _get("ReleaseId", "") # e.g., "2004" on older Windows 10
build_number = _get("CurrentBuildNumber", "") or _get("CurrentBuild", "")
ubr = _get("UBR", None) # Update Build Revision (int)
try:
build_int = int(str(build_number).split(".")[0]) if build_number else 0
except Exception:
build_int = 0
if build_int >= 22000:
major_label = "11"
elif build_int >= 10240:
major_label = "10"
else:
major_label = platform.release()
edition = ""
pn = product_name or ""
if pn.lower().startswith("windows "):
tokens = pn.split()
if len(tokens) >= 3:
edition = " ".join(tokens[2:])
if not edition and edition_id:
eid_map = {
"Professional": "Pro",
"ProfessionalN": "Pro N",
"ProfessionalEducation": "Pro Education",
"ProfessionalWorkstation": "Pro for Workstations",
"Enterprise": "Enterprise",
"EnterpriseN": "Enterprise N",
"EnterpriseS": "Enterprise LTSC",
"Education": "Education",
"EducationN": "Education N",
"Core": "Home",
"CoreN": "Home N",
"CoreSingleLanguage": "Home Single Language",
"IoTEnterprise": "IoT Enterprise",
}
edition = eid_map.get(edition_id, edition_id)
os_name = f"Windows {major_label}"
version_label = display_version or release_id or ""
if isinstance(ubr, int):
build_str = f"{build_number}.{ubr}" if build_number else str(ubr)
else:
try:
build_str = f"{build_number}.{int(ubr)}" if build_number and ubr is not None else build_number
except Exception:
build_str = build_number
parts = ["Microsoft", os_name]
if edition:
parts.append(edition)
if version_label:
parts.append(version_label)
if build_str:
parts.append(f"Build {build_str}")
return " ".join(p for p in parts if p).strip()
except Exception:
return f"Windows {platform.release()}"
elif plat.startswith('linux'):
try:
import distro # External package, better for Linux OS detection
name = distro.name(pretty=True) # e.g., "Fedora Workstation 42"
if name:
return name
else:
return f"{platform.system()} {platform.release()}"
except ImportError:
return f"{platform.system()} {platform.release()}"
elif plat.startswith('darwin'):
version = platform.mac_ver()[0]
macos_names = {
"14": "Sonoma",
"13": "Ventura",
"12": "Monterey",
"11": "Big Sur",
"10.15": "Catalina"
}
pretty_name = macos_names.get(".".join(version.split(".")[:2]), "")
return f"macOS {pretty_name or version}"
else:
return f"Unknown OS ({platform.system()} {platform.release()})"
except Exception as e:
print(f"[WARN] OS detection failed: {e}")
return "Unknown"
def _detect_virtual_machine() -> bool:
"""Best-effort detection of whether this system is a virtual machine.
Uses platform-specific signals but avoids heavy dependencies.
"""
try:
plat = platform.system().lower()
if plat == "linux":
# Prefer systemd-detect-virt if available
try:
out = subprocess.run([
"systemd-detect-virt", "--vm"
], capture_output=True, text=True, timeout=3)
if out.returncode == 0 and (out.stdout or "").strip():
return True
except Exception:
pass
# Fallback to DMI sysfs strings
for p in (
"/sys/class/dmi/id/product_name",
"/sys/class/dmi/id/sys_vendor",
"/sys/class/dmi/id/board_vendor",
):
try:
with open(p, "r", encoding="utf-8", errors="ignore") as fh:
s = (fh.read() or "").lower()
if any(k in s for k in (
"kvm", "vmware", "virtualbox", "qemu", "xen", "hyper-v", "microsoft corporation")):
return True
except Exception:
pass
elif plat == "windows":
# Inspect model/manufacturer via CIM
try:
ps_cmd = (
"$cs = Get-CimInstance Win32_ComputerSystem; "
"$model = [string]$cs.Model; $manu = [string]$cs.Manufacturer; "
"Write-Output ($model + '|' + $manu)"
)
out = _run_powershell_hidden(ps_cmd, timeout=6)
s = (out.stdout or "").strip().lower()
if any(k in s for k in ("virtual", "vmware", "virtualbox", "kvm", "qemu", "xen", "hyper-v")):
return True
except Exception:
pass
# Fallback: registry BIOS strings often include virtualization hints
try:
import winreg # type: ignore
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"HARDWARE\DESCRIPTION\System") as k:
for name in ("SystemBiosVersion", "VideoBiosVersion"):
try:
val, _ = winreg.QueryValueEx(k, name)
s = str(val).lower()
if any(x in s for x in ("virtual", "vmware", "virtualbox", "qemu", "xen", "hyper-v")):
return True
except Exception:
continue
except Exception:
pass
elif plat == "darwin":
# macOS guest detection is tricky; limited heuristic
try:
out = subprocess.run([
"sysctl", "-n", "machdep.cpu.features"
], capture_output=True, text=True, timeout=3)
s = (out.stdout or "").lower()
if "vmm" in s:
return True
except Exception:
pass
except Exception:
pass
return False
def _detect_device_type_non_vm() -> str:
"""Classify non-VM device as Laptop, Desktop, or Server.
This is intentionally conservative; if unsure, returns 'Desktop'.
"""
try:
plat = platform.system().lower()
if plat == "windows":
# Prefer PCSystemTypeEx, then chassis types, then battery presence
try:
ps_cmd = (
"$cs = Get-CimInstance Win32_ComputerSystem; "
"$typeEx = [int]($cs.PCSystemTypeEx); "
"$type = [int]($cs.PCSystemType); "
"$ch = (Get-CimInstance Win32_SystemEnclosure).ChassisTypes; "
"$hasBatt = @(Get-CimInstance Win32_Battery).Count -gt 0; "
"Write-Output ($typeEx.ToString() + '|' + $type.ToString() + '|' + "
"([string]::Join(',', $ch)) + '|' + $hasBatt)"
)
out = _run_powershell_hidden(ps_cmd, timeout=6)
resp = (out.stdout or "").strip()
parts = resp.split("|")
type_ex = int(parts[0]) if len(parts) > 0 and parts[0].isdigit() else None
type_pc = int(parts[1]) if len(parts) > 1 and parts[1].isdigit() else None
chassis = []
if len(parts) > 2 and parts[2].strip():
for t in parts[2].split(','):
t = t.strip()
if t.isdigit():
chassis.append(int(t))
has_batt = False
if len(parts) > 3:
has_batt = parts[3].strip().lower() in ("true", "1")
# PCSystemTypeEx mapping per MS docs
if type_ex in (4, 5, 7, 8):
return "Server"
if type_ex == 2:
return "Laptop"
if type_ex in (1, 3):
return "Desktop"
# Fallback to PCSystemType
if type_pc in (4, 5):
return "Server"
if type_pc == 2:
return "Laptop"
if type_pc in (1, 3):
return "Desktop"
# ChassisType mapping (DMTF)
laptop_types = {8, 9, 10, 14, 30, 31}
server_types = {17, 23}
desktop_types = {3, 4, 5, 6, 7, 15, 16, 24, 35}
if any(ct in laptop_types for ct in chassis):
return "Laptop"
if any(ct in server_types for ct in chassis):
return "Server"
if any(ct in desktop_types for ct in chassis):
return "Desktop"
if has_batt:
return "Laptop"
except Exception:
pass
return "Desktop"
if plat == "linux":
# hostnamectl exposes chassis when available
try:
out = subprocess.run(["hostnamectl"], capture_output=True, text=True, timeout=3)
for line in (out.stdout or "").splitlines():
if ":" in line:
k, v = line.split(":", 1)
if k.strip().lower() == "chassis":
val = v.strip().lower()
if "laptop" in val:
return "Laptop"
if "desktop" in val:
return "Desktop"
if "server" in val:
return "Server"
break
except Exception:
pass
# DMI chassis type numeric
try:
with open("/sys/class/dmi/id/chassis_type", "r", encoding="utf-8", errors="ignore") as fh:
s = (fh.read() or "").strip()
ct = int(s)
laptop_types = {8, 9, 10, 14, 30, 31}
server_types = {17, 23}
desktop_types = {3, 4, 5, 6, 7, 15, 16, 24, 35}
if ct in laptop_types:
return "Laptop"
if ct in server_types:
return "Server"
if ct in desktop_types:
return "Desktop"
except Exception:
pass
# Battery presence heuristic
try:
if os.path.isdir("/sys/class/power_supply"):
for name in os.listdir("/sys/class/power_supply"):
if name.lower().startswith("bat"):
return "Laptop"
except Exception:
pass
return "Desktop"
if plat == "darwin":
try:
out = subprocess.run(["sysctl", "-n", "hw.model"], capture_output=True, text=True, timeout=3)
model = (out.stdout or "").strip()
if model:
if model.lower().startswith("macbook"):
return "Laptop"
# iMac, Macmini, MacPro -> treat as Desktop
return "Desktop"
except Exception:
pass
return "Desktop"
except Exception:
pass
return "Desktop"
def detect_device_type() -> str:
"""Return one of: 'Laptop', 'Desktop', 'Server', 'Virtual Machine'."""
try:
if _detect_virtual_machine():
return "Virtual Machine"
return _detect_device_type_non_vm()
except Exception:
return "Desktop"
def _get_internal_ip():
"""Best-effort detection of primary IPv4 address without external reachability.
Order of attempts:
1) psutil.net_if_addrs() first non-loopback, non-APIPA IPv4
2) UDP connect trick to 8.8.8.8 (common technique)
3) Windows: PowerShell Get-NetIPAddress
4) Linux/macOS: `ip -o -4 addr show` or `hostname -I`
"""
# 1) psutil interfaces
try:
if psutil:
for name, addrs in (psutil.net_if_addrs() or {}).items():
for a in addrs:
if getattr(a, "family", None) == socket.AF_INET:
ip = a.address
if (
ip
and not ip.startswith("127.")
and not ip.startswith("169.254.")
and ip != "0.0.0.0"
):
return ip
except Exception:
pass
# 2) UDP connect trick
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
ip = s.getsockname()[0]
s.close()
if ip:
return ip
except Exception:
pass
plat = platform.system().lower()
# 3) Windows PowerShell
if plat == "windows":
try:
ps_cmd = (
"Get-NetIPAddress -AddressFamily IPv4 | "
"Where-Object { $_.IPAddress -and $_.IPAddress -notmatch '^169\\.254\\.' -and $_.IPAddress -notmatch '^127\\.' } | "
"Sort-Object -Property PrefixLength | Select-Object -First 1 -ExpandProperty IPAddress"
)
out = _run_powershell_hidden(ps_cmd, timeout=20)
val = (out.stdout or "").strip()
if val:
return val
except Exception:
pass
# 4) Linux/macOS
try:
out = subprocess.run(["ip", "-o", "-4", "addr", "show"], capture_output=True, text=True, timeout=10)
for line in out.stdout.splitlines():
parts = line.split()
if len(parts) >= 4:
ip = parts[3].split("/")[0]
if ip and not ip.startswith("127.") and not ip.startswith("169.254."):
return ip
except Exception:
pass
try:
out = subprocess.run(["hostname", "-I"], capture_output=True, text=True, timeout=5)
val = (out.stdout or "").strip().split()
for ip in val:
if ip and not ip.startswith("127.") and not ip.startswith("169.254."):
return ip
except Exception:
pass
return "unknown"
def collect_summary(config):
try:
username = getpass.getuser()
domain = os.environ.get("USERDOMAIN") or socket.gethostname()
last_user = f"{domain}\\{username}" if username else "unknown"
except Exception:
last_user = "unknown"
try:
last_reboot = "unknown"
# First, prefer psutil if available
if psutil:
try:
last_reboot = time.strftime(
"%Y-%m-%d %H:%M:%S",
time.localtime(psutil.boot_time()),
)
except Exception:
last_reboot = "unknown"
if last_reboot == "unknown":
plat = platform.system().lower()
if plat == "windows":
# Try WMIC, then robust PowerShell fallback regardless of WMIC presence
raw = ""
try:
out = _run_hidden(["wmic", "os", "get", "lastbootuptime"], timeout=20)
raw = "".join(out.stdout.splitlines()[1:]).strip()
except Exception:
raw = ""
# If WMIC didn't yield a value, try CIM and format directly in PowerShell
if not raw:
try:
ps_cmd = (
"(Get-CimInstance Win32_OperatingSystem).LastBootUpTime | "
"ForEach-Object { (Get-Date -Date $_ -Format 'yyyy-MM-dd HH:mm:ss') }"
)
out = _run_powershell_hidden(ps_cmd, timeout=20)
raw = (out.stdout or "").strip()
if raw:
last_reboot = raw
except Exception:
raw = ""
# Parse WMIC-style if we had it
if last_reboot == "unknown" and raw:
try:
boot = datetime.datetime.strptime(raw.split(".")[0], "%Y%m%d%H%M%S")
last_reboot = boot.strftime("%Y-%m-%d %H:%M:%S")
except Exception:
pass
else:
try:
out = subprocess.run(["uptime", "-s"], capture_output=True, text=True, timeout=10)
val = out.stdout.strip()
if val:
last_reboot = val
except Exception:
pass
except Exception:
last_reboot = "unknown"
created = config.data.get("created")
if not created:
created = time.strftime("%Y-%m-%d %H:%M:%S")
config.data["created"] = created
try:
config._write()
except Exception:
pass
# External IP detection with fallbacks
external_ip = "unknown"
for url in ("https://api.ipify.org", "https://api64.ipify.org", "https://ifconfig.me/ip"):
try:
external_ip = requests.get(url, timeout=5).text.strip()
if external_ip:
break
except Exception:
continue
return {
"hostname": socket.gethostname(),
"operating_system": config.data.get("agent_operating_system", detect_agent_os()),
"device_type": config.data.get("device_type", detect_device_type()),
"last_user": last_user,
"internal_ip": _get_internal_ip(),
"external_ip": external_ip,
"last_reboot": last_reboot,
"created": created,
}
def collect_software():
items = []
plat = platform.system().lower()
try:
if plat == "windows":
try:
out = _run_hidden(["wmic", "product", "get", "name,version"], timeout=60)
for line in out.stdout.splitlines():
if line.strip() and not line.lower().startswith("name"):
parts = line.strip().split(" ")
name = parts[0].strip()
version = parts[-1].strip() if len(parts) > 1 else ""
if name:
items.append({"name": name, "version": version})
except FileNotFoundError:
ps_cmd = (
"Get-ItemProperty "
"'HKLM:\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*',"
"'HKLM:\\Software\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*' "
"| Where-Object { $_.DisplayName } "
"| Select-Object DisplayName,DisplayVersion "
"| ConvertTo-Json"
)
out = _run_powershell_hidden(ps_cmd, timeout=60)
data = json.loads(out.stdout or "[]")
if isinstance(data, dict):
data = [data]
for pkg in data:
name = pkg.get("DisplayName")
if name:
items.append({
"name": name,
"version": pkg.get("DisplayVersion", "")
})
elif plat == "linux":
out = subprocess.run(["dpkg-query", "-W", "-f=${Package}\t${Version}\n"], capture_output=True, text=True)
for line in out.stdout.splitlines():
if "\t" in line:
name, version = line.split("\t", 1)
items.append({"name": name, "version": version})
else:
out = subprocess.run([sys.executable, "-m", "pip", "list", "--format", "json"], capture_output=True, text=True)
data = json.loads(out.stdout or "[]")
for pkg in data:
items.append({"name": pkg.get("name"), "version": pkg.get("version")})
except Exception as e:
print(f"[WARN] collect_software failed: {e}")
return items[:100]
def collect_memory():
entries = []
plat = platform.system().lower()
try:
if plat == "windows":
try:
out = _run_hidden(["wmic", "memorychip", "get", "BankLabel,Speed,SerialNumber,Capacity"], timeout=60)
lines = [l for l in out.stdout.splitlines() if l.strip() and "BankLabel" not in l]
for line in lines:
parts = [p for p in line.split() if p]
if len(parts) >= 4:
entries.append({
"slot": parts[0],
"speed": parts[1],
"serial": parts[2],
"capacity": parts[3],
})
except FileNotFoundError:
ps_cmd = (
"Get-CimInstance Win32_PhysicalMemory | "
"Select-Object BankLabel,Speed,SerialNumber,Capacity | ConvertTo-Json"
)
out = _run_powershell_hidden(ps_cmd, timeout=60)
data = json.loads(out.stdout or "[]")
if isinstance(data, dict):
data = [data]
for stick in data:
entries.append({
"slot": stick.get("BankLabel", "unknown"),
"speed": str(stick.get("Speed", "unknown")),
"serial": stick.get("SerialNumber", "unknown"),
"capacity": stick.get("Capacity", "unknown"),
})
elif plat == "linux":
out = subprocess.run(["dmidecode", "-t", "17"], capture_output=True, text=True)
slot = speed = serial = capacity = None
for line in out.stdout.splitlines():
line = line.strip()
if line.startswith("Locator:"):
slot = line.split(":", 1)[1].strip()
elif line.startswith("Speed:"):
speed = line.split(":", 1)[1].strip()
elif line.startswith("Serial Number:"):
serial = line.split(":", 1)[1].strip()
elif line.startswith("Size:"):
capacity = line.split(":", 1)[1].strip()
elif not line and slot:
entries.append({
"slot": slot,
"speed": speed or "unknown",
"serial": serial or "unknown",
"capacity": capacity or "unknown",
})
slot = speed = serial = capacity = None
if slot:
entries.append({
"slot": slot,
"speed": speed or "unknown",
"serial": serial or "unknown",
"capacity": capacity or "unknown",
})
except Exception as e:
print(f"[WARN] collect_memory failed: {e}")
if not entries:
try:
if psutil:
vm = psutil.virtual_memory()
entries.append({
"slot": "physical",
"speed": "unknown",
"serial": "unknown",
"capacity": vm.total,
})
except Exception:
pass
return entries
def collect_storage():
disks = []
plat = platform.system().lower()
try:
if psutil:
for part in psutil.disk_partitions():
try:
usage = psutil.disk_usage(part.mountpoint)
except Exception:
continue
disks.append({
"drive": part.device,
"disk_type": "Removable" if "removable" in part.opts.lower() else "Fixed Disk",
"usage": usage.percent,
"total": usage.total,
"free": usage.free,
"used": usage.used,
})
elif plat == "windows":
found = False
for letter in string.ascii_uppercase:
drive = f"{letter}:\\"
if os.path.exists(drive):
try:
usage = shutil.disk_usage(drive)
except Exception:
continue
disks.append({
"drive": drive,
"disk_type": "Fixed Disk",
"usage": (usage.used / usage.total * 100) if usage.total else 0,
"total": usage.total,
"free": usage.free,
"used": usage.used,
})
found = True
if not found:
try:
out = _run_hidden(["wmic", "logicaldisk", "get", "DeviceID,Size,FreeSpace"], timeout=60)
lines = [l for l in out.stdout.splitlines() if l.strip()][1:]
for line in lines:
parts = line.split()
if len(parts) >= 3:
drive, free, size = parts[0], parts[1], parts[2]
try:
total = float(size)
free_bytes = float(free)
used = total - free_bytes
usage = (used / total * 100) if total else 0
disks.append({
"drive": drive,
"disk_type": "Fixed Disk",
"usage": usage,
"total": total,
"free": free_bytes,
"used": used,
})
except Exception:
pass
except Exception:
pass
else:
try:
out = subprocess.run(["df", "-hP"], capture_output=True, text=True)
for line in out.stdout.splitlines()[1:]:
parts = line.split()
if len(parts) >= 6:
try:
usage_str = parts[4].rstrip('%')
usage = float(usage_str)
except Exception:
usage = 0
total = parts[1]
free_bytes = parts[3]
used = parts[2]
disks.append({
"drive": parts[0],
"disk_type": "Mounted",
"usage": usage,
"total": total,
"free": free_bytes,
"used": used,
})
except Exception:
pass
except Exception as e:
print(f"[WARN] collect_storage failed: {e}")
return disks
def collect_network():
adapters = []
plat = platform.system().lower()
try:
if psutil:
for name, addrs in psutil.net_if_addrs().items():
ips = [a.address for a in addrs if getattr(a, "family", None) == socket.AF_INET]
mac = next((a.address for a in addrs if getattr(a, "family", None) == getattr(psutil, "AF_LINK", object)), "unknown")
adapters.append({"adapter": name, "ips": ips, "mac": mac})
elif plat == "windows":
ps_cmd = (
"Get-NetIPConfiguration | "
"Select-Object InterfaceAlias,@{Name='IPv4';Expression={$_.IPv4Address.IPAddress}},"
"@{Name='MAC';Expression={$_.NetAdapter.MacAddress}} | ConvertTo-Json"
)
out = _run_powershell_hidden(ps_cmd, timeout=60)
data = json.loads(out.stdout or "[]")
if isinstance(data, dict):
data = [data]
for a in data:
ip = a.get("IPv4")
adapters.append({
"adapter": a.get("InterfaceAlias", "unknown"),
"ips": [ip] if ip else [],
"mac": a.get("MAC", "unknown"),
})
else:
out = subprocess.run(["ip", "-o", "-4", "addr", "show"], capture_output=True, text=True, timeout=60)
for line in out.stdout.splitlines():
parts = line.split()
if len(parts) >= 4:
name = parts[1]
ip = parts[3].split("/")[0]
adapters.append({"adapter": name, "ips": [ip], "mac": "unknown"})
except Exception as e:
print(f"[WARN] collect_network failed: {e}")
return adapters
async def send_agent_details(agent_id, config):
"""Collect detailed agent data and send to server periodically."""
while True:
try:
details = {
"summary": collect_summary(config),
"software": collect_software(),
"memory": collect_memory(),
"storage": collect_storage(),
"network": collect_network(),
}
url = config.data.get("borealis_server_url", "http://localhost:5000") + "/api/agent/details"
payload = {
"agent_id": agent_id,
"hostname": details.get("summary", {}).get("hostname", socket.gethostname()),
"details": details,
}
async with aiohttp.ClientSession() as session:
await session.post(url, json=payload, timeout=10)
except Exception as e:
print(f"[WARN] Failed to send agent details: {e}")
# Report every ~2 minutes
await asyncio.sleep(120)

View File

@@ -1,352 +0,0 @@
import os
import asyncio
import concurrent.futures
from functools import partial
from io import BytesIO
import base64
import traceback
import random
import importlib.util
from PyQt5 import QtCore, QtGui, QtWidgets
from PIL import ImageGrab
class RolesContext:
def __init__(self, sio, agent_id, config):
self.sio = sio
self.agent_id = agent_id
self.config = config
# Load macro engines from the local Python_API_Endpoints directory
MACRO_ENGINE_PATH = os.path.join(os.path.dirname(__file__), "Python_API_Endpoints", "macro_engines.py")
spec = importlib.util.spec_from_file_location("macro_engines", MACRO_ENGINE_PATH)
macro_engines = importlib.util.module_from_spec(spec)
spec.loader.exec_module(macro_engines)
# Overlay visuals
overlay_green_thickness = 4
overlay_gray_thickness = 2
handle_size = overlay_green_thickness * 2
extra_top_padding = overlay_green_thickness * 2 + 4
# Track active screenshot overlay widgets per node_id
overlay_widgets: dict[str, QtWidgets.QWidget] = {}
def get_window_list():
"""Return a list of windows from macro engines."""
try:
return macro_engines.list_windows()
except Exception:
return []
def close_overlay(node_id: str):
w = overlay_widgets.pop(node_id, None)
if w:
try:
w.close()
except Exception:
pass
def close_all_overlays():
for node_id, widget in list(overlay_widgets.items()):
try:
widget.close()
except Exception:
pass
overlay_widgets.clear()
class ScreenshotRegion(QtWidgets.QWidget):
def __init__(self, ctx: RolesContext, node_id, x=100, y=100, w=300, h=200, alias=None):
super().__init__()
self.ctx = ctx
self.node_id = node_id
self.alias = alias
self.setGeometry(
x - handle_size,
y - handle_size - extra_top_padding,
w + handle_size * 2,
h + handle_size * 2 + extra_top_padding,
)
self.setWindowFlags(QtCore.Qt.FramelessWindowHint | QtCore.Qt.WindowStaysOnTopHint)
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
self.resize_dir = None
self.drag_offset = None
self._start_geom = None
self._start_pos = None
self.setMouseTracking(True)
def paintEvent(self, event):
p = QtGui.QPainter(self)
p.setRenderHint(QtGui.QPainter.Antialiasing)
w = self.width()
h = self.height()
# draw gray capture box
p.setPen(QtGui.QPen(QtGui.QColor(130, 130, 130), overlay_gray_thickness))
p.drawRect(handle_size, handle_size + extra_top_padding, w - handle_size * 2, h - handle_size * 2 - extra_top_padding)
p.setPen(QtCore.Qt.NoPen)
p.setBrush(QtGui.QBrush(QtGui.QColor(0, 191, 255)))
edge = overlay_green_thickness * 3
# corner handles
p.drawRect(0, extra_top_padding, edge, overlay_green_thickness)
p.drawRect(0, extra_top_padding, overlay_green_thickness, edge)
p.drawRect(w - edge, extra_top_padding, edge, overlay_green_thickness)
p.drawRect(w - overlay_green_thickness, extra_top_padding, overlay_green_thickness, edge)
p.drawRect(0, h - overlay_green_thickness, edge, overlay_green_thickness)
p.drawRect(0, h - edge, overlay_green_thickness, edge)
p.drawRect(w - edge, h - overlay_green_thickness, edge, overlay_green_thickness)
p.drawRect(w - overlay_green_thickness, h - edge, overlay_green_thickness, edge)
# side handles
long = overlay_green_thickness * 6
p.drawRect((w - long) // 2, extra_top_padding, long, overlay_green_thickness)
p.drawRect((w - long) // 2, h - overlay_green_thickness, long, overlay_green_thickness)
p.drawRect(0, (h + extra_top_padding - long) // 2, overlay_green_thickness, long)
p.drawRect(w - overlay_green_thickness, (h + extra_top_padding - long) // 2, overlay_green_thickness, long)
# grabber bar
bar_width = overlay_green_thickness * 6
bar_height = overlay_green_thickness
bar_x = (w - bar_width) // 2
bar_y = 6
p.setBrush(QtGui.QColor(0, 191, 255))
p.drawRect(bar_x, bar_y - bar_height - 10, bar_width, bar_height * 4)
def get_geometry(self):
g = self.geometry()
return (
g.x() + handle_size,
g.y() + handle_size + extra_top_padding,
g.width() - handle_size * 2,
g.height() - handle_size * 2 - extra_top_padding,
)
def mousePressEvent(self, e):
if e.button() == QtCore.Qt.LeftButton:
pos = e.pos()
bar_width = overlay_green_thickness * 6
bar_height = overlay_green_thickness
bar_x = (self.width() - bar_width) // 2
bar_y = 2
bar_rect = QtCore.QRect(bar_x, bar_y, bar_width, bar_height)
if bar_rect.contains(pos):
self.drag_offset = e.globalPos() - self.frameGeometry().topLeft()
return
m = handle_size
dirs = []
if pos.x() <= m:
dirs.append('left')
if pos.x() >= self.width() - m:
dirs.append('right')
if pos.y() <= m + extra_top_padding:
dirs.append('top')
if pos.y() >= self.height() - m:
dirs.append('bottom')
if dirs:
self.resize_dir = '_'.join(dirs)
self._start_geom = self.geometry()
self._start_pos = e.globalPos()
else:
self.drag_offset = e.globalPos() - self.frameGeometry().topLeft()
def mouseMoveEvent(self, e):
if self.resize_dir and self._start_geom and self._start_pos:
dx = e.globalX() - self._start_pos.x()
dy = e.globalY() - self._start_pos.y()
geom = QtCore.QRect(self._start_geom)
if 'left' in self.resize_dir:
new_x = geom.x() + dx
new_w = geom.width() - dx
geom.setX(new_x)
geom.setWidth(new_w)
if 'right' in self.resize_dir:
geom.setWidth(self._start_geom.width() + dx)
if 'top' in self.resize_dir:
new_y = geom.y() + dy
new_h = geom.height() - dy
geom.setY(new_y)
geom.setHeight(new_h)
if 'bottom' in self.resize_dir:
geom.setHeight(self._start_geom.height() + dy)
self.setGeometry(geom)
elif self.drag_offset and e.buttons() & QtCore.Qt.LeftButton:
self.move(e.globalPos() - self.drag_offset)
def mouseReleaseEvent(self, e):
self.drag_offset = None
self.resize_dir = None
self._start_geom = None
self._start_pos = None
x, y, w, h = self.get_geometry()
self.ctx.config.data['regions'][self.node_id] = {'x': x, 'y': y, 'w': w, 'h': h}
try:
self.ctx.config._write()
except Exception:
pass
# Emit a zero-image update so the server knows new geometry immediately
asyncio.create_task(self.ctx.sio.emit('agent_screenshot_task', {
'agent_id': self.ctx.agent_id,
'node_id': self.node_id,
'image_base64': '',
'x': x, 'y': y, 'w': w, 'h': h
}))
async def screenshot_task(ctx: RolesContext, cfg):
nid = cfg.get('node_id')
alias = cfg.get('alias', '')
r = ctx.config.data['regions'].get(nid)
if r:
region = (r['x'], r['y'], r['w'], r['h'])
else:
region = (
cfg.get('x', 100),
cfg.get('y', 100),
cfg.get('w', 300),
cfg.get('h', 200),
)
ctx.config.data['regions'][nid] = {'x': region[0], 'y': region[1], 'w': region[2], 'h': region[3]}
try:
ctx.config._write()
except Exception:
pass
if nid not in overlay_widgets:
widget = ScreenshotRegion(ctx, nid, *region, alias=alias)
overlay_widgets[nid] = widget
widget.show()
await ctx.sio.emit('agent_screenshot_task', {
'agent_id': ctx.agent_id,
'node_id': nid,
'image_base64': '',
'x': region[0], 'y': region[1], 'w': region[2], 'h': region[3]
})
interval = cfg.get('interval', 1000) / 1000.0
loop = asyncio.get_event_loop()
executor = concurrent.futures.ThreadPoolExecutor(max_workers=ctx.config.data.get('max_task_workers', 8))
try:
while True:
x, y, w, h = overlay_widgets[nid].get_geometry()
grab = partial(ImageGrab.grab, bbox=(x, y, x + w, y + h))
img = await loop.run_in_executor(executor, grab)
buf = BytesIO(); img.save(buf, format='PNG')
encoded = base64.b64encode(buf.getvalue()).decode('utf-8')
await ctx.sio.emit('agent_screenshot_task', {
'agent_id': ctx.agent_id,
'node_id': nid,
'image_base64': encoded,
'x': x, 'y': y, 'w': w, 'h': h
})
await asyncio.sleep(interval)
except asyncio.CancelledError:
print(f"[TASK] Screenshot role {nid} cancelled.")
except Exception as e:
print(f"[ERROR] Screenshot task {nid} failed: {e}")
traceback.print_exc()
async def macro_task(ctx: RolesContext, cfg):
"""Improved macro task supporting operation modes, live config, and feedback."""
nid = cfg.get('node_id')
last_trigger_value = 0
has_run_once = False
while True:
window_handle = cfg.get('window_handle')
macro_type = cfg.get('macro_type', 'keypress')
operation_mode = cfg.get('operation_mode', 'Continuous')
key = cfg.get('key')
text = cfg.get('text')
interval_ms = int(cfg.get('interval_ms', 1000))
randomize = cfg.get('randomize_interval', False)
random_min = int(cfg.get('random_min', 750))
random_max = int(cfg.get('random_max', 950))
active = cfg.get('active', True)
trigger = int(cfg.get('trigger', 0))
async def emit_macro_status(success, message=""):
await ctx.sio.emit('macro_status', {
"agent_id": ctx.agent_id,
"node_id": nid,
"success": success,
"message": message,
"timestamp": int(asyncio.get_event_loop().time() * 1000)
})
if not (active is True or str(active).lower() == "true"):
await asyncio.sleep(0.2)
continue
try:
send_macro = False
if operation_mode == "Run Once":
if not has_run_once:
send_macro = True
has_run_once = True
elif operation_mode == "Continuous":
send_macro = True
elif operation_mode == "Trigger-Continuous":
send_macro = (trigger == 1)
elif operation_mode == "Trigger-Once":
if last_trigger_value == 0 and trigger == 1:
send_macro = True
last_trigger_value = trigger
else:
send_macro = True
if send_macro:
if macro_type == 'keypress' and key:
result = macro_engines.send_keypress_to_window(window_handle, key)
elif macro_type == 'typed_text' and text:
result = macro_engines.type_text_to_window(window_handle, text)
else:
await emit_macro_status(False, "Invalid macro type or missing key/text")
await asyncio.sleep(0.2)
continue
if isinstance(result, tuple):
success, err = result
else:
success, err = bool(result), ""
if success:
await emit_macro_status(True, f"Macro sent: {macro_type}")
else:
await emit_macro_status(False, err or "Unknown macro engine failure")
else:
await asyncio.sleep(0.05)
if send_macro:
if randomize:
ms = random.randint(random_min, random_max)
else:
ms = interval_ms
await asyncio.sleep(ms / 1000.0)
else:
await asyncio.sleep(0.1)
except asyncio.CancelledError:
print(f"[TASK] Macro role {nid} cancelled.")
break
except Exception as e:
print(f"[ERROR] Macro task {nid} failed: {e}")
traceback.print_exc()
await emit_macro_status(False, str(e))
await asyncio.sleep(0.5)

View File

@@ -1,321 +0,0 @@
import os
import sys
import time
import subprocess
import threading
import datetime
import json
import ctypes
from ctypes import wintypes
# Optional pywin32 imports for per-session launching
try:
import win32ts
import win32con
import win32process
import win32security
import win32profile
import win32api
import pywintypes
except Exception:
win32ts = None
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir))
AGENT_DIR = os.path.join(ROOT, 'Agent')
BOREALIS_DIR = os.path.join(AGENT_DIR, 'Borealis')
LOG_DIR = os.path.join(ROOT, 'Logs', 'Agent')
os.makedirs(LOG_DIR, exist_ok=True)
LOG_FILE = os.path.join(LOG_DIR, 'Supervisor.log')
PID_FILE = os.path.join(LOG_DIR, 'script_agent.pid')
# Internal state for process + backoff
_script_proc = None
_spawn_backoff = 5 # seconds (exponential backoff start)
_max_backoff = 300 # cap at 5 minutes
_next_spawn_time = 0.0
_last_disable_log = 0.0
_last_fail_log = 0.0
def log(msg: str):
try:
# simple size-based rotation (~1MB)
try:
if os.path.isfile(LOG_FILE) and os.path.getsize(LOG_FILE) > 1_000_000:
bak = LOG_FILE + '.1'
try:
if os.path.isfile(bak):
os.remove(bak)
except Exception:
pass
try:
os.replace(LOG_FILE, bak)
except Exception:
pass
except Exception:
pass
ts = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
with open(LOG_FILE, 'a', encoding='utf-8') as f:
f.write(f"[{ts}] {msg}\n")
except Exception:
pass
def venv_python():
try:
exe_dir = os.path.join(AGENT_DIR, 'Scripts')
py = os.path.join(exe_dir, 'python.exe')
if os.path.isfile(py):
return py
except Exception:
pass
return sys.executable
def venv_pythonw():
try:
exe_dir = os.path.join(AGENT_DIR, 'Scripts')
pyw = os.path.join(exe_dir, 'pythonw.exe')
if os.path.isfile(pyw):
return pyw
except Exception:
pass
return venv_python()
def _settings_path():
return os.path.join(ROOT, 'agent_settings.json')
def load_settings():
cfg = {}
try:
path = _settings_path()
if os.path.isfile(path):
with open(path, 'r', encoding='utf-8') as f:
cfg = json.load(f)
except Exception:
cfg = {}
return cfg or {}
def _psutil_process_exists(pid: int) -> bool:
try:
import psutil # type: ignore
if pid <= 0:
return False
p = psutil.Process(pid)
return p.is_running() and (p.status() != psutil.STATUS_ZOMBIE)
except Exception:
return False
def _win_process_exists(pid: int) -> bool:
try:
if pid <= 0:
return False
PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
OpenProcess = kernel32.OpenProcess
OpenProcess.restype = wintypes.HANDLE
OpenProcess.argtypes = (wintypes.DWORD, wintypes.BOOL, wintypes.DWORD)
CloseHandle = kernel32.CloseHandle
CloseHandle.argtypes = (wintypes.HANDLE,)
h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, pid)
if h:
try:
CloseHandle(h)
except Exception:
pass
return True
return False
except Exception:
return False
def process_exists(pid: int) -> bool:
# Prefer psutil if available; else Win32 API
return _psutil_process_exists(pid) or _win_process_exists(pid)
def _read_pid_file() -> int:
try:
if os.path.isfile(PID_FILE):
with open(PID_FILE, 'r', encoding='utf-8') as f:
s = f.read().strip()
return int(s)
except Exception:
pass
return 0
def _write_pid_file(pid: int):
try:
with open(PID_FILE, 'w', encoding='utf-8') as f:
f.write(str(pid))
except Exception:
pass
def _clear_pid_file():
try:
if os.path.isfile(PID_FILE):
os.remove(PID_FILE)
except Exception:
pass
def ensure_script_agent():
"""Ensure LocalSystem script_agent.py is running; restart if not, with backoff and PID tracking."""
global _script_proc, _spawn_backoff, _next_spawn_time, _last_disable_log, _last_fail_log
# Allow disabling via config
try:
cfg = load_settings()
if not cfg.get('enable_system_script_agent', True):
now = time.time()
if now - _last_disable_log > 60:
log('System script agent disabled by config (enable_system_script_agent=false)')
_last_disable_log = now
return
except Exception:
pass
# If we have a running child process, keep it
try:
if _script_proc is not None:
if _script_proc.poll() is None:
return
else:
# Child exited; clear PID file for safety
_clear_pid_file()
_script_proc = None
except Exception:
pass
# If PID file points to a living process, don't spawn
try:
pid = _read_pid_file()
if pid and process_exists(pid):
return
elif pid and not process_exists(pid):
_clear_pid_file()
except Exception:
pass
# Honor backoff window
if time.time() < _next_spawn_time:
return
py = venv_python()
script = os.path.join(ROOT, 'Data', 'Agent', 'script_agent.py')
try:
proc = subprocess.Popen(
[py, '-W', 'ignore::SyntaxWarning', script],
creationflags=(0x08000000 if os.name == 'nt' else 0),
)
_script_proc = proc
_write_pid_file(proc.pid)
log(f'Launched script_agent.py (pid {proc.pid})')
# reset backoff on success
_spawn_backoff = 5
_next_spawn_time = 0.0
except Exception as e:
msg = f'Failed to launch script_agent.py: {e}'
now = time.time()
# rate-limit identical failure logs to once per 10s
if now - _last_fail_log > 10:
log(msg)
_last_fail_log = now
# exponential backoff
_spawn_backoff = min(_spawn_backoff * 2, _max_backoff)
_next_spawn_time = time.time() + _spawn_backoff
def _enable_privileges():
try:
hProc = win32api.GetCurrentProcess()
hTok = win32security.OpenProcessToken(hProc, win32con.TOKEN_ADJUST_PRIVILEGES | win32con.TOKEN_QUERY)
for name in [
win32security.SE_ASSIGNPRIMARYTOKEN_NAME,
win32security.SE_INCREASE_QUOTA_NAME,
win32security.SE_TCB_NAME,
win32security.SE_BACKUP_NAME,
win32security.SE_RESTORE_NAME,
]:
try:
luid = win32security.LookupPrivilegeValue(None, name)
win32security.AdjustTokenPrivileges(hTok, False, [(luid, win32con.SE_PRIVILEGE_ENABLED)])
except Exception:
pass
except Exception:
pass
def active_sessions():
ids = []
try:
if win32ts is None:
return ids
for s in win32ts.WTSEnumerateSessions(None, 1, 0):
sid, _, state = s
if state == win32ts.WTSActive:
ids.append(sid)
except Exception:
pass
return ids
def launch_helper_in_session(session_id):
try:
if win32ts is None:
return False
_enable_privileges()
hUser = win32ts.WTSQueryUserToken(session_id)
primary = win32security.DuplicateTokenEx(
hUser,
win32con.MAXIMUM_ALLOWED,
win32security.SECURITY_ATTRIBUTES(),
win32security.SecurityImpersonation,
win32con.TOKEN_PRIMARY,
)
env = win32profile.CreateEnvironmentBlock(primary, True)
si = win32process.STARTUPINFO()
si.lpDesktop = 'winsta0\\default'
cmd = f'"{venv_pythonw()}" -W ignore::SyntaxWarning "{os.path.join(BOREALIS_DIR, "borealis-agent.py")}"'
flags = getattr(win32con, 'CREATE_UNICODE_ENVIRONMENT', 0x00000400)
win32process.CreateProcessAsUser(primary, None, cmd, None, None, False, flags, env, BOREALIS_DIR, si)
log(f'Started user helper in session {session_id}')
return True
except Exception as e:
log(f'Failed to start helper in session {session_id}: {e}')
return False
def manage_user_helpers_loop():
known = set()
while True:
try:
cur = set(active_sessions())
for sid in cur:
if sid not in known:
launch_helper_in_session(sid)
known = cur
except Exception:
pass
time.sleep(3)
def main():
log('Supervisor starting')
t = threading.Thread(target=manage_user_helpers_loop, daemon=True)
t.start()
while True:
ensure_script_agent()
time.sleep(5)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,95 @@
import os
import importlib.util
from typing import Dict, List, Optional
class RoleManager:
"""
Discovers and loads role modules from Data/Agent/Roles.
Each role module should expose:
- ROLE_NAME: str
- ROLE_CONTEXTS: List[str] (e.g., ["interactive"], ["system"], or ["interactive","system"])
- class Role(ctx): with methods:
- register_events(): optional, bind socket events
- on_config(roles: List[dict]): optional, apply server config
- stop_all(): optional, cancel tasks/cleanup
The ctx passed to each Role is a simple object storing common references.
"""
class Ctx:
def __init__(self, sio, agent_id, config, loop, hooks: Optional[dict] = None):
self.sio = sio
self.agent_id = agent_id
self.config = config
self.loop = loop
self.hooks = hooks or {}
def __init__(self, base_dir: str, context: str, sio, agent_id: str, config, loop, hooks: Optional[dict] = None):
self.base_dir = base_dir
self.context = context # "interactive" or "system"
self.sio = sio
self.agent_id = agent_id
self.config = config
self.loop = loop
self.hooks = hooks or {}
self.roles: Dict[str, object] = {}
def _iter_role_files(self) -> List[str]:
roles_dir = os.path.join(self.base_dir, 'Roles')
if not os.path.isdir(roles_dir):
return []
files = []
for fn in os.listdir(roles_dir):
if fn.lower().startswith('role_') and fn.lower().endswith('.py'):
files.append(os.path.join(roles_dir, fn))
return sorted(files)
def load(self):
for path in self._iter_role_files():
try:
spec = importlib.util.spec_from_file_location(os.path.splitext(os.path.basename(path))[0], path)
mod = importlib.util.module_from_spec(spec)
assert spec and spec.loader
spec.loader.exec_module(mod)
except Exception:
continue
role_name = getattr(mod, 'ROLE_NAME', None)
role_contexts = getattr(mod, 'ROLE_CONTEXTS', ['interactive', 'system'])
RoleClass = getattr(mod, 'Role', None)
if not role_name or not RoleClass:
continue
if self.context not in (role_contexts or []):
continue
try:
ctx = RoleManager.Ctx(self.sio, self.agent_id, self.config, self.loop, hooks=self.hooks)
role_obj = RoleClass(ctx)
# Optional event registration
if hasattr(role_obj, 'register_events'):
try:
role_obj.register_events()
except Exception:
pass
self.roles[role_name] = role_obj
except Exception:
continue
def on_config(self, roles_cfg: List[dict]):
for role in list(self.roles.values()):
try:
if hasattr(role, 'on_config'):
role.on_config(roles_cfg)
except Exception:
pass
def stop_all(self):
for role in list(self.roles.values()):
try:
if hasattr(role, 'stop_all'):
role.stop_all()
except Exception:
pass

View File

@@ -1,260 +0,0 @@
import os
import sys
import time
import socket
import asyncio
import json
import subprocess
import tempfile
from typing import Optional
import socketio
import platform
import time
import uuid
import tempfile
import contextlib
def get_project_root():
return os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
def get_server_url():
# Try to reuse the agent config if present
cfg_path = os.path.join(get_project_root(), "agent_settings.json")
try:
if os.path.isfile(cfg_path):
with open(cfg_path, "r", encoding="utf-8") as f:
data = json.load(f)
url = data.get("borealis_server_url")
if isinstance(url, str) and url.strip():
return url.strip()
except Exception:
pass
return "http://localhost:5000"
def run_powershell_script_content(content: str):
# Store ephemeral script under <ProjectRoot>/Temp
temp_dir = os.path.join(get_project_root(), "Temp")
os.makedirs(temp_dir, exist_ok=True)
fd, path = tempfile.mkstemp(prefix="sj_", suffix=".ps1", dir=temp_dir, text=True)
with os.fdopen(fd, 'w', encoding='utf-8', newline='\n') as fh:
fh.write(content or "")
ps = os.path.expandvars(r"%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe")
if not os.path.isfile(ps):
ps = "powershell.exe"
try:
flags = 0x08000000 if os.name == 'nt' else 0 # CREATE_NO_WINDOW
proc = subprocess.run(
[ps, "-ExecutionPolicy", "Bypass", "-NoProfile", "-File", path],
capture_output=True,
text=True,
timeout=60*60,
creationflags=flags,
)
return proc.returncode, proc.stdout or "", proc.stderr or ""
except Exception as e:
return -1, "", str(e)
finally:
# Best-effort cleanup of the ephemeral script
try:
if os.path.isfile(path):
os.remove(path)
except Exception:
pass
async def main():
sio = socketio.AsyncClient(reconnection=True)
hostname = socket.gethostname()
@sio.event
async def connect():
print("[ScriptAgent] Connected to server")
# Identify as script agent (no heartbeat to avoid UI duplication)
try:
await sio.emit("connect_agent", {"agent_id": f"{hostname}-script"})
except Exception:
pass
@sio.on("quick_job_run")
async def on_quick_job_run(payload):
# Treat as generic script_run internally
try:
target = (payload.get('target_hostname') or '').strip().lower()
if target and target != hostname.lower():
return
run_mode = (payload.get('run_mode') or 'current_user').lower()
# Only the SYSTEM service handles system-mode jobs; ignore others
if run_mode != 'system':
return
job_id = payload.get('job_id')
script_type = (payload.get('script_type') or '').lower()
content = payload.get('script_content') or ''
if script_type != 'powershell':
await sio.emit('quick_job_result', {
'job_id': job_id,
'status': 'Failed',
'stdout': '',
'stderr': f"Unsupported type: {script_type}"
})
return
# Preferred: run via ephemeral scheduled task under SYSTEM for isolation
rc, out, err = run_powershell_via_system_task(content)
if rc == -999:
# Fallback to direct execution if task creation not available
rc, out, err = run_powershell_script_content(content)
status = 'Success' if rc == 0 else 'Failed'
await sio.emit('quick_job_result', {
'job_id': job_id,
'status': status,
'stdout': out,
'stderr': err,
})
except Exception as e:
try:
await sio.emit('quick_job_result', {
'job_id': payload.get('job_id') if isinstance(payload, dict) else None,
'status': 'Failed',
'stdout': '',
'stderr': str(e),
})
except Exception:
pass
@sio.event
async def disconnect():
print("[ScriptAgent] Disconnected")
async def heartbeat_loop():
# Minimal heartbeat so device appears online even without a user helper
while True:
try:
await sio.emit("agent_heartbeat", {
"agent_id": f"{hostname}-script",
"hostname": hostname,
"agent_operating_system": f"{platform.system()} {platform.release()} (Service)",
"last_seen": int(time.time())
})
except Exception:
pass
await asyncio.sleep(30)
url = get_server_url()
while True:
try:
await sio.connect(url, transports=['websocket'])
# Heartbeat while connected
hb = asyncio.create_task(heartbeat_loop())
try:
await sio.wait()
finally:
try:
hb.cancel()
except Exception:
pass
except Exception as e:
print(f"[ScriptAgent] reconnect in 5s: {e}")
await asyncio.sleep(5)
def run_powershell_via_system_task(content: str):
"""Create an ephemeral scheduled task under SYSTEM to run the script.
Returns (rc, stdout, stderr). If the environment lacks PowerShell ScheduledTasks module, returns (-999, '', 'unavailable').
"""
ps_exe = os.path.expandvars(r"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe")
if not os.path.isfile(ps_exe):
ps_exe = 'powershell.exe'
try:
os.makedirs(os.path.join(get_project_root(), 'Temp'), exist_ok=True)
# Write the target script
script_fd, script_path = tempfile.mkstemp(prefix='sys_task_', suffix='.ps1', dir=os.path.join(get_project_root(), 'Temp'), text=True)
with os.fdopen(script_fd, 'w', encoding='utf-8', newline='\n') as f:
f.write(content or '')
# Output capture path
out_path = os.path.join(get_project_root(), 'Temp', f'out_{uuid.uuid4().hex}.txt')
task_name = f"Borealis Agent - Task - {uuid.uuid4().hex} @ SYSTEM"
# Build PS to create/run task with DeleteExpiredTaskAfter
task_ps = f"""
$ErrorActionPreference='Continue'
$task = "{task_name}"
$ps = "{ps_exe}"
$scr = "{script_path}"
$out = "{out_path}"
try {{ Unregister-ScheduledTask -TaskName $task -Confirm:$false -ErrorAction SilentlyContinue }} catch {{}}
$action = New-ScheduledTaskAction -Execute $ps -Argument ('-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File "' + $scr + '" *> "' + $out + '"')
$settings = New-ScheduledTaskSettingsSet -DeleteExpiredTaskAfter (New-TimeSpan -Minutes 5) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
$principal= New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest
Register-ScheduledTask -TaskName $task -Action $action -Settings $settings -Principal $principal -Force | Out-Null
Start-ScheduledTask -TaskName $task | Out-Null
Start-Sleep -Seconds 2
Get-ScheduledTask -TaskName $task | Out-Null
"""
# Run task creation
proc = subprocess.run([ps_exe, '-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', task_ps], capture_output=True, text=True)
if proc.returncode != 0:
return -999, '', (proc.stderr or proc.stdout or 'scheduled task creation failed')
# Wait up to 60s for output to be written
deadline = time.time() + 60
out_data = ''
while time.time() < deadline:
try:
if os.path.isfile(out_path) and os.path.getsize(out_path) > 0:
with open(out_path, 'r', encoding='utf-8', errors='replace') as f:
out_data = f.read()
break
except Exception:
pass
time.sleep(1)
# Cleanup task (best-effort)
cleanup_ps = f"try {{ Unregister-ScheduledTask -TaskName '{task_name}' -Confirm:$false }} catch {{}}"
subprocess.run([ps_exe, '-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', cleanup_ps], capture_output=True, text=True)
# Best-effort removal of temp script and output files
try:
if os.path.isfile(script_path):
os.remove(script_path)
except Exception:
pass
try:
if os.path.isfile(out_path):
os.remove(out_path)
except Exception:
pass
return 0, out_data or '', ''
except Exception as e:
return -999, '', str(e)
if __name__ == '__main__':
# Ensure only a single instance of the script agent runs (Windows-only lock)
def _acquire_singleton_lock() -> bool:
try:
lock_dir = os.path.join(get_project_root(), 'Logs', 'Agent')
os.makedirs(lock_dir, exist_ok=True)
lock_path = os.path.join(lock_dir, 'script_agent.lock')
# Keep handle open for process lifetime
fh = open(lock_path, 'a')
try:
import msvcrt # type: ignore
# Lock 1 byte non-blocking; released on handle close/process exit
msvcrt.locking(fh.fileno(), msvcrt.LK_NBLCK, 1)
globals()['_LOCK_FH'] = fh
return True
except Exception:
try:
fh.close()
except Exception:
pass
return False
except Exception:
# If we cannot establish a lock, continue (do not prevent agent)
return True
if not _acquire_singleton_lock():
print('[ScriptAgent] Another instance is running; exiting.')
sys.exit(0)
asyncio.run(main())

View File

@@ -1,117 +0,0 @@
import os
import sys
import subprocess
import signal
from PyQt5 import QtWidgets, QtGui
def project_paths():
# Expected layout when running from venv: <Root>\Agent\Borealis
borealis_dir = os.path.dirname(os.path.abspath(__file__))
agent_dir = os.path.abspath(os.path.join(borealis_dir, os.pardir))
venv_scripts = os.path.join(agent_dir, 'Scripts')
pyw = os.path.join(venv_scripts, 'pythonw.exe')
py = os.path.join(venv_scripts, 'python.exe')
icon_path = os.path.join(borealis_dir, 'Borealis.ico')
agent_script = os.path.join(borealis_dir, 'borealis-agent.py')
return {
'borealis_dir': borealis_dir,
'venv_scripts': venv_scripts,
'pythonw': pyw if os.path.isfile(pyw) else sys.executable,
'python': py if os.path.isfile(py) else sys.executable,
'agent_script': agent_script,
'icon': icon_path if os.path.isfile(icon_path) else None,
}
class TrayApp(QtWidgets.QSystemTrayIcon):
def __init__(self, app):
self.app = app
paths = project_paths()
self.paths = paths
icon = QtGui.QIcon(paths['icon']) if paths['icon'] else app.style().standardIcon(QtWidgets.QStyle.SP_ComputerIcon)
super().__init__(icon)
self.setToolTip('Borealis Agent')
self.menu = QtWidgets.QMenu()
self.action_show_console = self.menu.addAction('Switch to Foreground Mode')
self.action_hide_console = self.menu.addAction('Switch to Background Mode')
self.action_restart = self.menu.addAction('Restart Agent')
self.menu.addSeparator()
self.action_quit = self.menu.addAction('Quit Agent and Tray')
self.action_show_console.triggered.connect(self.switch_to_console)
self.action_hide_console.triggered.connect(self.switch_to_background)
self.action_restart.triggered.connect(self.restart_agent)
self.action_quit.triggered.connect(self.quit_all)
self.setContextMenu(self.menu)
self.proc = None
self.console_mode = False
# Start in background mode by default
self.switch_to_background()
self.show()
def _start_agent(self, console=False):
self._stop_agent()
exe = self.paths['python'] if console else self.paths['pythonw']
args = [exe, '-W', 'ignore::SyntaxWarning', self.paths['agent_script']]
creationflags = 0
if not console and os.name == 'nt':
# CREATE_NO_WINDOW
creationflags = 0x08000000
try:
self.proc = subprocess.Popen(args, cwd=self.paths['borealis_dir'], creationflags=creationflags)
self.console_mode = console
self._update_actions(console)
except Exception:
self.proc = None
def _stop_agent(self):
if self.proc is not None:
try:
if os.name == 'nt':
self.proc.send_signal(signal.SIGTERM)
else:
self.proc.terminate()
except Exception:
pass
try:
self.proc.wait(timeout=3)
except Exception:
try:
self.proc.kill()
except Exception:
pass
self.proc = None
def _update_actions(self, console):
self.action_show_console.setEnabled(not console)
self.action_hide_console.setEnabled(console)
def switch_to_console(self):
self._start_agent(console=True)
def switch_to_background(self):
self._start_agent(console=False)
def restart_agent(self):
# Restart using current mode
self._start_agent(console=self.console_mode)
# Service controls removed in task-centric architecture
def quit_all(self):
self._stop_agent()
self.hide()
self.app.quit()
def main():
app = QtWidgets.QApplication(sys.argv)
tray = TrayApp(app)
return app.exec_()
if __name__ == '__main__':
sys.exit(main())

View File

@@ -1,43 +0,0 @@
# ---------------------- Information Gathering -----------------------
import os
from ahk import AHK
# Get the directory containing this script (cross-platform)
script_dir = os.path.dirname(os.path.abspath(__file__))
# Build the path to the AutoHotkey binary (adjust filename if needed)
ahk_bin_path = os.path.join(script_dir, 'AutoHotKey', 'AutoHotkey64.exe')
# ---------------------- Information Analysis ----------------------
# Confirm that the AHK binary exists at the given path
if not os.path.isfile(ahk_bin_path):
raise FileNotFoundError(f"AutoHotkey binary not found at: {ahk_bin_path}")
# ---------------------- Information Processing ----------------------
# Initialize AHK instance with explicit executable_path
ahk = AHK(executable_path=ahk_bin_path)
window_title = 'New Tab - Google Chrome' # Change this to your target window
# Find the window by its title
target_window = ahk.find_window(title=window_title)
if target_window is None:
print(f"Window with title '{window_title}' not found.")
else:
# Bring the target window to the foreground
target_window.activate()
# Wait briefly to ensure the window is focused
import time
time.sleep(1)
# Send keystrokes/text to the window
text = "Hello from Python and AutoHotkey!"
for c in text:
ahk.send(c)
import time
time.sleep(0.05) # slow down for debugging
ahk.send('{ENTER}')
print("Sent keystrokes to the window.")

View File

@@ -1,4 +0,0 @@
#////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: <ProjectRoot>/Data/Experimental/Macros/macro-requirements.txt
# Macro Automation
ahk # AutoHotKey