Fixed Script Execution Issues

This commit is contained in:
2025-09-18 23:20:47 -06:00
parent 8a3f2ecd77
commit 6436cafc1c
8 changed files with 504 additions and 55 deletions

View File

@@ -56,12 +56,42 @@ All runtime logs live under `Logs/<ServiceName>` relative to the project root (`
- `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.
- 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`. 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.
- Agent installs/repairs now stop only Agent venv Python processes (scoped to `Agent\\*`) and no longer kill global `node.exe`. This prevents accidental termination of the dev WebUI (Vite/esbuild) when working on agents.
- 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.
## New: Agent Launch Model, Tasks, and Logging
- SYSTEM mode is launched via a wrapper to guarantee WorkingDirectory and capture stdout/stderr:
- `Agent\\Borealis\\launch_service.ps1` is registered as the scheduled task action for the SYSTEM agent.
- The wrapper runs `Agent\\Scripts\\pythonw.exe Agent\\Borealis\\agent.py --system-service --config svc` with `Set-Location` to `Agent\\Borealis` and redirects output to `%ProgramData%\\Borealis\\svc.out.log` and `svc.err.log`.
- This avoids 0x1/0x2 Task Scheduler errors on hosts where WorkingDirectory is ignored.
- UserHelper (interactive) is still a direct task action to `pythonw.exe "Agent\\Borealis\\agent.py" --config user`.
- Config files and inheritance:
- Base config lives at `<ProjectRoot>\\agent_settings.json`.
- On first run per-suffix, the agent seeds: `agent_settings_svc.json` (SYSTEM), `agent_settings_user.json` (interactive) from the base.
- Set `borealis_server_url` in the base file so both inherit the same server endpoint.
- Logging:
- Early bootstrap log: `<ProjectRoot>\\Logs\\Agent\\bootstrap.log` (helps verify launch + mode).
- Main logs: `<ProjectRoot>\\Logs\\Agent\\agent.log`, `agent.error.log`.
- Wrapper logs (SYSTEM task): `%ProgramData%\\Borealis\\svc.out.log`, `svc.err.log`.
- Last SYSTEM script for debugging: `<ProjectRoot>\\Logs\\Agent\\system_last.ps1`.
## Recommended Dev Flows
- Start the server in Flask-only or dev mode before the agent so WebSocket connect succeeds:
- Flask quick start: `.\\Borealis.ps1 -Server -Flask -Quick`.
- Dev UI separately (if needed): `cd Server\\web-interface && npm run dev`.
- Launch/repair agent (elevated PowerShell): `.\\Borealis.ps1 -Agent -AgentAction install`.
- Manual short-run agent checks (non-blocking):
- `Start-Process .\\Agent\\Scripts\\pythonw.exe -ArgumentList '".\\Agent\\Borealis\\agent.py" --system-service --config svc'`
- Verify logs under `Logs\\Agent` and presence of `agent_settings_svc.json`.
## Troubleshooting Checklist
- Agent task “Ready” with 0x1: ensure the SYSTEM task uses `launch_service.ps1` and that WorkingDirectory is `Agent\\Borealis`.
- No logs/configs created: verify venv exists under `Agent\\Scripts` and that wrapper points at the right paths.
- Agent connects but Devices empty: check `agent.error.log` for aiohttp errors and confirm `borealis_server_url` is reachable; device details post occurs once on connect and then every ~5 minutes.
- Quick jobs “Running” forever: ensure SYSTEM and UserHelper agents are both running; check `system_last.ps1` and wrapper logs for PowerShell errors.
## State & Persistence
`database.db` currently stores device inventory, runtime facts, and job history. Workflow and scheduling metadata are not yet persisted, and no internal scheduler exists beyond WebUI prototypes. Planned scheduling work will need schema updates and migration guidance once implemented.
@@ -85,3 +115,4 @@ Security and authentication are intentionally deferred. There is currently no ag

View File

@@ -34,7 +34,7 @@ if ($Server) {
'repair' { $agentSubChoice = '2' }
'remove' { $agentSubChoice = '3' }
'launch' { $agentSubChoice = '4' }
default { }
default { $agentSubChoice = '1' }
}
}
@@ -130,10 +130,30 @@ function Remove-BorealisServicesAndTasks {
Write-AgentLog -FileName $LogName -Message "Attempting to delete service: $n"
try { sc.exe delete $n 2>$null | Out-Null } catch {}
}
# Remove scheduled task if it exists
$taskName = 'Borealis Agent'
Write-AgentLog -FileName $LogName -Message "Attempting to delete scheduled task: $taskName"
try { schtasks.exe /Delete /TN "$taskName" /F 2>$null | Out-Null } catch {}
# Remove all Borealis scheduled tasks (supervisor/watchdog/legacy/user helper)
try {
$tasks = @()
try { $tasks = Get-ScheduledTask -ErrorAction SilentlyContinue | Where-Object { $_.TaskName -like 'Borealis Agent*' -or $_.TaskName -like 'Borealis*Supervisor*' -or $_.TaskName -like 'Borealis*Watchdog*' } } catch {}
foreach ($t in $tasks) {
Write-AgentLog -FileName $LogName -Message ("Deleting scheduled task: {0}" -f $t.TaskName)
try { Unregister-ScheduledTask -TaskName $t.TaskName -Confirm:$false -ErrorAction SilentlyContinue } catch {}
}
# Fallback to schtasks for machines without the ScheduledTasks module
foreach ($tn in @('Borealis Agent','Borealis Agent (UserHelper)','Borealis Agent - Supervisor','Borealis Agent - Watchdog')) {
try { schtasks.exe /Delete /TN "$tn" /F 2>$null | Out-Null } catch {}
}
} catch {}
# Gracefully stop only Agent venv Python processes (avoid killing dev web UI/node)
Write-Host "Stopping Agent Python processes scoped to Agent venv..." -ForegroundColor Yellow
Write-AgentLog -FileName $LogName -Message "Stopping Agent Python processes in Agent\\*"
try {
Get-Process python,pythonw -ErrorAction SilentlyContinue |
Where-Object { $_.Path -like (Join-Path $scriptDir 'Agent\*') } |
ForEach-Object { try { $_ | Stop-Process -Force } catch {} }
} catch {}
# Remove legacy watchdog script if present
try { Remove-Item -Force -ErrorAction SilentlyContinue (Join-Path $env:ProgramData 'Borealis\watchdog.ps1') } catch {}
}
# Repair routine: cleans services, ensures venv files, reinstalls and starts BorealisAgent
@@ -363,20 +383,37 @@ function Install_Agent_Dependencies {
function Ensure-AgentTasks {
param([string]$ScriptRoot)
$userTaskName = 'Borealis Agent'
$userExe = Join-Path $ScriptRoot 'Agent\Scripts\pythonw.exe'
$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
$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 {}
$pyw = Join-Path $ScriptRoot 'Agent\Scripts\pythonw.exe'
$agentPy = Join-Path $ScriptRoot 'Agent\Borealis\agent.py'
$svcWrapper = Join-Path $ScriptRoot 'Agent\Borealis\launch_service.ps1'
if (-not (Test-Path $pyw)) { Write-Host "pythonw.exe not found under Agent\Scripts" -ForegroundColor Yellow; return }
if (-not (Test-Path $agentPy)) { Write-Host "Agent script not found under Agent\Borealis" -ForegroundColor Yellow; return }
if (-not (Test-Path $svcWrapper)) { Write-Host "launch_service.ps1 not found under Agent\Borealis" -ForegroundColor Yellow; return }
# Clean old tasks first
try { Unregister-ScheduledTask -TaskName 'Borealis Agent' -Confirm:$false -ErrorAction SilentlyContinue } catch {}
try { Unregister-ScheduledTask -TaskName 'Borealis Agent (UserHelper)' -Confirm:$false -ErrorAction SilentlyContinue } catch {}
# SYSTEM startup task
# Use a wrapper PowerShell to enforce WorkingDirectory and capture stdout/stderr
$sysArg = ('-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File "{0}"' -f $svcWrapper)
$sysAction = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument $sysArg -WorkingDirectory (Split-Path $svcWrapper -Parent)
$sysTrigger = New-ScheduledTaskTrigger -AtStartup
$sysSet = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -Hidden -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) -ExecutionTimeLimit ([TimeSpan]::Zero)
$sysPrin = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest
Register-ScheduledTask -TaskName 'Borealis Agent' -Action $sysAction -Trigger $sysTrigger -Settings $sysSet -Principal $sysPrin -Force | Out-Null
try { Start-ScheduledTask -TaskName 'Borealis Agent' | Out-Null } catch {}
# Optional user-session helper for interactive roles (tray, overlays)
$helperName = 'Borealis Agent (UserHelper)'
$usrArg = ('"{0}" --config user' -f $agentPy)
$usrAction = New-ScheduledTaskAction -Execute $pyw -Argument $usrArg -WorkingDirectory (Split-Path $agentPy -Parent)
$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
$usrPrin = New-ScheduledTaskPrincipal -UserId $currentUser -LogonType Interactive -RunLevel Limited
Register-ScheduledTask -TaskName $helperName -Action $usrAction -Trigger $usrTrig -Settings $usrSet -Principal $usrPrin -Force | Out-Null
try { Start-ScheduledTask -TaskName $helperName | Out-Null } catch {}
}
function InstallOrUpdate-BorealisAgent {
Write-Host "Ensuring Agent Dependencies Exist..." -ForegroundColor DarkCyan
@@ -387,6 +424,8 @@ function InstallOrUpdate-BorealisAgent {
exit 1
}
$env:PATH = '{0};{1}' -f (Split-Path $pythonExe), $env:PATH
Write-Host "Cleaning previous agent tasks/processes..." -ForegroundColor Yellow
Remove-BorealisServicesAndTasks -LogName 'Install.log'
Write-Host "Deploying Borealis Agent..." -ForegroundColor Blue
$venvFolder = "Agent"
@@ -423,6 +462,7 @@ function InstallOrUpdate-BorealisAgent {
Copy-Item "Data\Agent\agent_deployment.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 }
if (Test-Path "Data\Agent\launch_service.ps1") { Copy-Item "Data\Agent\launch_service.ps1" $agentDestinationFolder -Force }
}
. "$venvFolder\Scripts\Activate"
}

View File

@@ -53,8 +53,17 @@ def _run_powershell_via_system_task(content: str):
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 '')
try:
log_dir = os.path.join(_project_root(), 'Logs', 'Agent')
os.makedirs(log_dir, exist_ok=True)
with open(os.path.join(log_dir, 'system_last.ps1'), 'w', encoding='utf-8', newline='\n') as df:
df.write(content or '')
except Exception:
pass
out_path = os.path.join(_project_root(), 'Temp', f'out_{uuid.uuid4().hex}.txt')
task_name = f"Borealis Agent - Task - {uuid.uuid4().hex} @ SYSTEM"
# Use WorkingDirectory set to the script folder to avoid 0x2 'file not found' issues
# on some systems when PowerShell resolves relative paths.
task_ps = f"""
$ErrorActionPreference='Continue'
$task = "{task_name}"
@@ -62,7 +71,7 @@ $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 + '"')
$action = New-ScheduledTaskAction -Execute $ps -Argument ('-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File "' + $scr + '" *> "' + $out + '"') -WorkingDirectory (Split-Path -Parent $scr)
$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
@@ -150,4 +159,3 @@ class Role:
})
except Exception:
pass

View File

@@ -28,18 +28,45 @@ except Exception:
import aiohttp
import socketio
# Reduce noisy Qt output and attempt to avoid Windows OleInitialize warnings
os.environ.setdefault("QT_LOGGING_RULES", "qt.qpa.*=false;*.debug=false")
from qasync import QEventLoop
from PyQt5 import QtCore, QtGui, QtWidgets
try:
# Swallow Qt framework messages to keep console clean
def _qt_msg_handler(mode, context, message):
return
QtCore.qInstallMessageHandler(_qt_msg_handler)
except Exception:
pass
from PIL import ImageGrab
# Early bootstrap logging (path relative to this file)
def _bootstrap_log(msg: str):
try:
base = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'Logs', 'Agent'))
os.makedirs(base, exist_ok=True)
ts = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
with open(os.path.join(base, 'bootstrap.log'), 'a', encoding='utf-8') as fh:
fh.write(f'[{ts}] {msg}\n')
except Exception:
pass
# Headless/service mode flag (skip Qt and interactive UI)
SYSTEM_SERVICE_MODE = ('--system-service' in sys.argv) or (os.environ.get('BOREALIS_AGENT_MODE') == 'system')
_bootstrap_log(f'agent.py loaded; SYSTEM_SERVICE_MODE={SYSTEM_SERVICE_MODE}; argv={sys.argv!r}')
def _argv_get(flag: str, default: str = None):
try:
if flag in sys.argv:
idx = sys.argv.index(flag)
if idx >= 0 and idx + 1 < len(sys.argv):
return sys.argv[idx + 1]
except Exception:
pass
return default
CONFIG_NAME_SUFFIX = _argv_get('--config', None)
if not SYSTEM_SERVICE_MODE:
# Reduce noisy Qt output and attempt to avoid Windows OleInitialize warnings
os.environ.setdefault("QT_LOGGING_RULES", "qt.qpa.*=false;*.debug=false")
from qasync import QEventLoop
from PyQt5 import QtCore, QtGui, QtWidgets
try:
# Swallow Qt framework messages to keep console clean
def _qt_msg_handler(mode, context, message):
return
QtCore.qInstallMessageHandler(_qt_msg_handler)
except Exception:
pass
from PIL import ImageGrab
# New modularized components
from role_manager import RoleManager
@@ -86,6 +113,18 @@ def _find_project_root():
# Heuristic fallback: two levels up from Agent/Borealis
return os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
# Simple file logger under Logs/Agent
def _log_agent(message: str, fname: str = 'agent.log'):
try:
root = _find_project_root()
log_dir = os.path.join(root, 'Logs', 'Agent')
os.makedirs(log_dir, exist_ok=True)
ts = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
with open(os.path.join(log_dir, fname), 'a', encoding='utf-8') as fh:
fh.write(f'[{ts}] {message}\n')
except Exception:
pass
def _resolve_config_path():
"""
Decide where to store agent_settings.json, per users requirement:
@@ -109,10 +148,31 @@ def _resolve_config_path():
# Target config in project root
project_root = _find_project_root()
cfg_path = os.path.join(project_root, "agent_settings.json")
cfg_basename = 'agent_settings.json'
try:
if CONFIG_NAME_SUFFIX:
suffix = ''.join(ch for ch in CONFIG_NAME_SUFFIX if ch.isalnum() or ch in ('_', '-')).strip()
if suffix:
cfg_basename = f"agent_settings_{suffix}.json"
except Exception:
pass
cfg_path = os.path.join(project_root, cfg_basename)
if os.path.exists(cfg_path):
return cfg_path
# If using a suffixed config and there is a base config in the project root, seed from it
try:
if CONFIG_NAME_SUFFIX:
base_cfg = os.path.join(project_root, 'agent_settings.json')
if os.path.exists(base_cfg):
try:
shutil.copy2(base_cfg, cfg_path)
return cfg_path
except Exception:
pass
except Exception:
pass
# Migration: from legacy user dir or script dir
legacy_user = _user_config_default_path()
legacy_script_dir = os.path.join(os.path.dirname(__file__), "agent_settings.json")
@@ -186,7 +246,15 @@ CONFIG.load()
def init_agent_id():
if not CONFIG.data.get('agent_id'):
CONFIG.data['agent_id'] = f"{socket.gethostname().lower()}-agent-{uuid.uuid4().hex[:8]}"
host = socket.gethostname().lower()
rand = uuid.uuid4().hex[:8]
if SYSTEM_SERVICE_MODE:
aid = f"{host}-agent-svc-{rand}"
elif (CONFIG_NAME_SUFFIX or '').lower() == 'user':
aid = f"{host}-agent-{rand}-script"
else:
aid = f"{host}-agent-{rand}"
CONFIG.data['agent_id'] = aid
CONFIG._write()
return CONFIG.data['agent_id']
@@ -362,6 +430,7 @@ role_tasks = {}
background_tasks = []
AGENT_LOOP = None
ROLE_MANAGER = None
ROLE_MANAGER_SYS = None
# ---------------- Local IPC Bridge (Service -> Agent) ----------------
def start_agent_bridge_pipe(loop_ref):
@@ -567,6 +636,11 @@ async def stop_all_roles():
ROLE_MANAGER.stop_all()
except Exception:
pass
try:
if ROLE_MANAGER_SYS is not None:
ROLE_MANAGER_SYS.stop_all()
except Exception:
pass
# ---------------- Heartbeat ----------------
async def send_heartbeat():
@@ -884,13 +958,37 @@ async def send_agent_details():
}
async with aiohttp.ClientSession() as session:
await session.post(url, json=payload, timeout=10)
_log_agent('Posted agent details to server.')
except Exception as e:
print(f"[WARN] Failed to send agent details: {e}")
_log_agent(f'Failed to send agent details: {e}', fname='agent.error.log')
await asyncio.sleep(300)
async def send_agent_details_once():
try:
details = {
"summary": collect_summary(),
"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)
_log_agent('Posted agent details (once) to server.')
except Exception as e:
_log_agent(f'Failed to post agent details once: {e}', fname='agent.error.log')
@sio.event
async def connect():
print(f"[INFO] Successfully Connected to Borealis Server!")
_log_agent('Connected to server.')
await sio.emit('connect_agent', {"agent_id": AGENT_ID})
# Send an immediate heartbeat so the UI can populate instantly.
@@ -903,6 +1001,7 @@ async def connect():
})
except Exception as e:
print(f"[WARN] initial heartbeat failed: {e}")
_log_agent(f'Initial heartbeat failed: {e}', fname='agent.error.log')
# Let server know collector is active and who the user is
try:
@@ -917,6 +1016,11 @@ async def connect():
pass
await sio.emit('request_config', {"agent_id": AGENT_ID})
# Kick off a one-time device details post for faster UI population
try:
asyncio.create_task(send_agent_details_once())
except Exception:
pass
@sio.event
async def disconnect():
@@ -953,7 +1057,12 @@ async def on_agent_config(cfg):
if ROLE_MANAGER is not None:
ROLE_MANAGER.on_config(roles)
except Exception as e:
print(f"[WARN] role manager apply config failed: {e}")
print(f"[WARN] role manager (interactive) apply config failed: {e}")
try:
if ROLE_MANAGER_SYS is not None:
ROLE_MANAGER_SYS.on_config(roles)
except Exception as e:
print(f"[WARN] role manager (system) apply config failed: {e}")
## Script execution and list windows handlers are registered by roles
@@ -974,6 +1083,108 @@ async def idle_task():
print(f"[FATAL] Idle task crashed: {e}")
traceback.print_exc()
# ---------------- Quick Job Helpers (System/Local) ----------------
def _project_root_for_temp():
try:
return os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
except Exception:
return os.path.abspath(os.path.dirname(__file__))
def _run_powershell_script_content_local(content: str):
try:
temp_dir = os.path.join(_project_root_for_temp(), "Temp")
os.makedirs(temp_dir, exist_ok=True)
import tempfile as _tf
fd, path = _tf.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"
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 'path' in locals() and 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'
script_path = None
out_path = None
try:
temp_root = os.path.join(_project_root_for_temp(), 'Temp')
os.makedirs(temp_root, exist_ok=True)
import tempfile as _tf
fd, script_path = _tf.mkstemp(prefix='sys_task_', suffix='.ps1', dir=temp_root, text=True)
with os.fdopen(fd, 'w', encoding='utf-8', newline='\n') as f:
f.write(content or '')
try:
log_dir = os.path.join(_project_root_for_temp(), 'Logs', 'Agent')
os.makedirs(log_dir, exist_ok=True)
debug_copy = os.path.join(log_dir, 'system_last.ps1')
with open(debug_copy, 'w', encoding='utf-8', newline='\n') as df:
df.write(content or '')
except Exception:
pass
out_path = os.path.join(temp_root, 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 + '"') -WorkingDirectory (Split-Path -Parent $scr)
$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 best-effort
try:
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)
except Exception:
pass
try:
if script_path and os.path.isfile(script_path):
os.remove(script_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)
async def _run_powershell_via_user_task(content: str):
ps = None
if IS_WINDOWS:
@@ -1043,13 +1254,14 @@ Get-ScheduledTask -TaskName $task | Out-Null
pass
# ---------------- Dummy Qt Widget to Prevent Exit ----------------
class PersistentWindow(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("KeepAlive")
self.setGeometry(-1000,-1000,1,1)
self.setAttribute(QtCore.Qt.WA_DontShowOnScreen)
self.hide()
if not SYSTEM_SERVICE_MODE:
class PersistentWindow(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("KeepAlive")
self.setGeometry(-1000,-1000,1,1)
self.setAttribute(QtCore.Qt.WA_DontShowOnScreen)
self.hide()
# //////////////////////////////////////////////////////////////////////////
# MAIN & EVENT LOOP
@@ -1060,47 +1272,150 @@ async def connect_loop():
try:
url=CONFIG.data.get('borealis_server_url',"http://localhost:5000")
print(f"[INFO] Connecting Agent to {url}...")
_log_agent(f'Connecting to {url}...')
await sio.connect(url,transports=['websocket'])
break
except Exception as e:
print(f"[WebSocket] Server unavailable: {e}. Retrying in {retry}s...")
_log_agent(f'Server unavailable: {e}', fname='agent.error.log')
await asyncio.sleep(retry)
if __name__=='__main__':
app=QtWidgets.QApplication(sys.argv)
loop=QEventLoop(app); asyncio.set_event_loop(loop)
AGENT_LOOP = loop
try:
start_agent_bridge_pipe(loop)
_bootstrap_log('enter __main__')
except Exception:
pass
dummy_window=PersistentWindow(); dummy_window.show()
if SYSTEM_SERVICE_MODE:
loop = asyncio.new_event_loop(); asyncio.set_event_loop(loop)
else:
app=QtWidgets.QApplication(sys.argv)
loop=QEventLoop(app); asyncio.set_event_loop(loop)
AGENT_LOOP = loop
try:
if not SYSTEM_SERVICE_MODE:
start_agent_bridge_pipe(loop)
except Exception:
pass
if not SYSTEM_SERVICE_MODE:
dummy_window=PersistentWindow(); dummy_window.show()
# Initialize roles context for role tasks
# Initialize role manager and hot-load roles from Roles/
try:
hooks = {'send_service_control': send_service_control}
ROLE_MANAGER = RoleManager(
if not SYSTEM_SERVICE_MODE:
# Load interactive-context roles (tray/UI, current-user execution, screenshot, etc.)
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()
# Load system roles when headless or alongside interactive
ROLE_MANAGER_SYS = RoleManager(
base_dir=os.path.dirname(__file__),
context='interactive',
context='system',
sio=sio,
agent_id=AGENT_ID,
config=CONFIG,
loop=loop,
hooks=hooks,
)
ROLE_MANAGER.load()
except Exception:
pass
ROLE_MANAGER_SYS.load()
except Exception as e:
try:
_bootstrap_log(f'role load init failed: {e}')
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()))
# Periodic device details upload so Devices view populates
try:
background_tasks.append(loop.create_task(send_agent_details()))
except Exception:
pass
# Register unified Quick Job handler last to avoid role override issues
@sio.on('quick_job_run')
async def _quick_job_dispatch(payload):
try:
_log_agent(f"quick_job_run received: mode={payload.get('run_mode')} type={payload.get('script_type')} job_id={payload.get('job_id')}")
import socket as _s
hostname = _s.gethostname()
target = (payload.get('target_hostname') or '').strip().lower()
if target and target not in ('unknown', '*', '(unknown)') and target != hostname.lower():
return
job_id = payload.get('job_id')
script_type = (payload.get('script_type') or '').lower()
content = payload.get('script_content') or ''
run_mode = (payload.get('run_mode') or 'current_user').lower()
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 = -1; out = ''; err = ''
if run_mode == 'system':
if not SYSTEM_SERVICE_MODE:
# Let the SYSTEM service handle these exclusively
return
try:
# Save last SYSTEM script for debugging
dbg_dir = os.path.join(_find_project_root(), 'Logs', 'Agent')
os.makedirs(dbg_dir, exist_ok=True)
with open(os.path.join(dbg_dir, 'system_last.ps1'), 'w', encoding='utf-8', newline='\n') as df:
df.write(content or '')
except Exception:
pass
rc, out, err = _run_powershell_script_content_local(content)
if rc == -999:
# Fallback: attempt scheduled task (should not be needed in service mode)
rc, out, err = _run_powershell_via_system_task(content)
elif 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:
# Fallback to plain local run
rc, out, err = _run_powershell_script_content_local(content)
status = 'Success' if rc == 0 else 'Failed'
await sio.emit('quick_job_result', {
'job_id': job_id,
'status': status,
'stdout': out or '',
'stderr': err or '',
})
_log_agent(f"quick_job_result sent: job_id={job_id} status={status}")
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
_log_agent(f"quick_job_run handler error: {e}", fname='agent.error.log')
_bootstrap_log('starting event loop...')
loop.run_forever()
except Exception as e:
try:
_bootstrap_log(f'FATAL: event loop crashed: {e}')
except Exception:
pass
print(f"[FATAL] Event loop crashed: {e}")
traceback.print_exc()
finally:
try:
_bootstrap_log('Agent exited unexpectedly.')
except Exception:
pass
print("[FATAL] Agent exited unexpectedly.")
# (moved earlier so async tasks can log immediately)

View File

@@ -0,0 +1,37 @@
#////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: <ProjectRoot>/Data/Agent/launch_service.ps1
[CmdletBinding()]
param(
[switch]$Console
)
try {
$ErrorActionPreference = 'Stop'
$scriptDir = Split-Path -Path $PSCommandPath -Parent
Set-Location -Path $scriptDir
# Ensure a place for wrapper/stdout logs
$pd = Join-Path $env:ProgramData 'Borealis'
if (-not (Test-Path $pd)) { New-Item -ItemType Directory -Path $pd -Force | Out-Null }
$wrapperLog = Join-Path $pd 'svc.wrapper.log'
$venvBin = Join-Path $scriptDir '..\Scripts'
$pyw = Join-Path $venvBin 'pythonw.exe'
$py = Join-Path $venvBin 'python.exe'
$agentPy = Join-Path $scriptDir 'agent.py'
if (-not (Test-Path $pyw) -and -not (Test-Path $py)) { throw "Python not found under: $venvBin" }
if (-not (Test-Path $agentPy)) { throw "Agent script not found: $agentPy" }
$exe = if ($Console) { $py } else { if (Test-Path $pyw) { $pyw } else { $py } }
$args = @("`"$agentPy`"","--system-service","--config","svc")
# Launch and keep the task in Running state by waiting on the child
$p = Start-Process -FilePath $exe -ArgumentList $args -WindowStyle Hidden -PassThru -WorkingDirectory $scriptDir `
-RedirectStandardOutput (Join-Path $pd 'svc.out.log') -RedirectStandardError (Join-Path $pd 'svc.err.log')
try { Wait-Process -Id $p.Id } catch {}
} catch {
try {
"[$(Get-Date -Format s)] $_" | Out-File -FilePath $wrapperLog -Append -Encoding utf8
} catch {}
exit 1
}

9
agent_settings_svc.json Normal file
View File

@@ -0,0 +1,9 @@
{
"borealis_server_url": "http://localhost:5000",
"max_task_workers": 8,
"config_file_watcher_interval": 2,
"agent_id": "lab-operator-01-agent-66ba3ad3",
"regions": {},
"agent_operating_system": "Microsoft Windows 11 Pro 24H2 Build 26100.6584",
"created": "2025-09-02 23:57:00"
}

9
agent_settings_user.json Normal file
View File

@@ -0,0 +1,9 @@
{
"borealis_server_url": "http://localhost:5000",
"max_task_workers": 8,
"config_file_watcher_interval": 2,
"agent_id": "lab-operator-01-agent-66ba3ad3",
"regions": {},
"agent_operating_system": "Windows 11 Pro 24H2 Build 26100.6584",
"created": "2025-09-02 23:57:00"
}